feat(git-tools): add guarded git_rebase tool #41
No reviewers
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck!41
Loading…
Reference in a new issue
No description provided.
Delete branch "40-add-a-guarded-git-rebase-tool"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a
git_rebasetool so agents can rebase their own (non-default) branch onto an updated base, resolve conflicts withcontinue, and reorder/reword commits through a two-step plan file inlocal://storage, without raw git. While a rebase is stopped or a plan is pending, history/branch-changing tools refuse and asession_stopextension sends the agent back whenever it tries to end its turn. After a completed rebase,git_push force_after_rebase:trueforce-pushes the branch once with a lease on the remote sha seen at rebase start. Commit identity/signing setup was extracted fromgit_commitfirst so both tools share it.Decisions
Posted in comment 16296 and comment 16298:
ontoand the expected order (old sha + expected subject), so the old→new mapping works acrosscontinuecalls and pairs by subject.droppedsha7s for commits git dropped as already upstream.startreturns its before-report (commits,upstreamGained) on both stopped and done outcomes.next.pickare rejected.forced?onGitPushResultlanded with the other protocol types in the git_rebase commit.git_commitpasses the gate but answers with its existing "already staged" guard; the test pins that.permissions-system-rule.test.tsneeded no edit (it asserts the constant).docs/git-tools.md: the default-branch sentence was corrected, and agit_rebasesection and a CHANGELOG entry were added.force_after_rebaseis refused when the remote branch moved after the rebase started.Verification
bun run typecheck: protocol, server and web clean.bun test src/bridge/git-tools/rebase-plan.test.ts: 13 pass (reorder, reword message, rejected verbs with line numbers, missing/duplicate/ambiguous/unknown shas, reword without message).bun test src/bridge/git-tools/rebase.test.ts: 12 pass (start onto new base; conflict stop → marker refusal → continue; plan/apply reorder + reword; rejected plan stays pending → abort; default branch / detached HEAD refused; gate refuses push/branch switch/fetch ff while stopped, lets branch list and git_commit through, releases after abort; stop guard; force_after_rebase once with bare remote, refused without marker and after the remote moved).bun testinapps/server: 797 pass, 0 fail.Closes #40
Decisions so far (git_rebase core):
ontoand the expected order. The plan's record shape (branch, remote, remoteSha, origHead) can't build the old→new mapping after acontinuein a later call, sorebase-start.jsonalso storesontoandorder(old full sha + expected subject, in new order; reword uses the new subject). The mapping pairsonto..HEADagainst that order by subject, so it stays correct when git drops a commit as already upstream.droppedin the done result. Old commits without a rewritten counterpart are listed asdropped(sha7s) rather than silently vanishing frommapping.startincludes its before-report in the result.commits(sha7 + subject) andupstreamGainedare returned on both stopped and done outcomes, which is where the brief's "what changes before/after" lands.nextcarries git's output instead of the conflict instructions.pickare rejected with their line number.forced?onGitPushResultlanded in the git_rebase commit with the rest of the protocol types;git_pushstarts setting it in the force-push commit.Decisions for the guards, force push and wiring:
git_commitduring a conflict. While stopped on a conflict, the conflicted path counts as staged, sogit_commitanswers with its existing "already staged" guard (include_staged:trueneeded). The gate passes it through as the plan wants; the test pins that the answer comes from git_commit, not the gate.git_commititself is unchanged.permissions-system-rule.test.tsneeded no edit: it asserts against theGIT_BASH_RULE_MESSAGEconstant, not a literal, so the new text flows through. Onlygit-bash-rule.test.tsgainedgit rebase origin/mainin DENY.docs/git-tools.mdsaid there is no default-branch guard; it now says a plaingit_pushhas none, whilegit_rebaseandforce_after_rebaserefuse the default branch. Also added a CHANGELOG entry and a### git_rebasesection.force_after_rebaseis refused with the stale-info message.@ -0,0 +35,4 @@base: string;/** Full shas in original order, oldest first. */commits: string[];sessionId: string | null;Why
| null? When would this be needed without a session?It wasn't needed. The
| nullonly mirrored the SDK'sgetSessionId?: () => string | null, and nothing ever read the field. RemovedsessionIdfrom the plan marker ina92af7b.@ -0,0 +102,4 @@const marker = await readPlanMarker(root, signal);if (marker?.planPath) await rm(marker.planPath, { force: true });await removeGitPath(root, PLAN_MARKER, signal);}Why do you need three separate sets of functions for each marker type that implement exactly the same things?
No good reason; they differed only in file name and type.
a92af7breplaces them withreadMarker/writeMarker/clearMarker, keyed by kind ("plan" | "start" | "force") through a kind → type map, so each call is still typed. The one real difference stays inclearMarker: clearing the plan marker also deletes the plan file.RebaseStartRecordnow holdsonto/orderdirectly, which removes the extending type and its cast inrebase.ts. git-tools tests: 153 pass; server typecheck clean.