v1.92.25.0 fix(ship): Step 3 stops by name on an unfinished merge, cherry-pick or revert (GST-732) (#3785)

* test(ship): an unfinished merge or cherry-pick stops Step 3 by name (GST-732, failing first)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship): an unfinished merge, cherry-pick or revert stops Step 3 by name before any git merge (GST-732)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(ship): Step 3 names the MERGE: up to date outcome (GST-732)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(ship): regenerate ship goldens for the Step 3 MERGE: up to date wording (GST-732)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (GST-732: cover the unfinished revert stop)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (GST-732: only a real operation state file stops Step 3; unresolved conflicts with none in progress stop by name)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): scope GST-732 to repositories that merge the base in /ship

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (GST-732: a failed --git-path probe is not an operation in progress)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(ship): shorten Step 3's MERGE: up to date note so the ship skeleton fits its cap on current main (GST-732)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-01 10:32:27 +00:00
1 parent a2ce5bd888
commit f739756389
13 files changed
+69 -23

No files matched your search

+4
View File
@@ -1,5 +1,9 @@
# Changelog
## [1.92.25.0] - 2026-11-01
- In a repository that merges the base during `/ship` (any `queue_mode` but `stamp-at-merge`), Step 3 now stops by name when an earlier merge, cherry-pick or revert is still unfinished (`STOP: an unfinished cherry-pick is in progress (CHERRY_PICK_HEAD): …`), with any conflicted files listed, and runs no `git merge`. Conflicted files left with no operation in progress, as after a conflicted `git stash pop`, stop with those files named instead of a merge message. Before, both fell through to `git merge`, so on a machine whose shared-tree guard refuses `git merge`, the guard's refusal was the only message (GST-732). A branch or tag named `MERGE_HEAD`, `CHERRY_PICK_HEAD` or `REVERT_HEAD` is not mistaken for one. The ship skill's Step 3 now names the `MERGE: up to date` outcome.
## [1.92.24.0] - 2026-11-01
- `/ship` can finish after the owner accepts a Step 14.5 documentation risk. `gstack-docs-candidate snapshot --exception .gstack/tmp/docs-audit.json` writes the post-audit snapshot from the blocked record, naming who accepted it, the date, how and the base SHA, and `gstack-ship-release finish` prints that exception on its `DOCS:` line. Finish still stops on a blocked audit with no accepted exception, and on an exception snapshot whose docs-audit record no longer carries it (GST-713).
+1 -1
View File
@@ -1 +1 @@
1.92.24.0
1.92.25.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.92.24.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.92.25.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+16 -3
View File
@@ -67,9 +67,22 @@ const forkPoint = (cwd: string, base: string) => git(cwd, ['merge-base', `origin
function merge(cwd: string, base: string, r: Report): boolean {
const policy = shipPolicy(cwd, base);
if (policy?.queue_mode === 'stamp-at-merge') r.add('MERGE: skipped (queue_mode stamp-at-merge: the slot stamp merges the base; docs/ship-policy.md#ship-before-ready)');
else if (!git(cwd, ['ls-files', '-u']).stdout.trim() && ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD'].every(ref => git(cwd, ['rev-parse', '-q', '--verify', ref]).status !== 0)
&& git(cwd, ['merge-base', '--is-ancestor', `origin/${base}`, 'HEAD']).status === 0) r.add('MERGE: up to date');
const stampAtMerge = policy?.queue_mode === 'stamp-at-merge';
const pending = stampAtMerge ? undefined : ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD'].find(ref => {
const p = git(cwd, ['rev-parse', '--git-path', ref]);
return p.status === 0 && !!p.stdout.trim() && fs.existsSync(path.resolve(cwd, p.stdout.trim()));
});
const unmerged = stampAtMerge ? [] : git(cwd, ['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').filter(Boolean);
if (pending) {
r.stop(`an unfinished ${pending.replace(/_HEAD$/, '').toLowerCase().replace('_', '-')} is in progress (${pending}${unmerged.length ? `, conflicts in ${short(unmerged, 8)}` : ''}): resolve and commit it, or abort it, then rerun (Step 3)`);
return false;
}
if (unmerged.length) {
r.stop(`unresolved conflicts in ${short(unmerged, 8)} with no merge, cherry-pick or revert in progress: resolve them (git add) or restore the files, then rerun (Step 3)`);
return false;
}
if (stampAtMerge) r.add('MERGE: skipped (queue_mode stamp-at-merge: the slot stamp merges the base; docs/ship-policy.md#ship-before-ready)');
else if (git(cwd, ['merge-base', '--is-ancestor', `origin/${base}`, 'HEAD']).status === 0) r.add('MERGE: up to date');
else {
const before = git(cwd, ['rev-parse', 'HEAD']).stdout.trim();
const m = git(cwd, ['merge', `origin/${base}`, '--no-edit'], 300_000);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.92.24",
"version": "1.92.25",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -455,7 +455,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+1 -1
View File
@@ -185,7 +185,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+1 -1
View File
@@ -455,7 +455,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+1 -1
View File
@@ -455,7 +455,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+1 -1
View File
@@ -454,7 +454,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+1 -1
View File
@@ -456,7 +456,7 @@ not web services already deployed.
## Step 3: Merge the base branch (BEFORE tests)
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
+36 -11
View File
@@ -44,20 +44,45 @@ describe('preflight merge when the base is already in HEAD (GST-691)', () => {
expect(r.stdout).toContain('STOP: git merge origin/main failed: shared-tree guard: git merge is not allowed in this checkout');
});
// Value: protects=an unfinished merge of another branch still stops at Step 3 with its conflicted files named, though the base is in HEAD;
// fails_when=the ancestor shortcut reports up to date over an index with unmerged paths and the run tests conflict markers;
// why_new=GST-691 /ship adversarial finding 1; seam=git ls-files -u; no_claim=how the agent resolves it
test('an unfinished merge with the base already in HEAD still stops, conflicted or resolved and staged', () => {
const { f } = guarded();
// Value: protects=an unfinished merge, cherry-pick or revert stops at Step 3 with its name and conflicted files, though the base is in HEAD, and no git merge runs;
// fails_when=the ancestor shortcut reports up to date over one, or the stop is a guard's refusal that names no cause;
// why_new=GST-691 /ship adversarial findings; seam=MERGE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD; no_claim=how the agent resolves it
test('an unfinished merge, cherry-pick or revert with the base already in HEAD stops and names it, conflicted or resolved', () => {
const { f, merges } = guarded();
f.git('checkout', '-q', '-b', 'other', 'main'); f.write({ 'src/a.ts': 'export const a = 5;\n' }); f.commit('other edits a');
f.git('checkout', '-q', 'feat'); f.write({ 'src/a.ts': 'export const a = 6;\n' }); f.commit('feat edits a');
expect(f.git('merge', '--no-edit', 'other').status).not.toBe(0);
const r = f.run(PREFLIGHT, ['--base', 'main']);
expect([r.status, lineOf(r.stdout, 'MERGE')]).toEqual([3, '']);
expect(r.stdout).toMatch(/STOP: merge conflicts in src\/a\.ts/);
const conflicted = f.run(PREFLIGHT, ['--base', 'main']);
expect([conflicted.status, lineOf(conflicted.stdout, 'MERGE')]).toEqual([3, '']);
expect(conflicted.stdout).toContain('STOP: an unfinished merge is in progress (MERGE_HEAD, conflicts in src/a.ts)');
f.write({ 'src/a.ts': 'export const a = 7;\n' }); f.git('add', 'src/a.ts');
const staged = f.run(PREFLIGHT, ['--base', 'main']);
expect([staged.status, lineOf(staged.stdout, 'MERGE')]).toEqual([3, '']);
expect(staged.stdout).toContain('STOP: git merge origin/main failed:');
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished merge is in progress (MERGE_HEAD):');
f.git('merge', '--abort');
expect(f.git('cherry-pick', 'other').status).not.toBe(0);
f.write({ 'src/a.ts': 'export const a = 8;\n' }); f.git('add', 'src/a.ts');
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished cherry-pick is in progress (CHERRY_PICK_HEAD):');
f.git('cherry-pick', '--abort');
f.write({ 'src/a.ts': 'export const a = 9;\n' }); f.commit('feat edits a again');
expect(f.git('revert', '--no-edit', 'HEAD~1').status).not.toBe(0);
f.write({ 'src/a.ts': 'export const a = 10;\n' }); f.git('add', 'src/a.ts');
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished revert is in progress (REVERT_HEAD):');
expect(merges()).toEqual([]);
});
// Value: protects=only a real in-progress operation stops Step 3, and unresolved index entries with none in progress stop with their files named before any git merge;
// fails_when=a branch or tag named like MERGE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD gives a stop no abort can clear, or a conflicted stash pop reaches git merge and gets a merge-specific stop;
// why_new=GST-732 /ship adversarial findings 2 and 4; seam=git rev-parse --git-path, git diff --diff-filter=U; no_claim=git am or sequencer state
test('a ref named REVERT_HEAD is not an unfinished revert, and a conflicted stash pop stops with its files named', () => {
const { f, merges } = guarded();
f.git('branch', 'REVERT_HEAD'); f.git('tag', 'CHERRY_PICK_HEAD');
const named = f.run(PREFLIGHT, ['--base', 'main']);
expect([named.status, lineOf(named.stdout, 'MERGE')]).toEqual([0, 'MERGE: up to date']);
f.write({ 'src/a.ts': 'export const a = 6;\n' }); f.git('stash');
f.write({ 'src/a.ts': 'export const a = 7;\n' }); f.commit('feat edits a');
expect(f.git('stash', 'pop').status).not.toBe(0);
const r = f.run(PREFLIGHT, ['--base', 'main']);
expect(r.status).toBe(3);
expect(r.stdout).toContain('STOP: unresolved conflicts in src/a.ts with no merge, cherry-pick or revert in progress');
expect(merges()).toEqual([]);
});
});
@@ -0,0 +1,4 @@
{
"bytes": 29,
"why": "GST-732: Step 3 names the MERGE: up to date outcome"
}