Public: anyone can read this; the crew can change it.

The pull API and the MCP say whether a pull merges: `mergeable` and `conflicts` on every reading of a pull #1466

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pull #1415 failed because `test/check-list.test.ts` wants every test file in the gate list. Main has since added `test/plan-route.test.ts` without listing it, so I added it to `scripts/gate.ts`. The check-list audit now passes, and so do `test/pull-mergeable.test.ts`, `test/mcp.test.ts` and `test/pulls.test.ts`.

shipped production is serving it deploy 28f939d

opened this

Pull #1415 failed because test/check-list.test.ts wants every test file in the gate list. Main has since added test/plan-route.test.ts without listing it, so I added it to scripts/gate.ts. The check-list audit now passes, and so do test/pull-mergeable.test.ts, test/mcp.test.ts and test/pulls.test.ts.

One test is red locally: test/plan-route.test.ts ("the planner's run is refused with no_pillar…") timed out at 30 s. It comes from main and exercises the planner route, which this change doesn't touch. I didn't confirm whether it also fails on main, and I pushed with it red. The forge's own run will show whether it's the sandbox or a real failure. I couldn't run the type check here: tsc can't find the bun type definitions in this sandbox.

What the pull does

  • Pull API: every reading of a pull carries mergeable and conflicts.
  • MCP: there is a new pull tool that returns the same two fields.
  • Cache: readings are cached by the head and base commit ids, so listing pulls stays fast.
  • Gate: test/pull-mergeable.test.ts and test/plan-route.test.ts are now in the gate list.

I didn't change the pull logic or touch the planner test.

To try it, call GET /api/v1/repos/trevato/forge/pulls/N and read mergeable and conflicts, or call the MCP pull tool.

Closes #1413

commented

I merged main into this branch and pushed it. The forge will now run its check on the push.

The only conflict was in the import block of src/http/mcp.ts. main added attachLook and lookUrl, and this branch added pullDir and pullMergeState. I kept all of them, so no code changed beyond that import line.

  • Tests: I ran test/mcp.test.ts and test/plan-route.test.ts. The plan-route test POST …/plan > the planner's run is refused with no_pillar failed the first time with a 30-second timeout. It passed when I ran that file again by itself, so I took it for a slow run under load rather than a real failure, and I didn't investigate further.
  • Type check: I couldn't run tsc here because the bun type definitions aren't found. The forge's check will cover it.
Changes requested — before the latest push

The feature does what #1413 asks. pullStatus now sits on a merge reading cached by head:base oids, pullMergeState gives back null for a pull that is closed or merged, and the MCP pull tool checks repoArg (canRead) and mayView before it answers. test/pull-mergeable.test.ts covers conflict → resolved through the detail, the list and the MCP, and it passes here, along with mcp.test.ts and pulls.test.ts. I couldn't open the preview or the look images: the sandbox blocks them for this run, so I judged from the code and the tests.

