diff --git a/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/orch.test.ts b/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/orch.test.ts index 2ef435d7d..be8edd505 100644 --- a/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/orch.test.ts +++ b/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/orch.test.ts @@ -573,6 +573,40 @@ describe("Store", () => { }); }); + it(`resolves a replacement ${forge} PR alongside closed branch history`, async () => { + for (const historicalState of ["closed", "merged"] as const) { + const { directory, store } = await initializedStore(); + const stack = await makeGitStack(directory); + await withFakeForge({ + directory, forge, + currentRows: forgeRows(forge, [ + { number: 9, state: historicalState, head: "stack/open", base: "main" }, + { number: 11, state: "open", head: "stack/open", base: "stack/closed" }, + ]), + operation: async () => { + const frontier = await store.frontier.set({ repo: stack.repo, forge }); + expect(frontier.prs.map((row) => row.pr)).toEqual([10, 13, 11, 14]); + expect(frontier.lowestUnmerged).toBe(11); + }, + }); + } + }); + + it(`still rejects multiple active ${forge} PRs on one branch`, async () => { + const { directory, store } = await initializedStore(); + const stack = await makeGitStack(directory); + await withFakeForge({ + directory, forge, + currentRows: forgeRows(forge, [ + { number: 11, state: "open", head: "stack/open", base: "stack/closed" }, + { number: 12, state: "open", head: "stack/open", base: "main" }, + ]), + operation: async () => { + await expect(store.frontier.set({ repo: stack.repo, forge })).rejects.toThrow("multiple pull requests"); + }, + }); + }); + it(`rejects unparseable ${forge} output loudly`, async () => { const { directory, store } = await initializedStore(); const stack = await makeGitStack(directory); diff --git a/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/store.ts b/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/store.ts index 417368918..a7cef5af8 100644 --- a/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/store.ts +++ b/harness/pstack/claude-code/skills/poteto-mode/scripts/orch/store.ts @@ -1113,15 +1113,19 @@ function branchPullRequest({ repo: string; }): ForgePullRequest { const rows = listPullRequests({ filter: "--head", branch, forge, repo }); - const row = rows[0]; + // A branch may have closed or merged historical PRs after stack surgery. + // Prefer its sole active PR, but retain historical anchors when none is open. + const active = rows.filter((row) => row.state === "OPEN"); + const candidates = active.length > 0 ? active : rows; + const row = candidates[0]; if (row === undefined) { throw new UserError( `branch ${branch} has no pull request; open one with ${forge} pr create, or resolve the frontier from a branch whose chain is fully submitted` ); } - if (rows.length > 1) { + if (candidates.length > 1) { throw new UserError( - `branch ${branch} has multiple pull requests (${rows.map((item) => `#${item.pr}`).join(", ")}); close the stale ones first` + `branch ${branch} has multiple pull requests (${candidates.map((item) => `#${item.pr}`).join(", ")}); close the stale ones first` ); } return row; diff --git a/pstack/skills/poteto-mode/scripts/orch/orch.test.ts b/pstack/skills/poteto-mode/scripts/orch/orch.test.ts index 2ef435d7d..be8edd505 100644 --- a/pstack/skills/poteto-mode/scripts/orch/orch.test.ts +++ b/pstack/skills/poteto-mode/scripts/orch/orch.test.ts @@ -573,6 +573,40 @@ describe("Store", () => { }); }); + it(`resolves a replacement ${forge} PR alongside closed branch history`, async () => { + for (const historicalState of ["closed", "merged"] as const) { + const { directory, store } = await initializedStore(); + const stack = await makeGitStack(directory); + await withFakeForge({ + directory, forge, + currentRows: forgeRows(forge, [ + { number: 9, state: historicalState, head: "stack/open", base: "main" }, + { number: 11, state: "open", head: "stack/open", base: "stack/closed" }, + ]), + operation: async () => { + const frontier = await store.frontier.set({ repo: stack.repo, forge }); + expect(frontier.prs.map((row) => row.pr)).toEqual([10, 13, 11, 14]); + expect(frontier.lowestUnmerged).toBe(11); + }, + }); + } + }); + + it(`still rejects multiple active ${forge} PRs on one branch`, async () => { + const { directory, store } = await initializedStore(); + const stack = await makeGitStack(directory); + await withFakeForge({ + directory, forge, + currentRows: forgeRows(forge, [ + { number: 11, state: "open", head: "stack/open", base: "stack/closed" }, + { number: 12, state: "open", head: "stack/open", base: "main" }, + ]), + operation: async () => { + await expect(store.frontier.set({ repo: stack.repo, forge })).rejects.toThrow("multiple pull requests"); + }, + }); + }); + it(`rejects unparseable ${forge} output loudly`, async () => { const { directory, store } = await initializedStore(); const stack = await makeGitStack(directory); diff --git a/pstack/skills/poteto-mode/scripts/orch/store.ts b/pstack/skills/poteto-mode/scripts/orch/store.ts index 417368918..a7cef5af8 100644 --- a/pstack/skills/poteto-mode/scripts/orch/store.ts +++ b/pstack/skills/poteto-mode/scripts/orch/store.ts @@ -1113,15 +1113,19 @@ function branchPullRequest({ repo: string; }): ForgePullRequest { const rows = listPullRequests({ filter: "--head", branch, forge, repo }); - const row = rows[0]; + // A branch may have closed or merged historical PRs after stack surgery. + // Prefer its sole active PR, but retain historical anchors when none is open. + const active = rows.filter((row) => row.state === "OPEN"); + const candidates = active.length > 0 ? active : rows; + const row = candidates[0]; if (row === undefined) { throw new UserError( `branch ${branch} has no pull request; open one with ${forge} pr create, or resolve the frontier from a branch whose chain is fully submitted` ); } - if (rows.length > 1) { + if (candidates.length > 1) { throw new UserError( - `branch ${branch} has multiple pull requests (${rows.map((item) => `#${item.pr}`).join(", ")}); close the stale ones first` + `branch ${branch} has multiple pull requests (${candidates.map((item) => `#${item.pr}`).join(", ")}); close the stale ones first` ); } return row;