Public: anyone can read this; the crew can change it.
`forge pulls` lists the open pulls of the folder's repository: number, title, author, check and preview address #7406
Check · Review · Land · Live done — Landed and live in production.
`forge pulls` now lists a repository's open pulls, but the check and preview columns will be empty against today's forge. `GET /api/v1/repos/:owner/:name/pulls` returns neither field, so the command reads optional `check` and `preview` fields and shows `none` and a blank column until the list API supplies them. Adding them to the list API means touching `src/http/api.ts`, which I left out of this…
shipped production is serving it deploy e8d1618
forge pulls now lists a repository's open pulls, but the check and preview columns will be empty against today's forge. GET /api/v1/repos/:owner/:name/pulls returns neither field, so the command reads optional check and preview fields and shows none and a blank column until the list API supplies them. Adding them to the list API means touching src/http/api.ts, which I left out of this pull. I ran the new test file and the other CLI tests (53 pass) and tsc --noEmit. I did not run the full suite, and I did not try the command against a live forge.
- Listing: each line is
#N, the title cut to the terminal width, the author, the check (green,red,runningornone) and the preview address when there is one. The repository comes from--repo owner/name, or from the folder'soriginremote when that points at the configured workbench (https or scp-style ssh). - Flags:
--closedlists closed pulls.--jsonprints the API's answer unchanged. With nothing to list it printsNo open pulls.(orNo closed pulls.) and exits 0. - Exit codes: a folder that isn't a clone of the workbench, with no
--repo, prints one line and exits 2. A malformed--repoalso exits 2. An API error exits 1 and a missing sign-in exits 3.forge helpand the README list the command. - Safety: server text (title, author, preview) has control characters replaced before it reaches the terminal.
- Left out: paging past the first 100 pulls.
The code is src/cli/main.ts and src/cli/api.ts, plus a new src/cli/folder-repo.ts for the origin remote lookup. The tests are in test/cli-pulls.test.ts.
To try it, run forge pulls in a clone, or forge pulls --repo trevato/forge --closed from anywhere.
🤖 Generated with Claude Code
The failing check is fixed and pushed. I ran test/spawn-pipes.test.ts and test/cli-pulls.test.ts locally and both pass (9 tests). I did not re-run the full check.
The cause was the new git remote get-url origin call in src/cli/folder-repo.ts, which test/spawn-pipes.test.ts didn't know about. That test requires every Bun.spawn that pipes a stream to be listed with the reason it reads it. The new spawn reads stdout to the end with the exit and ignores stderr. I added it to the registry with that reason and changed nothing else.
Nothing about the command itself changed: forge pulls still shows none and a blank preview until the pulls list API returns check and preview.
Changes needed, one round:
- (first) The check and preview columns are the point of the issue, and against the real API they are always
noneand blank. I fetched the pulls list from the forge: items carry nocheckand nopreviewfield. The CLI reads fields that do not exist, and the tests pass only because the fake server invents them. Add both to the list API (src/http/api.ts, from the data the pull page already uses), with a test on the API, then make the CLI test use that real shape. Without that the issue is not delivered. - The row author reads
originAuthor ?? authorName, but the API already returnsauthor(src/http/api.ts:557 shape). Read that one field instead of re-deriving it. checkWordguesses at many state names (ok/passed/success/error/pending...). Once the API defines the field, accept exactly its values and drop the guesses.- Paging: 100 pulls are fetched and the rest are silently dropped. Either follow
next_cursor, or print a final line saying the list is cut. A silent cut is a wrong answer. - Both doors: the terminal UI (src/tty.ts / TUI) is not touched. Say in the README why the CLI is the only door, or give the TUI the same list. The README line is also one cryptic row; add a short sentence on folder detection and
--repo. - Tests: the single-quoted
closedcase does not check output; thevoid out/ unusedconsole.loginrunis dead code, remove it.
forge pulls now has real check and preview data to show, because the pulls list API carries them. I ran the CLI, API, OpenAPI, spawn-pipes, MCP-pulls and README tests (36 pass), and tsc is clean. I did not run the full suite or try the command against a live forge. scripts/fast.ts named 928 test files for this diff, so I picked the ones that cover the change; the forge's check runs the rest.
What changed, point by point
- Check and preview (item 1).
GET /api/v1/repos/:owner/:name/pullsitems now carryauthor,checkandpreview.checkisgreen,red,runningornone, from the samecheckWordthe pull page uses and for check runs only.previewis the pull's preview address when the repository is an app and a runtime is up, andnullotherwise, including for closed pulls. The newtest/api-pulls-check-preview.test.tsruns a real server with an app and checks the four words (including a stale head counting asnone), the author, the addresses and the closed case. - Author (item 2). The CLI reads
authorand no longer works it out itself. That field was not on the pulls list before, so I added it there with the sameoriginAuthor ?? authorNameexpression as the issues list. - Check values (item 3). The CLI no longer guesses state names. It prints the API's
checkstring as it comes, and an unknown value from an older forge gets the faint colour. - Paging (item 4).
ForgeApi.pullsfollowsnext_cursorthrough up to 20 pages of 100.--jsonprints the merged answer, which is the API's answer unchanged when there is one page. If the cap is hit, the last line saysThe list is cut after N pulls; the forge has more. - README (item 5). The README now explains folder detection (the
originremote on the configured workbench),--repo,--closedand--json. It says why this is a shell command only: the SSH terminal already has its own pulls screen, and the local UI is about working trees. I did not add a pulls screen to the local UI. - Tests (item 6).
test/cli-pulls.test.tsnow uses the real API shape. The--closedcase checks its output, a paging test covers both following the cursor and the cut line, and the deadconsole.log/void outis gone.
I left the "Done means reachable" test of two taps from the feed alone; this is a shell command and the API change reaches agents through the existing pulls endpoint.
The ENOENT … posix_spawn 'git' lines from api-list-shape.test.ts come from the sandbox lacking git for that test; they were not failures.
🤖 Generated with Claude Code
The CLI half is clean: argv-only git spawn, the origin is matched to the workbench host, server text is made inert before it reaches the terminal, the cursor is followed and a cut list says so, and the tests cover it. The API half has one gating miss, plus a mismatch that comes from the same lines. One round fixes both.
- (High, fix first) The preview address skips the environments gate.
src/apps/runtime.tsenvsForsays "every surface that shows a viewer an app's environments reads them here". For an app withaccess: private, it shows the environments only to viewers who passcanEnterApp. The newpreviewfield inGET /api/v1/repos/:owner/:name/pullscallsruntime.urlFordirectly. So any reader of the repository, including a guest on a public repo whose app is private, now gets every preview address that the pull page's preview card (previewCard→appView→envsFor) withholds from them. Fix: in the list handler, callenvsFor(ctx.app, ctx.auth.user, record)once and setpreviewtoruntime.urlFor(record, env)only when that list holdspreviewEnv(item.number), otherwisenull. Add a test with a private-access app on a public repo: a guest or plain reader getspreview: nullfor every item, and a writer gets the address.
- (Medium) An address is printed for a preview that does not exist. The same fix covers this. Today any open pull of an app gets an address, even when nothing was ever deployed for it. The test asserts that for
bare(no run at all). The pull page says there is no preview yet, whileforge pullsprints a URL that leads nowhere. Once the field comes fromenvsFor, change thebareexpectation tonull. Seed an environment, or use a runtime status stub, for the one that should carry an address.
- (Low) The check word disagrees with the other doors.
src/apps/door_pulls.ts(behind the SSH pulls screen,tty_pulls.ts, and the app door) takes the latest run of kinddeployorcheck. The API passes["check"]only, so for an app repo the same pull can readgreenon the door andnoneinforge pulls. Pick one rule and use it in both places. The simplest islatestRunsForIssues(db, ids)with its defaultCHECK_KINDS, if that matches what the pull page shows. Then add a test where the latest run is a deploy.
Everything else, including the README, help, exit codes and the spawn-pipes registry entry, looks right to me.
Both of @sentinel's API findings are fixed and pushed. I ran the related test files (21 pass) and tsc is clean. I did not run the full suite; the forge's check does that.
Preview gate (finding 1). The pulls list handler in src/http/api.ts now calls envsFor(ctx.app, ctx.auth.user, record) once. A pull gets a preview address only when that list holds its pull-N environment, and only while the pull is open and unmerged. For a private-access app, a guest or reader who can't enter it gets null for every pull, as the pull page's preview card does.
Nonexistent previews (finding 2). A pull with no environment now reads preview: null instead of an address that leads nowhere.
Check word (finding 3). The list now uses latestRunsForIssues with its default kinds (deploy or check), the same rule as door_pulls.ts. The same pull now reads the same word in forge pulls, the SSH pulls screen and the app door.
Tests (test/api-pulls-check-preview.test.ts):
- A pull whose latest run is a deploy reads
red, and so do the door and the CLI. - With a private app on a public repository, a guest and a plain reader get
nullfor every item, while the check words still show. The owner gets the address. - On a public app, each pull's
previewis null exactly when the runtime holds no environment for it. - A closed pull has no address.
I do not assert a fixed null for a pull with no run. The runtime starts its own deploys for pulls it sees, so a "bare" pull may legitimately hold an environment by the time the list is read. The test checks the address against what the runtime holds instead.
Earlier push. My first push for this round went out with a type error and a flaky test, and I fixed both in the pushes after it. The test's runs now use ids that sort last, so the runtime's own deploys of the made-up heads can't outrank them.
🤖 Generated with Claude Code
Round 2, reviewing the changes since 51123b92 (only src/http/api.ts and test/api-pulls-check-preview.test.ts changed).
My earlier asks:
- (High) The preview address skipped the environments gate: met. The list handler now calls
envsFor(ctx.app, ctx.auth.user, record)once and gives an address only when that list holdspull-N. For a private-access app that runscanEnterApp, the same check the pull page's preview card uses. The new test covers a private app on a public repo: a guest and a plain reader getnullfor every item, and the owner gets the address. - (Medium) An address was printed for a preview that does not exist: met. A pull with no environment now reads
null, and the test compares each item against what the runtime actually holds. - (Low) The check word disagreed with the other doors: met.
latestRunsForIssuesnow uses its default kinds (deploy or check), the same rule asdoor_pulls.ts. A test covers a pull whose latest run is a deploy and readsred.
What I checked: at head bca522fc, test/api-pulls-check-preview.test.ts and test/cli-pulls.test.ts pass (12 tests). On the preview, / answers 200. A guest asking for the pulls of a repository they cannot see gets 404 not_found, so the new fields do not widen what a guest can read. The CLI half (an argv-only git spawn, inert server text, following the cursor, the cut line) was passed last round and is not reopened.
Non-blocking: serveEnv in the API test writes into the runtime's private envs map through a cast. That ties the test to the runtime's internals. A small test seam on AppsRuntime would be sturdier, but it is not a reason to hold the pull.
Approved.
I merged main into the branch and pushed it. The three conflicted files (README.md, src/cli/api.ts, src/cli/main.ts) are resolved and the merge is clean. tsc is clean and 69 tests pass across the CLI suites, the new API test, spawn-pipes and hollow-cli. I did not run the whole suite; the forge's check does that.
The conflicts were all with forge asks, which landed on main after my branch:
- Both commands stay.
forge pullsandforge askseach keep theirForgeApimethod, command function,casein the dispatch, line inforge helpand README entry. - Duplicates removed.
mainnow exportsinertfromsrc/cli/out.tsand imports the width helpers itself. I dropped my copy ofinertand my second import, soforge pullsuses the shared ones.
🤖 Generated with Claude Code
Round 3. The only change since bca522fc is a04f6901, which merges main in to resolve conflicts with forge asks (#7381). I compared this pull's own lines before and after the merge. README.md and src/cli/api.ts are unchanged. In src/cli/main.ts the only difference is that the private inert copy, the duplicate width.ts import and the repo entry in VALUE_FLAGS are gone, because main now provides all three. inert is exported from src/cli/out.ts with the same control-character regex, so server text is still made safe before it is printed. src/http/api.ts, src/cli/folder-repo.ts and the tests are untouched since my last approval.
My earlier asks are still met:
- The preview gate goes through
envsFor: met. - No address is given for a preview that does not exist: met.
- The check word uses the same rule as
door_pulls.ts: met.
What I checked at a04f6901: test/cli-pulls.test.ts, test/api-pulls-check-preview.test.ts and test/spawn-pipes.test.ts pass (13 tests), and tsc --noEmit is clean. On the preview, / answers 200. A guest asking for the pulls of a repository they cannot see gets 404 not_found, so a guest can read nothing new.
Non-blocking, from last round: serveEnv in the API test writes into the runtime's private envs map through a cast. A small test seam would be sturdier.
Approved.
forge pullsnow lists a repository's open pulls, but the check and preview columns will be empty against today's forge.GET /api/v1/repos/:owner/:name/pullsreturns neither field, so the command reads optionalcheckandpreviewfields and showsnoneand a blank column until the list API supplies them. Adding them to the list API means touchingsrc/http/api.ts, which I left out of this pull. I ran the new test file and the other CLI tests (53 pass) andtsc --noEmit. I did not run the full suite, and I did not try the command against a live forge.#N, the title cut to the terminal width, the author, the check (green,red,runningornone) and the preview address when there is one. The repository comes from--repo owner/name, or from the folder'soriginremote when that points at the configured workbench (https or scp-style ssh).--closedlists closed pulls.--jsonprints the API's answer unchanged. With nothing to list it printsNo open pulls.(orNo closed pulls.) and exits 0.--repo, prints one line and exits 2. A malformed--repoalso exits 2. An API error exits 1 and a missing sign-in exits 3.forge helpand the README list the command.The code is
src/cli/main.tsandsrc/cli/api.ts, plus a newsrc/cli/folder-repo.tsfor theoriginremote lookup. The tests are intest/cli-pulls.test.ts.To try it, run
forge pullsin a clone, orforge pulls --repo trevato/forge --closedfrom anywhere.🤖 Generated with Claude Code
Closes #7370