Repository navigation
wip init refuses an adopted checkout ahead of origin's main by anything but its own seed, before a byte is written (#64) - #139
Conversation
…n` by any commit but its own seed, before a byte is written, and step 6a and step 8 read origin's `main` once between them (#64) Brett Heap ruled on 2026-10-02, verbatim "go with option 1 on #64". Step 6a asked about the worktree and the index and never about history, so a clean adopted checkout holding a person's own unpushed commits had them pushed to the `main` of their workspace repository by step 8's `push origin HEAD:main`. Step 6a now asks `git ls-remote origin main` before step 7 writes anything, lists `<origin main>..HEAD` (all of HEAD where the remote has no `main`), and refuses with exit 2, naming every commit in that range, unless each one is this command's own seed. A seed is recognised by its subject, its `Written by` line, one parent or none, and a tree that differs from its parent's only at template paths, with at least one changed. The exemption keeps step 8's promise that a re-run after a ruleset-rejected push only has to push. The remote is asked once, and step 8 reads that same answer, so the range checked and the push decided are one reading of `main`. The line it reads is `refs/heads/main` by name. The first line of a `main` pattern can belong to a branch such as `backup/main`. Where the remote cannot be asked, the check is skipped with one line saying so and the push stays the probe, as the ruling keeps it. A range that cannot be listed, usually because `main` has moved and this checkout has not fetched it, is a refusal and never counts as an empty range. The recognizer passes `--no-show-signature` because `log.showSignature` writes the signature report on stdout ahead of the subject. Tests cover a person's commit ahead, a seed left by a refused push on an adopted `main`, a remote that cannot be asked, a commit that copies the seed's message (four ways), an empty `main`, a `main` this checkout has not fetched, a `backup/main` branch, and a signed seed. The README's seed paragraph gains one sentence, and its cap moves 472 -> 474 with a dated entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
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 21 hours and 20 minutes by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideIntroduces a shared, exact remote-main history check before wip init writes anything, preventing accidental publication of user commits while allowing only valid prior seed commits to be re-pushed; the change also hardens seed recognition and adds extensive regression coverage and documentation. Sequence diagram for wip init remote-main history checksequenceDiagram
participant W as wip init
participant R as origin
participant G as Git history
participant T as Checkout
W->>R: git ls-remote origin refs/heads/main
alt remote main is available
W->>G: git rev-list remote_head..HEAD
G-->>W: commits ahead of main
W->>G: git log --no-show-signature --oneline
W->>G: wip_is_own_seed(commit)
alt any commit is not an own seed
W-->>T: Refuse before writing
else all commits are valid seeds
W->>T: Write template and create seed
W->>R: Push HEAD:main using remote_head
end
else remote cannot be asked
W-->>W: Skip history check
W->>T: Write template and create seed
W->>R: Push HEAD:main as probe
end
Flow diagram for wip init history safety gateflowchart TD
A["wip init"] --> B["Step 1: pointer-file idempotence check"]
B -->|already initialized| Z["Exit without network call"]
B -->|adopted checkout| C["Existing worktree and index checks"]
C --> D["git ls-remote origin refs/heads/main"]
D -->|remote cannot be asked| E["Skip history check; continue to push probe"]
D -->|remote main available| F["git rev-list remote_head..HEAD"]
F -->|cannot list range| R["Refuse: nothing written or staged"]
F -->|user commits in range| R
F -->|only valid prior seed commits| G["Write template and create seed if needed"]
E --> G
G --> H["Push HEAD:main using shared remote_head"]
H -->|ruleset refusal| I["Leave clean unpushed seed for rerun"]
H -->|success| J["Complete"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The safety gate matches the stated requirements and is thoroughly covered by focused tests.
Review effort: Balanced
Findings: None
What changed in this PR
Prevents wip init from publishing unreviewed local commits when adopting an existing workspace checkout.
Changes:
- Validates commits ahead of remote
main, allowing only recognized seed commits. - Reuses an exact remote-main lookup for validation and pushing.
- Adds documentation and comprehensive regression coverage.
| File | Description |
|---|---|
openRepoTools |
Adds the pre-write history gate and seed recognizer. |
tests/test_wip_init_command.py |
Covers refusal, recovery, remote, signature, and seed edge cases. |
README.md |
Documents the new refusal behavior. |
tests/test_repo_hygiene.py |
Adjusts the documented README line budget. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…this run and nothing was staged." on one line, as every other step 6a refusal does Rewrapping the refusal to shorten its lines split that sentence between "NOTHING was" and "written". Every test of the history gate asserts the sentence's own words, so seven of them failed on CI's Linux job at b5e80d5 ("7 failed, 697 passed"), all on that one assertion. The sentence is now a line of its own, so a person reading the refusal, or a test of it, finds it whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Local verification on Eagle. Every run went through After: this branch @ 73bba01. Scoped to the wip-init and hygiene modules (every other module passed with Before: the same tests,
Without CI on b5e80d5. It failed 7 tests on one assertion: my rewrap had split "NOTHING was / written". 73bba01 fixes that. Filed rather than fixed here: #140. Step 8 reads every refused push as the PR-only ruleset, so a checkout that is merely behind |
…Copilot capped at two rounds, CI the suite of record) into the #64 branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Landing note from lane openRepoTools-1 (2026-10-03). CI on c587c4f (this branch + main a44a4ed): |
…e-listing #139 (wip init refuses an adopted checkout ahead of origin's main) landed after #83. It touches `openRepoTools`, its own tests, README.md and a different docstring of tests/test_repo_hygiene.py than this branch does; the merge is clean and nothing of it is resolved by hand. Lane: openRepoTools-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bring in #119, #103, #83, #139 and #93 so #61 lands last in the queue on the main it will actually land on. One textual conflict: - lanes-edit.sh, the `*)` arm's unknown-subcommand list: main added `binding`, `request-handoff`, `register-row-local`, `retired-identity` (#83) and `retire-rows`, `archive-rows` (#93); this branch added `duplicate-holder`. Every name is kept, in main's order with `duplicate-holder` after `forks` as before, and the list still equals the dispatcher's arms exactly (test_repo_hygiene's derived check). Checked against #61's code, nothing else needed a change: the suite's fake-command block (tmux, claude, pgrep, ps, gh) is intact and no new main code calls pgrep or ps; #93's columns 14-17 and #83's column 13 are in the `lanes` listing rows, which #61 never reads; LOG_AWK's fields, live_holder's, transcript_holders' and lane_forks' output formats are unchanged; #83's HANDOFF-REQUESTED is a lane-kind verb that changes no state and holder_is_dead's five-verb whitelist skips it as every last-line reader must; and lane-end's --retire-dormant sweep is refused beside --retire and exits before the fork/duplicate retire path. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring #97 onto a current base before the seam rework Brett Heap ruled on 2026-10-04 ("managed ledger owns enrolled lanes; #97 owns legacy — rework both"). Main moved 14 commits past 957a26f, to daed209: #81 lane-rename, #83 Amendment 18, #93 Amendment 19, #61 claim --force takeover, #146, #119, #101-#103, #129, #131, #133, #135, #136, #139 and #134. Four files conflicted, and in every one both intents survive: - lanes-edit.sh, the function sections: both sides appended after migrate_state_cells. Main's Amendment 19(c)/(d) sweep and archive stay directly under it, because its header says "the shape `migrate-state-cells` has one screen up"; #97's lifecycle and inventory section follows them. - lanes-edit.sh, the unknown-subcommand refusal: the union of both lists, 56 names, the same set as the dispatcher's 56 arms. - lanes-edit.sh, the exit-code table, and the manual's copy of it: #97's 7 row ("another act got there first", in three verbs) is kept, and exit 9 now has two meanings, one per verb. #61 spent 9 on `claim --force`'s abandoned takeover and #97 spent it on `lane-state`'s unreadable snapshot, and each PR took it as unused. Neither verb can return the other's 9, so the table names both and nothing is renumbered in a merge. The manual's 9 row said "(`claim` only)", which the merge would have made false; its 7 row now names #97's two fenced writers too, as the code table already did. The next commit moves #97's 9 to 10. - lane-handoff, the late-record restart line: #129's `$lane_profile_word` (lclaude) with #97's lifecycle line under it. - docs/README-lanes.md: Amendment 19's section, then #97's #91 section, so the amendment sections stay in number order. - tests/test_lane_helpers.sh: main's Amendment 19 and Amendment 18 sections run first, in main's order (A18's restores its fixtures), and #97's self-contained #91 section follows them, before the workstation-seam section, as it did on #97's branch. lane-start auto-merged around #134's rewrite. #97's section 4a still prints the reconciliation after Amendment 18's binding gate and before section 5 and the STARTED/RESUMED write in 5b. AGENTS.md, README.md, tests/test_repo_hygiene.py and skills/ are byte-identical to main, so the 316- and 486-line caps already match the merged counts. Locally, tests/run.sh -k 'lane_helpers_suite or repo_hygiene or lane_start_claude_current or guard_launch_mode or install_skill_and_hook': 274 passed, 600 deselected, in 1560 s. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Builds #64 as Brett Heap ruled it on 2026-10-02, verbatim: "go with option 1 on #64".
What step 6a now refuses
Step 6a already refused an ADOPTED checkout whose worktree or index carries anything the template does not name. It never looked at history, and step 8 pushes
HEAD:mainwherever HEAD is not what origin'smainalready carries. So a clean adopted checkout with a person's own unpushed commits (a register edit made here and never pushed, a note committed on the wrong clone) had every one of them published to themainof their workspace repository. The command that published them came only to place a template.Step 6a now asks the remote before step 7 writes a byte, lists
git rev-list <origin main>..HEAD, and refuses with exit 2 unless every commit in that range is this command's own seed. Where the remote has nomainyet, the range is all ofHEAD. The refusal:git log --oneline, indented like the other refusals, and says whichmainit compared against (<target>'s main (abc1234), or(it has none yet));main, so it would publish them unreviewed;openRepoTools wip init.What it exempts, and why
This command's own seed. If the PR-only ruleset rejects the first run's push, that run leaves a clean worktree and exactly one commit ahead of
main: the seed. Step 8's refusal promises that "a re-run after that has only to push".wip_is_own_seedrecognises a seed by all of what makes step 8's commit its own:diff-treenames no path for a merge unless asked for a combined diff, so without this check a merge would pass the tree test by naming nothing;Seed this workspace repository from;Written by `openRepoTools wip init`;--root) at one or more template paths and at no other. Step 8 commits only template paths by pathspec, and never makes an empty commit.Every read that fails counts as "not a seed". The seed's bytes are not compared with the template: that run may have pinned a different template commit, and the ruling names the subject, the trailer and the tree. The comment block says so.
The shared remote read
git ls-remote origin mainis asked once, at step 6a. Step 8 reads the sameremote_head, so the range checked and the push decided are one reading ofmain. Step 8's ownls-remoteis gone, and itslocal_head != remote_headgate is unchanged. Two changes come with moving the read:refs/heads/mainby name, notNR==1. A pattern ofmainmatches every ref whose name ends in it, andrefs/heads/backup/mainsorts first. Step 8 used to read that branch's tip asmain, and would then say thatmain"already carries this commit" without pushing to it. With the new gate the same misreading would have let the branch's commits through. Pinned by a test.remote_askedis kept separate fromremote_headso that an unanswered question is never read as "nomainyet", which would put every commit ahead and refuse the seed re-run. The comment names the remaining window: a remote that refuses the question but accepts the push a moment later.Two refusals and one flag that come with the gate
wip initstep 6a's git-status safety gate masks a failure as clean (|| :) #49's lesson, applied to history). The usual cause is amainthis checkout has never fetched. Before this change, that case wrote the template, committed, and then died at a non-fast-forward push with the ruleset message. Now the refusal names the fetch, includes git's own words, and nothing has been written.--no-show-signatureon everygit logthe recognizer runs.log.showSignatureprints the signature report on stdout, ahead of--format. On a machine withcommit.gpgSignset, the command's own signed seed would read back withNo signature/Good "git" signature …as its first line and be refused. Measured with an SSH-signed commit; pinned by a test.Left unchanged: step 1's idempotence (the pointer-file test), the seed commit's message, and the push itself.
How it was tested
New tests in
tests/test_wip_init_command.py:test_an_adopted_checkout_ahead_of_main_by_a_persons_commit_is_refused: clean by the old gate, one commit of the person's own ahead. Checks exit 2, the commit's sha and subject in stderr, no template byte written, nothing staged, nothing pushed, no pointer file.test_a_seed_a_refused_push_left_on_an_adopted_main_is_pushed_by_the_rerun: the ruleset is switched on for an existing repository, and the first run leaves one seed on top of a non-emptymain. Once the ruleset is lifted, the re-run recognises the seed, pushes it and exits 0. The existingtest_the_rerun_after_the_administrator_acts_has_only_to_pushnow asserts the same for a root seed.test_a_remote_that_cannot_be_asked_skips_the_history_check_for_the_probe:originpoints at a path where nothing answers, with a person's commit ahead. The check is skipped with its line, and the run reaches step 8's ruleset refusal, not 6a's.test_a_commit_wearing_the_seeds_message_is_not_taken_for_the_seed[non-template-path|no-trailer|merge|empty]: the tree test has teeth, and so does each other part.test_an_empty_main_with_a_persons_local_commit_is_refusedtest_a_main_this_checkout_never_fetched_is_refused_not_read_as_nothing_aheadtest_a_branch_whose_name_ends_in_main_is_not_read_as_maintest_a_signed_seed_is_still_known_where_log_shows_signatures(skipped withoutssh-keygenor git ≥ 2.34)Local runs used
tests/run.shscoped to the wip-init and hygiene modules. On the branch: 220 passed, 40 skipped, 0 failed. Againstmain'sopenRepoTools, all 12 targeted tests fail. With--no-show-signatureremoved, the signed-seed test fails. Details are in this comment.CI
testsfailed 7 tests, all on one assertion. My own rewrap had split "NOTHING was / written". 73bba01 fixed it.tests(Linux) passed, 704 passed.tests-no-submoduleandtests-windowspassed.tests-macosfailed one case insidetests/test_lane_helpers.sh, "the liveness fixture was still running when the run ended" (2787 passed, 1 failed). That case is already red onmain:main's lasttests-macos(run 36734926260 @ 69bf48d) fails it among 19. It is not from this PR.main@ 92bd21f (tests-macos becomes a landing gate on thereadylabel plus nightly on main; Copilot capped at two rounds; CI is the suite of record — Brett Heap's three rulings of 2026-09-16 #119).tests(Linux) passed, 704 passed.tests-no-submodule,tests-windows,parse-macosandguard-launch-modepassed.tests-macosis skipped until thereadylabel is on the PR.Copilot round 1 (b5e80d5) reported no findings, with "Approval recommended". It did not fire on the later pushes, and re-requesting it was a no-op.
Filed while building this: #140. Step 8 reads every refused push as the PR-only ruleset, so a checkout that is merely behind
mainis sent to an administrator. This predates the PR, and step 8's push is outside it.README and its cap
One sentence in the
wip initseed paragraph: a checkout ahead of that repository'smainby anything but an earlier run's unpushed seed is refused, and it names the commits. That adds two lines, so the hygiene cap moves 472 → 474, with a dated 2026-10-02 entry naming #64. #103 raises the same cap to 484 and is expected to land first; the lane owner reconciles the cap after it lands (per the cap's own rule, the cap counts what merged).Closes #64
Lane: openRepoTools-1
🤖 Generated with Claude Code
Summary by Sourcery
Refuse to initialize adopted checkouts with unpushed history that would be published to
main, while safely permitting and validating earlier seed reruns.New Features:
wip initfrom adopting clean checkouts with unpushed commits that would be published to the workspace repository’smain, while allowing previously created seed commits to be pushed on rerun.mainlookup between the history safety check and the final push decision, including correct handling of missing or unreachable remote branches.Bug Fixes:
mainand avoid treating failures to enumerate commit history as an empty range.Enhancements:
Documentation:
wip initREADME guidance.Tests:
Chores: