Repository navigation
fix(init): retire side worktrees before replacing repo - #1327
Conversation
The PR description includes sufficient testing: a successful build and one representative compiled-CLI E2E run. I did not run, build, or install anything. |
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
@XY-code1 The basic replacement case works, but a clean side checkout behind origin prevents switching teams at 80b294e1.
- [P1 blocking]
retireImplunconditionally pushesHEAD:refs/heads/<branch>insrc/utils/branch-worktree.ts. After one member pulls and another member advancesteamai-reports,teamai init <other-team> --forceexits 1 with a non-fast-forward rejection even though every local commit is already preserved on origin. It tells the member to push commits they do not have. Pulling first clears the refusal. Fetch and distinguish already-preserved history from unpublished/diverged history before deciding whether a push is needed.
The built-CLI fixture reproduced this failure and verified that unpushed commits are pushed and dirty side worktrees are retained. The focused E2E test passed, but does not exercise the behind-origin case. Current CI jobs passed.
CI review reported this same blocking finding. We agree, based on the real-CLI reproduction.
#1328 is an alternative for #1320. This PR owns push/removal of the side worktrees; #1328 owns archiving the old clone and its worktrees. Only one path should land. #1328 also covers the issue's insteadOf same-repository trigger, which this PR leaves unchanged.
|
@SaulMoro I addressed the behind-origin blocker in d13f8ce. Retirement now inspects and fetches the remote branch, skips the push when origin already contains every local commit, pushes only local-ahead history, and fails closed on divergence or an unreadable remote. I added focused regression cases for behind-origin and diverged histories, and extended the compiled-CLI E2E fixture so a second clone advances teamai-reports before init --force replaces the repository. Build, typecheck, lint, 5 focused unit tests, the real-CLI E2E test, and git diff --check all pass. Could you please take another look? |
The earlier unconditional-push finding is resolved: the current diff fetches origin and skips pushing when local commits are already preserved remotely. The PR description documents sufficient testing: a successful build and one representative compiled-CLI E2E run. I did not run, build, or install anything. |
|
@SaulMoro The new lock-lifetime blocker is fixed in 10ecd1b. Clone replacement now nests reports and learnings retirement, so both writer locks remain held until the old clone is deleted. Lock acquisition happens even when a checkout does not yet exist, preventing a concurrent writer from recreating a side worktree during replacement. I added a regression test that attempts to reacquire both locks inside the replacement callback and verifies both are released afterward. Build, typecheck, lint, 6 focused unit tests, the compiled-CLI E2E test, and git diff --check pass. Could you please re-review the updated head? |
|
No new findings I’m confident are real in the current diff. Both earlier findings are resolved:
The PR description documents sufficient testing: a successful build and one representative compiled-CLI E2E run. The record names an older commit, which is non-blocking under the review rules. I did not run, build, or install anything from the PR. |
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
@XY-code1 Thanks, approving at 10ecd1ba. This replaces our Request changes from 80b294e1. The blocking case is fixed.
We rebuilt this head and reran the scenario with the real CLI. Two members, teamai-reports advanced by the other member, and a clean side checkout that is behind origin with nothing unpublished. teamai init <other-team> --force now exits 0 without pushing, and origin's head is unchanged. The other cases also hold:
- a local-only commit is pushed;
- a diverged checkout stops with exit 1 and keeps its commit;
- a dirty checkout stops with exit 1 and is kept;
- an unreachable origin stops with exit 1 and a clear message.
tsc, lint, branch-worktree-retire.test.ts (6/6) and the #1320 e2e (1/1) pass.
Suggestions:
[P2 non-blocking]If the fetch fails afterls-remotesucceeds,retireImplprints the raw git error without the "left unchanged, run init again" guidance the other refusals give, becausefetchTrackingRefisn't wrapped likels-remoteand push. Nothing is lost.[P2 non-blocking]The diverged message says "Reconcile and push the branch" but not how. Naming the checkout path and thegit pull --rebase/git pushsteps would help.[P3 nit]retireImplreturns beforewhileLockedon the http guard, which would skip the clone removal. It is unreachable today, becauseinit.tsdropsrepo.kindwhen it buildsprevious.
CI review: it reported no new findings. We agree with its points on the fetch-before-push logic, the nested locks and the testing record. We disagree that the record names an older commit: the body names 10ecd1ba. CI jobs at this head need a maintainer's approval to run, so only the review job ran.
Of the two fixes for #1320, this one lands. #1328 conflicts with it in src/init.ts.
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Withdrawing my approval. Before merging I ran this head together with current main on macOS with the default temp dir, and the retire path refuses its own checkout whenever the team repo path goes through a symlink.
[P1 blocking] retireImpl compares an unresolved path with git's resolved one. In src/utils/branch-worktree.ts, retireImpl checks path.resolve(registered) !== path.resolve(wt). git worktree list --porcelain reports the real path, while wt keeps the symlinked one. On macOS /tmp and /var are symlinks to /private/..., and a symlinked home does the same. In those cases init --force stops with "exists but is not registered to ... as its teamai-reports checkout", although it is this repository's own checkout, and the side worktree is never retired.
Repro on this head (macOS, TMPDIR unset):
npx vitest run src/__tests__/branch-worktree-retire.test.ts
Tests 4 failed | 2 passed (6) # ForeignCheckoutError: .../reports-wt exists but is not registered ...
npm run build && npm run test:e2e -- src/__tests__/e2e/init-replace-worktrees-1320.test.ts
Tests 1 failed (1)
With TMPDIR pointing at a directory without a symlink, both pass. That explains why it passed in my earlier run.
Comparing real paths fixes both. The same file already resolves git dirs with fse.realpath:
- if (registered === null || path.resolve(registered) !== path.resolve(wt)) {
+ if (registered === null || (await fse.realpath(registered)) !== (await fse.realpath(wt))) {With that change: branch-worktree-retire.test.ts 6/6 and init-replace-worktrees-1320.test.ts 1/1 pass, and npx tsc --noEmit is clean.
CI review: it reported no new findings at this head. It did not catch this, since it only shows up with a symlinked path. The CI jobs at this head were approved and are still queued; I expect the macOS legs to show this failure.
Follow-up to my request for changes. CI at 10ecd1b has finished. The three macOS [P1 blocking] E2E shard 2/3 is a separate failure that the realpath fix does not cover. Three existing tests in
In these scenarios the side checkouts belong to team A's clone, and init is pointing the project at team B. The new retire step now runs before the existing #808/#823 handling and stops
Those tests expect init to set team A's queue and config aside, or to keep team A's dirty learnings checkout and still finish. Either retire needs to leave alone the checkouts that path already handles (a team switch rather than a reclone of the same team), or, if refusing is the intended new behavior, the PR needs to change those tests and say why the #823 contract changes. As it stands it is a regression in existing coverage. |
Both earlier findings are resolved: already-preserved commits avoid a push, and both writer locks remain held through clone deletion. The PR description documents sufficient testing: a successful build and one representative compiled-CLI E2E run. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
Earlier findings: unnecessary pushes and dirty-checkout object deletion are resolved. The clean retirement path holds both locks, but the new dirty-checkout path bypasses that protection. The PR description documents sufficient testing: a successful build and representative compiled-CLI E2E run. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
|
No new findings I’m confident are real in the current diff. All earlier findings are resolved:
The PR description documents sufficient testing: a successful build and one representative compiled-CLI E2E run. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
|
@SaulMoro The blocking findings have been addressed in the latest commits. The review bot now reports no new findings and confirms all earlier findings are resolved. Could you please take another look when convenient? Thanks! |
Re-checked at 043e89c. The realpath blocker is fixed. With [P1 blocking] still open: the #808/#823 e2e regressions. On this head,
CI looked green because the CI and Code Erosion runs at this head were waiting for maintainer approval, so only the review job had run. I have approved them now. The CI review says the earlier findings are resolved, but it only read the diff and ran nothing. The e2e run above shows the #808 finding is not resolved. This head also conflicts with |
043e89c to
d8f810e
Compare
|
@SaulMoro I rebased the branch onto current main and addressed the three #808/#823 regressions at d8f810e. The previous install's queue and config are now settled before its clone is removed or relocated, so team-switch handoff data is preserved. Replacement also probes whether each managed checkout is actually registered to the current clone; stale or unregistered paths are left to the existing #808 foreign-checkout handling instead of blocking the switch. The focused retirement suite passes 9/9, the compiled-CLI #1320 E2E passes 2/2, and typecheck, lint, build, and diff-check pass. Could you please rerun the Linux self-mode-worktrees-808 suite and re-review the updated head? |
|
No new findings I’m confident are real in the scoped diff. All earlier findings are resolved:
The PR description documents sufficient testing: a successful build and one representative compiled-CLI E2E run. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
Re-checked at d8f810e. #823 item 17 (init reusing a clone of team B) passes now. [P1 blocking] still open: two #808 e2e tests fail.
The CI review at this head reports no new findings and calls every earlier finding resolved. We agree that the earlier findings are fixed. It only read the diff, though, and the E2E job above is red at the same head. |
|
No new findings I’m confident are real in the scoped diff. All earlier findings are resolved:
The PR description documents sufficient testing: a successful build and representative compiled-CLI E2E runs. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
|
@SaulMoro I addressed both remaining #808 regressions in
Validation completed locally:
The full E2E run needs the hosted Linux/macOS jobs; the local Windows runner hits the repository's Git hook/Cygwin fork limitation. The scoped Codex review and open-source scan are green at this head. Could you please approve the remaining workflows and re-check the two focused #808 cases? |
Re-checked at 41ef633. I approved the pending CI and Code Erosion runs for this head. The source change works: the same-repo re-init now reuses the clone ( [P1 blocking]
Since Windows cannot run this file locally, the Linux and macOS E2E jobs now running at this head will show the same two failures. The CI review at this head reports no new findings and calls the earlier ones resolved. We agree on the source. It did not run the e2e file, which is still red as shown above. |
|
No new findings I’m confident are real in the scoped diff. All earlier findings are resolved:
The PR description documents sufficient testing: a successful build and representative compiled-CLI E2E runs. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
|
@SaulMoro Updated both expectations in
|
Re-checked at 0a2b37f, merged onto current The dirty-reclone case passes now. [P1 blocking] #823 item 13 still fails, further down. The same-repo step now publishes team A's note, as the new assertions say, so nothing is queued when init switches to team B. The rest of the test still expects that note to be set aside. Please queue a fresh team A note with pushes rejected after the same-repo step and before The CI review at this head reports no new findings. We disagree only on this e2e case, which it did not run. |
One more note on 0a2b37f, non-blocking, in case you touch [P2 non-blocking] A failed Other paths I exercised with the built CLI behaved well. A push rejected during retirement exits 1 and keeps the learnings checkout at its exact HEAD, and the retry pushes it to team A, not team B. A dirty A → B → A switch keeps the commit and the draft, and team B receives neither. |
|
No new findings I’m confident are real in the scoped diff. All earlier findings are resolved:
The PR description documents sufficient testing: a successful build and representative compiled-CLI E2E runs. Its older commit reference is non-blocking under the review rules. I only read the scoped diff; I did not run, build, or install anything from the PR. |
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Approving at 50fcefd. This supersedes our earlier request for changes.
Checked with this head merged onto current main (841beed):
tsc, lint and the build are clean. The full unit suite passes (15440).- With
TMPDIRunset on macOS, these e2e files pass 81/81:self-mode-worktrees-808(29/29, matchingmain),init-replace-worktrees-1320,init-project-allandworkspace-children-913. branch-worktree-retire.test.tspasses 9/9, also withTMPDIRset to a symlink.
The two last points are fixed. Item 13 queues a fresh team A note before switching to team B and still checks that it is set aside. A failed worktree repair now names the preserved clone and the repair command, and the warning prints before repair runs. On earlier heads I also ran the risky paths with the built CLI:
- A push rejected during retirement keeps the learnings checkout at its exact HEAD, and the retry pushes it to team A, not team B.
- A dirty A → B → A switch keeps the commit and the draft, and team B receives neither.
[P3 nit] The last check in item 13, not.toContain('team-a-note-') against team B's log, does not match the new team-a-switch-note-… file. 'team-a-' would cover both notes.
The CI review at this head reports no new findings and calls the earlier ones resolved. We agree now. On earlier heads it did not run the #808 e2e file, which was red until this push.
Summary
Retire the old team repository's
teamai-reportsandteamai-learningsworktrees beforeinitreplaces its clone. This prevents the new clone from inheriting orphaned sibling checkouts that block member registration and later pulls.The replacement path now:
Both
--forceand the interactive confirmation flow use the same replacement logic.Type of Change
Test Plan
Verification at
d8f810ed:npx vitest run src/__tests__/branch-worktree-retire.test.ts— 9 passednpm run test:e2e -- src/__tests__/e2e/init-replace-worktrees-1320.test.ts— 2 passed using the compiled CLI and local Git remotesnpx tsc --noEmit— passednpm run lint— passednpm run build— passedgit diff --check— passedThe focused tests cover local-only, behind-origin, diverged, dirty, symlinked, unregistered, and lock-lifetime cases. The compiled-CLI E2E verifies both clean retirement and dirty-checkout preservation. The existing
self-mode-worktrees-808suite remains the regression gate for the mode-switch queue/config contracts.Related Issues
Fixes #1320
Notes for Reviewers
Please pay particular attention to the handoff order: retirement/preservation is proven first, the previous install's queue and config are then settled while both writer locks remain held, and only then is the old clone removed or relocated.