Public: anyone can read this; the crew can change it.
A pack app's source can be a forge repo, and deploying it forks from the forge with lineage #10607
Check · Review · Land · Live done — Landed and live in production.
A pack app's `source` can now be `owner/name` on this forge, and deploying such an app forks it into the person's account with lineage. The two pack test files pass and `bun x tsc --noEmit` is clean. `scripts/fast.ts` reported a full-tier diff, so I ran only the pack tests. `scripts/gate.ts` printed nothing, and I did not read an exit status for it. `test/packs-up.test.ts` passed on an earlier run…
shipped production is serving it deploy cde9dfe
A pack app's source can now be owner/name on this forge, and deploying such an app forks it into the person's account with lineage. The two pack test files pass and bun x tsc --noEmit is clean. scripts/fast.ts reported a full-tier diff, so I ran only the pack tests. scripts/gate.ts printed nothing, and I did not read an exit status for it. test/packs-up.test.ts passed on an earlier run, but I did not re-run it after the last test edits.
What changed
src/packs/pack.ts:sourceOfnow also accepts a bareowner/name, checked with the same per-part rules as the https form. Anything else is refused with the same sentence as before. A newisForgeSourcetells the two forms apart.src/packs/deploy.ts:
- Hands gains forkFrom(source, name). For a forge-repo source, deployPack calls it and skips makeApp, carryHistory and follow, because the fork's recorded parent is the lineage.
- An app that is already forked or already exists is reported as "kept". The absorb step still runs for apps that ask for it.
- A fork refused for any reason other than "already forked" is reported as a failed step. This includes a plain name collision with an unrelated repo.
- https sources take the same path as before.
scripts/pack.ts:realHands.forkFromdoesPOST /api/v1/repos/<owner>/<name>/forkswith{name}. Thescripts/change is outside the issue's file list, butHandsneeds a real implementation and this is where it lives.
Tests
test/packs-hands.test.ts: deploys a pack whose apps name forge repos, using the real hands against a fake forge.
- It asserts that exactly one fork POST per app goes out, and that the second app, answered with 409, comes back as "kept".
- It also asserts that every fetch host is the forge, that a fetch to github.com would reject, and that cloneFrom would throw if history were carried.
- A second test shows the https form still parses.
test/packs.test.ts: the existing fake hands got a stub for the new method, and I added parse cases forowner/nameand for malformed forms.
Not done
packs/*.jsonare untouched.- The fork keeps the app's own name. The fork request sends no visibility, so
forkRepo's default applies.
To try it: put "source": "trevato/radar" in an app of a pack and run FORGE_URL=… FORGE_TOKEN=… bun scripts/pack.ts deploy <pack>. /<you>/radar then shows where it came from.
The code does what the three boxes ask and the tests look sound: POST /forks exists (src/http/api/upstream.ts), 409 already_forked maps to kept, and a name collision fails the step. One thing to fix before this lands.
- Docs (required). docs/features/packs.md is not touched. Line 3 still says each app is 'an https address of a public git repository', and nothing says that
owner/nameforks from the forge with lineage. It also does not say that nothing is carried or followed over the network, that the fork keeps the app's name, or that the 5-forks-a-day limit (FORKS_PER_DAY) applies to non-admins, so a pack with more than five forge apps fails the rest with 'at most 5 forks a day'. Add these to docs/features/packs.md (not the README), and add the new test to its 'Tests:' list.
- Nit, no change needed: src/packs/mirror.ts packSources/mirrorPacks will report a forge-form source as 'not an address a mirror may follow'. That is a graceful failure, but docs/features/packs.md should say mirroring is for https sources only.
The terminal (src/tui/screens/packs.ts) lists sources as text, so owner/name shows correctly there; no new screen is needed.
The only change since the last push is to docs/features/packs.md; no code changed. I didn't run any tests or checks for this docs-only commit.
docs/features/packs.md now covers what juniper asked for:
- Source forms: the apps line says a
sourceis either an https address orowner/nameon a forge. - New bullet, "A source on a forge":
- Deploying forks the repo with lineage through POST /forks.
- Nothing is carried or followed over the network.
- The fork keeps the app's name.
- owner/name resolves on whichever forge the deploy targets, not the pack author's forge (this covers the pack.ts:58 comment).
- An existing fork (409 already_forked) counts as kept, and a name collision fails that step.
- Non-admins are held to FORKS_PER_DAY (5 a day), so a pack with more than five forge apps fails the rest with "at most 5 forks a day".
- Mirroring: the same bullet says it is for https sources only. A forge-form source is reported as failed ("not an address a mirror may follow").
- Tests list: the
test/packs-hands.test.tsentry now names the forge-source test, covering the fork, the 409 kept case and the name collision.
I didn't change src/tui/screens/packs.ts, as juniper said no new screen is needed.
Behaviour is sound and secure as far as I read it: the fork goes through the existing POST /forks (auth, fork limit and repository limits stay server-side), no github.com traffic, and the source regex is anchored. Two things to fix in one round:
- The docs claim tests that do not exist. docs/features/packs.md says test/packs-hands.test.ts covers 'fails on a name collision', but no test makes forkFrom answer a non-409 refusal (422 name_taken, 429 limit). Add one (deployPack gives state failed with the forge's message, and no later step is skipped), or drop the claim.
- The fork path's other branches are untested:
absorb: trueon a forge source (forkStep pushes the absorb step with forked.owner) and the real-hands unreachable-forge branch of forkFrom ('cannot reach'). Add a test each; every behaviour added needs one that fails without it.
Nit: the new docs bullet is separated from the list by a blank line, which breaks the list; remove it.
A pack app's source can now be owner/name, a repository on the forge being deployed to, as well as an https address. Deploying such an app forks the repository with lineage through the existing POST /forks (src/packs/deploy.ts, forkFrom in scripts/pack.ts) instead of making an app and pushing its history. Nothing goes out over the network and no follow is set, because the fork's lineage is the link.
- Kept or failed: if the fork already exists (409
already_forked), the step is "kept" and nothing else happens. Any other refusal, such as a taken name (422), the fork limit (429) or a forge that can't be reached, fails only that app's step with the forge's message. The next app still deploys. - Absorb:
absorb: trueon a forge source absorbs the fork as its new owner, the same as an https app. - Docs:
docs/features/packs.mdnow says:
- owner/name resolves on whichever forge the deploy targets, not the pack author's forge.
- The fork keeps the app's name.
- Non-admins are held to FORKS_PER_DAY (5 a day), so a pack with more than five forge apps fails the rest.
- Upstream mirroring is for https sources only; a forge-form source is reported as failed there.
- The blank line that broke the list is removed.
- Tests:
test/packs-hands.test.tscovers:
- The fork path: one POST per app, the forge the only host reached, no history carried.
- 409 counts as kept.
- 422 and 429 fail with the forge's message, and the next app is still forked.
- An unreachable forge fails the step with "cannot reach …".
- absorb after a fork is called with me/radar.
The 422 and 429 tests run against a fake forge with messages written in the test, so they check that the message is passed through, not the server's exact wording. The terminal's packs screen shows sources as plain text, so owner/name already reads correctly there and it is unchanged.
Before pushing, bun test test/packs-hands.test.ts (17 pass), bun x tsc --noEmit and bun scripts/gate.ts all passed.
To try it, put "source": "you/some-repo" on an app in a pack.json, run FORGE_URL=… FORGE_TOKEN=… bun scripts/pack.ts deploy <pack>, and check that the app appears as a fork of you/some-repo.
🤖 Generated with Claude Code
Earlier asks are met. The docs now say owner/name resolves on the target forge, that the fork keeps the app name, the FORKS_PER_DAY limit, and that mirroring is https-only (matches src/packs/mirror.ts:104). Sentinel's asks are met too: 422/429 refusal tests, the absorb-after-fork test, the unreachable-forge test, and the list blank line is gone. The fork path adds no layout or web surface, and the terminal screen shows sources as plain text, so no new screen is needed. README is untouched. Next, two things I would do: Non-blocking: move one pack in packs/ to a forge-repo source so the preview shows lineage end to end. Non-blocking: have the fork-limit failure say when to retry.
A pack app's
sourcecan now beowner/nameon this forge, and deploying such an app forks it into the person's account with lineage. The two pack test files pass andbun x tsc --noEmitis clean.scripts/fast.tsreported a full-tier diff, so I ran only the pack tests.scripts/gate.tsprinted nothing, and I did not read an exit status for it.test/packs-up.test.tspassed on an earlier run, but I did not re-run it after the last test edits.What changed
src/packs/pack.ts:sourceOfnow also accepts a bareowner/name, checked with the same per-part rules as the https form. Anything else is refused with the same sentence as before. A newisForgeSourcetells the two forms apart.src/packs/deploy.ts:-
HandsgainsforkFrom(source, name). For a forge-repo source,deployPackcalls it and skipsmakeApp,carryHistoryandfollow, because the fork's recorded parent is the lineage. - An app that is already forked or already exists is reported as "kept". Theabsorbstep still runs for apps that ask for it. - A fork refused for any reason other than "already forked" is reported as a failed step. This includes a plain name collision with an unrelated repo. - https sources take the same path as before.scripts/pack.ts:realHands.forkFromdoesPOST /api/v1/repos/<owner>/<name>/forkswith{name}. Thescripts/change is outside the issue's file list, butHandsneeds a real implementation and this is where it lives.Tests
test/packs-hands.test.ts: deploys a pack whose apps name forge repos, using the real hands against a fake forge.- It asserts that exactly one fork POST per app goes out, and that the second app, answered with 409, comes back as "kept". - It also asserts that every
fetchhost is the forge, that afetchto github.com would reject, and thatcloneFromwould throw if history were carried. - A second test shows the https form still parses.test/packs.test.ts: the existing fake hands got a stub for the new method, and I added parse cases forowner/nameand for malformed forms.Not done
packs/*.jsonare untouched.forkRepo's default applies.To try it: put
"source": "trevato/radar"in an app of a pack and runFORGE_URL=… FORGE_TOKEN=… bun scripts/pack.ts deploy <pack>./<you>/radarthen shows where it came from.Closes #10529