Fix Trail Command Behavior and API Alignment

Investigate every "entire trail" command whether it's functional. Is there anything missing/broken, any bugs?
I inspected the full entire trail command tree and ran the relevant tests.
Commands present:
entire trailentire trail listentire trail createentire trail updateentire trail watchentire trail findingentire trail finding list|add|show|apply|resolve|dismiss|reopen|watch
Validation run:
go test ./cmd/entire/cli -run 'Trail|trail'✅go test ./cmd/entire/cli/...✅- Help output renders for the command tree ✅
I did not live-hit the real API, so this is code/test investigation.
Findings:
-
entire trailis hidden from top-level helpcmd/entire/cli/trail_cmd.go:45- Direct invocation works, but discoverability is intentionally/accidentally disabled.
-
trail createmutates git before auth/API succeedscmd/entire/cli/trail_cmd.go:508+- It may create a local branch and push it before
runAuthenticatedDataAPI. - If the user is not logged in or the API rejects the request, the repo may already be changed.
-
trail create/trail updatesilently accept extra positional args- They do not set
Args: cobra.NoArgs. - Example:
entire trail create junk --title xwould ignorejunk.
- They do not set
-
trail create --statusis not locally validatedtrail updatevalidates status;trail createsends invalid statuses to the server.
-
Trail lookup only searches the first 200 trails
cmd/entire/cli/trail_cmd.go:895- Affects: default
trail,update,watch,finding, selectors by branch/number/id. - Code comment already acknowledges trails beyond 200 are invisible.
-
trail update --body ""cannot clear a bodybuildTrailUpdateRequestonly sends body/title if non-empty.- Interactive update also cannot clear body/title cleanly because empty means “do not send”.
-
Finding filters are weakly validated
trail finding/list --status,--severity,--staleare mostly passed through to the API.- Invalid values are not caught locally.
--include-dismissedis potentially misleading because default--status openmay still exclude dismissed unless combined with--status any.
-
SSE watch treats clean EOF as
io.ErrUnexpectedEOFcmd/entire/cli/trail_watch_cmd.go:378- Probably okay if the server always sends
event: reconnect, but--oncemay fail on a clean close without a reconnect frame.
Overall: the command tree is wired and tests pass, but there are real functional/UX bugs around create side effects, lookup pagination, update clearing fields, arg/status validation, and watch EOF handling.
Implemented fixes for items 2–8.
Changed:
-
trail create- Now validates status locally.
- Defaults empty status to
draft. - Rejects unexpected positional args.
- Creates the remote/API trail before local branch creation/push, so auth/API failures don’t leave local mutations.
-
trail update- Rejects unexpected positional args.
- Can now send explicit empty
--body ""/--title ""values instead of treating them as “unchanged”.
-
Trail lookup
findTrailnow paginates through all trail-list pages instead of only searching the first 200.
-
trail findingfilters- Added local validation for status/severity/stale/limit/offset.
- Normalizes comma-separated filters.
--include-dismissednow broadens the default open-only filter unless--statuswas explicitly set.
-
SSE watch
- Clean EOF now returns a clean
streamCloseDoneinstead ofio.ErrUnexpectedEOF.
- Clean EOF now returns a clean
Added/updated tests covering pagination, update clearing, arg rejection, filter validation, include-dismissed behavior, and clean SSE EOF.
Validation:
go test ./cmd/entire/cli -run 'Trail|trail'✅go test ./cmd/entire/cli/...✅
- "title" is a required field though, without a title the trail is completely useless
- there's also a new API field for "phase" which we should leverage as well. It's basically showing if a trails has or has no code.
Done.
Additional fixes:
-
Title is now enforced as required:
trail createerrors if the resolved title is empty.trail update --title ""is rejected.- Body can still be cleared with
--body "".
-
Added API/model support for trail
phase:api.TrailResourceapi.TrailCreateRequestapi.TrailUpdateRequesttrail.Metadata
-
Surfaced phase in CLI:
entire traildetail view printsPhase.entire trail listshows aPHASEcolumn when any listed trail has phase data.entire trail create --phase <value>entire trail update --phase <value>- Interactive update includes phase input.
-
Added tests for phase propagation/display and required-title validation.
Validation:
go test ./cmd/entire/cli/...✅
My colleague told me that e.g. "entire trail" showing information on the CLI breaks the pattern of other commands. What would you recommend doing though?
I’d recommend making the data-fetching behavior explicit.
Best shape:
entire trail→ prints help/overview onlyentire trail show [<trail>]→ shows one trail- default: current branch’s trail
- selector: number, id, or branch
entire trail list→ lists trails- No implicit “show current, else list” fallback at the root
Why:
- It matches normal CLI command patterns: noun root = namespace/help, verb subcommands do work.
- It avoids surprising API calls from just typing
entire trail. - It avoids ambiguous behavior where the same command sometimes shows details and sometimes lists.
- It gives agents/users a deterministic command:
entire trail show.
Migration path I’d use:
- Add
entire trail show. - Change injected agent hint to:
Use entire trail show/list/create/update/watch... - For one release, keep
entire trailas a compatibility alias forshow, but print a short stderr notice:entire trail now maps to 'entire trail show'; use that explicitly. - Later, make root print help only.
I would not keep the “if no current trail, list recent trails” behavior for show; I’d make it say:
No trail found for branch "x". Run 'entire trail create' or 'entire trail list --status any'.
That’s clearer and easier to script around.
Agreed. I implemented that shape:
entire trailnow prints help/overview only; no API/auth call.- Added
entire trail show [<trail>].- No selector: current branch’s trail.
- Selector: number, id, or branch.
- Removed implicit root behavior of “show current trail, else list”.
- Did not change agent hint.
- No migration alias/warning.
Validation:
go test ./cmd/entire/cli -run 'Trail|trail'✅go test ./cmd/entire/cli/...✅
Done.
- Branch:
fix-trail-command-behavior - Commit:
a5e8ef66c—Fix trail command behavior - Pushed to:
origin/fix-trail-command-behavior - Trail created:
- Number:
579 - ID:
019ed585-7b97-745b-9b55-aaf146031970 - Status:
open - Phase:
building
- Number:
PR URL:
https://github.com/entireio/cli/pull/new/fix-trail-command-behavior
Done — I documented the changes on the trail.
Note: entire trail update --body currently does not persist to the API’s body_document; the trail body remained empty after update attempts. I added the full documentation as a trail discussion comment instead so it’s recorded on the trail.
Investigated. The body does not persist because the server no longer treats body as a PATCH-able trail field.
Evidence:
PATCH /api/v1/trails/gh/entireio/cli/579with only{"body": "..."}returns:400 {"error":"At least one field is required"}
PATCHwith{"title": "...", "body": "..."}succeeds, but only title/metadata changes; body remains empty.GET /api/v1/trails/gh/entireio/cli/579returns body as:trail.body_document.text_snapshot- not
trail.body
- The web app updates the trail body through a collaborative document websocket:
/api/v1/trails/gh/{org}/{repo}/{number}/body/ws
So the CLI’s current TrailUpdateRequest.Body is stale API shape. It serializes body, but the server ignores/rejects it because trail body is now body_document.
Recommendation:
- Do not claim
entire trail update --bodyworks until we implement the document API/websocket path. - Either:
- remove/disable
--bodywith a clear error, or - implement proper body-document updates against the server-supported mechanism.
- remove/disable
- Also parse
body_document.text_snapshotin trail detail responses so CLI display can show existing web-authored body text.
For now, I documented the changes in a trail discussion comment because that endpoint does persist.
Print me the body here, I'll update in the UI and investigate. We don't do anything, it's apparently an API problem.
No.
Our commit only touched trail-related files:
cmd/entire/cli/api/trail_types.gocmd/entire/cli/api/trail_types_test.gocmd/entire/cli/trail/trail.gocmd/entire/cli/trail_cmd.gocmd/entire/cli/trail_cmd_test.gocmd/entire/cli/trail_review_cmd.gocmd/entire/cli/trail_review_cmd_test.gocmd/entire/cli/trail_watch_cmd.gocmd/entire/cli/trail_watch_cmd_test.go
No line attribution/blame files were touched. Also working tree is clean.
Reviewed again. No changes made.
Main concerns before merge:
-
trail createnew-branch flow may now be brokencmd/entire/cli/trail_cmd.go:625-680- We now call the API before creating/pushing the local branch.
- Original code pushed first because “API can reference it”. If the API requires the branch to exist remotely,
entire trail create --branch new-branchwill fail.
-
SSE watch may stop instead of reconnecting
cmd/entire/cli/trail_watch_cmd.go:378- Clean EOF now returns
streamCloseDone, which makes normal watch exit. For--oncethat’s good; for continuous watch, a proxy/server clean-close withoutevent: reconnectshould probably reconnect.
-
--include-dismissedmay include too muchcmd/entire/cli/trail_review_cmd.go:539-544- It changes default
status=opentostatus=any, so resolved findings are included too. If the intent is “open + dismissed”, this is too broad.
-
Trail body support is still stale
cmd/entire/cli/trail_cmd.go:908-910--bodystill serializesbody, but API currently usesbody_document. So body update can fail or silently not persist depending on other fields.- Tests around body clearing are only unit-level and don’t reflect actual API behavior.
-
Phase may be server-derived, not user-editable
cmd/entire/cli/trail_cmd.go:560,cmd/entire/cli/trail_cmd.go:810-812- Since phase represents whether the trail has code, exposing
--phaseon create/update may be wrong if the server computes it from branch/code state. Display support is good; mutation support is questionable.
Everything else looks consistent with the intended root/help + explicit show command shape.
Reviewed and live-tested against the current API. No code changes made.
Findings:
-
Bare
entire trailbehavior is good- Prints help only.
- No auth/API call.
-
entire trail showworks, but does not display body- Body now persists in API as
body_document.text_snapshot. - CLI
showresolves via list endpoint, which returnsbody: "", notbody_document. - Result: persisted body is invisible in CLI.
- Body now persists in API as
-
trail update --bodyis still partially broken--bodyalone fails:API error: At least one field is required
--title ... --body ...succeeds and body persists.- So API accepts/processes body only when another recognized PATCH field is present.
-
trail createfor a new branch is broken- Current code creates the API trail before creating/pushing the branch.
- Live test with a non-existing branch failed with API
500. - Creating/pushing the branch first, then running
entire trail create, works.
-
--phasemutation does not worktrail update --phase buildingfails if it’s the only field.trail update --title ... --phase buildingsucceeds but phase remains unchanged.trail create --phase buildingalso produced a trail withphase: null.- So phase appears read-only/server-derived from the API side. Display support is fine; create/update flags are misleading.
-
Finding filter validation works
- Invalid
--status,--severity,--stalefail locally as expected. --include-dismissedcommand runs.
- Invalid
-
Watch connects
entire trail watch 579 --once --show-pingsconnected and streamed events.- I didn’t wait for server close/reconnect behavior.
-
Tests still pass
go test ./cmd/entire/cli/...✅
Main things I’d fix before merge:
- Restore local branch create/push before API trail create, or server must support non-existing branches.
- Remove/disable
--phasemutation unless API supports it. - Fix body update semantics/API, and make
showfetch/displaybody_document.text_snapshot.
Fixed the confirmed findings and pushed.
What changed:
-
Restored
trail createbranch workflow:- Auth/remote resolution happens first.
- Then local branch creation + push happens before API trail creation.
- Live-tested with a new branch; it now succeeds.
-
Removed misleading phase mutation support:
- Removed
trail create --phase. - Removed
trail update --phase. - Removed phase from create/update request bodies.
- Kept phase parsing/display from API responses.
- Removed
-
Left body behavior alone, per your note that you’re investigating API/platform side.
Also: I noticed the remote branch had a merge from main that my first force-push would have overwritten. I restored that history and pushed the corrected branch with the merge preserved.
Validation:
- Live
entire trail createnew branch test ✅ go test ./cmd/entire/cli/...✅- Branch pushed:
fix-trail-command-behavior - Current tip:
ad9b4cd7d
Validated successfully.
Checks run:
go test ./cmd/entire/cli/...✅entire trailroot prints help only, no data/auth output ✅entire trail create/update --helpno longer expose--phase✅entire trail show fix-trail-command-behaviorworks and displays phase from API ✅entire trail list --jsonincludes the current trail and phase ✅- Live
entire trail createwith a new branch:- created local branch ✅
- pushed remote branch ✅
- created trail ✅
- body persisted into API
body_document.text_snapshoton create ✅ - cleaned up validation trail and branch afterward ✅
entire trail update --title ""rejects empty title ✅entire trail update --status noperejects invalid status ✅entire trail finding list --status noperejects invalid finding filter ✅entire trail finding list --include-dismissedworks ✅entire trail watch 579 --once --show-pingsconnects and streams events ✅
Final repo state:
- Working tree clean ✅
- Branch tracks
origin/fix-trail-command-behavior✅
Entire Trail review comment ID: 019ed621-7c22-7be1-9d23-c3f25814acef URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:632-647 Selected context:
Branch creation and push now happen inside the API callback but before the trail create API request. If the API request fails after the branch is pushed (network error, server validation, etc.), the user is left with an orphaned branch on origin with no corresponding trail record. The original code avoided this by creating/pushing the branch before entering the API context, establishing a clear separation between local git ops and remote API ops.
Fixed the review finding and pushed.
What changed:
- Removed branch creation/push from the API callback.
trail createnow:- Authenticates / constructs API client.
- Resolves trail remote.
- Creates local branch if needed.
- Pushes branch if needed.
- Creates the trail via API.
- If push fails after creating a local branch, it cleans up the local branch.
- If the trail API create fails after pushing a newly-created branch, it best-effort deletes the pushed remote branch and local branch to avoid orphan branches.
Validation:
- Live create test with new branch ✅
- Cleanup of test branch/trail ✅
go test ./cmd/entire/cli/...✅- Working tree clean ✅
- Pushed to PR branch:
eb8b098ab
Entire Trail review comment ID: 019ed62e-2634-7639-a22e-439894fb2669 URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:673 Selected context:
When api.DecodeJSON fails after a successful trail creation API call, cleanupCreatedTrailBranch is not called. This leaves orphaned local and remote branches when JSON decoding fails, causing data inconsistency. Add cleanupCreatedTrailBranch(repo, branch, localBranchCreated, remoteBranchPushed, errW) before returning the error.
Fixed that review comment.
Change:
- Added
cleanupCreatedTrailBranch(...)before returning theapi.DecodeJSONerror intrail create.
Validation:
go test ./cmd/entire/cli -run 'Trail|trail'✅go test ./cmd/entire/cli/...✅- Pushed updated branch:
df1921f81
Remote checks have started and are currently queued/in progress.
Address the PR review comments if applicable and resolve them https://github.com/entireio/cli/pull/1447
Entire Trail review comment ID: 019ed638-fca2-7722-b3a3-e9f1b7b0f046 URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:648 Selected context:
The cleanup call on line 648 uses remoteBranchPushed which is still false at this point (it's only set to true on line 651). If pushBranchToOrigin fails, the remote branch may have been partially pushed but deleteBranchFromOrigin won't be called during cleanup. This can leave orphaned remote branches. Set remoteBranchPushed = true before calling pushBranchToOrigin, or handle partial push failures differently.
Entire Trail review comment ID: 019ed63e-9746-7d91-a829-17dc9c3d386b URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:648 Selected context:
remoteBranchPushed is set to true before pushBranchToOrigin() is called. If the push fails, cleanupCreatedTrailBranch() will incorrectly attempt to delete a remote branch that was never successfully pushed. Move this line after the successful push (after line 650).
Entire Trail review comment ID: 019ed644-d496-7cde-aff3-69677df002fd URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:648 Selected context:
The cleanupCreatedTrailBranch call passes hardcoded true for remotePushed parameter when the push failed, causing it to attempt deleting a remote branch that was never successfully pushed. This should pass false instead, or use the remoteBranchPushed variable (which is false at this point).
Entire Trail review comment ID: 019ed64b-01ea-787a-9b24-cb5cbec7c96d URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: medium Context: cmd/entire/cli/trail_cmd.go:649 Selected context:
When pushBranchToOrigin fails but branchExistsOnOrigin returns true (branch exists remotely), the cleanup deletes the remote branch. This could destroy a branch that existed from a previous attempt or was pushed despite the error. Consider only cleaning up the remote branch if we can confirm we just created it in this invocation, or skip remote cleanup when the push command itself fails.
Entire Trail review comment ID: 019ed64b-038a-73a5-b07e-f04986d07540 URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: low Context: cmd/entire/cli/trail_cmd.go:1155 Selected context:
The cleanup function deletes remote branch before local branch. If remote deletion succeeds but local deletion fails, branches are in an inconsistent state with remote gone but local remains. Consider reversing the order (local first, then remote) so the less reversible operation happens last.
Entire Trail review comment ID: 019ed64e-81bc-73fd-9d8a-edc9358aa5ad URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: medium Context: cmd/entire/cli/trail_cmd.go:1024-1067 Selected context:
The pagination loop in findTrail can run indefinitely if the server returns Total == 0 (unpaginated mode) and keeps serving different full-length pages on subsequent offset requests. The signature-based loop detection only catches identical repeated pages. Add a max-iterations safeguard (e.g., 100 pages = 20,000 trails) to prevent runaway loops from server bugs.
Entire Trail review comment ID: 019ed64e-835d-77f9-8f28-790c5d66dcee URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: low Context: cmd/entire/cli/trail_cmd.go:1156 Selected context:
If createBranch creates a branch that the user somehow ends up on before the cleanup runs (unlikely but possible in concurrent scenarios), RemoveReference will silently fail. Consider checking if the current HEAD points to the branch before attempting deletion, or accept that the warning message is sufficient for this edge case.
Entire Trail review comment ID: 019ed656-3981-7196-83ea-e4d98cad4a46 URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:1067 Selected context:
The findTrail pagination logic can enter an infinite loop when listResp.Total == 0 and the server keeps returning full pages (200 trails) where the signature changes on every request (e.g., due to concurrent updates or timestamp-based ordering). The loop only breaks on signature match, but if trails are being updated frequently, trailListPageSignature may never match previousPageSignature, causing the loop to hit trailFindMaxPages (100 requests). This could cause 100 unnecessary API calls and significant delays. Consider also breaking when offset > 0 && pageLen == trailListServerMaxLimit && listResp.Total == 0 to limit pagination when the server doesn't provide totals.
That was just me polling GitHub Actions until the PR checks finished.
The script:
- calls
gh pr view 1447 --json statusCheckRollup - prints pending/failed counts
- sleeps 15 seconds between polls
- stops when all checks are complete or if one fails
The timeout=1800 is the tool-level max runtime: 1800 seconds / 30 minutes, so the polling command doesn’t run forever if GitHub Actions hangs. It doesn’t affect the repo or PR; it only limits my local wait loop.
Understood — I’ll stop force-pushing.
Current state:
- Working tree clean.
- Branch is synced with
origin/fix-trail-command-behavior. - All GitHub PR review threads are resolved.
- Latest PR head:
1a6c8a505 - Remote checks:
- lint ✅
- license ✅
- binary-size ✅
- tests still in progress
I won’t push again unless there’s a new actual code change required.
Yes — entire trail finding has been addressed.
Verified:
- Command surface uses
findingwith subcommands:list,show,add,apply,resolve,dismiss,reopen,watch. - Removed/old review naming is covered by tests.
- Filter validation is in place:
- invalid status rejects with clear error.
- severity/stale/limit/offset normalization is tested.
--include-dismissedbehavior is implemented: when no explicit status is provided, it changes defaultopenfiltering toany.- Relevant tests pass:
go test ./cmd/entire/cli -run 'TrailCommandSurfaceUsesFindings|NormalizeTrailReviewListOptions|Invalid|TrailFinding|trail finding'
Also checked PR status:
- All GitHub checks are green on current head
1a6c8a505. - Working tree clean.
- No force-push performed after your instruction.
Resolved the open entire trail finding.
Verified:
entire trail finding list 579 --status open→No findings found.- Working tree clean.
Merged latest origin/main into fix-trail-command-behavior and pushed normally.
Validation:
go test ./cmd/entire/cli/...✅- No force-push used.
- New branch tip:
60792ef1c
Entire Trail review comment ID: 019ed668-ba03-743f-a47b-ce52ccda1ba6 URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: high Context: cmd/entire/cli/trail_cmd.go:138 Selected context:
findTrailBySelector is called in runTrailShow but is never defined in this file. This will cause a compile-time error or runtime panic. The function needs to be implemented to handle trail selectors (number, ID, or branch name).
Entire Trail review comment ID: 019ed668-bba4-73a9-8dc4-93c8351bf91d URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: medium Context: cmd/entire/cli/trail_cmd.go:1181 Selected context:
The cleanup logic in cleanupCreatedTrailBranch will skip remote deletion if local deletion fails, but it only logs warnings. If the local branch is currently checked out (preventing local deletion), the remote branch remains orphaned with no way for the user to recover except manual git push --delete. Consider returning an error or providing clearer guidance when cleanup cannot complete.
Entire Trail review comment ID: 019ed668-bd40-7457-b758-25179292500a URL: https://entire.io/gh/entireio/cli/trails/579/fix-trail-command-behavior Severity: low Context: cmd/entire/cli/trail_cmd.go:1057 Selected context:
When Total == 0 and offset > 0, pagination stops after only 2 pages (first full page + one more). This means findTrail may miss trails beyond position 400 when the API doesn't provide a total count. If the server consistently omits Total, older trails become invisible to branch/number/ID lookups.
Addressed and pushed normally (no force push).
Changes:
- Moved
findTrailBySelectorintotrail_cmd.gonear the other trail lookup helpers. - Updated
findTrailpagination:- no longer stops after only 2 pages when
Total == 0 - still detects repeated pages
- bounded to 10 pages to avoid runaway loops
- no longer stops after only 2 pages when
- Improved cleanup warnings with explicit manual recovery commands.
Validation:
go test ./cmd/entire/cli/...✅- GitHub review threads: none unresolved ✅
- Trail open findings: none open ✅
- Remote checks are currently running on
271673602; binary-size already passed.
Double check if this is a reasonable comment: 1. (Medium) cleanupCreatedTrailBranch can delete a remote branch the command didn't create
needsCreation is decided purely from local branch presence (branchNeedsCreation → repo.Reference, local only). But if a branch exists on the remote yet not locally (e.g. a teammate pushed it, never fetched), trail create --branch that-branch will push -u (a no-op/fast-forward), set remoteBranchPushed = true, and then on an API-create failure run git push --delete origin that-branch — destroying a remote branch the user didn't create.
Realistically narrow (the user named the branch explicitly and API-create usually succeeds), but it's irreversible remote-state deletion gated on a local-only signal. Consider only setting remoteBranchPushed when the push actually created the ref, or checking remote existence before deciding the branch is ours to delete.
Yes — it’s a reasonable and technically accurate comment. I traced the code paths:
The core claim holds.
branchNeedsCreationonly checksrefs/heads/<branch>locally (repo.Reference(NewBranchReferenceName, true)). A branch that exists only on the remote (never fetched, no remote-tracking ref) →needsCreation == true.- We then
createBranchat local HEAD andpushBranchToOriginwithgit push -u origin <branch>. - On success
remoteBranchPushed = true, and a later API-create failure runsgit push --delete origin <branch>.
So the "is this branch ours to delete?" decision is indeed gated on a local-only signal, and the deletion is irreversible remote state. That part is correct.
The danger is real but narrower than "push is a no-op/fast-forward" implies:
- Identical commit (remote == local HEAD): push is a no-op, succeeds →
remoteBranchPushed=true→ cleanup deletes a branch we didn't create. ✅ data loss. - Remote behind local HEAD (fast-forward): push succeeds and advances the remote, then cleanup deletes it entirely. ✅ data loss (and we also moved it first).
- Remote diverged/ahead:
git pushis rejected (non-fast-forward) →pushBranchToOriginerrors → cleanup called withremotePushed=false→ no remote deletion. ✅ safe.
So the failure mode requires: remote-only branch + push that fast-forwards or is a no-op + API-create failure. The comment’s own "realistically narrow" caveat is fair.
Severity: Medium is defensible because the outcome is irreversible remote deletion, though given the conjunction of conditions, low would also be reasonable.
Suggested fix is sound. The cleanest guard is to check remote existence before pushing (e.g. git ls-remote --heads origin <branch>); if it already exists remotely, it isn’t ours, so don’t set remoteBranchPushed for cleanup purposes. Note we previously had a branchExistsOnOrigin helper but removed it for an unrelated lint reason — a pre-push existence check would address this more directly than the post-failure check we removed.
One thing I’d push back on slightly: it overlaps conceptually with the earlier resolved finding about deleting a branch "from a previous attempt," but it’s a distinct trigger (teammate’s remote branch + local-only needsCreation), so it’s worth tracking separately rather than dismissing as a duplicate.
I haven’t changed any code, since you only asked for an assessment. Want me to implement the pre-push existence guard?
Fixed.
The comment was valid, so I added a pre-push guard:
- Before pushing in
trail create,branchExistsOnOrigin(branch)checksgit ls-remote --heads origin <branch>. remoteBranchPushedis now set to!existedOnOrigin, so cleanup only deletes the remote branch when our push is what introduced it.- If the existence check itself errors (e.g. network), we conservatively treat the branch as pre-existing and never delete it, emitting a warning.
This closes the data-loss path: a teammate’s remote-only branch can no longer be deleted by cleanup just because needsCreation was derived from local-only state.
Validation:
go build ./...✅go test ./cmd/entire/cli/...✅- Committed and pushed normally (no force push):
57a22a335