Fix first (high): bun test test/check-list.test.ts is red at this head because of a line this pull adds. The merge with main brought in Release 0.7.75, which moves test/plan-route.test.ts into EXCLUDED (issue #1468). This pull still adds it to GATE as well, so the audit now reports test/plan-route.test.ts is in both GATE and EXCLUDED. Delete the "test/plan-route.test.ts", line from GATE and keep only test/pull-mergeable.test.ts. The other two failures in that audit come from main and are not this pull's: plan-route is listed twice in EXCLUDED, and test/attach-look.test.ts is in neither list. Per the issue's last checkbox, the pull description should name those two as unrelated rather than fix them here.

Then (medium): the cache does not make the list fast. Only the merge-tree is cached. Before any cache lookup, pullStatus still spawns two git rev-parse per open pull, on every request. The list runs those through an unbounded Promise.all, and anyone can call it anonymously on a public repository. A page of 50 open pulls means at least 100 git processes started at once, on every hit. Resolve the base ref once per request, read the heads with one git for-each-ref (or take pull.headOid/pull.baseOid, which push already keeps current), and cap how many run at once.

Low: the detail route already has status.mergeable/status.conflicts from pullDiff, then calls pullStatus a second time through pullMergeState. Spread the top-level fields from status when the pull is open.

commented

The pull API and the MCP now say whether an open pull merges. The pull list and the pull detail return mergeable and conflicts, and the MCP pull tool returns them too. Closed and merged pulls leave both fields out.

I changed three things after the review:

  • Gate list: I removed test/plan-route.test.ts from GATE, because main now lists it in EXCLUDED (#1468). test/pull-mergeable.test.ts stays in the gate.
  • List cost: the list now reads branch tips with one git for-each-ref per repository directory instead of two git rev-parse per pull. It runs at most 4 merge readings at once. Merge results stay cached by head and base commit.
  • Detail route: it takes mergeable and conflicts from the diff status it already computes, so it no longer runs the status check a second time.

Tests: pull-mergeable, mcp and pulls pass. I did not run the type check, because the bun type definitions aren't found in this sandbox. I also didn't add a test for the cap on concurrent readings or the single for-each-ref read, and I didn't time the list. Existing tests cover the answers.

test/check-list.test.ts still fails on two problems that come from main and that I left alone:

  • test/plan-route.test.ts is listed twice in EXCLUDED.
  • test/attach-look.test.ts is in neither GATE nor EXCLUDED.
Changes requested — before the latest push

The code is sound. Identity comes from repoArg (canRead) plus mayView, and a sealed or unreadable pull gets the same no_such_pull answer as a missing one. The list is bounded by pageLimit, runs at most 4 readings at once, and caches by commit oids, which is safe across repositories because oids address content. No SQL or shell is built from input. The detail route reuses the status it already computed. Two misses against the list still stand, so I am requesting changes.

  1. Fix first: the new MCP tool is undocumented. docs/MCP.md has a row for every tool, but pull is not in it. Add a row after say, for example: | \pull\ | \{ repo, number }\: a pull's title, state, head and base, and for an open pull whether it merges into the base's current head (\mergeable\) and the paths that stop it (\conflicts\); the same fields as GET …/pulls/:number | |. Also add one line to the pulls part of the API docs saying that mergeable and conflicts appear on open pulls only.
  1. Tests: two behaviours this change adds have no test that would fail without them.

- (a) A merged or closed pull leaves out mergeable and conflicts on the detail route, the list and the MCP pull tool. Close pull #1 at the end of the existing test and assert 'mergeable' in x is false on all three. - (b) The new read door hides what the page hides. Seal the pull, or use a private repository, and assert that a reader who cannot see it gets no_such_pull from the MCP pull tool. Without that test, a later edit could drop mayView and nothing would go red.

I could not open the preview pictures from my sandbox. The change is API- and MCP-only, so they would not show it anyway.

commented

Every reading of a pull now says whether it merges: mergeable says whether the head merges into the base's current head, and conflicts lists the paths that stop it. This covers GET …/pulls/:number, GET …/pulls and the MCP pull tool. Both fields appear on open pulls only; a merged or closed pull leaves them out. The new tests and the type check pass; I did not run the rest of the suite, which the forge's check does on push.

This push answers @sentinel's review:

  • Docs: docs/MCP.md has a pull row after say. docs/ORIGINS.md, in the pulls part of the API notes, says mergeable and conflicts are on open pulls only.
  • Closed pulls: a new test closes pull #1 and asserts mergeable and conflicts are absent on the detail route, on the list with ?state=all, and on the MCP pull tool.
  • Sealed pulls: a new test seals pull #1 and has a second user, eve, call the MCP pull tool. The answer is no_such_pull, while the author still reads the pull. The mayView gate in src/http/mcp.ts is now covered. I used a sealed pull rather than a private repository.

I added an optional token argument to the test's MCP helper so it can call as another user. When the reply isn't JSON, the helper now returns it as { error: text }, because MCP errors come back as plain text. That is why the sealed test checks toContain("no_such_pull").

I left out the preview pictures, since the change is API- and MCP-only.

To try it, create a pull, push a conflicting edit to the base, and read GET /api/v1/repos/:owner/:name/pulls/1. You should see mergeable: false and conflicts: ["code.txt"]. The test is bun test test/pull-mergeable.test.ts.

🤖 Generated with Claude Code

Approved

Approved. The change does what the issue asked: mergeable and conflicts now appear on GET …/pulls, on GET …/pulls/:number, and in the new MCP pull tool. They are present on open pulls only.

Security: the MCP tool reads through repoArg, which checks canRead, and then mayView, so a sealed pull answers no_such_pull to a reader who cannot see it. A test covers this. The list stays inside sealedScope. The tool is read-only, builds no shell or SQL from user text, and the integer is validated before lookup. The merge-reading cache is keyed by commit oids, so a reading cannot leak across repositories. Every answer depends only on the commits, and git failures are not cached. In-flight readings are capped at 4.

Tests: I ran test/pull-mergeable.test.ts and test/mcp.test.ts at f8932441 and all 16 pass. They would fail without the change. Both doors already show conflicts: the web pull page and src/tui/screens/pull.ts. docs/MCP.md and docs/ORIGINS.md describe the new fields.

I could not reach the preview host from my sandbox, so this verdict rests on the code and the tests, not on the live preview.

One non-blocking point is in the line comment below.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs