Fork button next to Clone on the code page + fork-creation page (owner/name/visibility/branch/description) and a completed server-side fork operation #424

Closed
opened 2026-09-12 23:00:33 +00:00 by crueber · 4 comments
Owner

Fork button on the code page + fork-creation form + completed fork operation

What's requested

  1. Fork button next to Clone on the repo code page header (web/src/pages/Repo.jsx, same pill row as <CloneMenu> — line ~98). It navigates to a fork-creation page (route under the repo, e.g. /:owner/:repo/fork), styled with the existing pill/CTA vocabulary.
  2. Fork-creation page (web/src/pages/Fork.jsx, new), Forgejo-inspired — same shape, our own design language (cite the sibling New.jsx create-repo form as the in-repo reference implementation for field layout, visibility control, and the submit pattern). Fields:
    • Owner — target namespace owner (default: current principal; same-owner forks are the primary target; cross-owner is v1 scope only where the owners listing already serves it, mirroring the #328 PR-composer scope rule).
    • Name — target repo name (default: <repo>-fork, matching the server default; validate with the existing repo-part rules; show a taken-name conflict inline, not just a toast).
    • Visibility — public / authenticated / private (default public, matching repo create; internal/api/placeholder.go accepts exactly public|authenticated|private).
    • Branch — optional starting branch (default: parent Head). The server currently has no branch parameter — see gaps below.
    • Description — optional; note that repo descriptions ride the WAL-published settings TOML (internal/api/env.go:399-401), so the fork flow must either accept a settings write post-create or the fork input must thread it — planner's call.
  3. Server-side fork operation completed — today StartFork/runFork (internal/pulls/merge.go:548, :596) only writes the collaboration objects and narrates the manifest step:
    • Exists (verified in tree): route POST /api/v1/repos/{owner}/{repo}/forks top-level twins (internal/pulls/http.go:32, :278-299); ForkInput{target_owner?, name?} (merge.go:531); fork.json Create-once provenance (ForkKey, pulls.go:285; ForkDoc model.go:147); parent meta/forks.json CAS'd child index (merge.go:609-631); social forks counter via the nil-safe ForksCounter seam (merge.go:637-643 → internal/social/service.go:354 IncForks); (repo, kind) single-flight task with 202 + TaskRecord; SDK repo.forks.fork() (web/sdk/src/pulls.js:71-79); API docs page lists it (Apidocs.jsx:70).
    • Must be built: ForkExecutor.ShareManifest (merge.go:543-545) is nil in this wave — no fork manifest is created, so a "successful" fork leaves an unborn prefix (repos/<o2>/<r2>/fork.json with no child manifest; the #150 race is permanent, not transient). Wire the executor at composition (cmd/walhub): copy the parent manifest referencing the shared pack set verbatim, fresh refs snapshot + checkpoint, Create child manifest.pb with min_seq = seq+1 — per docs/features/03_pull_requests.md §7, which specifies all of this and is NOT yet implemented. The fork-network GC rule (maintain consults children's manifests before pack deletion) must land in the same change or packs can be collected out from under a live fork.
    • Input gaps: ForkInput has no visibility, branch, or description. Extend it (or accept follow-up settings writes) so the form's fields are real; define the server behavior for each (visibility threaded to EnsureRepoAccess; branch = a selected ref becomes the child's Head in the fresh refs snapshot; description per the settings-TOML note above). Reject explicitly what is out of scope rather than silently ignoring fields.
  4. Forked-from indication — the data exists (fork.json parent pointer + meta/forks.json) but nothing renders it: no summary projection (internal/api/summary.go has no fork field; mind the ETag contract — any new summary field must be covered by the ETag, ~-suffix precedent), and no UI shows "forked from <parent>" on the repo page. Add the projection and render the indication in the repo header (near the title/identity block, composed into the existing layout — not an orphaned row), linking to the parent repo. Grafted onto this ticket because the parent pointer is the same feature's data; if the projection is contentious it can be sequenced, but the fork page is not shippable without the completed fork operation.

Acceptance criteria

  • Fork button renders in the Repo page header next to Clone; navigates to the fork form.
  • Fork form covers owner / name / visibility / branch / description with server-honored semantics; conflicts (taken name) surface inline.
  • ForkExecutor is wired: a completed fork yields a servable child repo (manifest + shared packs + refs snapshot), not an unborn prefix.
  • Fork-network GC rule enforced: maintain never deletes a pack still referenced by any live fork-network manifest.
  • fork.json/meta/forks.json/social counter behavior unchanged (tests already pin these).
  • Repo page shows "forked from "; summary ETag covers the new field.
  • Headless unit tests for the new input handling + fork-completion path; node --test and go test green.
# Fork button on the code page + fork-creation form + completed fork operation ## What's requested 1. **Fork button next to Clone** on the repo code page header (`web/src/pages/Repo.jsx`, same pill row as `<CloneMenu>` — line ~98). It navigates to a fork-creation page (route under the repo, e.g. `/:owner/:repo/fork`), styled with the existing pill/CTA vocabulary. 2. **Fork-creation page** (`web/src/pages/Fork.jsx`, new), Forgejo-inspired — same shape, our own design language (cite the sibling `New.jsx` create-repo form as the in-repo reference implementation for field layout, visibility control, and the submit pattern). Fields: - **Owner** — target namespace owner (default: current principal; same-owner forks are the primary target; cross-owner is v1 scope only where the owners listing already serves it, mirroring the #328 PR-composer scope rule). - **Name** — target repo name (default: `<repo>-fork`, matching the server default; validate with the existing repo-part rules; show a taken-name conflict inline, not just a toast). - **Visibility** — public / authenticated / private (default public, matching repo create; `internal/api/placeholder.go` accepts exactly `public|authenticated|private`). - **Branch** — optional starting branch (default: parent Head). The server currently has no branch parameter — see gaps below. - **Description** — optional; note that repo descriptions ride the WAL-published settings TOML (`internal/api/env.go:399-401`), so the fork flow must either accept a settings write post-create or the fork input must thread it — planner's call. 3. **Server-side fork operation completed** — today `StartFork`/`runFork` (`internal/pulls/merge.go:548`, `:596`) only writes the collaboration objects and *narrates* the manifest step: - **Exists (verified in tree):** route `POST /api/v1/repos/{owner}/{repo}/forks` top-level twins (`internal/pulls/http.go:32`, `:278-299`); `ForkInput{target_owner?, name?}` (`merge.go:531`); `fork.json` Create-once provenance (`ForkKey`, `pulls.go:285`; `ForkDoc` `model.go:147`); parent `meta/forks.json` CAS'd child index (`merge.go:609-631`); social forks counter via the nil-safe `ForksCounter` seam (`merge.go:637-643` → `internal/social/service.go:354 IncForks`); `(repo, kind)` single-flight task with 202 + TaskRecord; SDK `repo.forks.fork()` (`web/sdk/src/pulls.js:71-79`); API docs page lists it (`Apidocs.jsx:70`). - **Must be built:** `ForkExecutor.ShareManifest` (`merge.go:543-545`) is **nil in this wave** — no fork manifest is created, so a "successful" fork leaves an unborn prefix (`repos/<o2>/<r2>/fork.json` with no child manifest; the #150 race is permanent, not transient). Wire the executor at composition (`cmd/walhub`): copy the parent manifest referencing the shared pack set verbatim, fresh refs snapshot + checkpoint, `Create` child `manifest.pb` with `min_seq = seq+1` — per `docs/features/03_pull_requests.md` §7, which specifies all of this and is NOT yet implemented. The fork-network GC rule (maintain consults children's manifests before pack deletion) must land in the same change or packs can be collected out from under a live fork. - **Input gaps:** `ForkInput` has no `visibility`, `branch`, or `description`. Extend it (or accept follow-up settings writes) so the form's fields are real; define the server behavior for each (visibility threaded to `EnsureRepoAccess`; branch = a selected ref becomes the child's Head in the fresh refs snapshot; description per the settings-TOML note above). Reject explicitly what is out of scope rather than silently ignoring fields. 4. **Forked-from indication** — the data exists (`fork.json` parent pointer + `meta/forks.json`) but **nothing renders it**: no summary projection (`internal/api/summary.go` has no fork field; mind the ETag contract — any new summary field must be covered by the ETag, `~`-suffix precedent), and no UI shows "forked from `<parent>`" on the repo page. Add the projection and render the indication in the repo header (near the title/identity block, composed into the existing layout — not an orphaned row), linking to the parent repo. Grafted onto this ticket because the parent pointer is the same feature's data; if the projection is contentious it can be sequenced, but the fork page is not shippable without the completed fork operation. ## Acceptance criteria - [ ] Fork button renders in the Repo page header next to Clone; navigates to the fork form. - [ ] Fork form covers owner / name / visibility / branch / description with server-honored semantics; conflicts (taken name) surface inline. - [ ] `ForkExecutor` is wired: a completed fork yields a servable child repo (manifest + shared packs + refs snapshot), not an unborn prefix. - [ ] Fork-network GC rule enforced: maintain never deletes a pack still referenced by any live fork-network manifest. - [ ] `fork.json`/`meta/forks.json`/social counter behavior unchanged (tests already pin these). - [ ] Repo page shows "forked from <parent>"; summary ETag covers the new field. - [ ] Headless unit tests for the new input handling + fork-completion path; `node --test` and `go test` green.
crueber added this to the v1 milestone 2026-09-12 23:00:41 +00:00
Author
Owner

Amendment (user-directed): fork-network tracking is a first-class requirement

The fork relationship graph carries valuable information and must be tracked, counted, and queryable — not just displayed once as a header line. Folding into this ticket's server-side scope:

  1. Fork counts, always current: every repo surfaces its fork count alongside the star/watch counts (ForksCounter seam already exists — extend the summary projection so the count is version-keyed in the ETag like the other counters, per the #382 mutability rule). The count updates on fork creation (and on fork-network GC if §7 enforcement lands).
  2. Where the forks are: the parent meta/forks.json CAS index must be a queryable, listable relation — GET /repos/{o}/{r}/forks returns the live fork list (owner, name, forked-at timestamp), paginated per the API conventions. The index is the source of truth; the counter derives from it (never independently maintained).
  3. Network shape preserved on the child: each fork's fork.json provenance must record its immediate parent AND the root ancestor (the network is a tree, not just one hop), so "where they are" is answerable for the whole network from any node. Verify the existing Create-once provenance captures both or extend it.
  4. UI: the code page shows the fork count next to the Fork button (same family as star/watch counts), and the repo's fork list is reachable (a forks view or section — implementer's call, but the data must be exposed regardless of UI shape).
  5. Consistency: fork-count/ETag/summary-projection work in this ticket must not violate the #382 cache-class law (counts are user-mutable → no-cache/ccMutable class, version-keyed ETag).

These are requirements of this ticket, not follow-ups: a fork that lands without its count/index/network records intact is not done.

## Amendment (user-directed): fork-network tracking is a first-class requirement The fork relationship graph carries valuable information and must be **tracked, counted, and queryable** — not just displayed once as a header line. Folding into this ticket's server-side scope: 1. **Fork counts, always current**: every repo surfaces its fork count alongside the star/watch counts (`ForksCounter` seam already exists — extend the summary projection so the count is version-keyed in the ETag like the other counters, per the #382 mutability rule). The count updates on fork creation (and on fork-network GC if §7 enforcement lands). 2. **Where the forks are**: the parent `meta/forks.json` CAS index must be a **queryable, listable relation** — `GET /repos/{o}/{r}/forks` returns the live fork list (owner, name, forked-at timestamp), paginated per the API conventions. The index is the source of truth; the counter derives from it (never independently maintained). 3. **Network shape preserved on the child**: each fork's `fork.json` provenance must record its immediate parent AND the root ancestor (the network is a tree, not just one hop), so "where they are" is answerable for the whole network from any node. Verify the existing Create-once provenance captures both or extend it. 4. **UI**: the code page shows the fork count next to the Fork button (same family as star/watch counts), and the repo's fork list is reachable (a forks view or section — implementer's call, but the data must be exposed regardless of UI shape). 5. **Consistency**: fork-count/ETag/summary-projection work in this ticket must not violate the #382 cache-class law (counts are user-mutable → `no-cache`/ccMutable class, version-keyed ETag). These are requirements of this ticket, not follow-ups: a fork that lands without its count/index/network records intact is not done.
Author
Owner

Fixes in #431 (fix/issue-424, against main — review only, do not merge).

All 7 acceptance criteria: Fork button + Fork.jsx (owner/name/visibility/branch/description, inline conflicts) + servable child via wired ForkExecutor + fork-network GC + unchanged fork.json/forks.json/counter pins + forked-from + ETag + headless/Go tests (race, cover gates hold).

Two findings beyond the plan, both documented in 03 Decisions: (1) repo-prefixed pack keys cannot serve verbatim references, so §7 sharing rides a failure-path ancestor read fallback (law-12 amendment); (2) merge/update-branch commits never reached the bucket (merged parents unclonable) — fixed in the same change via PackTip + atomic pack-carrying publish, required for any fork of a merged repo.

Fixes in #431 (fix/issue-424, against main — review only, do not merge). All 7 acceptance criteria: Fork button + Fork.jsx (owner/name/visibility/branch/description, inline conflicts) + servable child via wired ForkExecutor + fork-network GC + unchanged fork.json/forks.json/counter pins + forked-from + ETag + headless/Go tests (race, cover gates hold). Two findings beyond the plan, both documented in 03 Decisions: (1) repo-prefixed pack keys cannot serve verbatim references, so §7 sharing rides a failure-path ancestor read fallback (law-12 amendment); (2) merge/update-branch commits never reached the bucket (merged parents unclonable) — fixed in the same change via PackTip + atomic pack-carrying publish, required for any fork of a merged repo.
Author
Owner

Review #431 (fix/issue-424) — findings

Verified in a scratch worktree; one fix commit pushed to origin/fix/issue-424 (1d90aff). No browser drive per instructions (tests + reasoning; noted explicitly below).

Fixed during review (pushed as 1d90aff)

GC fail-open edges, now fail-closed (internal/maintain/forknet.go): a child-manifest GET error other than 404 (e.g. a transient store 500) skipped the subtree and the sweep proceeded to delete — packs of a live fork unprotected. Now: 404 → skip (deleted fork pins nothing, pinned by TestForkNetworkGCDeletedChild); any other transport error → abort with nothing deleted. Same treatment for corrupt child manifests and unreadable child indexes (were skip → now abort), and for probe-cap exhaustion with unvisited children remaining (was silent partial live set → now aborts; triggers at 33+ direct forks). Rationale: deleted packs are unrecoverable, a deferred sweep just retries. Four new regression tests (TestForkNetworkGCTransientChildError, ...ChildIndexError, ...CorruptChild, ...CapExceeded); docs/features/03_pull_requests.md §7 Decisions records the rule (law 12). Maintain coverage 95.3% → 95.5%.

Non-blocking finding (follow-up, precise remediation)

Post-share failure strands the target name (internal/pulls/merge.go runFork + StartFork pre-check): commit order is share (manifest Create) → access → fork.json, but the sync-409 pre-check fires when EITHER fork.json OR the child manifest exists, while the task adopt path only rescues when fork.json is ours. Consequences, all verified by code reading: (a) crash between manifest Create and fork.json → retry hits sync 409 on the manifest; (b) transient AccessBoot failure after share → same; (c) crash between the two checkpoint PUTs → retry 412s on orphan checkpoints → fork.json absent → task 409. Each permanently burns the name (no in-change repair path). No data loss — orphans are unreferenced — but recovery is manual bucket cleanup. Remediation options: (i) provenance-before-share plus a rescue path in StartFork (fork.json ours + manifest absent → proceed to adopt); (ii) manifest-aware adopt (verify child packs == parent packs verbatim + HeadSeq match, then Create fork.json); (iii) for (c) only, semantic-compare on checkpoint 412 (seq/packs/refs/head, ignoring timestamps) then continue to the authoritative manifest Create. Recommend (i)+(ii) as follow-up; not merge-blocking.

Checked green (adversarial pass)

  1. Fork completion: ShareExecutor copies the pack set verbatim (forkexec.go:176-183), fresh sorted refs snapshot + checkpoint under the child prefix, child manifest Create with min_seq=head+1, rev 1 (:198-217); consistentParent manifest→refs→manifest stability gate (:233-271); closure HEAD-check fails loud on a missing pack (:145-157); empty parent → fresh empty manifest, branch-on-unborn rejected (:103-122). Taken-name sync-409 pre-check + task CAS adopt. E2E polls the child summary (fork_parent + description), lists the live index, pushes a cross-fork branch and opens the cross-fork PR — servable confirmed. Note: no pure cold-clone assertion exists; the fork push exercises parent-object resolution through the fallback on the receive path.
  2. PackTip: real argv (for-each-ref exclusions + pack-objects --revs --stdout with stdin caret lines; --not/--all correctly avoided as an exit-129 usage error), index-pack in a scratch git-dir, empty tip → ref-only, pack failure fails loud (mergepack_test.go pins all five cases). Atomic txn+pack via UpdateRefWithPack — a ref can never dangle. The "merged parents unclonable" root cause is credible and the fix sound.
  3. Ancestor fallback: own-prefix-first everywhere (sharedGet, downloadShared, remote readRaw); ancestors only after own-NotFound; transport errors fail at once; total miss returns the OWN miss verbatim — failure-path-only confirmed, non-forks byte-identical. Depth cap 8 + visited set + Parent/Root pointers (dead-middle survival). forkMu is a leaf lock, resolution outside the lock (13 §2 rule 4). Caveat: every repo pays +1 fork.json GET per remote-index rebuild (cached per handle lifetime); sim budgets are green so law 6 holds empirically.
  4. Admission/pins: #346 OwnerGate (self/member-org, admin bypass; nil = legacy-open in tests) with compile-time seam assertions. fork.json Root additive omitempty; meta/forks.json shape untouched; social counter still incremented best-effort. Full pulls suite green.
  5. Inputs: visibility validated + threaded to EnsureRepoAccess (fail-loud pre-commit); branch normalized (short→refs/heads/, tags/other namespaces explicitly rejected) + existence-checked under the task; description ≤1024, C0-rejected, TOML-escaped by construction into child settings (atomic at Create). Strict-decode allows exactly the five keys (unknown → 400).
  6. Forked-from: summary fork_parent omitempty + always-present forks count; ETag ~f<version>.<count>.<parent-hash> suffix; mutable-collab class; nil/absent hook → byte-identical ETag (pinned incl. a revalidation test). UI: "forked from" parent link in the identity block, Fork pill with count in the Clone row, /forks list page. Nit: the fork-list rail link only renders when count > 0, so a zero-fork repo has no UI path to its (empty) /forks page.
  7. Tasking: (child, pull-fork) single-flight + 202 + TaskRecord ✓. Note: the flight key is the child name only — two different parents forking to the same target would join each other's task; vanishingly rare, flagging for awareness.
  8. Seams/deps: Service.ForkExec/OwnerGate/AccessBoot + api.Env.ForkInfo, all nil-safe (pre-#424 behavior preserved); core never imports upward (mirrored structs); no new deps.

Test results (scratch worktree @1d90aff)

  • gofmt clean; go vet clean (pulls/wal/maintain/api/cmd); go build ./... ✓
  • go test -race: pulls ✓, wal(+rw) ✓, maintain ✓, api ✓, cmd ✓
  • coverage: pulls 95.8, wal 95.1, maintain 95.5, api 95.3 (all ≥95%)
  • sim tier ✓ (11.9s); e2e ✓ (46s, incl. the fork→cross-fork-PR chain)
  • node --test: 895/895 ✓ (fork.test.js 4/4; smoke run against a scratch PR server on :18099 — this env's :8080 occupant is an unrelated auth-gated instance that smoke's default base URL hits; not a PR issue)
  • vite build + esbuild SDK ✓ (chunk-size warning pre-existing)
  • Main worktree left untouched (verified clean before/after apart from the pre-existing untracked .opencode/).

MERGE RECOMMENDATION: ready to merge — with the name-stranding finding accepted as follow-up work. No pack-safety hole remains: the two GC fail-open edges found are fixed and tested in 1d90aff.

## Review #431 (fix/issue-424) — findings Verified in a scratch worktree; one fix commit pushed to `origin/fix/issue-424` (`1d90aff`). No browser drive per instructions (tests + reasoning; noted explicitly below). ### Fixed during review (pushed as 1d90aff) **GC fail-open edges, now fail-closed** (`internal/maintain/forknet.go`): a child-manifest GET error other than 404 (e.g. a transient store 500) skipped the subtree and the sweep proceeded to delete — packs of a live fork unprotected. Now: 404 → skip (deleted fork pins nothing, pinned by `TestForkNetworkGCDeletedChild`); any other transport error → abort with nothing deleted. Same treatment for corrupt child manifests and unreadable child indexes (were skip → now abort), and for probe-cap exhaustion with unvisited children remaining (was silent partial live set → now aborts; triggers at 33+ direct forks). Rationale: deleted packs are unrecoverable, a deferred sweep just retries. Four new regression tests (`TestForkNetworkGCTransientChildError`, `...ChildIndexError`, `...CorruptChild`, `...CapExceeded`); `docs/features/03_pull_requests.md` §7 Decisions records the rule (law 12). Maintain coverage 95.3% → 95.5%. ### Non-blocking finding (follow-up, precise remediation) **Post-share failure strands the target name** (`internal/pulls/merge.go` `runFork` + `StartFork` pre-check): commit order is share (manifest Create) → access → fork.json, but the sync-409 pre-check fires when EITHER fork.json OR the child manifest exists, while the task adopt path only rescues when fork.json is ours. Consequences, all verified by code reading: (a) crash between manifest Create and fork.json → retry hits sync 409 on the manifest; (b) transient `AccessBoot` failure after share → same; (c) crash between the two checkpoint PUTs → retry 412s on orphan checkpoints → fork.json absent → task 409. Each permanently burns the name (no in-change repair path). No data loss — orphans are unreferenced — but recovery is manual bucket cleanup. Remediation options: (i) provenance-before-share plus a rescue path in `StartFork` (fork.json ours + manifest absent → proceed to adopt); (ii) manifest-aware adopt (verify child packs == parent packs verbatim + HeadSeq match, then Create fork.json); (iii) for (c) only, semantic-compare on checkpoint 412 (seq/packs/refs/head, ignoring timestamps) then continue to the authoritative manifest Create. Recommend (i)+(ii) as follow-up; not merge-blocking. ### Checked green (adversarial pass) 1. **Fork completion:** `ShareExecutor` copies the pack set verbatim (`forkexec.go:176-183`), fresh sorted refs snapshot + checkpoint under the child prefix, child manifest Create with `min_seq=head+1`, rev 1 (`:198-217`); `consistentParent` manifest→refs→manifest stability gate (`:233-271`); closure HEAD-check fails loud on a missing pack (`:145-157`); empty parent → fresh empty manifest, branch-on-unborn rejected (`:103-122`). Taken-name sync-409 pre-check + task CAS adopt. E2E polls the child summary (`fork_parent` + description), lists the live index, pushes a cross-fork branch and opens the cross-fork PR — servable confirmed. Note: no pure cold-clone assertion exists; the fork push exercises parent-object resolution through the fallback on the receive path. 2. **PackTip:** real argv (for-each-ref exclusions + `pack-objects --revs --stdout` with stdin caret lines; `--not/--all` correctly avoided as an exit-129 usage error), `index-pack` in a scratch git-dir, empty tip → ref-only, pack failure fails loud (`mergepack_test.go` pins all five cases). Atomic txn+pack via `UpdateRefWithPack` — a ref can never dangle. The "merged parents unclonable" root cause is credible and the fix sound. 3. **Ancestor fallback:** own-prefix-first everywhere (`sharedGet`, `downloadShared`, remote `readRaw`); ancestors only after own-NotFound; transport errors fail at once; total miss returns the OWN miss verbatim — failure-path-only confirmed, non-forks byte-identical. Depth cap 8 + visited set + Parent/Root pointers (dead-middle survival). `forkMu` is a leaf lock, resolution outside the lock (13 §2 rule 4). Caveat: every repo pays +1 `fork.json` GET per remote-index rebuild (cached per handle lifetime); sim budgets are green so law 6 holds empirically. 4. **Admission/pins:** #346 `OwnerGate` (self/member-org, admin bypass; nil = legacy-open in tests) with compile-time seam assertions. `fork.json` Root additive omitempty; `meta/forks.json` shape untouched; social counter still incremented best-effort. Full pulls suite green. 5. **Inputs:** visibility validated + threaded to `EnsureRepoAccess` (fail-loud pre-commit); branch normalized (short→`refs/heads/`, tags/other namespaces explicitly rejected) + existence-checked under the task; description ≤1024, C0-rejected, TOML-escaped by construction into child settings (atomic at Create). Strict-decode allows exactly the five keys (unknown → 400). 6. **Forked-from:** summary `fork_parent` omitempty + always-present `forks` count; ETag `~f<version>.<count>.<parent-hash>` suffix; mutable-collab class; nil/absent hook → byte-identical ETag (pinned incl. a revalidation test). UI: "forked from" parent link in the identity block, Fork pill with count in the Clone row, `/forks` list page. Nit: the fork-list rail link only renders when count > 0, so a zero-fork repo has no UI path to its (empty) `/forks` page. 7. **Tasking:** `(child, pull-fork)` single-flight + 202 + `TaskRecord` ✓. Note: the flight key is the child name only — two different parents forking to the same target would join each other's task; vanishingly rare, flagging for awareness. 8. **Seams/deps:** `Service.ForkExec/OwnerGate/AccessBoot` + `api.Env.ForkInfo`, all nil-safe (pre-#424 behavior preserved); core never imports upward (mirrored structs); no new deps. ### Test results (scratch worktree @1d90aff) - `gofmt` clean; `go vet` clean (pulls/wal/maintain/api/cmd); `go build ./...` ✓ - `go test -race`: pulls ✓, wal(+rw) ✓, maintain ✓, api ✓, cmd ✓ - coverage: pulls 95.8, wal 95.1, maintain 95.5, api 95.3 (all ≥95%) - sim tier ✓ (11.9s); e2e ✓ (46s, incl. the fork→cross-fork-PR chain) - `node --test`: 895/895 ✓ (`fork.test.js` 4/4; smoke run against a scratch PR server on :18099 — this env's :8080 occupant is an unrelated auth-gated instance that smoke's default base URL hits; not a PR issue) - `vite build` + esbuild SDK ✓ (chunk-size warning pre-existing) - Main worktree left untouched (verified clean before/after apart from the pre-existing untracked `.opencode/`). **MERGE RECOMMENDATION: ready to merge** — with the name-stranding finding accepted as follow-up work. No pack-safety hole remains: the two GC fail-open edges found are fixed and tested in `1d90aff`.
Author
Owner

Fixed by PR #431 (review clean + two fail-open GC edges fixed by reviewer with regression tests; servable child proven via cross-fork-PR e2e; stranded-name follow-up filed as #432), merged. Closing.

Fixed by PR #431 (review clean + two fail-open GC edges fixed by reviewer with regression tests; servable child proven via cross-fork-PR e2e; stranded-name follow-up filed as #432), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#424
No description provided.