fix(cli): watch's self-pull no longer abandons stashed local changes on pull failure - #1640
Merged
tamirdresher merged 1 commit intoAug 8, 2026
Conversation
…on pull failure Fixes bradygaster#1639 SelfPullCapability stashed uncommitted local changes before `git pull --ff-only`, but the pop only ran on the success path — a thrown pull error (diverged history, no tracking branch, network failure) jumped past the pop straight to the outer catch, which reported success anyway. The user's local changes were left sitting in `git stash`, invisible in the working tree, with nothing in the round output indicating it happened. Restructured so fetch/pull failure is caught locally instead of by the outer catch, and the stash-pop always runs afterward regardless of pull outcome. A stash that genuinely can't be restored (pop conflict, or pop failing after a pull failure) now comes back as a failed capability result instead of being folded into the same "skipped" message used for the benign no-tracking-branch case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
🟡 Impact Analysis — PR #1640Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
squad-cli (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
Contributor
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | No Copilot review threads |
| ❌ | CI passing | 8 check(s) still running |
Files Changed (3 files, +108 −16)
| File | +/− |
|---|---|
.changeset/fix-1639-self-pull-stash-loss.md |
+5 −0 |
packages/squad-cli/src/cli/commands/watch/capabilities/self-pull.ts |
+36 −13 |
test/cli/watch-capabilities.test.ts |
+67 −3 |
Total: +108 −16
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
tamirdresher
approved these changes
Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
SelfPullCapability.execute()now always attemptsgit stash popafter fetch/pull, regardless of whether the pull succeeded, and reports failure (not success) when the stash genuinely can't be restored.Why
Closes #1639
Full repro and root cause are in the issue. Short version: the capability stashed uncommitted local changes before
git pull --ff-only, but the pop only ran on the path where fetch+pull both succeeded — the whole method was onetryblock, so a thrown pull error (diverged history, no tracking branch, network blip) jumped straight to the outercatch, skipping the pop entirely, and that catch reported{success: true, summary: 'git pull skipped ...'}. The user's uncommitted work was left sitting ingit stash, working tree looking clean, nothing in the round output saying so. Watch runs unattended, so this can happen more than once with no one watching.How
packages/squad-cli/src/cli/commands/watch/capabilities/self-pull.ts: fetch/pull failure is now caught locally intopullErrorinstead of via the outer catch. The stash-pop block runs unconditionally right after, tracked viastashRestored. If the stash isn't restored (pop threw, whether or not pull also failed), that's now{success: false, summary: '...run \git stash pop` in manually'}so the watch loop's existing⚠warning line (index.ts:490-492) actually surfaces it. If pull failed but the stash *did* pop cleanly, behavior is unchanged from before —{success: true, summary: 'git pull skipped (not on tracking branch or conflicts)'}`, since nothing was actually lost.'stash'call sites —self-pull.tsis the only one. No sibling move path to fix.Testing
Real end-to-end repro (not mocked), against the actual bug scenario — diverged local branch with an uncommitted change on top, pull rejected as non-fast-forward:
Before the fix, the same setup produced
STATUS AFTER:empty and the file content silently missing the last line, with it sitting ingit stash listinstead.Added to
test/cli/watch-capabilities.test.ts(existingSelfPullCapabilitydescribe block, same mock style as the rest of the file):success: truebefore, which was the same silent-success bug in miniature; now assertssuccess: falseand the surfaced summary.git stash pop(fails on unfixed code —stashCallsnever contains['stash', 'pop']there since the pop is unreachable) and comes backsuccess: trueonce the stash is confirmed restored.success: false, summary names both causes.devfirst to confirm they fail (3 failures, exactly the ones exercising the bug), then against the fix (all pass).Full
test/cli/watch-capabilities.test.ts: 54/55 pass. The 1 failure (ExecuteCapability > buildAgentPrompt > checks .squad/ralph-instructions.md inside teamRoot) is a pre-existing Windows path-separator mismatch unrelated to this change — confirmed present on unmodifieddevtoo (stashed my diff, reran, same failure).Full-suite (
npx vitest run, freshdistfor both packages): 108 failures / 7202 passed. None touchself-pullorwatch-capabilities. Spot-checkedwatch-health.test.ts(11 failures in the full run, allTest timed out in 5000ms) — passes 11/11 clean in isolation, so that's resource contention under the full parallel run, not a real regression; it also has zero code relationship toself-pull.ts(only references the string'self-pull'in capability-name fixtures).npm run build -w packages/squad-clipasses (after rebuildingsquad-sdkfirst — itsdistwas stale on this box, unrelated to this PR, same class of issue as documented on prior PRs).npx tsc --noEmit -p packages/squad-cli/tsconfig.jsonclean.eslinton both changed files: 0 errors, 8 warnings — all pre-existingn/no-sync/no-consolestyle warnings already present in this file's existing patterns, not new.Diff hygiene:
git diffvsgit diff -wdiffer only in the fetch/pull block, which is exactly the 2-space reindent from wrapping it in the new localtry— not stray whitespace. Both files are LF in this repo; verified no CRLF got introduced..changeset/fix-1639-self-pull-stash-loss.md(patch@bradygaster/squad-cli)PR Readiness Checklist
Branch & Commit
devdevBuild & Test
npm run build -w packages/squad-clipassesnpm test— targeted suite 10/10 (SelfPullCapability), full file 54/55 (1 pre-existing unrelated failure), full-suite baseline compared against unmodifieddevnpm run lintpasses (tsc clean)eslint— 0 errorsChangeset
Docs
Exports
Breaking Changes
None.
CapabilityResult.successfor this capability can now befalsein a case it previously always reportedtruefor — that's the fix, not a break: the watch loop already treatssuccess: falseas a normal per-round warning (⚠line), not a fatal error, same as every other capability.Waivers
None.