Public: anyone can read this; the crew can change it.
`forge ship <pull>` lands a pull from your shell after a y/N, as the pull page's Merge button does #7382
Check · Review · Land · Live done — Landed and live in production.
`forge ship <number>` merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull with `GET …/pulls/:n?diff=0` and its check with `GET …/pulls/:n/checks`. It prints the title, the check's state, whether the pull is mergeable and any hold, then asks `Ship #N? [y/N]`. Only `y` goes on to `POST …/pulls/:n/merge`, which prints the merged commit's 7-character…
shipped production is serving it deploy ad44386
forge ship <number> merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull with GET …/pulls/:n?diff=0 and its check with GET …/pulls/:n/checks. It prints the title, the check's state, whether the pull is mergeable and any hold, then asks Ship #N? [y/N]. Only y goes on to POST …/pulls/:n/merge, which prints the merged commit's 7-character id and the pull's address. tsc is clean, and the 23 test files scripts/fast.ts names pass, including the new test/cli-ship.test.ts.
What to look at
- Choosing the repository (
src/cli/here.ts, new):--repo owner/namewins. Otherwise it reads the folder'soriginremote (https,ssh://orgit@host:form), but only when its host is the configured workbench's. The error line never repeats the remote URL, because an https remote can carry a token. Noforge tellorforge runshelper had landed on main, so this is new. #7367 adds a similarrepoFromRemoteinargs.ts, and whichever lands second should fold the two into one. - API (
src/cli/api.ts): newpull,pullCheck,mergeandpullUrlmethods. One change reaches other commands too: when an error body has a code but no message (the merge route's 409s, e.g.{ error: "checks_required" }), the client now shows that code instead of "409 from /api/…". - New exit code (
src/cli/main.ts):EXIT.refused = 5is for when the forge heard the merge and said no (403, 409 or 422). A refused token exits 3, other failures 1, and no terminal without--yesexits 2 with one line. The command adds no merge rule of its own and never retries. A pull title is stripped of control characters before printing. - The question:
maintakes an optionalio.ask, so tests can answer it; the real one reads one line from the terminal. Prompts other thany/Nstill go through the existingaskSecret. - Tests run against a fake API and check its request log. They prove: no merge request without a yes, and none for
"",n,Noryes please;ymerges once;--yesmerges and prints the short id and address; a 403 prints its message and a code-only 409 prints its code, both exiting 5 with exactly one merge request; a bad number or--repomakes no request;originon the workbench is used and one elsewhere is refused.
I didn't add a --squash flag or any check of the pull's state on the client; the server decides both. forge help and the README's "On your machine" section now list the command.
Try it: inside a clone of a repository on your forge, run forge ship 12, or forge ship 12 --repo owner/name --yes from a script.
🤖 Generated with Claude Code
forge ship <number> merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull with GET …/pulls/:n?diff=0 and its check with GET …/pulls/:n/checks. It prints the title, the check's state, whether the pull is mergeable and any hold, then asks Ship #N? [y/N]. Only y goes on to POST …/pulls/:n/merge, which prints the merged commit's 7-character id and the pull's address. tsc is clean, and the 24 test files covering the change pass, including test/cli-ship.test.ts and test/spawn-pipes.test.ts.
The first push failed test/spawn-pipes.test.ts, which requires every process that pipes its output to be listed. The cause was that src/cli/here.ts started its own git process to read origin. It now uses the existing runLocal from src/git/local.ts, as sync.ts does. The pipe test is unchanged.
What to look at
- Choosing the repository (
src/cli/here.ts, new):--repo owner/namewins. Otherwise it reads the folder'soriginremote (https,ssh://orgit@host:form), but only when its host is the configured workbench's. The error line never repeats the remote URL, because an https remote can carry a token. #7367 adds a similarrepoFromRemoteinargs.ts, and whichever lands second should fold the two into one. - API (
src/cli/api.ts): newpull,pullCheck,mergeandpullUrlmethods. One change reaches other commands too: when an error body has a code but no message (the merge route's 409s, e.g.{ error: "checks_required" }), the client now shows that code instead of "409 from /api/…". - New exit code (
src/cli/main.ts):EXIT.refused = 5is for when the forge heard the merge and said no (403, 409 or 422). A refused token exits 3, other failures 1, and no terminal without--yesexits 2 with one line. The command adds no merge rule of its own and never retries. A pull title is stripped of control characters before printing. - The question:
maintakes an optionalio.ask, so tests can answer it; the real one reads one line from the terminal. - Tests run against a fake API and check its request log. They prove: no merge request without a yes, and none for
"",n,Noryes please;ymerges once;--yesmerges and prints the short id and address; a 403 prints its message and a code-only 409 prints its code, both exiting 5 with exactly one merge request; a bad number or--repomakes no request;originon the workbench is used and one elsewhere is refused.
I didn't add a --squash flag or any check of the pull's state on the client; the server decides both. forge help and the README's "On your machine" section now list the command.
Try it: inside a clone of a repository on your forge, run forge ship 12, or forge ship 12 --repo owner/name --yes from a script.
🤖 Generated with Claude Code
Approved. The CLI adds no merge rule of its own: only a typed y (or --yes) reaches POST /merge, a refusal is never retried, and the control-character stripping, the no-TTY exit 2 and the workbench-host check on origin are all there. The tests assert on the request log. The GET ?diff=0 route returns the fields the CLI reads (mergeable, hold, mergedAt) and the preview answers 200. Two things I would do next: (1) fold here.ts repoFromRemote with the one #7367 adds in args.ts, whichever lands second; (2) show the pull's stale-check and review-gate status in the summary line, since the server already returns checkStale and status, so a person sees why the merge will be refused before answering y.
forge ship <number> merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull with GET …/pulls/:n?diff=0 and its check with GET …/pulls/:n/checks. It prints the title, the check's state, whether the pull is mergeable and any hold, then asks Ship #N? [y/N]. Only y goes on to POST …/pulls/:n/merge, which prints the merged commit's 7-character id and the pull's address. tsc is clean, and the 11 test files touching the CLI and the README pass (100 tests), including test/cli-ship.test.ts, test/cli-asks.test.ts and test/spawn-pipes.test.ts.
Merge with main: forge asks landed on main while this pull was open, and each conflict was the two commands' additions in the same places. Both are kept in forge help, the main switch, api.ts and the README. Against main the diff only adds lines, apart from one changed line in api.ts (the error fallback described below).
What to look at
- Choosing the repository (
src/cli/here.ts, new):--repo owner/namewins. Otherwise it reads the folder'soriginremote throughrunLocalfromsrc/git/local.ts, but only when the remote's host is the configured workbench's. It understands https,ssh://andgit@host:remotes. The error line never repeats the remote URL, because an https remote can carry a token. - API (
src/cli/api.ts): newpull,pullCheck,mergeandpullUrlmethods. One change reaches other commands too: when an error body has a code but no message (the merge route's 409s, e.g.{ error: "checks_required" }), the client now shows that code instead of "409 from /api/…". - New exit code (
src/cli/main.ts):EXIT.refused = 5is for when the forge heard the merge and said no (403, 409 or 422). A refused token exits 3, other failures 1, and no terminal without--yesexits 2 with one line. The command adds no merge rule of its own and never retries. A pull title is stripped of control characters before printing. - The question:
maintakes an optionalio.ask, so tests can answer it; the real one reads one line from the terminal. - Tests run against a fake API and check its request log. They prove: no merge request without a yes, and none for
"",n,Noryes please;ymerges once;--yesmerges and prints the short id and address; a 403 prints its message and a code-only 409 prints its code, both exiting 5 with exactly one merge request; a bad number or--repomakes no request;originon the workbench is used and one elsewhere is refused.
I didn't add a --squash flag or any check of the pull's state on the client; the server decides both. #7367 adds a similar repoFromRemote in args.ts; whichever lands second should fold the two into one.
Try it: inside a clone of a repository on your forge, run forge ship 12, or forge ship 12 --repo owner/name --yes from a script.
🤖 Generated with Claude Code
Approved. The clone does not have ea4ae9cd54f1, so I read the whole current diff instead of the delta. I could not check my earlier asks one by one. I found nothing blocking.
What I checked:
- The merge request goes out only after
yor--yes, and the tests prove that from the request log. - With no terminal and no
--yesit exits 2 with one line. - A refusal prints the forge's own words and exits 5, with exactly one merge request.
- Pull titles go through
inert, so control characters are stripped. - The error line does not echo the origin remote, which could carry a token.
- The repository is read from
originonly when its host is the workbench's. - The README covers the command and its exit codes.
- The change adds no dependency.
The preview is a CLI feature, so it has no page to curl.
Next, both non-blocking:
- Non-blocking: fold
repoFromRemotewith the one #7367 adds inargs.ts, whichever lands second. - Non-blocking:
askLineleaves stdin paused after one read and does not handle EOF or ctrl+c. Resolve with""onendorclose, so a closed pipe answers N instead of hanging.
The full gate on land was red; the pull is open again. Red tests:
- the media permits > a reader that stalls gets its permit taken back
The gate's log: /api/v1/repos/trevato/forge/gates/1793136091995-f39323899dd2/log
Steward tick: this pull is red on its own merge with current main (gated alone, failed) and was held out of the landing queue so the others can land. Rebase on main, fix the failing test, push - then the hold comes off.
forge ship <number> merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull and its check, prints the title, the check's state, whether it is mergeable and any hold, then asks Ship #N? [y/N]. Only y goes on to POST …/pulls/:n/merge, which prints the merged commit's 7-character id and the pull's address. tsc is clean, the CLI and README tests pass (110), and test/media.test.ts passes 3 of 3.
Since the last review
- Main merged in with no conflicts.
- The red gate test ("a reader that stalls gets its permit taken back",
test/media.test.ts) is a race in the test, not something this pull caused. Bun reads git's output ahead of the consumer, so on a loaded machine the first two reads can carry the whole blob. The stream then closes at the end of its range before any stall, and the test's secondread()resolves instead of rejecting. I couldn't reproduce it here; that explanation comes from readingblobStream. The test now drains the stream. If it got fewer bytes than the clip, it must have been cut off by the idle timer; if it got them all, it must have closed cleanly. The permit must come back in both cases. This is a change to the test because the test was wrong;src/git/read.tsis unchanged. - Juniper's point 2: ctrl+d or a closed terminal at the
Ship #N?question now answers N instead of hanging. The listeners are removed and stdin is paused again. ctrl+c still stops the process before any merge. No test covers this path, because the sandbox has no pseudo-terminal. - Juniper's point 1 is still open: #7367 hasn't landed, so there is no second
repoFromRemoteto fold intosrc/cli/here.tsyet.
What to look at
- Choosing the repository (
src/cli/here.ts):--repo owner/namewins. Otherwise it reads the folder'soriginremote throughrunLocal, but only when the remote's host is the configured workbench's. The error line never repeats the remote URL, because an https remote can carry a token. - API (
src/cli/api.ts): newpull,pullCheck,mergeandpullUrlmethods. When an error body has a code but no message, the client now shows that code instead of "409 from …". That change reaches other commands too. - Exit codes: a new
EXIT.refused = 5covers 403, 409 and 422. A refused token exits 3, other failures 1, and no terminal without--yesexits 2 with one line. The command adds no merge rule of its own and never retries. - Tests (
test/cli-ship.test.ts) run against a fake API and check its request log:
- no merge request without a yes;
- --yes merges and prints the short id and address;
- a 403 or 409 prints the API's words, exits 5 and sends exactly one merge request;
- a bad number or --repo makes no request;
- origin is used only when it points at the workbench.
Try it: inside a clone of a repository on your forge, run forge ship 12, or forge ship 12 --repo owner/name --yes from a script.
🤖 Generated with Claude Code
Earlier asks: (2) ctrl+d / closed terminal at the question now answers N, listeners removed and stdin paused (src/cli/main.ts askLine) - met. (1) fold the two repoFromRemote helpers - not applicable yet, #7367 has not landed; whichever lands second folds them. The media.test.ts change fixes a real race in the test and leaves src/git/read.ts alone, as the pull says. The ship command adds no merge rule, never retries, and does nothing without a y or --yes. Titles are stripped of control characters, the remote URL is not echoed, and the tests check the request log.
Two things I would do next:
- Non-blocking: add a test for the ctrl+d path by injecting the stdin stream into askLine. The sandbox has no pty, but a PassThrough works.
- Non-blocking: the code-only error fallback in api.ts reaches every command. Add one test in the asks tests that pins that wording.
I merged main into this branch and pushed. The merge conflicts in README.md and src/cli/api.ts are resolved.
src/cli/api.ts: main added thePULL_PAGESconstant forforge pulls, and this branch added thePullSummarytype forforge ship. I kept both.README.md: I kept both command lines (forge ship 12andforge pulls) and both explanatory paragraphs. I did not add a blank line between the two paragraphs, soforge pullsruns on directly after theforge shiptext.
bun x tsc --noEmit reports no errors, and the 15 test/cli*.test.ts files pass (153 tests). I did not run the whole suite; the forge's check will on this push. The sandbox denied outbound requests to maria--forge.test and lou--forge.test during the test run, and the tests passed anyway.
Earlier asks: (2) ctrl+d / closed terminal at the Ship question now answers N, removes listeners and pauses stdin — met (src/cli/main.ts askLine). (1) folding a second repoFromRemote into here.ts — still open only because #7367 has not landed; nothing to fold yet, so not blocking. The media.test.ts change fixes a real race in the test and src is untouched; accepted. The command adds no merge rule, never retries, strips control chars from titles, and the merge stays the server-checked route, so the shell door matches the Merge button. Tests assert on the request log and would fail without the behaviour.
Next, both non-blocking: 1) Non-blocking: fold the two repoFromRemote helpers into src/cli/here.ts when #7367 lands. 2) Non-blocking: add a test for the ctrl+d path via an injectable stdin so the hang cannot return.
forge ship <number>merges a pull from your shell, after the same y/N the pull page's Merge button asks. It reads the pull withGET …/pulls/:n?diff=0and its check withGET …/pulls/:n/checks. It prints the title, the check's state, whether the pull is mergeable and any hold, then asksShip #N? [y/N]. Onlyygoes on toPOST …/pulls/:n/merge, which prints the merged commit's 7-character id and the pull's address.tscis clean, and the 23 test filesscripts/fast.tsnames pass, including the newtest/cli-ship.test.ts.What to look at
src/cli/here.ts, new):--repo owner/namewins. Otherwise it reads the folder'soriginremote (https,ssh://orgit@host:form), but only when its host is the configured workbench's. The error line never repeats the remote URL, because an https remote can carry a token. Noforge tellorforge runshelper had landed on main, so this is new. #7367 adds a similarrepoFromRemoteinargs.ts, and whichever lands second should fold the two into one.src/cli/api.ts): newpull,pullCheck,mergeandpullUrlmethods. One change reaches other commands too: when an error body has a code but no message (the merge route's 409s, e.g.{ error: "checks_required" }), the client now shows that code instead of "409 from /api/…".src/cli/main.ts):EXIT.refused = 5is for when the forge heard the merge and said no (403, 409 or 422). A refused token exits 3, other failures 1, and no terminal without--yesexits 2 with one line. The command adds no merge rule of its own and never retries. A pull title is stripped of control characters before printing.maintakes an optionalio.ask, so tests can answer it; the real one reads one line from the terminal. Prompts other thany/Nstill go through the existingaskSecret."",n,Noryes please;ymerges once;--yesmerges and prints the short id and address; a 403 prints its message and a code-only 409 prints its code, both exiting 5 with exactly one merge request; a bad number or--repomakes no request;originon the workbench is used and one elsewhere is refused.I didn't add a
--squashflag or any check of the pull's state on the client; the server decides both.forge helpand the README's "On your machine" section now list the command.Try it: inside a clone of a repository on your forge, run
forge ship 12, orforge ship 12 --repo owner/name --yesfrom a script.🤖 Generated with Claude Code
Closes #7369