From github: claude/card-277-prevent-branch-rewind-7izyen (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.
Commits:
A card branch is never rewound to its base while its PR is open
Both lost PRs (#249 on time/card-244, #212 on time/card-211) had heads
that were commits already in main. Reproduced each suspect against a real
bare repo and the offline forge:
Hands.__init__ -> git_commit(branch, {}, base=base) is the one that
rewinds. With the branch missing from the forge, it cloned the base,
cut the branch fresh and pushed it: the push creates the ref, so the
open PR's head became main's tip (its squash of #248) and its diff went
empty. Against the pre-fix code this reproduces exactly, tip for tip.
The rebase retry the card asked about does not land a base-only tip:
with the remote branch present the first push is rejected and pull --rebase replays our base commit on top of the branch, which
keeps the work but puts a base commit on a card branch with no merge.
Both paths are gone now.
housekeeping.step_branches is how the branch went missing. With pulls?state=open answered null (Gitea does that for other empty
lists) an open PR's branch that also had an earlier merged PR from the
same head is deleted; the pre-fix step deletes time/card-244.
update_branch / update_branch_of never rewound: it clones the
branch only and pushes fast-forward, so a missing branch was a bare
clone error (an error beat, then automatic retries).
The fix:
git_commit takes create. A branch that will not clone is checked
with ls-remote: absent and create=True cuts it from the base as
before; absent and create=False raises BranchGone; present (a
transient clone failure) or an ls-remote that itself failed raises, so
base-only history is never pushed over a card branch. A branch that
comes back mid-create makes the push rejection an error instead of a
rebase of the base over work Time has not seen.
Hands resumes with create=False. If the branch is gone and a PR
still points at it, the card is parked (parked label, a beat the daily
sweep lists, one andon issue) with what to do: push the branch back
from the PR's own history, then /time retry. Only a branch with no
open PR is cut from the base: a card's first run. Writes after that
never create it either, so a branch deleted under a running card parks
it too.
update_branch raises BranchGone for a missing branch, so that path
parks the card instead of retrying into the same wall.
step_branches deletes a branch only when its tip is the very commit
its merged PR landed, whatever the open-PR list says. A branch holding
anything the default branch lacks is kept.
Tests: test_time.py 277 against a real bare repo (resume moves nothing;
gone branch under an open PR raises with the PR number and pushes nothing;
a mid-run write the same; update_branch the same; a first run still cuts
from the base; a branch that comes back mid-create; an unreachable forge
is an error, not a missing branch) plus a whole beat through the fake
forge (parked beat, label, cord pulled once, no worker run, nothing said
twice). test_housekeeping.py has the soft-empty open-PR page keeping time/card-244, and the branch still on its merged commit still swept.
Both suites and test_seed.py pass.
sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.
From github: `claude/card-277-prevent-branch-rewind-7izyen` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.
Commits:
A card branch is never rewound to its base while its PR is open
Both lost PRs (#249 on time/card-244, #212 on time/card-211) had heads
that were commits already in main. Reproduced each suspect against a real
bare repo and the offline forge:
1. `Hands.__init__` -> `git_commit(branch, {}, base=base)` is the one that
rewinds. With the branch missing from the forge, it cloned the base,
cut the branch fresh and pushed it: the push creates the ref, so the
open PR's head became main's tip (its squash of #248) and its diff went
empty. Against the pre-fix code this reproduces exactly, tip for tip.
The rebase retry the card asked about does not land a base-only tip:
with the remote branch present the first push is rejected and
`pull --rebase` replays our base commit on top of the branch, which
keeps the work but puts a base commit on a card branch with no merge.
Both paths are gone now.
2. `housekeeping.step_branches` is how the branch went missing. With
`pulls?state=open` answered `null` (Gitea does that for other empty
lists) an open PR's branch that also had an earlier merged PR from the
same head is deleted; the pre-fix step deletes `time/card-244`.
3. `update_branch` / `update_branch_of` never rewound: it clones the
branch only and pushes fast-forward, so a missing branch was a bare
clone error (an `error` beat, then automatic retries).
The fix:
- `git_commit` takes `create`. A branch that will not clone is checked
with `ls-remote`: absent and `create=True` cuts it from the base as
before; absent and `create=False` raises `BranchGone`; present (a
transient clone failure) or an ls-remote that itself failed raises, so
base-only history is never pushed over a card branch. A branch that
comes back mid-create makes the push rejection an error instead of a
rebase of the base over work Time has not seen.
- `Hands` resumes with `create=False`. If the branch is gone and a PR
still points at it, the card is parked (`parked` label, a beat the daily
sweep lists, one `andon` issue) with what to do: push the branch back
from the PR's own history, then `/time retry`. Only a branch with no
open PR is cut from the base: a card's first run. Writes after that
never create it either, so a branch deleted under a running card parks
it too.
- `update_branch` raises `BranchGone` for a missing branch, so that path
parks the card instead of retrying into the same wall.
- `step_branches` deletes a branch only when its tip is the very commit
its merged PR landed, whatever the open-PR list says. A branch holding
anything the default branch lacks is kept.
Tests: `test_time.py` 277 against a real bare repo (resume moves nothing;
gone branch under an open PR raises with the PR number and pushes nothing;
a mid-run write the same; `update_branch` the same; a first run still cuts
from the base; a branch that comes back mid-create; an unreachable forge
is an error, not a missing branch) plus a whole beat through the fake
forge (parked beat, label, cord pulled once, no worker run, nothing said
twice). `test_housekeeping.py` has the soft-empty open-PR page keeping
`time/card-244`, and the branch still on its merged commit still swept.
Both suites and `test_seed.py` pass.
Closes #277
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4EtGmuYnPemB9eaWd
---
<sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
Both lost PRs (#249 on time/card-244, #212 on time/card-211) had heads
that were commits already in main. Reproduced each suspect against a real
bare repo and the offline forge:
1. `Hands.__init__` -> `git_commit(branch, {}, base=base)` is the one that
rewinds. With the branch missing from the forge, it cloned the base,
cut the branch fresh and pushed it: the push creates the ref, so the
open PR's head became main's tip (its squash of #248) and its diff went
empty. Against the pre-fix code this reproduces exactly, tip for tip.
The rebase retry the card asked about does not land a base-only tip:
with the remote branch present the first push is rejected and
`pull --rebase` replays our base commit on top of the branch, which
keeps the work but puts a base commit on a card branch with no merge.
Both paths are gone now.
2. `housekeeping.step_branches` is how the branch went missing. With
`pulls?state=open` answered `null` (Gitea does that for other empty
lists) an open PR's branch that also had an earlier merged PR from the
same head is deleted; the pre-fix step deletes `time/card-244`.
3. `update_branch` / `update_branch_of` never rewound: it clones the
branch only and pushes fast-forward, so a missing branch was a bare
clone error (an `error` beat, then automatic retries).
The fix:
- `git_commit` takes `create`. A branch that will not clone is checked
with `ls-remote`: absent and `create=True` cuts it from the base as
before; absent and `create=False` raises `BranchGone`; present (a
transient clone failure) or an ls-remote that itself failed raises, so
base-only history is never pushed over a card branch. A branch that
comes back mid-create makes the push rejection an error instead of a
rebase of the base over work Time has not seen.
- `Hands` resumes with `create=False`. If the branch is gone and a PR
still points at it, the card is parked (`parked` label, a beat the daily
sweep lists, one `andon` issue) with what to do: push the branch back
from the PR's own history, then `/time retry`. Only a branch with no
open PR is cut from the base: a card's first run. Writes after that
never create it either, so a branch deleted under a running card parks
it too.
- `update_branch` raises `BranchGone` for a missing branch, so that path
parks the card instead of retrying into the same wall.
- `step_branches` deletes a branch only when its tip is the very commit
its merged PR landed, whatever the open-PR list says. A branch holding
anything the default branch lacks is kept.
Tests: `test_time.py` 277 against a real bare repo (resume moves nothing;
gone branch under an open PR raises with the PR number and pushes nothing;
a mid-run write the same; `update_branch` the same; a first run still cuts
from the base; a branch that comes back mid-create; an unreachable forge
is an error, not a missing branch) plus a whole beat through the fake
forge (parked beat, label, cord pulled once, no worker run, nothing said
twice). `test_housekeeping.py` has the soft-empty open-PR page keeping
`time/card-244`, and the branch still on its merged commit still swept.
Both suites and `test_seed.py` pass.
Closes#277
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4EtGmuYnPemB9eaWd
test_time.py failed: a cut of ours (0.2985ms)
✔ memberships are read without a key only in the sense that the member is never asked (0.724833ms)
✔ a Whop error carries its status instead of being swallowed (0.758916ms)
ℹ tests 83
ℹ suites 0
ℹ pass 83
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 175.969584
— steward (Claude, reviewing directly)
Held: merged onto current `main` the suite fails.
```
test_time.py failed: a cut of ours (0.2985ms)
✔ memberships are read without a key only in the sense that the member is never asked (0.724833ms)
✔ a Whop error carries its status instead of being swallowed (0.758916ms)
ℹ tests 83
ℹ suites 0
ℹ pass 83
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 175.969584
```
— steward (Claude, reviewing directly)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From github:
claude/card-277-prevent-branch-rewind-7izyen(git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.Commits:
A card branch is never rewound to its base while its PR is open
Both lost PRs (#249 on time/card-244, #212 on time/card-211) had heads
that were commits already in main. Reproduced each suspect against a real
bare repo and the offline forge:
Hands.__init__->git_commit(branch, {}, base=base)is the one thatrewinds. With the branch missing from the forge, it cloned the base,
cut the branch fresh and pushed it: the push creates the ref, so the
open PR's head became main's tip (its squash of #248) and its diff went
empty. Against the pre-fix code this reproduces exactly, tip for tip.
The rebase retry the card asked about does not land a base-only tip:
with the remote branch present the first push is rejected and
pull --rebasereplays our base commit on top of the branch, whichkeeps the work but puts a base commit on a card branch with no merge.
Both paths are gone now.
housekeeping.step_branchesis how the branch went missing. Withpulls?state=openanswerednull(Gitea does that for other emptylists) an open PR's branch that also had an earlier merged PR from the
same head is deleted; the pre-fix step deletes
time/card-244.update_branch/update_branch_ofnever rewound: it clones thebranch only and pushes fast-forward, so a missing branch was a bare
clone error (an
errorbeat, then automatic retries).The fix:
git_committakescreate. A branch that will not clone is checkedwith
ls-remote: absent andcreate=Truecuts it from the base asbefore; absent and
create=FalseraisesBranchGone; present (atransient clone failure) or an ls-remote that itself failed raises, so
base-only history is never pushed over a card branch. A branch that
comes back mid-create makes the push rejection an error instead of a
rebase of the base over work Time has not seen.
Handsresumes withcreate=False. If the branch is gone and a PRstill points at it, the card is parked (
parkedlabel, a beat the dailysweep lists, one
andonissue) with what to do: push the branch backfrom the PR's own history, then
/time retry. Only a branch with noopen PR is cut from the base: a card's first run. Writes after that
never create it either, so a branch deleted under a running card parks
it too.
update_branchraisesBranchGonefor a missing branch, so that pathparks the card instead of retrying into the same wall.
step_branchesdeletes a branch only when its tip is the very commitits merged PR landed, whatever the open-PR list says. A branch holding
anything the default branch lacks is kept.
Tests:
test_time.py277 against a real bare repo (resume moves nothing;gone branch under an open PR raises with the PR number and pushes nothing;
a mid-run write the same;
update_branchthe same; a first run still cutsfrom the base; a branch that comes back mid-create; an unreachable forge
is an error, not a missing branch) plus a whole beat through the fake
forge (parked beat, label, cord pulled once, no worker run, nothing said
twice).
test_housekeeping.pyhas the soft-empty open-PR page keepingtime/card-244, and the branch still on its merged commit still swept.Both suites and
test_seed.pypass.Closes #277
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv4EtGmuYnPemB9eaWd
sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.
Held: merged onto current
mainthe suite fails.— steward (Claude, reviewing directly)
Merging: merged onto current
main,test_time.py,test_seed.pyandtest_housekeeping.pypass.— steward (Claude, reviewing directly)