From 9b964ad1039f53c47d59f7656fb93f924128156a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:25:30 -0700 Subject: [PATCH 01/11] chore(ship-skill): check branch is synced with origin/staging before pushing Prevents PRs from silently picking up extraneous commits when a branch/worktree is cut from a stale or diverged local staging/main. --- .agents/skills/ship/SKILL.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index db61568d6c0..9eacc8437e2 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -12,21 +12,22 @@ You help ship code by creating commits, pushing to the remote branch, and creati When the user runs `/ship`: 1. **Check git status** - See what files have changed -2. **Generate a commit message** following this format: `type(scope): description` +2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Must list only this session's commit(s) — a worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits. If it shows extra commits, fix before pushing: `git rebase origin/staging`; if that hits conflicts on commits you don't recognize, `git rebase --abort` and instead `git checkout -b tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD && git checkout ` and `git push --force-with-lease`. +3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) - Keep it concise -3. **Run the cleanup pass** — only if the diff modifies UI code (any `.tsx` file, or anything under `apps/sim/components/`, `apps/sim/hooks/`, or `apps/sim/stores/`): `/cleanup` +4. **Run the cleanup pass** — only if the diff modifies UI code (any `.tsx` file, or anything under `apps/sim/components/`, `apps/sim/hooks/`, or `apps/sim/stores/`): `/cleanup` - The six code-quality skills (effects, memo, callbacks, state, React Query, emcn) only apply to React code, so skip this step entirely when no UI was touched. When it runs, it applies fixes so they land in this commit. -4. **Run migration safety** — only if the diff touches `packages/db/migrations/**` or `packages/db/schema.ts`: +5. **Run migration safety** — only if the diff touches `packages/db/migrations/**` or `packages/db/schema.ts`: - Run `/db-migrate` to review the migration for zero-downtime safety (expand/contract phasing, backward-compatibility with the deployed app version). - `bun run check:migrations origin/staging` must pass (staging is the PR base). Do not silence a flagged statement with a `-- migration-safe:` annotation unless `/db-migrate` confirmed the old code no longer depends on it; otherwise split the destructive change into a later deploy. -5. **Run pre-ship checks** from the repo root before staging: +6. **Run pre-ship checks** from the repo root before staging: - `bun run lint` to fix formatting issues - `bun run check:api-validation:strict` to catch boundary contract failures before CI -6. **Stage and commit** the changes with the generated message -7. **Push to origin** using the current branch name -8. **Create a PR** to staging with a description in the user's voice +7. **Stage and commit** the changes with the generated message +8. **Push to origin** using the current branch name +9. **Create a PR** to staging with a description in the user's voice, then confirm `gh pr view --json commits -q '.commits | length'` matches the commit count from step 2 — otherwise redo the sync fix and force-push ## Commit Message Format From 7eaec2cb17b7803c89617debb8866c245d3bd40c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:37:05 -0700 Subject: [PATCH 02/11] feat(babysit): add skill to drive a PR through review to a clean 5/5 Ships (via /ship's sync check), waits for Greptile/Cursor Bugbot, triages every open thread (fix real findings, push back on false positives), replies + resolves each thread, re-triggers review, and loops until Greptile is 5/5 with zero open threads. --- .agents/skills/babysit/SKILL.md | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .agents/skills/babysit/SKILL.md diff --git a/.agents/skills/babysit/SKILL.md b/.agents/skills/babysit/SKILL.md new file mode 100644 index 00000000000..7076459beaa --- /dev/null +++ b/.agents/skills/babysit/SKILL.md @@ -0,0 +1,112 @@ +--- +name: babysit +description: Drive a PR to a clean review (Greptile 5/5, zero open threads) — ships if needed, triggers Greptile/Cursor Bugbot, fixes real findings, replies to and resolves every thread, and loops until clean +--- + +# Babysit PRs + +Owns a PR end-to-end through review: ship it, wait for the automatic review round, and if it +isn't already clean, drive fix → reply → resolve → re-review cycles until Greptile reports 5/5 +and there are zero open comment threads. Designed to be run under `/loop` (no fixed interval — +let it self-pace on review latency) so it survives across multiple wakeups in the same session. + +## When to use + +- The user says "babysit this PR", "keep working the reviews until it's clean", or similar +- As the natural follow-up to `/ship` when the user wants the review loop automated rather than + manually re-triggering reviews and answering comments themselves + +## Inputs + +Needs a PR number. If none is given and there's no open PR for the current branch, run `/ship` +first (which includes the `origin/staging` sync check — see `.agents/skills/ship/SKILL.md`) to +create one. + +## Definition of "clean" + +Both must hold: +1. The latest Greptile summary comment reports **Confidence Score: 5/5** +2. `reviewThreads` (GraphQL, see below) has **zero threads with `isResolved: false`** + +Do not stop early on "no new comments this round" alone — a thread can be open from an earlier +round. Always check both conditions freshly after every push. + +## Loop + +1. **Check current state** before doing anything: + ```bash + gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 + gh api graphql -f query=' + query { repository(owner: "", name: "") { pullRequest(number: ) { + reviewThreads(first: 50) { nodes { id isResolved path line + comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' + ``` + If Greptile is 5/5 and every thread's `isResolved` is `true`, stop — report the outcome (see + "Reporting" below) and skip the rest of this list. + +2. **If no review has run yet** (fresh PR, no Greptile/Cursor comments): they usually run + automatically on PR open — confirm via `gh pr checks ` (look for `Cursor Bugbot` / + `Greptile Review`) and wait for that first round before doing anything else. + +3. **If a review round has landed and it isn't clean**: for every thread where + `isResolved: false`, triage the finding on its own merits — this is the part that requires + judgment, not a mechanical loop: + - **Real bug**: fix it in the cleanest way available. Match the codebase's existing + conventions for that kind of problem before inventing a new one (e.g. an SSRF-prone + user-supplied-host fetch should use whatever `validateUrlWithDNS`/`secureFetchWithPinnedIP` + pattern the rest of the codebase already uses for that exact situation — grep for a sibling + integration solving the same problem first). Never patch around a finding with a + workaround, a broad try/catch, or a suppression comment — fix the actual cause. + - **False positive**: don't change code. Reply with the specific reason it doesn't apply + (cite the type definition, the established pattern it matches, or the doc it follows) so + the reviewer bot and a human skimming later both understand why it was left as-is. + - **Already fixed by an earlier finding in the same round**: note that and resolve without a + duplicate code change. + +4. **Reply to every thread individually** before resolving it — never resolve silently: + ```bash + gh api repos///pulls//comments//replies -f body="" + ``` + Then resolve via GraphQL (needs the thread `id` from step 1, not the comment id): + ```bash + gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: ""}) { thread { isResolved } } }' + ``` + +5. **Before pushing, re-run the sync check** from `/ship` step 2 + (`git fetch origin staging && git log --oneline origin/staging..HEAD` should list only this + session's commits) — a babysit loop that runs over a long session is exactly the scenario + where a branch can drift. Then run the repo's lint/typecheck/boundary-validation gates the + same way `/ship` does before committing. + +6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's + sync check required a rebuild). + +7. **Re-trigger review** by posting `@greptile` and `@cursor review` as **two separate PR + comments** — never combine them into one comment, each bot only responds to its own mention: + ```bash + gh pr comment --body "@greptile" + gh pr comment --body "@cursor review" + ``` + +8. **Wait for the new round**, then go back to step 1. Pace the wait with `ScheduleWakeup` using + a fallback delay of ~250–300s (Greptile/Cursor typically take 1–3 minutes) — never busy-poll + in a sleep loop. Pass the same `/loop babysit PR ` prompt on each wakeup so the loop + resumes correctly. + +9. **Stop conditions**: clean state reached (see above), or the same unresolved finding survives + two consecutive rounds with no new information (surface it to the user instead of looping + forever), or the user interrupts. + +## Reporting + +When the loop ends, summarize: how many rounds it took, what was actually fixed (one line each), +what was pushed back on as a false positive and why, and the final Greptile score / thread count. + +## Hard rules + +- Never post the two re-review mentions as a single combined comment. +- Never resolve a thread without replying to it first. +- Never fix a finding with a hacky workaround — if the clean fix isn't obvious, find the sibling + pattern elsewhere in the codebase solving the same class of problem and match it. +- Never silently drop a finding — every thread gets either a code fix or a reasoned reply. +- Always re-run the `/ship`-style sync check before every push in the loop, not just the first. From 78c55989b6343d822f74bf75ff2c3de1490b864d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:39:18 -0700 Subject: [PATCH 03/11] chore(cursor-commands): mirror sync-check into ship.md, add babysit.md Keeps the Cursor /commands mirrors of ship and babysit in sync with the Claude Code .agents/skills versions so they don't drift. --- .cursor/commands/babysit.md | 104 ++++++++++++++++++++++++++++++++++++ .cursor/commands/ship.md | 11 ++-- 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .cursor/commands/babysit.md diff --git a/.cursor/commands/babysit.md b/.cursor/commands/babysit.md new file mode 100644 index 00000000000..24f7c10ef24 --- /dev/null +++ b/.cursor/commands/babysit.md @@ -0,0 +1,104 @@ +# Babysit Command + +Owns a PR end-to-end through review: ship it, wait for the automatic review round, and if it +isn't already clean, drive fix → reply → resolve → re-review cycles until Greptile reports 5/5 +and there are zero open comment threads. + +## When to use + +- The user says "babysit this PR", "keep working the reviews until it's clean", or similar +- As the natural follow-up to `/ship` when the user wants the review loop automated rather than + manually re-triggering reviews and answering comments themselves + +## Inputs + +Needs a PR number. If none is given and there's no open PR for the current branch, run `/ship` +first (which includes the `origin/staging` sync check — see `.cursor/commands/ship.md`) to +create one. + +## Definition of "clean" + +Both must hold: +1. The latest Greptile summary comment reports **Confidence Score: 5/5** +2. `reviewThreads` (GraphQL, see below) has **zero threads with `isResolved: false`** + +Do not stop early on "no new comments this round" alone — a thread can be open from an earlier +round. Always check both conditions freshly after every push. + +## Loop + +1. **Check current state** before doing anything: + ```bash + gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 + gh api graphql -f query=' + query { repository(owner: "", name: "") { pullRequest(number: ) { + reviewThreads(first: 50) { nodes { id isResolved path line + comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' + ``` + If Greptile is 5/5 and every thread's `isResolved` is `true`, stop — report the outcome (see + "Reporting" below) and skip the rest of this list. + +2. **If no review has run yet** (fresh PR, no Greptile/Cursor comments): they usually run + automatically on PR open — confirm via `gh pr checks ` (look for `Cursor Bugbot` / + `Greptile Review`) and wait for that first round before doing anything else. + +3. **If a review round has landed and it isn't clean**: for every thread where + `isResolved: false`, triage the finding on its own merits — this is the part that requires + judgment, not a mechanical loop: + - **Real bug**: fix it in the cleanest way available. Match the codebase's existing + conventions for that kind of problem before inventing a new one (e.g. an SSRF-prone + user-supplied-host fetch should use whatever `validateUrlWithDNS`/`secureFetchWithPinnedIP` + pattern the rest of the codebase already uses for that exact situation — grep for a sibling + integration solving the same problem first). Never patch around a finding with a + workaround, a broad try/catch, or a suppression comment — fix the actual cause. + - **False positive**: don't change code. Reply with the specific reason it doesn't apply + (cite the type definition, the established pattern it matches, or the doc it follows) so + the reviewer bot and a human skimming later both understand why it was left as-is. + - **Already fixed by an earlier finding in the same round**: note that and resolve without a + duplicate code change. + +4. **Reply to every thread individually** before resolving it — never resolve silently: + ```bash + gh api repos///pulls//comments//replies -f body="" + ``` + Then resolve via GraphQL (needs the thread `id` from step 1, not the comment id): + ```bash + gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: ""}) { thread { isResolved } } }' + ``` + +5. **Before pushing, re-run the sync check** from `/ship` step 2 + (`git fetch origin staging && git log --oneline origin/staging..HEAD` should list only this + session's commits) — a babysit loop that runs over a long session is exactly the scenario + where a branch can drift. Then run the repo's lint/typecheck/boundary-validation gates the + same way `/ship` does before committing. + +6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's + sync check required a rebuild). + +7. **Re-trigger review** by posting `@greptile` and `@cursor review` as **two separate PR + comments** — never combine them into one comment, each bot only responds to its own mention: + ```bash + gh pr comment --body "@greptile" + gh pr comment --body "@cursor review" + ``` + +8. **Wait for the new round**, then go back to step 1. Never busy-poll in a sleep loop — pace the + wait to roughly how long Greptile/Cursor take (1–3 minutes). + +9. **Stop conditions**: clean state reached (see above), or the same unresolved finding survives + two consecutive rounds with no new information (surface it to the user instead of looping + forever), or the user interrupts. + +## Reporting + +When the loop ends, summarize: how many rounds it took, what was actually fixed (one line each), +what was pushed back on as a false positive and why, and the final Greptile score / thread count. + +## Hard rules + +- Never post the two re-review mentions as a single combined comment. +- Never resolve a thread without replying to it first. +- Never fix a finding with a hacky workaround — if the clean fix isn't obvious, find the sibling + pattern elsewhere in the codebase solving the same class of problem and match it. +- Never silently drop a finding — every thread gets either a code fix or a reasoned reply. +- Always re-run the `/ship`-style sync check before every push in the loop, not just the first. diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index 2530e463ee3..c268166cd30 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -7,20 +7,21 @@ You help ship code by creating commits, pushing to the remote branch, and creati When the user runs `/ship`: 1. **Check git status** - See what files have changed -2. **Generate a commit message** following this format: `type(scope): description` +2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Must list only this session's commit(s) — a worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits. If it shows extra commits, fix before pushing: `git rebase origin/staging`; if that hits conflicts on commits you don't recognize, `git rebase --abort` and instead `git checkout -b tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD && git checkout ` and `git push --force-with-lease`. +3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) - Keep it concise -3. **Run pre-ship checks** from the repo root before staging: +4. **Run pre-ship checks** from the repo root before staging: - `bun run lint` to fix formatting issues - `bun run check:api-validation:strict` to catch boundary contract failures before CI -4. **Stage and commit** the changes with the generated message +5. **Stage and commit** the changes with the generated message -5. **Push to origin** using the current branch name +6. **Push to origin** using the current branch name -6. **Create a PR** to staging with a description in the user's voice +7. **Create a PR** to staging with a description in the user's voice, then confirm `gh pr view --json commits -q '.commits | length'` matches the commit count from step 2 — otherwise redo the sync fix and force-push ## Commit Message Format From 59ed18a0dc15c5e9dced59091376dfdf4a5a6bf6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:42:51 -0700 Subject: [PATCH 04/11] fix(ship-skill): compare commit content not count, handle WIP before rebase, avoid fixed temp branch name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Final verify now diffs actual commit subjects between git log and the PR, not just a count — a corrupted branch's inflated count could coincidentally match a later count check - The pre-commit count in step 2 was being compared against the post-commit PR in step 9, which would mismatch even on success; now compares content at the point it's actually available - Recovery flow stashes uncommitted work before rebasing so it isn't blocked by a dirty tree, and checks for an existing tmp branch before reusing the name --- .agents/skills/ship/SKILL.md | 14 ++++++++++++-- .cursor/commands/ship.md | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index 9eacc8437e2..756b10a3242 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -12,7 +12,12 @@ You help ship code by creating commits, pushing to the remote branch, and creati When the user runs `/ship`: 1. **Check git status** - See what files have changed -2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Must list only this session's commit(s) — a worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits. If it shows extra commits, fix before pushing: `git rebase origin/staging`; if that hits conflicts on commits you don't recognize, `git rebase --abort` and instead `git checkout -b tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD && git checkout ` and `git push --force-with-lease`. +2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Read the actual commit list, not just how many there are — it must show ONLY commits you can attribute to this session (recognizable subjects/SHAs). A worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits; a corrupted branch's inflated commit *count* can coincidentally match a later check even when the *commits* are wrong, so always compare content, never just a number. + - If it shows commits you don't recognize, fix it now, **before** staging/committing any new work (step 7 hasn't run yet): + - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. + - Try `git rebase origin/staging` first. + - If that walks through unrelated history and conflicts on commits you don't recognize, `git rebase --abort` and rebuild instead: pick a temp branch name that isn't already in use (`git branch --list ship-sync-tmp`; if it exists, delete it or pick another name), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete the temp branch. + - Re-verify with `git log --oneline origin/staging..HEAD` — it should list only commits you recognize. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) @@ -27,7 +32,12 @@ When the user runs `/ship`: - `bun run check:api-validation:strict` to catch boundary contract failures before CI 7. **Stage and commit** the changes with the generated message 8. **Push to origin** using the current branch name -9. **Create a PR** to staging with a description in the user's voice, then confirm `gh pr view --json commits -q '.commits | length'` matches the commit count from step 2 — otherwise redo the sync fix and force-push +9. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: + ```bash + git log --oneline origin/staging..HEAD + gh pr view --json commits -q '.commits[].messageHeadline' + ``` + These two lists must describe the same commits (same subjects, one of which is the commit from step 7). If they don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index c268166cd30..b0f74164566 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -7,7 +7,12 @@ You help ship code by creating commits, pushing to the remote branch, and creati When the user runs `/ship`: 1. **Check git status** - See what files have changed -2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Must list only this session's commit(s) — a worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits. If it shows extra commits, fix before pushing: `git rebase origin/staging`; if that hits conflicts on commits you don't recognize, `git rebase --abort` and instead `git checkout -b tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD && git checkout ` and `git push --force-with-lease`. +2. **Sync check**: `git fetch origin staging && git log --oneline origin/staging..HEAD`. Read the actual commit list, not just how many there are — it must show ONLY commits you can attribute to this session (recognizable subjects/SHAs). A worktree/branch can silently be cut from a stale local `staging`, dragging in unrelated commits; a corrupted branch's inflated commit *count* can coincidentally match a later check even when the *commits* are wrong, so always compare content, never just a number. + - If it shows commits you don't recognize, fix it now, **before** staging/committing any new work (step 5 hasn't run yet): + - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. + - Try `git rebase origin/staging` first. + - If that walks through unrelated history and conflicts on commits you don't recognize, `git rebase --abort` and rebuild instead: pick a temp branch name that isn't already in use (`git branch --list ship-sync-tmp`; if it exists, delete it or pick another name), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete the temp branch. + - Re-verify with `git log --oneline origin/staging..HEAD` — it should list only commits you recognize. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) @@ -21,7 +26,12 @@ When the user runs `/ship`: 6. **Push to origin** using the current branch name -7. **Create a PR** to staging with a description in the user's voice, then confirm `gh pr view --json commits -q '.commits | length'` matches the commit count from step 2 — otherwise redo the sync fix and force-push +7. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: + ```bash + git log --oneline origin/staging..HEAD + gh pr view --json commits -q '.commits[].messageHeadline' + ``` + These two lists must describe the same commits (same subjects, one of which is the commit from step 5). If they don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format From 422f81af02bd7eb4659d1007f08c2ed345f9d28b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:49:54 -0700 Subject: [PATCH 05/11] fix(ship-skill,babysit): clean rebase can still hide drift, fixed temp-branch name blocks recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A clean git rebase can replay stray commits with zero conflicts, so 'rebase succeeded' was being treated as sufficient — now the log is always re-checked after rebasing, clean or not, before trusting it - The recovery text told the agent to 'pick another name' if ship-sync-tmp was taken but then hardcoded that literal name anyway; now it just deletes the leftover (disposable, single-purpose) branch instead of introducing a naming scheme - babysit's step 5 pointed at re-running the log check alone; it now points at the full /ship step 2 recovery flow, since a review loop spanning a long session is exactly where drift compounds silently --- .agents/skills/babysit/SKILL.md | 12 +++++++----- .agents/skills/ship/SKILL.md | 5 +++-- .cursor/commands/babysit.md | 12 +++++++----- .cursor/commands/ship.md | 5 +++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.agents/skills/babysit/SKILL.md b/.agents/skills/babysit/SKILL.md index 7076459beaa..c0df0a9b2b9 100644 --- a/.agents/skills/babysit/SKILL.md +++ b/.agents/skills/babysit/SKILL.md @@ -72,11 +72,13 @@ round. Always check both conditions freshly after every push. gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: ""}) { thread { isResolved } } }' ``` -5. **Before pushing, re-run the sync check** from `/ship` step 2 - (`git fetch origin staging && git log --oneline origin/staging..HEAD` should list only this - session's commits) — a babysit loop that runs over a long session is exactly the scenario - where a branch can drift. Then run the repo's lint/typecheck/boundary-validation gates the - same way `/ship` does before committing. +5. **Before pushing, re-run the full sync check from `/ship` step 2** — not just the log command, + the whole check-and-recover flow (stash WIP if needed, rebase, verify the rebase didn't just + cleanly replay stray commits, cherry-pick rebuild if it did or if it conflicted). A babysit + loop spanning a long session is exactly the scenario where a branch can drift, and pushing + review fixes on top of undetected drift is how an oversized PR happens even after the branch + was fixed once. Then run the repo's lint/typecheck/boundary-validation gates the same way + `/ship` does before committing. 6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's sync check required a rebuild). diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index 756b10a3242..1e3d59907f9 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -16,8 +16,9 @@ When the user runs `/ship`: - If it shows commits you don't recognize, fix it now, **before** staging/committing any new work (step 7 hasn't run yet): - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - - If that walks through unrelated history and conflicts on commits you don't recognize, `git rebase --abort` and rebuild instead: pick a temp branch name that isn't already in use (`git branch --list ship-sync-tmp`; if it exists, delete it or pick another name), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete the temp branch. - - Re-verify with `git log --oneline origin/staging..HEAD` — it should list only commits you recognize. + - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: delete any leftover `ship-sync-tmp` branch from an earlier attempt (`git branch -D ship-sync-tmp` — safe, it's disposable), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) diff --git a/.cursor/commands/babysit.md b/.cursor/commands/babysit.md index 24f7c10ef24..bffe148db9b 100644 --- a/.cursor/commands/babysit.md +++ b/.cursor/commands/babysit.md @@ -66,11 +66,13 @@ round. Always check both conditions freshly after every push. gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: ""}) { thread { isResolved } } }' ``` -5. **Before pushing, re-run the sync check** from `/ship` step 2 - (`git fetch origin staging && git log --oneline origin/staging..HEAD` should list only this - session's commits) — a babysit loop that runs over a long session is exactly the scenario - where a branch can drift. Then run the repo's lint/typecheck/boundary-validation gates the - same way `/ship` does before committing. +5. **Before pushing, re-run the full sync check from `/ship` step 2** — not just the log command, + the whole check-and-recover flow (stash WIP if needed, rebase, verify the rebase didn't just + cleanly replay stray commits, cherry-pick rebuild if it did or if it conflicted). A babysit + loop spanning a long session is exactly the scenario where a branch can drift, and pushing + review fixes on top of undetected drift is how an oversized PR happens even after the branch + was fixed once. Then run the repo's lint/typecheck/boundary-validation gates the same way + `/ship` does before committing. 6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's sync check required a rebuild). diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index b0f74164566..7a91c65fe40 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -11,8 +11,9 @@ When the user runs `/ship`: - If it shows commits you don't recognize, fix it now, **before** staging/committing any new work (step 5 hasn't run yet): - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - - If that walks through unrelated history and conflicts on commits you don't recognize, `git rebase --abort` and rebuild instead: pick a temp branch name that isn't already in use (`git branch --list ship-sync-tmp`; if it exists, delete it or pick another name), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete the temp branch. - - Re-verify with `git log --oneline origin/staging..HEAD` — it should list only commits you recognize. + - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: delete any leftover `ship-sync-tmp` branch from an earlier attempt (`git branch -D ship-sync-tmp` — safe, it's disposable), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` - Scope: short identifier (e.g., `undo-redo`, `api`, `ui`) From 1379443364533dc33246c2d26245560c3a83641f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 18:58:16 -0700 Subject: [PATCH 06/11] fix(ship-skill,babysit): guard tmp-branch delete, fetch before final verify, paginate threads, verify after every push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ship-sync-tmp deletion is now conditional on the branch existing — a bare 'git branch -D' on a first recovery attempt with no leftover would fail and block the rebuild before it started - step 9's final commit-content verify now fetches origin/staging first, matching step 2; a stale local ref could mask real drift - babysit's reviewThreads query now checks pageInfo.hasNextPage and pages through all threads instead of assuming 50 is always enough - babysit now re-runs the sync-content verify after every push, not just before it, so a bad push or a PR that drifted mid-loop doesn't go unnoticed across review rounds --- .agents/skills/babysit/SKILL.md | 20 ++++++++++++++++---- .agents/skills/ship/SKILL.md | 9 ++++++--- .cursor/commands/babysit.md | 20 ++++++++++++++++---- .cursor/commands/ship.md | 9 ++++++--- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.agents/skills/babysit/SKILL.md b/.agents/skills/babysit/SKILL.md index c0df0a9b2b9..ef5d5686fef 100644 --- a/.agents/skills/babysit/SKILL.md +++ b/.agents/skills/babysit/SKILL.md @@ -38,11 +38,15 @@ round. Always check both conditions freshly after every push. gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 gh api graphql -f query=' query { repository(owner: "", name: "") { pullRequest(number: ) { - reviewThreads(first: 50) { nodes { id isResolved path line + reviewThreads(first: 50) { pageInfo { hasNextPage endCursor } nodes { id isResolved path line comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' ``` - If Greptile is 5/5 and every thread's `isResolved` is `true`, stop — report the outcome (see - "Reporting" below) and skip the rest of this list. + `reviewThreads(first: 50)` is a single page — check `pageInfo.hasNextPage`. If `true`, don't + stop yet: re-run the same query with `after: ""` and keep paging until + `hasNextPage` is `false` before evaluating "clean." A PR with more than 50 threads is rare but + stopping on a partial page would silently miss unresolved ones past the cutoff. + If Greptile is 5/5 and every thread across all pages has `isResolved: true`, stop — report the + outcome (see "Reporting" below) and skip the rest of this list. 2. **If no review has run yet** (fresh PR, no Greptile/Cursor comments): they usually run automatically on PR open — confirm via `gh pr checks ` (look for `Cursor Bugbot` / @@ -81,7 +85,15 @@ round. Always check both conditions freshly after every push. `/ship` does before committing. 6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's - sync check required a rebuild). + sync check required a rebuild), then run `/ship` step 9's post-push verify — not just before + the first push, every push in the loop: + ```bash + git fetch origin staging && git log --oneline origin/staging..HEAD + gh pr view --json commits -q '.commits[].messageHeadline' + ``` + These two lists must describe the same commits. A review loop runs many pushes across many + rounds; checking sync only before the push (step 5) and never after is how a bad push or a + PR whose commit history quietly went stale between rounds goes unnoticed. 7. **Re-trigger review** by posting `@greptile` and `@cursor review` as **two separate PR comments** — never combine them into one comment, each bot only responds to its own mention: diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index 1e3d59907f9..6e8b5bc3514 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -17,7 +17,7 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: delete any leftover `ship-sync-tmp` branch from an earlier attempt (`git branch -D ship-sync-tmp` — safe, it's disposable), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` @@ -35,10 +35,13 @@ When the user runs `/ship`: 8. **Push to origin** using the current branch name 9. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash - git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` - These two lists must describe the same commits (same subjects, one of which is the commit from step 7). If they don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. + Re-fetch first — comparing against a stale local `origin/staging` ref can mask real drift or + flag a false mismatch even when the branch and push are correct. These two lists must + describe the same commits (same subjects, one of which is the commit from step 7). If they + don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format diff --git a/.cursor/commands/babysit.md b/.cursor/commands/babysit.md index bffe148db9b..90559003b67 100644 --- a/.cursor/commands/babysit.md +++ b/.cursor/commands/babysit.md @@ -32,11 +32,15 @@ round. Always check both conditions freshly after every push. gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 gh api graphql -f query=' query { repository(owner: "", name: "") { pullRequest(number: ) { - reviewThreads(first: 50) { nodes { id isResolved path line + reviewThreads(first: 50) { pageInfo { hasNextPage endCursor } nodes { id isResolved path line comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' ``` - If Greptile is 5/5 and every thread's `isResolved` is `true`, stop — report the outcome (see - "Reporting" below) and skip the rest of this list. + `reviewThreads(first: 50)` is a single page — check `pageInfo.hasNextPage`. If `true`, don't + stop yet: re-run the same query with `after: ""` and keep paging until + `hasNextPage` is `false` before evaluating "clean." A PR with more than 50 threads is rare but + stopping on a partial page would silently miss unresolved ones past the cutoff. + If Greptile is 5/5 and every thread across all pages has `isResolved: true`, stop — report the + outcome (see "Reporting" below) and skip the rest of this list. 2. **If no review has run yet** (fresh PR, no Greptile/Cursor comments): they usually run automatically on PR open — confirm via `gh pr checks ` (look for `Cursor Bugbot` / @@ -75,7 +79,15 @@ round. Always check both conditions freshly after every push. `/ship` does before committing. 6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's - sync check required a rebuild). + sync check required a rebuild), then run `/ship` step 9's post-push verify — not just before + the first push, every push in the loop: + ```bash + git fetch origin staging && git log --oneline origin/staging..HEAD + gh pr view --json commits -q '.commits[].messageHeadline' + ``` + These two lists must describe the same commits. A review loop runs many pushes across many + rounds; checking sync only before the push (step 5) and never after is how a bad push or a + PR whose commit history quietly went stale between rounds goes unnoticed. 7. **Re-trigger review** by posting `@greptile` and `@cursor review` as **two separate PR comments** — never combine them into one comment, each bot only responds to its own mention: diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index 7a91c65fe40..cefb4a4dce4 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -12,7 +12,7 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: delete any leftover `ship-sync-tmp` branch from an earlier attempt (`git branch -D ship-sync-tmp` — safe, it's disposable), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` @@ -29,10 +29,13 @@ When the user runs `/ship`: 7. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash - git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` - These two lists must describe the same commits (same subjects, one of which is the commit from step 5). If they don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. + Re-fetch first — comparing against a stale local `origin/staging` ref can mask real drift or + flag a false mismatch even when the branch and push are correct. These two lists must + describe the same commits (same subjects, one of which is the commit from step 5). If they + don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format From 627f99ad1b9f66380cfdf75ea6dea7d0ab2a6143 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 19:14:13 -0700 Subject: [PATCH 07/11] fix(ship-skill,babysit): fix reversed commit-list order, tail-1 grabbing wrong line, stale step reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git log --oneline is newest-first, gh pr view commits is oldest-first — the two content-verify commands now use --reverse so a positional comparison doesn't spuriously fail on any multi-commit branch - 'select(...) | .body | tail -1' pipes every matching comment's full body through the pipeline and keeps only the last LINE of the combined output (the review-count footer), not the last COMMENT — demonstrated this myself this session reading the footer instead of the actual score. Now uses '[.comments[]] | last | .body' - Cursor's babysit.md pointed at '/ship step 9', but Cursor's ship.md only has 7 steps (no cleanup/migration steps) — now points at step 7 and cross-references the 9-step Claude Code skill copy --- .agents/skills/babysit/SKILL.md | 10 ++++++++-- .agents/skills/ship/SKILL.md | 11 +++++++---- .cursor/commands/babysit.md | 15 +++++++++++---- .cursor/commands/ship.md | 11 +++++++---- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/.agents/skills/babysit/SKILL.md b/.agents/skills/babysit/SKILL.md index ef5d5686fef..f3d6057daf4 100644 --- a/.agents/skills/babysit/SKILL.md +++ b/.agents/skills/babysit/SKILL.md @@ -35,12 +35,16 @@ round. Always check both conditions freshly after every push. 1. **Check current state** before doing anything: ```bash - gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 + gh pr view --json comments -q '[.comments[] | select(.author.login=="greptile-apps")] | last | .body' gh api graphql -f query=' query { repository(owner: "", name: "") { pullRequest(number: ) { reviewThreads(first: 50) { pageInfo { hasNextPage endCursor } nodes { id isResolved path line comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' ``` + `[.comments[]] | last | .body`, not `... | .body | tail -1` — the latter pipes every matching + comment's full multi-line body through the pipeline and keeps only the final *line* of that + combined output (usually the "Reviews (n): Last reviewed commit..." footer), not the last + *comment*, so it silently misses the actual "Confidence Score: X/5" line. `reviewThreads(first: 50)` is a single page — check `pageInfo.hasNextPage`. If `true`, don't stop yet: re-run the same query with `after: ""` and keep paging until `hasNextPage` is `false` before evaluating "clean." A PR with more than 50 threads is rare but @@ -88,9 +92,11 @@ round. Always check both conditions freshly after every push. sync check required a rebuild), then run `/ship` step 9's post-push verify — not just before the first push, every push in the loop: ```bash - git fetch origin staging && git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline --reverse origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` + `--reverse` matches `git log`'s newest-first default to the PR commit list's oldest-first + order — without it a positional comparison can spuriously fail on any multi-commit branch. These two lists must describe the same commits. A review loop runs many pushes across many rounds; checking sync only before the push (step 5) and never after is how a bad push or a PR whose commit history quietly went stale between rounds goes unnoticed. diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index 6e8b5bc3514..50db21fc6c1 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -35,13 +35,16 @@ When the user runs `/ship`: 8. **Push to origin** using the current branch name 9. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash - git fetch origin staging && git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline --reverse origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` Re-fetch first — comparing against a stale local `origin/staging` ref can mask real drift or - flag a false mismatch even when the branch and push are correct. These two lists must - describe the same commits (same subjects, one of which is the commit from step 7). If they - don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. + flag a false mismatch even when the branch and push are correct. `--reverse` makes the git log + oldest-first, matching the PR commit list's order — plain `git log` is newest-first, and a + positional/line-by-line comparison against the PR's oldest-first list can spuriously fail on + any multi-commit branch. These two lists must describe the same commits in the same order + (same subjects, the last one being the commit from step 7). If they don't match, the branch + still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format diff --git a/.cursor/commands/babysit.md b/.cursor/commands/babysit.md index 90559003b67..4feab43d096 100644 --- a/.cursor/commands/babysit.md +++ b/.cursor/commands/babysit.md @@ -29,12 +29,16 @@ round. Always check both conditions freshly after every push. 1. **Check current state** before doing anything: ```bash - gh pr view --json comments -q '.comments[] | select(.author.login=="greptile-apps") | .body' | tail -1 + gh pr view --json comments -q '[.comments[] | select(.author.login=="greptile-apps")] | last | .body' gh api graphql -f query=' query { repository(owner: "", name: "") { pullRequest(number: ) { reviewThreads(first: 50) { pageInfo { hasNextPage endCursor } nodes { id isResolved path line comments(first: 5) { nodes { id databaseId author { login } body } } } } } } }' ``` + `[.comments[]] | last | .body`, not `... | .body | tail -1` — the latter pipes every matching + comment's full multi-line body through the pipeline and keeps only the final *line* of that + combined output (usually the "Reviews (n): Last reviewed commit..." footer), not the last + *comment*, so it silently misses the actual "Confidence Score: X/5" line. `reviewThreads(first: 50)` is a single page — check `pageInfo.hasNextPage`. If `true`, don't stop yet: re-run the same query with `after: ""` and keep paging until `hasNextPage` is `false` before evaluating "clean." A PR with more than 50 threads is rare but @@ -79,12 +83,15 @@ round. Always check both conditions freshly after every push. `/ship` does before committing. 6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's - sync check required a rebuild), then run `/ship` step 9's post-push verify — not just before - the first push, every push in the loop: + sync check required a rebuild), then run `/ship` step 7's post-push verify — not just before + the first push, every push in the loop (the Cursor `/ship` has 7 steps; the Claude Code skill + version's equivalent is step 9 — see `.agents/skills/babysit/SKILL.md` if working from that copy): ```bash - git fetch origin staging && git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline --reverse origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` + `--reverse` matches `git log`'s newest-first default to the PR commit list's oldest-first + order — without it a positional comparison can spuriously fail on any multi-commit branch. These two lists must describe the same commits. A review loop runs many pushes across many rounds; checking sync only before the push (step 5) and never after is how a bad push or a PR whose commit history quietly went stale between rounds goes unnoticed. diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index cefb4a4dce4..0f959d3b2aa 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -29,13 +29,16 @@ When the user runs `/ship`: 7. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash - git fetch origin staging && git log --oneline origin/staging..HEAD + git fetch origin staging && git log --oneline --reverse origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' ``` Re-fetch first — comparing against a stale local `origin/staging` ref can mask real drift or - flag a false mismatch even when the branch and push are correct. These two lists must - describe the same commits (same subjects, one of which is the commit from step 5). If they - don't match, the branch still has a problem — redo step 2's fix and `git push --force-with-lease`. + flag a false mismatch even when the branch and push are correct. `--reverse` makes the git log + oldest-first, matching the PR commit list's order — plain `git log` is newest-first, and a + positional/line-by-line comparison against the PR's oldest-first list can spuriously fail on + any multi-commit branch. These two lists must describe the same commits in the same order + (same subjects, the last one being the commit from step 5). If they don't match, the branch + still has a problem — redo step 2's fix and `git push --force-with-lease`. ## Commit Message Format From 4407729a0388839b8efc473f4309d363a7f51391 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 19:22:04 -0700 Subject: [PATCH 08/11] fix(ship-skill): checkout before deleting ship-sync-tmp, cherry-pick oldest-first - git refuses to delete the branch currently checked out, so if an earlier interrupted recovery left the agent sitting on ship-sync-tmp, the conditional delete would find the branch and then fail to remove it, blocking the rest of the rebuild. Now checks out the original branch first (a no-op if already there). - git log's default order is newest-first; cherry-picking session SHAs in that displayed order applies the newest commit before older ones, which can fail or produce the wrong history with 2+ session commits. Now explicit: cherry-pick oldest-first, using --reverse to get them in that order directly. --- .agents/skills/ship/SKILL.md | 7 ++++++- .cursor/commands/ship.md | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index 50db21fc6c1..bcf42eeabb9 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -17,7 +17,12 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: + 1. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. + 2. Delete any leftover from an earlier attempt: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts). + 3. `git checkout -b ship-sync-tmp origin/staging`. + 4. `git cherry-pick ` **in oldest-first order** — `git log --oneline origin/staging..HEAD` lists newest-first by default, so read the SHAs bottom-to-top, or just use `git log --oneline --reverse origin/staging..HEAD` to get them oldest-first directly before cherry-picking. Cherry-picking more than one session commit out of order can fail or produce the wrong history. + 5. Resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index 0f959d3b2aa..42ca1a3e481 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -12,7 +12,12 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts), `git checkout -b ship-sync-tmp origin/staging`, `git cherry-pick `, resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: + 1. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. + 2. Delete any leftover from an earlier attempt: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts). + 3. `git checkout -b ship-sync-tmp origin/staging`. + 4. `git cherry-pick ` **in oldest-first order** — `git log --oneline origin/staging..HEAD` lists newest-first by default, so read the SHAs bottom-to-top, or just use `git log --oneline --reverse origin/staging..HEAD` to get them oldest-first directly before cherry-picking. Cherry-picking more than one session commit out of order can fail or produce the wrong history. + 5. Resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` From 9fbaa6056dba005b07c24841c2d5a283c8c15168 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 19:30:05 -0700 Subject: [PATCH 09/11] fix(ship-skill): capture cherry-pick SHAs before switching to the temp branch, make tmp delete idempotent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step 4 of the rebuild checks out ship-sync-tmp at origin/staging, so HEAD no longer contains the session's commits by the time the old step 4 (now step 5) ran 'origin/staging..HEAD' to find them — that range was always empty by then, following the steps literally would cherry-pick nothing. Now captures the SHA list first, against the original branch name explicitly, before any branch switch. - The show-ref-guarded delete still exits 1 (git show-ref's own failure) on the common first-attempt case where there's no leftover branch, which could halt a caller that stops on nonzero exit before ever reaching the rebuild. Replaced with a plain delete + || true, which always succeeds whether or not there was anything to delete. --- .agents/skills/ship/SKILL.md | 13 +++++++------ .cursor/commands/ship.md | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index bcf42eeabb9..a805f0c0c66 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -17,12 +17,13 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: - 1. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. - 2. Delete any leftover from an earlier attempt: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts). - 3. `git checkout -b ship-sync-tmp origin/staging`. - 4. `git cherry-pick ` **in oldest-first order** — `git log --oneline origin/staging..HEAD` lists newest-first by default, so read the SHAs bottom-to-top, or just use `git log --oneline --reverse origin/staging..HEAD` to get them oldest-first directly before cherry-picking. Cherry-picking more than one session commit out of order can fail or produce the wrong history. - 5. Resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead, in this exact order: + 1. **While still on ``**, capture the commits to preserve: `git log --oneline --reverse origin/staging..` — explicitly against the branch name, not bare `HEAD`. Do this *before* touching any temp branch: once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits, and re-running the same lookup at that point returns nothing. + 2. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. + 3. Delete any leftover from an earlier attempt: `git branch -D ship-sync-tmp 2>/dev/null || true` — always succeeds, including when there's nothing to delete (a first attempt), so it never blocks the rest of the rebuild on its own exit code. + 4. `git checkout -b ship-sync-tmp origin/staging`. + 5. `git cherry-pick` the SHAs captured in step 1, **in that oldest-first order** — cherry-picking more than one session commit out of order can fail or produce the wrong history. Resolve conflicts. + 6. `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp` (`git branch -D ship-sync-tmp`). - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index 42ca1a3e481..be04202e393 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -12,12 +12,13 @@ When the user runs `/ship`: - If the working tree has uncommitted changes, stash them first — `git stash push -u -m ship-sync-fix` — so the rebase below isn't blocked by dirty state. Restore with `git stash pop` once the branch is fixed. - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead: - 1. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. - 2. Delete any leftover from an earlier attempt: `git show-ref --verify --quiet refs/heads/ship-sync-tmp && git branch -D ship-sync-tmp` (only deletes if it exists — a bare `git branch -D` on a first attempt with no leftover would fail and stop you before the rebuild ever starts). - 3. `git checkout -b ship-sync-tmp origin/staging`. - 4. `git cherry-pick ` **in oldest-first order** — `git log --oneline origin/staging..HEAD` lists newest-first by default, so read the SHAs bottom-to-top, or just use `git log --oneline --reverse origin/staging..HEAD` to get them oldest-first directly before cherry-picking. Cherry-picking more than one session commit out of order can fail or produce the wrong history. - 5. Resolve conflicts, then `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp`. + - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead, in this exact order: + 1. **While still on ``**, capture the commits to preserve: `git log --oneline --reverse origin/staging..` — explicitly against the branch name, not bare `HEAD`. Do this *before* touching any temp branch: once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits, and re-running the same lookup at that point returns nothing. + 2. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. + 3. Delete any leftover from an earlier attempt: `git branch -D ship-sync-tmp 2>/dev/null || true` — always succeeds, including when there's nothing to delete (a first attempt), so it never blocks the rest of the rebuild on its own exit code. + 4. `git checkout -b ship-sync-tmp origin/staging`. + 5. `git cherry-pick` the SHAs captured in step 1, **in that oldest-first order** — cherry-picking more than one session commit out of order can fail or produce the wrong history. Resolve conflicts. + 6. `git branch -f HEAD`, `git checkout `, and delete `ship-sync-tmp` (`git branch -D ship-sync-tmp`). - Re-verify with `git log --oneline origin/staging..HEAD` — it must list only commits you recognize before you proceed to committing new work. 3. **Generate a commit message** following this format: `type(scope): description` - Types: `fix`, `feat`, `improvement`, `chore` From 59eec9a242a7c72004364aa326bbec055b6df547 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 19:37:08 -0700 Subject: [PATCH 10/11] fix(ship-skill,babysit): rebuild was replaying the exact stray commits it was meant to leave behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The 'capture the range' step from the previous round's fix captured origin/staging.. wholesale — but in exactly the scenario that triggers this rebuild, that range also contains the unrecognized/stray commits, so cherry-picking 'all of it' recreated the same polluted branch. Now explicit: read the log, write down only the SHA(s) you recognize as your own, never the whole range. - babysit's pre-push gate list named lint/typecheck/boundary-validation but dropped ship's conditional /cleanup and /db-migrate steps, which a review-fix round can trip just as easily as the original commit. - babysit's force-with-lease condition only covered the cherry-pick rebuild path, but a plain 'git rebase origin/staging' that completes with no conflicts also rewrites already-published history and needs the same force push. --- .agents/skills/babysit/SKILL.md | 17 +++++++++++------ .agents/skills/ship/SKILL.md | 2 +- .cursor/commands/babysit.md | 16 +++++++++++----- .cursor/commands/ship.md | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.agents/skills/babysit/SKILL.md b/.agents/skills/babysit/SKILL.md index f3d6057daf4..6e5b883ee31 100644 --- a/.agents/skills/babysit/SKILL.md +++ b/.agents/skills/babysit/SKILL.md @@ -85,12 +85,17 @@ round. Always check both conditions freshly after every push. cleanly replay stray commits, cherry-pick rebuild if it did or if it conflicted). A babysit loop spanning a long session is exactly the scenario where a branch can drift, and pushing review fixes on top of undetected drift is how an oversized PR happens even after the branch - was fixed once. Then run the repo's lint/typecheck/boundary-validation gates the same way - `/ship` does before committing. - -6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's - sync check required a rebuild), then run `/ship` step 9's post-push verify — not just before - the first push, every push in the loop: + was fixed once. Then run the repo's pre-ship checks the same way `/ship` does before + committing — not just lint/typecheck/boundary-validation, but also the conditional `/cleanup` + (if this round's fix touched UI code) and `/db-migrate` (if it touched schema/migrations) + gates from `/ship` steps 4 and 5. A review-fix round is still a code change and can trip + either gate just as easily as the original commit did. + +6. **Commit and push** the round's fixes as one commit — `--force-with-lease` whenever step 5's + sync check rewrote history, which includes a plain `git rebase origin/staging` that completed + with no conflicts, not only the cherry-pick rebuild path; both rewrite commits already + published to the remote, so a plain `git push` can be rejected either way — then run `/ship` + step 9's post-push verify — not just before the first push, every push in the loop: ```bash git fetch origin staging && git log --oneline --reverse origin/staging..HEAD gh pr view --json commits -q '.commits[].messageHeadline' diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index a805f0c0c66..a79a0eeeee1 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -18,7 +18,7 @@ When the user runs `/ship`: - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead, in this exact order: - 1. **While still on ``**, capture the commits to preserve: `git log --oneline --reverse origin/staging..` — explicitly against the branch name, not bare `HEAD`. Do this *before* touching any temp branch: once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits, and re-running the same lookup at that point returns nothing. + 1. **While still on ``**, identify the SHA(s) to preserve — **not** the whole range. `git log --oneline --reverse origin/staging..` lists everything ahead of `origin/staging`, but in exactly this scenario that range also contains the unrecognized/stray commits you're trying to leave behind — blindly cherry-picking the full range recreates the same polluted branch. Read the list and write down only the SHA(s) you recognize as your own session's work (e.g. `abc1234 def5678`); do this *before* touching any temp branch, since once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits and the same lookup at that point returns nothing. 2. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. 3. Delete any leftover from an earlier attempt: `git branch -D ship-sync-tmp 2>/dev/null || true` — always succeeds, including when there's nothing to delete (a first attempt), so it never blocks the rest of the rebuild on its own exit code. 4. `git checkout -b ship-sync-tmp origin/staging`. diff --git a/.cursor/commands/babysit.md b/.cursor/commands/babysit.md index 4feab43d096..3935f6b5e30 100644 --- a/.cursor/commands/babysit.md +++ b/.cursor/commands/babysit.md @@ -79,11 +79,17 @@ round. Always check both conditions freshly after every push. cleanly replay stray commits, cherry-pick rebuild if it did or if it conflicted). A babysit loop spanning a long session is exactly the scenario where a branch can drift, and pushing review fixes on top of undetected drift is how an oversized PR happens even after the branch - was fixed once. Then run the repo's lint/typecheck/boundary-validation gates the same way - `/ship` does before committing. - -6. **Commit and push** the round's fixes as one commit (`--force-with-lease` only if step 5's - sync check required a rebuild), then run `/ship` step 7's post-push verify — not just before + was fixed once. Then run the repo's pre-ship checks — not just lint/typecheck/boundary- + validation, but also the conditional `/cleanup` (UI changes) and `/db-migrate` + (schema/migration changes) gates from `.agents/skills/ship/SKILL.md` steps 4 and 5 (Cursor's + own 7-step `ship.md` doesn't carry those steps, but a review-fix round is still a code change + and can trip either gate just as easily as the original commit did). + +6. **Commit and push** the round's fixes as one commit — `--force-with-lease` whenever step 5's + sync check rewrote history, which includes a plain `git rebase origin/staging` that completed + with no conflicts, not only the cherry-pick rebuild path; both rewrite commits already + published to the remote, so a plain `git push` can be rejected either way — then run `/ship` + step 7's post-push verify — not just before the first push, every push in the loop (the Cursor `/ship` has 7 steps; the Claude Code skill version's equivalent is step 9 — see `.agents/skills/babysit/SKILL.md` if working from that copy): ```bash diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index be04202e393..7444f204179 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -13,7 +13,7 @@ When the user runs `/ship`: - Try `git rebase origin/staging` first. - **A rebase finishing without conflicts does NOT by itself mean the branch is clean** — it can replay stray commits onto the new base with no conflict at all. After the rebase (clean or not), re-run `git log --oneline origin/staging..HEAD` and re-check the commit list against what you recognize. - If the rebase conflicted on commits you don't recognize, OR it finished cleanly but the re-checked log still shows commits you don't recognize, abandon that result (`git rebase --abort` if still mid-rebase) and rebuild instead, in this exact order: - 1. **While still on ``**, capture the commits to preserve: `git log --oneline --reverse origin/staging..` — explicitly against the branch name, not bare `HEAD`. Do this *before* touching any temp branch: once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits, and re-running the same lookup at that point returns nothing. + 1. **While still on ``**, identify the SHA(s) to preserve — **not** the whole range. `git log --oneline --reverse origin/staging..` lists everything ahead of `origin/staging`, but in exactly this scenario that range also contains the unrecognized/stray commits you're trying to leave behind — blindly cherry-picking the full range recreates the same polluted branch. Read the list and write down only the SHA(s) you recognize as your own session's work (e.g. `abc1234 def5678`); do this *before* touching any temp branch, since once you check out `ship-sync-tmp` at `origin/staging` in step 4, `HEAD` no longer contains these commits and the same lookup at that point returns nothing. 2. `git checkout ` — harmless no-op if you're already there, but required if an earlier interrupted attempt left you sitting on `ship-sync-tmp`: git refuses to delete the branch you're currently on, so deleting it before switching away silently fails and blocks the rest of the rebuild. 3. Delete any leftover from an earlier attempt: `git branch -D ship-sync-tmp 2>/dev/null || true` — always succeeds, including when there's nothing to delete (a first attempt), so it never blocks the rest of the rebuild on its own exit code. 4. `git checkout -b ship-sync-tmp origin/staging`. From 04017a8b537c8b09e4e74ecea17613f911c88b7f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Jul 2026 19:43:10 -0700 Subject: [PATCH 11/11] fix(ship-skill): push step needs force-with-lease too, not just babysit's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2's sync recovery rewrites already-pushed commits on a branch that's been pushed before, but the push step only said 'push using the current branch name' with no force-with-lease — a plain push would be rejected in exactly the polluted-remote case step 2 exists to fix. babysit already got this fix; ship's own push step didn't. --- .agents/skills/ship/SKILL.md | 5 ++++- .cursor/commands/ship.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md index a79a0eeeee1..43024747fe4 100644 --- a/.agents/skills/ship/SKILL.md +++ b/.agents/skills/ship/SKILL.md @@ -38,7 +38,10 @@ When the user runs `/ship`: - `bun run lint` to fix formatting issues - `bun run check:api-validation:strict` to catch boundary contract failures before CI 7. **Stage and commit** the changes with the generated message -8. **Push to origin** using the current branch name +8. **Push to origin** using the current branch name — `--force-with-lease` if step 2's sync + check did any history rewrite (a clean rebase or a cherry-pick rebuild) on a branch that had + already been pushed once; a plain push would be rejected in exactly the polluted-remote case + step 2 exists to fix 9. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash git fetch origin staging && git log --oneline --reverse origin/staging..HEAD diff --git a/.cursor/commands/ship.md b/.cursor/commands/ship.md index 7444f204179..b08075ceb3c 100644 --- a/.cursor/commands/ship.md +++ b/.cursor/commands/ship.md @@ -31,7 +31,10 @@ When the user runs `/ship`: 5. **Stage and commit** the changes with the generated message -6. **Push to origin** using the current branch name +6. **Push to origin** using the current branch name — `--force-with-lease` if step 2's sync + check did any history rewrite (a clean rebase or a cherry-pick rebuild) on a branch that had + already been pushed once; a plain push would be rejected in exactly the polluted-remote case + step 2 exists to fix 7. **Create a PR** to staging with a description in the user's voice, then do a final content check — not a count check — comparing what actually landed: ```bash