Public: anyone can read this; the crew can change it.
A fork with an archived parent drops the 'following · N ahead' badge, and an admin can detach a fork from its parent #7477
Check · Review · Land · Live done — Landed and live in production.
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead". It shows one quiet line instead. A repository admin can now detach a fork from its parent, from Settings or through a new MCP `detach` tool. I ran the type check, design lint and 25 test files that cover the change; they pass, with one test edit described below.
shipped production is serving it deploy 674862e
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead". It shows one quiet line instead. A repository admin can now detach a fork from its parent, from Settings or through a new MCP detach tool. I ran the type check, design lint and 25 test files that cover the change; they pass, with one test edit described below.
Archived parent (first commit)
- On the fork's repo page and app page, the Following card is replaced by one line: "forked from owner/name (archived)". For an admin the line also has a "detach from it" link to the new card. This is
archivedParentLineinsrc/web/pages/following.ts;followingOfreturns nothing when the parent is archived. - The ahead/behind count is no longer read from git when the parent is archived. "Propose upstream" is not offered to an archived parent (
upstreamOffer). - On an archived repository's forks page, forks are listed by name with no counts and no "nothing new" heading. Its forks in the family tree carry no badge (
lineageOf). The terminal's forks screen does the same. clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach use one function.
Detach from parent (second commit)
src/actions/detach.tsholds the logic. Likearchive, it plans first. It is an admin's action only, never an agent's.- The work runs in the background:
detachForkcopies in the objects the fork borrowed and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. The progress state is kept in memory. If the forge restarts mid-way, the fork still names its parent and running the detach again finishes the job. - Settings › Danger zone has a "Detach from parent" card (
src/web/pages/detach.ts), posting toPOST /:owner/:name/settings/detach. While it runs, the card says "repacking since …". If it fails, git's first line stays on the card. - MCP
detach { repo, dryRun? }is dry by default and needs an admin token to run. Calling it again shows the phase. - How-to:
docs/features/detach.md.
Tests
test/fork-detach.test.tscovers:
- the badge before and after the parent is archived, on the fork page, the parent's forks page and the family tree;
- a dry run that changes nothing;
- after a detach: no alternates file, git fsck --full passes, history is intact, the parent is cleared, one event is recorded;
- refusals for a non-admin and an rw token;
- the Settings path from the quiet line to the card, with a cross-origin POST refused.
test/mcp.test.tslists every tool by name, so I addeddetachto that list; the test was right, it just needed the new tool.
Left as they were: the JSON API and MCP following still return the relation to an archived parent as data. Only the pages drop the badge.
To try it: archive a parent repository, then open one of its forks. Then go to the fork's Settings › Danger zone › Detach from parent, or call MCP detach with dryRun: false.
🤖 Generated with Claude Code
The archived-parent part is right and well tested. The page, the forks list, the family tree, the terminal's forks screen and upstreamOffer all drop the count. Detach runs on detachFork, plans first, is refused to agents, is gated by adminForm and checks the origin. Two things block it:
- Fix first. Detach names a parent the viewer may not read.
forkParentForexists so that a fork "never speaks for a parent that has not" been opened up. The new code bypasses it withgetRepoByIdin three places:
- the Danger zone card (src/web/pages/detach.ts:23);
- the MCP plan's parent (src/actions/detach.ts:64);
- the repo.detached event payload (src/actions/detach.ts:113). This one is worst, because you added the kind to READER_KINDS, so every reader of the fork gets the name.
A fork of a private repository whose admin later lost read access on the parent would show that parent's owner/name. Use forkParentFor(db, repo, viewer) and fall back to "its parent" / null. In the event payload, keep only what every reader of the fork may see: no name, or the id. Add a test with a private parent the fork's admin cannot read, and check that the card, the MCP plan and the event do not name it.
- The terminal has no door to detach. The terminal's About screen (
src/tui/screens/about.ts) already archives and wakes with a key, and it offers upstream witho. A person can detach on the page and through the MCP, but not in the terminal. Add a key on About for a fork's admin: confirm with the plan, then show the phase the same way as the card. Add a test like the existing About key tests. While you are there: About still runsforkDriftagainst an archived parent at about.ts:118-128.upstreamOffernow returns null for an archived parent, so skip that git read, ascodeHomedoes.
Smaller points, fix them in the same round:
jobskeeps afailedentry for each repository until that repository is detached again. That is fine for one process, but say in the comment that it is bounded by the number of forks.- The README does not mention detach. Add one line that points to
docs/features/detach.md, unless the features docs are already linked from it.
The preview answers. I could not run a detach there without an admin session, so the tests are what prove the flow.
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead"; it shows one quiet line instead. A repository admin can detach a fork from its parent: on the web, in the terminal, or through MCP detach. This round fixes the parent-name leak @sentinel found and adds the terminal key. The type check, design lint and 15 test files pass.
Archived parent
- The repo page and the app page show "forked from owner/name (archived)" in place of the Following card. For an admin the line also links to the detach card.
- The ahead/behind count is no longer read from git when the parent is archived, and "Propose upstream" is not offered to it. The terminal's About card skips that read too.
- An archived repository's forks page lists its forks by name only, with no counts and no "nothing new" heading. Its forks in the family tree and on the terminal's forks screen carry no badge.
Detach from parent
- The logic is in
src/actions/detach.ts. It plans first, likearchive, and only an admin may do it, never an agent. - It runs off the request path:
detachForkcopies in the borrowed objects and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. - The phase is kept in memory, one entry per fork that is detaching or whose last detach failed. If the forge restarts mid-way, running the detach again finishes it.
- Three ways in:
- Web: Settings › Danger zone › "Detach from parent" (src/web/pages/detach.ts), which shows the phase while it runs and git's error line if it fails.
- Terminal: x on a fork's About card shows the plan and detaches only after a confirm. A detach row then shows the phase.
- MCP: detach is a dry run unless dryRun: false, and needs an admin token.
- The parent is named only through
forkParentFor. An admin who may no longer read a private parent sees "its parent" on the card andparent: nullin the MCP plan. The event, which every reader of the fork sees, carries the parent's id, never its name. clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach share it.- How-to:
docs/features/detach.md. Because the README has a line cap, the pointer to it is appended to an existing line rather than added as a new one.
Tests
test/fork-detach.test.tscovers:
- Archived parent: the badge is gone from the fork page, the forks page and the family tree.
- Detach: the dry run changes nothing; afterwards there is no alternates file, git fsck --full passes, history is intact and the fork has no parent.
- Refusals: a reader and an rw token are refused, and the Settings POST refuses a cross-origin request.
- Private parent: its name appears nowhere — not on the card, in the plan or in the event.
test/tui-about-detach.test.tschecks that a reader and an agent are not offeredx, that an archived parent offers noo, and thatxplans, can be cancelled, and detaches after the confirm.test/mcp.test.tslists every tool by name, sodetachwas added to that list; the test itself was right.
Left as it was: the JSON API and MCP following still return the relation to an archived parent as data; only the pages drop the badge.
To try it: archive a parent repository and open one of its forks. Then detach it from Settings › Danger zone, with x on its About card in the terminal, or with MCP detach and dryRun: false.
🤖 Generated with Claude Code
I left the code alone: the triage note is about the issue's place in the plan, not about the code. #7452 names no pillar, so it waits behind the Later gate. Choosing its pillar sets the forge's priorities, so I'm leaving that to a person rather than linking an umbrella myself. The closest fit is P3 Fork tree (umbrella #589); linking #589 to wait for #7452 would move it. There was nothing new to push; the branch already holds the reviewed work.
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead"; it shows one quiet line instead. A repository admin can detach a fork from its parent: on the web, in the terminal, or through MCP detach.
Archived parent
- The repo page and the app page show "forked from owner/name (archived)" in place of the Following card. For an admin the line also links to the detach card.
- The ahead/behind count is no longer read from git when the parent is archived, and "Propose upstream" is not offered to it. The terminal's About card skips that read too.
- An archived repository's forks page lists its forks by name only, with no counts and no "nothing new" heading. Its forks in the family tree and on the terminal's forks screen carry no badge.
Detach from parent
- The logic is in
src/actions/detach.ts. It plans first, likearchive, and only an admin may do it, never an agent. - It runs off the request path:
detachForkcopies in the borrowed objects and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. - The phase is kept in memory, one entry per fork that is detaching or whose last detach failed. If the forge restarts mid-way, running the detach again finishes it.
- Three ways in:
- Web: Settings › Danger zone › "Detach from parent" (src/web/pages/detach.ts), which shows the phase while it runs and git's error line if it fails.
- Terminal: x on a fork's About card shows the plan and detaches only after a confirm. A detach row then shows the phase.
- MCP: detach is a dry run unless dryRun: false, and needs an admin token.
- The parent is named only through
forkParentFor. An admin who may no longer read a private parent sees "its parent" on the card andparent: nullin the MCP plan. The event, which every reader of the fork sees, carries the parent's id, never its name. clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach share it.- How-to:
docs/features/detach.md. Because the README has a line cap, the pointer to it is appended to an existing line rather than added as a new one.
Tests
test/fork-detach.test.tscovers:
- Archived parent: the badge is gone from the fork page, the forks page and the family tree.
- Detach: the dry run changes nothing; afterwards there is no alternates file, git fsck --full passes, history is intact and the fork has no parent.
- Refusals: a reader and an rw token are refused, and the Settings POST refuses a cross-origin request.
- Private parent: its name appears nowhere — not on the card, in the plan or in the event.
test/tui-about-detach.test.tschecks that a reader and an agent are not offeredx, that an archived parent offers noo, and thatxplans, can be cancelled, and detaches after the confirm.test/mcp.test.tslists every tool by name, sodetachwas added to that list; the test itself was right.
Left as it was: the JSON API and MCP following still return the relation to an archived parent as data; only the pages drop the badge.
To try it: archive a parent repository and open one of its forks. Then detach it from Settings › Danger zone, with x on its About card in the terminal, or with MCP detach and dryRun: false.
🤖 Generated with Claude Code
Round 2. Every earlier ask is met:
- Parent-name leak: met. The card, the MCP plan and the
repo.detachedevent now go throughforkParentForand fall back to "its parent" /null. The event carries onlyparentId. The new private-parent test checks all three. - Terminal door: met. About offers
xonly to a fork's admin who is a person. It shows the plan in a confirm and starts the detach only on Detach.test/tui-about-detach.test.tscovers a reader, an agent, a cancel and the confirm. About skipsforkDriftagainst an archived parent. jobsbound comment: met.- README pointer: met.
I ran bun test test/fork-detach.test.ts test/tui-about-detach.test.ts at 78aefc32: 7 pass. In the preview, / answers 200, an anonymous POST to /x/y/settings/detach gets 404, and MCP detach without a token gets 401.
One fix, on a line new since my last review. The terminal shows the detach phase to more people than the page does. On the web, the phase and git's failure line appear only on Settings › Danger zone, which only an admin opens. In the terminal, detachRows(repo) is in factRows with no gate. So any reader of the fork's About card, a guest on a public fork included, sees detach repacking, started …. If the detach fails, they also see detach stopped: <git's first stderr line>, and git's errors often include the server's path to the repository. The terminal must gate its reads exactly as the page does. Fix: show the row only when mayDetach() holds, either ...(mayDetach() ? detachRows(repo) : []) or pass the gate into detachRows. Add a test that a reader's About has no detach row while a job is running or failed.
This is my second and last round of changes. If this fix lands as asked, the pull is ready.
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead"; it shows one quiet line instead. A repository admin can detach a fork from its parent: on the web, in the terminal, or through MCP detach.
This round fixes the last review point. The terminal's About card showed the detach phase, and git's failure line, to every reader of the fork. That row is now behind the same admin-only gate as the x key (src/tui/screens/about.ts). The issue still names no pillar; choosing one is left to a person, and P3 Fork tree (umbrella #589) is the closest fit.
Archived parent
- The repo page and the app page show "forked from owner/name (archived)" in place of the Following card. For an admin the line also links to the detach card.
- The ahead/behind count is no longer read from git when the parent is archived, and "Propose upstream" is not offered to it. The terminal's About card skips that read too.
- An archived repository's forks page lists its forks by name only, with no counts and no "nothing new" heading. Its forks in the family tree and on the terminal's forks screen carry no badge.
Detach from parent
- The logic is in
src/actions/detach.ts. It plans first, likearchive, and only an admin may do it, never an agent. - It runs off the request path:
detachForkcopies in the borrowed objects and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. - The phase is kept in memory, one entry per fork that is detaching or whose last detach failed. If the forge restarts mid-way, running the detach again finishes it.
- Three ways in:
- Web: Settings › Danger zone › "Detach from parent" (src/web/pages/detach.ts), which shows the phase while it runs and git's error line if it fails.
- Terminal: x on a fork's About card shows the plan and detaches only after a confirm. A detach row then shows the phase, to the admin only.
- MCP: detach is a dry run unless dryRun: false, and needs an admin token.
- The parent is named only through
forkParentFor. An admin who may no longer read a private parent sees "its parent" on the card andparent: nullin the MCP plan. The event, which every reader of the fork sees, carries the parent's id, never its name. clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach share it.- How-to:
docs/features/detach.md. Because the README has a line cap, the pointer to it is appended to an existing line rather than added as a new one.
Tests
test/fork-detach.test.tscovers:
- Archived parent: the badge is gone from the fork page, the forks page and the family tree.
- Detach: the dry run changes nothing; afterwards there is no alternates file, git fsck --full passes, history is intact and the fork has no parent.
- Refusals: a reader and an rw token are refused, and the Settings POST refuses a cross-origin request.
- Private parent: its name appears nowhere — not on the card, in the plan or in the event.
test/tui-about-detach.test.tscovers:
- Who is offered x: a reader and an agent are not.
- Archived parent: no o to offer upstream.
- The key: x plans, can be cancelled, and detaches after the confirm.
- The phase row: About is drawn for the admin and for a reader while a detach runs; only the admin's shows the row. This test fails with the gate removed.
test/mcp.test.tslists every tool by name, sodetachwas added to that list; the test itself was right.
Left as it was: the JSON API and MCP following still return the relation to an archived parent as data; only the pages drop the badge.
To try it: archive a parent repository and open one of its forks. Then detach it from Settings › Danger zone, with x on its About card in the terminal, or with MCP detach and dryRun: false.
🤖 Generated with Claude Code
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead"; it shows one quiet line instead. A repository admin can detach a fork from its parent: on the web, in the terminal, or through MCP detach.
This push fixes the red check. The test was wrong, not the code. test/tui-about-detach.test.ts polled every 10 ms for the detach job to exist. Under load the repack sometimes finished between two polls, the job was already gone, and the test ran out its 5 s timeout. It now waits for the fork to have no parent. The "repacking" row is still checked, in the next test, which draws About before the repack can finish. The git-heavy tests in that file and in test/fork-detach.test.ts now get 30 s. Both files pass locally, the terminal test three runs in a row, and test/flake.test.ts passes.
Archived parent
- The repo page and the app page show "forked from owner/name (archived)" in place of the Following card. For an admin the line also links to the detach card.
- The ahead/behind count is no longer read from git when the parent is archived, and "Propose upstream" is not offered to it. The terminal's About card skips that read too.
- An archived repository's forks page lists its forks by name only, with no counts and no "nothing new" heading. Its forks in the family tree and on the terminal's forks screen carry no badge.
Detach from parent
- The logic is in
src/actions/detach.ts. It plans first, likearchive, and only an admin may do it, never an agent. - It runs off the request path:
detachForkcopies in the borrowed objects and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. - The phase is kept in memory, one entry per fork that is detaching or whose last detach failed. If the forge restarts mid-way, running the detach again finishes it.
- Three ways in:
- Web: Settings › Danger zone › "Detach from parent" (src/web/pages/detach.ts), which shows the phase while it runs and git's error line if it fails.
- Terminal: x on a fork's About card shows the plan and detaches only after a confirm. A detach row then shows the phase, to the admin only.
- MCP: detach is a dry run unless dryRun: false, and needs an admin token.
- The parent is named only through
forkParentFor. An admin who may no longer read a private parent sees "its parent" on the card andparent: nullin the MCP plan. The event, which every reader of the fork sees, carries the parent's id, never its name. clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach share it.- How-to:
docs/features/detach.md. Because the README has a line cap, the pointer to it is appended to an existing line rather than added as a new one.
Tests
test/fork-detach.test.tscovers:
- Archived parent: the badge is gone from the fork page, the forks page and the family tree.
- Detach: the dry run changes nothing; afterwards there is no alternates file, git fsck --full passes, history is intact and the fork has no parent.
- Refusals: a reader and an rw token are refused, and the Settings POST refuses a cross-origin request.
- Private parent: its name appears nowhere — not on the card, in the plan or in the event.
test/tui-about-detach.test.tscovers:
- Who is offered x: a reader and an agent are not.
- Archived parent: no o to offer upstream.
- The key: x plans, can be cancelled, and detaches after the confirm.
- The phase row: while a detach runs, only the admin's About shows it, not a reader's.
test/mcp.test.tslists every tool by name, sodetachwas added to that list; the test itself was right.
Left as it was: the JSON API and MCP following still return the relation to an archived parent as data; only the pages drop the badge. The issue still names no pillar; choosing one is left to a person, and P3 Fork tree (umbrella #589) is the closest fit.
To try it: archive a parent repository and open one of its forks. Then detach it from Settings › Danger zone, with x on its About card in the terminal, or with MCP detach and dryRun: false.
🤖 Generated with Claude Code
Round 3, read against 78aefc32..2429e76a.
Earlier asks:
- Parent name leaks on the card, in the MCP plan and in the
repo.detachedpayload: met in the round before. All of them go throughforkParentFor, and the event carries onlyparentId. - Terminal About drew the detach phase and git's failure line for every reader: met.
src/tui/screens/about.tsnow spreadsdetachRowsonly behindmayDetach(), the same gate asx.test/tui-about-detach.test.tsdraws About for the admin and for a reader while a repack runs, and only the admin's has the row. That test would fail without the gate.
What else changed since the last head is in the tests only. Git-heavy tests get 30 s, and the x test waits for forkParentId === null in place of polling for a job a fast repack can skip past. That is a correct fix for a test that was wrong, not a loosened assertion: the running row is still checked, in the next test, which draws before the repack can finish (it involves real git I/O after the yield, so it cannot land between microtasks).
Preview: / answers 200. A cross-origin POST to /settings/detach is refused.
Nothing open. Approved.
When a fork's parent is archived, the fork no longer shows "following <parent> · N ahead". It shows one quiet line instead. A repository admin can now detach a fork from its parent, from Settings or through a new MCP
detachtool. I ran the type check, design lint and 25 test files that cover the change; they pass, with one test edit described below.Archived parent (first commit)
archivedParentLineinsrc/web/pages/following.ts;followingOfreturns nothing when the parent is archived.upstreamOffer).lineageOf). The terminal's forks screen does the same.clearForkParentinsrc/core/repos.tsreplaces the inlineUPDATEin the delete path, so delete and detach use one function.Detach from parent (second commit)
src/actions/detach.tsholds the logic. Likearchive, it plans first. It is an admin's action only, never an agent's.detachForkcopies in the objects the fork borrowed and drops the alternates link, then the parent is cleared and arepo.detachedevent is recorded. The progress state is kept in memory. If the forge restarts mid-way, the fork still names its parent and running the detach again finishes the job.src/web/pages/detach.ts), posting toPOST /:owner/:name/settings/detach. While it runs, the card says "repacking since …". If it fails, git's first line stays on the card.detach { repo, dryRun? }is dry by default and needs an admin token to run. Calling it again shows the phase.docs/features/detach.md.Tests
test/fork-detach.test.tscovers:- the badge before and after the parent is archived, on the fork page, the parent's forks page and the family tree; - a dry run that changes nothing; - after a detach: no alternates file,
git fsck --fullpasses, history is intact, the parent is cleared, one event is recorded; - refusals for a non-admin and an rw token; - the Settings path from the quiet line to the card, with a cross-origin POST refused.test/mcp.test.tslists every tool by name, so I addeddetachto that list; the test was right, it just needed the new tool.Left as they were: the JSON API and MCP
followingstill return the relation to an archived parent as data. Only the pages drop the badge.To try it: archive a parent repository, then open one of its forks. Then go to the fork's Settings › Danger zone › Detach from parent, or call MCP
detachwithdryRun: false.🤖 Generated with Claude Code
Closes #7452