Files
signed/docs/PLAN.md
T
2026-09-03 10:44:38 +07:00

30 KiB
Raw Blame History

PLAN — PR contribution flows: fork compare + GRASP-06 hosting + checkout suggestions

Status (2026-09-03): implemented. Steps 1-9 and 11 are done on feat/fork; step 10 (sidebar "Ready to contribute" group) remains deferred as planned (v2, optional). See docs/PR_FLOW.md for the resulting flow; the per-step sections below record what shipped and where the plan was refined during implementation.

Combined implementation plan for three coordinated improvements to Signed's pull request experience:

  • A. Fork-aware compare — the New PR panel's compare side can come from an announced fork repository's branch (fetched into the base repo's GitCache mirror), instead of only from a user-picked local checkout.
  • B. GRASP-06 hosting — PR tips are pushed to the author's own grasp servers under /prs/<author-npub>/<repo-id>.git and advertised in the PR's clone tag, so contributing to someone else's project never depends on their servers accepting anything from you.
  • C. Checkout associations & suggestions — remember/derive which local folders are checkouts of which announced repos, auto-prefill the New PR panel (no folder picker for the common case), and suggest creating a PR when a branch is ahead with no open PR (GitHub-like nudge, NIP-34-native dedupe).

Guiding principles (agreed): follow nostr + grasp + ngit, not GitHub; NIP-34/GRASP-06 event surface stays untouched (no new tags/kinds, forks never appear in events); every flow keeps the patch series as the source of truth; no over-engineering — reuse existing stores, patterns and git helpers.


1. Protocol grounding (what we may and may not do)

1.1 NIP-34 facts used by this plan

  • A PR (kind-1618) is addressed to the base repo coordinate (a tag) and carries c (tip), merge-base (common ancestor with the target branch), branch-name, clone (≥1 URL where the tip commit can be downloaded), e → root patch event, r (EUC), p (base owner). Patches are NIP-10-chained kind-1617 events, ≤60 KB each. Statuses 16301633 resolve the PR.
  • Repository announcements (30617): u tag marks a subordinate fork (30617:<pubkey>:<id> coordinate or git URL); the r/euc tag identifies the earliest unique commit, shared by every repo of the same project family (forks, mirrors). Both are read-only inputs for discovery.
  • Kind-10317 is the user grasp list (g tags, in preference order) — read-only input for hosting.
  • Anybody may open a PR on any announced repo; only the author may update it (1619); only the author or a maintainer may set status; merge is the maintainer's action.
  • "Patches and PRs to a repository SHOULD be sent to the relays specified in that repository's announcement" — i.e. the base repo's relays, always.

1.2 GRASP-06 facts (as ngit implements it — verified against ngit-cli)

  • GRASP-06 servers expose a contributor namespace: http(s)://<host>/prs/<contributor-npub>/<repo-id>.git (input ws:///wss:// base URLs normalize to http(s)://; npub in the URL, hex on the server's disk — a server detail). Anyone can push there; no announcement, no maintainer rights, no fork repo required.
  • The author's server is tried first; the base repo's announcement grasps still receive the same refs/nostr/<event-id> push as redundancy.
  • The PR event shape is unchanged; only which URLs the clone tag lists differs.

1.3 Consequences (locked decisions)

  • Publishing keeps today's event set and tag semantics. The fork changes only where the patch series is generated from; GRASP-06 changes only where the tip is pushed and advertised; suggestions change nothing on the wire.
  • We never publish a reference to the fork or to /prs/ hosting beyond legal clone URLs.
  • The PR clone tag is fixed before signing (the refs/nostr/<event-id> ref name embeds the event id), so it carries the full candidate URL set (author /prs/ URLs + base announcement clone URLs). Dead URLs are inert — patch events remain the truth — and ngit readers fail over across URLs. (ngit instead rebuilds the event per server to keep a single clone URL; we deliberately do not copy that.)

2. Workstream A — Fork-aware compare

2.1 Model

The panel keeps today's behavior as the default source and adds a second:

  • Checkout (existing): both selectors list a user-picked local checkout's branches; git ops + tip push run in the checkout.
  • Fork (new): the flow runs against the base repo's GitCache mirror P_base = GitStore::global(cx).cache().repo_path(&base_addr) (ensured via GitCache::ensure_clone(&base_addr, &base_clone_urls) + fetch_all):
    • "Merge Into" lists P_base branches (refs/remotes/origin/*);
    • "Pull From" lists the chosen fork's branches, imported into P_base;
    • merge-base, range commits/diff, format-patch, and the tip push all run against P_base — both histories share one object store, and commit-diff rows work because fork commits live there.

2.2 Git mechanics (import namespace)

Fork heads are fetched into P_base under a private namespace:

git -C P_base fetch <fork-clone-url> '+refs/heads/*:refs/fork/<owner-hex>/<sanitized-id>/*'
  • refs/fork/… keeps imported refs away from refs/remotes/* and refs/heads/*, so the repo browser, repo_branches and DWIM checkout never see them.
  • Fetch tries each announced clone URL until one works (grasp://https:// rewrite, GIT_TERMINAL_PROMPT=0), like clone_repo / push_commit_ref.
  • Switching fork or refreshing: prune the old refs/fork/<owner>/<id>/* prefix first (git update-ref --stdin fed by for-each-ref), then re-import. All-heads import in one fetch; subsequent branch switches within the same fork are offline.
  • Range work uses full refs: merge_base(P_base, "refs/remotes/origin/<base>", "refs/fork/…/<compare>"), then the existing worktree_commit_range_commits/worktree_commit_range_diff / format_patch_between. None of these touch the checkout state.
  • Base main and fork main are different refs: the "choose different branches" guard compares full refs, display names stay short.

2.3 Fork discovery

Candidates = RepoListStore::global(cx).read(cx).announcements (already deletion-filtered, latest-wins) where Announcement::is_fork_of(base_addr, base_euc):

  • upstream.addr == Some(base_addr) (the u tag — also covers permanent forks whose EUC changed), or
  • euc == base announcement's euc (shared earliest-unique-commit family), excluding the base repo itself.

Ordering (identity-coherent, ngit-style): your own forks first (30617 owner == signed-in user), then other authors' related repos (same mechanics, marked, niche). Announcements without clone URLs are excluded (unfetchable). Restricting to your own forks only later is a one-line ownership filter.

2.4 Panel behavior

  • Defaults mirror apply_checkout: base = announced store.head if present in mirror branches, else main, else first; compare = fork's main, else first fork branch.
  • submit: format_patch_between(P_base, merge_base, compare_ref); published branch-name = compare short name; push_from = Some(P_base) (fork objects are there after import). Publishing itself is workstream B.
  • open_commit_diff uses P_base in fork mode.
  • Errors: no common ancestor → existing message; unreachable base mirror or fork → inline error; empty range → existing "no commits to propose".

3. Workstream B — GRASP-06 author hosting

Applies to every PR publish from a repo path that has the objects — checkout mode and fork mode alike. RepoStore::open_pull_request keeps its signature; internals change:

  1. Resolve author grasp servers (new shared helper): latest kind-10317 grasp list of the signed-in user from the local DB (filters::grasp_list, g tags in order) → fallback to settings defaults (GraspServersSettings.default_servers / DEFAULT_GRASP_SERVERS, the same source the create-repo dialogs use) when no list is published.
  2. Build /prs/ URLs: grasp_base_url(server) + "/prs/" + user_npub + "/" + base_repo_id + ".git" (npub form, like ngit; grasp_base_url maps wss→https, ws→http).
  3. clone tag = dedup of /prs/ URLs plus the current base-announcement clone URLs (order: /prs/ first — the author's servers are the most likely to be alive and author-controlled).
  4. Push loop = author /prs/ servers first (guaranteed writable — the point of GRASP-06), then the base announcement grasp servers (existing behavior), all refs/nostr/<event-id> from push_from. Best-effort; zero successes → existing last_warning banner; publishing always proceeds.

Effect: a repo announced with relays but no reachable grasp hosting still gets a downloadable tip (on the author's own hosting), and git-native clients (ngit, git-remote-nostr) can fetch Signed PR tips from the clone URL.

1619 updates are out of scope for v1: the update dialog is paste-only, so no repo path holds the new tip's objects. Deferred until the existing "local-checkout generation for the update-PR dialog" TODO lands; then push the new tip to the same /prs/ set under the PR's stable ref (refs/nostr/<root-pr-event-id>, advanced per revision — convention to verify against ngit-grasp first).


4. Workstream C — Checkout associations & suggestions

Three tiers: Remember → Auto-pick → Suggest.

4.1 Remember (associations)

A local folder ↔ announced repo association comes from two sources:

  • Explicit (persistent settings records {path, addr, last_used}): recorded when the repo header Clone action succeeds (addr known) and when a folder pick succeeds in the New PR panel (store addr known).
  • Implicit (derived, no persistence): among LocalReposStore scan results (settings local_repos.scan_paths), a repo whose
    • origin URL matches an announcement clone URL (compare host+path, ignoring scheme: ws/wss/http/https/grasp are equivalent transports of the same grasp URL), or
    • root commit equals the announcement EUC is a checkout of that announced repo.

Resolution order per repo: remembered (freshest first) scanned-matched, deduplicated by path, skipping missing directories.

4.2 Auto-pick (New PR panel prefill)

open_new_pull_panel(…) gains a suggested-checkout parameter, resolved by the caller from the association store:

  • Exactly one checkout → auto-apply it: selectors populate, base = announced HEAD, compare = current branch, diff loads. The folder button becomes "Change…".
  • Several → a small folder combobox instead of the modal folder picker.
  • None → today's flow unchanged.
  • Successful manual folder picks are recorded back (learning).

4.3 Suggest (status + surfaces)

A small checkout-status computation (part of the association store), scoped to the bounded set of associated checkouts, on background threads:

  • Triggers: app open, window focus (debounced ~5 s), LocalReposStore rescan, BackendEvent::Synced.
  • Per checkout: current branch; commits ahead of the base branch (announced HEAD name if present locally, else main, else first local branch — the same rule as apply_checkout), via rev-list --count; whether the user has an open PR from that branch on the target repo (author == me, branch-name tag == branch, fallback: tip c tag == local HEAD).
  • Result states: ReadyToCreate { target, branch, ahead, base } / HasOpenPr { … } / Idle.
  • Noise rules: only when ahead > 0 and branch ≠ base; nothing for dirty worktrees; one entry per target repo.

Surfaces:

Surface Shows Dedupe data source Scope
Repo PR list banner (PullRequestsView) "branch feature is 3 commits ahead of main — Create pull request →" (opens prefilled New PR) live open RepoStore (precise) v1
Sidebar "Ready to contribute" group row per ReadyToCreate: target repo, branch ↑N → opens target repo + prefilled New PR v1: only targets with a live open store, else a local-DB query refreshed after a lazy per-target bootstrap activity sync; if the repo has no data yet, the group omits it (no false "ready") v2 (after v1 proves out)
Repo detail header chip tiny feature ↑3 on the repo whose checkout is ahead as PR-list banner optional

The PullRequestsView banner reuses the existing dismissible Alert banner pattern already used for store errors/warnings.


5. Combined flow (fork mode, end to end)

sequenceDiagram
    participant U as User (New PR panel)
    participant P as Base mirror (GitCache)
    participant F as Fork grasp server
    participant A as Author grasp (GRASP-06 /prs/)
    participant B as Base repo grasps
    participant R as Nostr relays

    U->>U: pick fork repo (u/EUC relation, yours first) + branch
    U->>P: ensure_clone(base) + fetch_all
    P-->>F: fetch +refs/heads/*:refs/fork/<owner>/<id>/*
    P-->>U: base branches (origin/*) + fork branches (refs/fork/…)
    U->>P: merge-base, range commits, range diff (Files/Commits tabs)
    U->>P: submit: format-patch merge-base..fork-ref
    U->>R: publish kind-1617 series (root + NIP-10 chain, ≤60 KB each)
    U->>R: sign kind-1618 (a=base, c=fork tip, merge-base, branch-name, clone=[/prs/…, base clone URLs], e=root patch, r=EUC)
    U->>A: push tip → refs/nostr/<event-id>  (author servers, first)
    U->>B: push tip → refs/nostr/<event-id>  (best-effort redundancy)
    U->>R: publish kind-1618
    Note over R: zero successful pushes → last_warning banner only

Checkout mode is identical except the fork-import step; suggestions (workstream C) only add entry-point shortcuts into this flow.


6. Step-by-step implementation

Phases are ordered so each step lands on green: foundations first, then the publish-side change (benefits the existing checkout flow immediately), then the fork UI, then the UX layer. Every step compiles, passes its tests, and keeps existing behavior unchanged.

Phase 0 — Foundations

Step 1 — signed_core: fork relation predicate

  • File: crates/signed_core/src/model.rs.
  • Add Announcement::is_fork_of(&self, base: &RepoAddr, base_euc: Option<&str>) -> bool: upstream.addr == Some(base) OR (base_euc present AND self.euc == base_euc), excluding self (same owner + id).
  • Tests: u-tag coordinate match; shared EUC match; permanent fork with different EUC matched via u; no-match; base-self exclusion.
  • Done when: predicate + tests green; used by Step 5.

Step 2 — signed_git: mirror/import primitives

  • File: crates/signed_git/src/lib.rs.
  • fetch_repo_refs(repo_path: &Path, urls: &[String], refspec: &str) -> Result<()> — CLI git fetch <url> <refspec>, grasp:// → https rewrite, GIT_TERMINAL_PROMPT=0, try each URL until one works (last-error on all failing, like clone_repo).
  • refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<Vec<String>> — full refnames under prefix (git for-each-ref --format=%(refname)), sorted.
  • delete_refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<()> — collect via for-each-ref, delete via git update-ref --stdin lines.
  • origin_url(workdir: &Path) -> Result<Option<String>> (used by Step 7).
  • Tests (existing file:// fixture infra): import under a target prefix; URL fallback to the working server; prefix listing; prefix deletion; origin URL read.
  • Done when: helpers + tests green.

Phase 1 — GRASP-06 hosting (publish side)

Step 3 — author grasp-server resolution

  • File: crates/signed_state/src/backend.rs (or a small new module).
  • resolve_user_grasp_servers(cx, user) -> Vec<RelayUrl>: latest kind-10317 of user from the local DB (filters::grasp_list; latest event wins; g tags in order) → fallback to settings GraspServersSettings defaults/DEFAULT_GRASP_SERVERS when the user has no grasp list.
  • Refactor the create-repo/init dialogs to share it (optional, keeps one resolution path).
  • Tests: latest-wins selection; missing list falls back; g order preserved.
  • Done when: helper + tests green.

Step 4 — open_pull_request hosting

  • File: crates/signed_state/src/repo.rs (open_pull_request, ~L657).
  • Pure helpers (unit-testable): grasp06_prs_url(base_url: &str, npub: &str, repo_id: &str) -> String; push-target assembly (author /prs/ URLs first, then announcement grasp URLs; dedup).
  • clone tag = /prs/ URLs (author npub from Backend::current_user(), repo id from self.addr().identifier) + base announcement clone URLs.
  • Push loop extended: existing per-relay loop stays; author servers push to the /prs/ URL instead of the repo URL. Warning semantics unchanged.
  • Tests: URL building; target order; dedup. Behavioral coverage of the full publish is manual/e2e (see §8) until a harness exists.
  • Done when: checkout-mode PRs push to the author's grasp list (/prs/<npub>/<repo-id>.git) first and the clone tag carries those URLs; all-servers-fail still publishes with a warning.

Phase 2 — Fork-aware compare

Step 5 — fork candidates

  • File: crates/workspace/src/views/repo_detail/new_pull_request.rs (helper) or crates/signed_state.
  • fork_candidates(cx) -> Vec<Announcement>: filter RepoListStore::global().announcements with is_fork_of (Step 1); exclude empty clone; sort own forks (owner == current user) first, then others, each group by recency/name. Re-read each time the picker opens.
  • Done when: helper returns the expected ordering for a mixed list.

Step 6 — New PR panel fork mode

  • File: crates/workspace/src/views/repo_detail/new_pull_request.rs.
  • State: CompareSource { Checkout, Fork { announcement } }; per-mode item sets for both selectors; mirror_path; fork branch list; full-ref base/compare tracking (display keeps short names).
  • choose_fork(announcement) + prepare_fork (mirror choose_checkout / apply_checkout async shape, compare_generation guard): ensure base mirror (ensure_clone + fetch_all), prune previous refs/fork/… prefix, import fork heads (fetch_repo_refs), list both ref sets (refs_with_prefix), populate selectors with defaults, reload_compare.
  • reload_compare / submit / open_commit_diff become mode-aware (path + base ref + compare ref resolution; format_patch_between and push_from on P_base; published branch-name = short name).
  • UI (compare bar): source control next to "Pull From" (local checkout / announced fork), fork-repo combobox (grouped, own forks first), refresh affordance, "Change…" back to checkout; loading spinner; inline errors.
  • Done when: checkout mode is byte-identical in behavior; fork mode shows base/fork selectors, Files/Commits tabs, commit diffs, and publishes with the correct tags (manual §8).

Phase 3 — Checkout associations & suggestions

Step 7 — association store

  • Files: crates/settings (extend the settings model like local_repos.scan_paths with remembered checkouts {path, addr, last_used}); new crates/signed_state/src/checkouts.rs (global store, Arc + debounce pattern from LocalReposStore/ RepoListStore).
  • API: associations_for(addr) -> Vec<PathBuf> (remembered freshest-first scanned-matched by origin URL/EUC via Step 2's origin_url + signed_git::root_commit; scheme-insensitive URL compare; dedup; skip missing dirs); record(path, addr).
  • Recording hooks: repo header clone_to_folder success (repo_detail/mod.rs) and choose_checkout success (panel).
  • Tests: matching by origin URL (scheme variants), by EUC, no match; dedup and ordering.
  • Done when: associations resolve correctly and persist.

Step 8 — New PR prefill

  • Files: new_pull_request.rs (open_new_pull_panel + new); callers repo_detail/mod.rs header and pull_requests.rs.
  • Entry param suggested_checkout: Option<PathBuf> (default None); panel applies it on construction when the folder still exists, else falls back to the empty state. When several candidates exist the caller passes the freshest and the panel offers the others through a folder combobox (new small control next to the source button).
  • Done when: opening New PR on a repo with a remembered/matched checkout never shows the folder dialog; manual picks get remembered.

Step 9 — status computation + PR-list banner

  • Files: checkouts.rs (status states + triggers + debounce), workspace pull_requests.rs (banner), new_pull_request.rs (accepts the banner's "create" click by opening prefilled).
  • Status rules from §4.3; banner dedupe against the live open RepoStore (author + branch-name, fallback tip match, open status only).
  • Done when: after committing on an associated checkout and opening the target repo's PR list, the banner appears exactly when ahead > 0 and no open PR exists, and disappears after creating/merging/evening.

Step 10 — sidebar "Ready to contribute" group (v2, optional)

  • File: crates/workspace/src/views/sidebar/mod.rs (+ checkouts.rs support).
  • Only list targets with reliable dedupe data (live open store, else a local-DB activity query refreshed after a lazy per-target bootstrap activity sync); omit everything uncertain. Clicking a row opens the target repo (open_repo_panel) + prefilled New PR.
  • Done when: rows appear without false "ready" entries (dedupe-uncertain targets omitted).

Phase 4 — Docs & validation

Step 11 — documentation and final validation

  • Update docs/PR_FLOW.md: fork compare path, GRASP-06 server set + clone tag, suggestion surfaces; the mermaid sequence in §5.
  • Update docs/TODO.md: tick "Fork-aware compare…", "GRASP-06 …"; add the checkout-suggestions item; keep deferred items (1619 update push, sidebar group, reading-side clone-URL fetch) explicit.
  • Run the manual validation checklist (§8) end to end.

7. Error handling & edge cases (all inline or warnings, as today)

  • Base mirror unreachable / no base clone URLs → panel error in fork mode; checkout mode unaffected.
  • Fork unreachable / without clone URLs (excluded from candidates) → panel error.
  • No common ancestor → existing error (range flow needs shared history; Send Patch remains the fallback).
  • Author has no 10317 list and no default servers → GRASP-06 adds nothing; today's warning stands.
  • Author's grasp server does not implement /prs/ → its push fails silently in the loop; its URL in clone is inert; base grasps still tried.
  • Fork branch deleted upstream / fork switched → prune prefix + re-import; generation guard discards stale compares.
  • Concurrency on P_base with the repo browser: we never checkout; git ref locks make overlapping fetches safe (same class as today's browser refresh).
  • Multiple checkouts of one repo → freshest first, "Change…"/combobox for the rest.
  • Branch renamed after a PR → dedupe falls back to tip-commit matching; otherwise a duplicate suggestion may appear once (accepted v1 tradeoff).
  • Suggestions never block UI; results arrive as Arc swaps.

8. Non-goals / deferred (explicitly out of scope)

  • 1619 update hosting (depends on the local-checkout update-dialog TODO).
  • Paste/Send-Patch flow keeps no git push (no object store; patches = truth; no scratch-apply resurrection).
  • Reading side: fetching other clients' PR tips from clone URLs into the mirror (ngit pr checkout analog) — only needed for patch-less PRs.
  • Fork creation UI (Signed still cannot announce forks; they come from ngit or by publishing a clone) — fork candidates simply won't include non-existent ones.
  • GitHub-isms rejected: no fork-network browser, no per-fork PR pages, no fork identity in events, no "compare across forks" for strangers' branches beyond what is listed above.
  • The panel never auto-submits anything; suggestions only navigate and prefill.

9. Validation checklist (manual e2e)

  1. Checkout mode regression: clone a repo to disk, branch + commit (external git), New PR → choose folder → diff/commits → Create → PR appears on the target repo's PR list; tip pushed to the author's /prs/ server(s) from the 10317 list (fallback: defaults); clone tag lists /prs/ URLs first.
  2. All grasp servers down/absent → PR still publishes; warning banner shows.
  3. Fork mode: with a fork announcement related to the base (own fork first, other author's fork listed), pick repo + branch → selectors, Files/Commits tabs, commit-diff rows correct; published 1618 carries a = base coordinate, c = fork tip, merge-base = fork point, branch-name = fork branch; tip fetchable from the advertised /prs/ URL via a plain git fetch.
  4. Prefill: reopen New PR for the same repo → folder auto-chosen, selectors populated; "Change…" works.
  5. Banner: with the repo's PR list open and an associated checkout ahead with no open PR → banner appears; disappears after publishing a PR, after merging, and when the branch is even.
  6. Interop: an ngit/git client fetches a Signed PR's tip from the /prs/ clone URL (requires a GRASP-06-enabled server).

10. Implementation log (2026-09-03, branch feat/fork)

All steps below landed with unit tests; cargo test across signed_core (44), signed_git (61), signed_state (15), workspace (14) and settings (9) is green, and cargo check on the whole workspace passes. The manual e2e checklist above still needs a real GRASP-06 server run.

  • Step 1Announcement::is_fork_of (signed_core::model) + 4 tests.
  • Step 2signed_git: fetch_repo_refs, refs_with_prefix, delete_refs_with_prefix, origin_url, GitCache::root() + 4 tests (import/list/prune against file:// fixtures incl. URL fallback).
  • Step 3 — backend grasp-list resolution: grasp_list_servers, latest_grasp_list_servers, user_grasp_list_servers (DB query, latest wins) + grasp06_prs_url and pr_clone_urls (author-first, dedup) + 4 tests. signed_state gained a settings dependency for the defaults fallback.
  • Step 4RepoStore::open_pull_request (signature unchanged): resolves the author's grasp servers (10317 → settings defaults) inside the publish task, builds the clone tag from /prs/ URLs first, pushes author /prs/ targets before the base announcement's servers, deduped; all-fail keeps the last_warning banner. 1619 updates untouched (deferred, as planned).
  • Step 5fork_candidates ordering helper + 2 tests (own forks first; base/unrelated/no-clone excluded; EUC-less base still matches via u).
  • Step 6 — New PR panel fork mode: ForkCompare state, choose_fork/ apply_fork (mirror ensure_clone → prune refs/fork → import → list both ref sets), mode-aware base_ref/compare_ref/work_path used by reload_compare/submit/open_commit_diff, stale-result guard, refresh-by-re-picking + refresh button, and a "Source" picker menu (checkout rows + forks) replacing the folder button. Checkout mode stays byte-identical in behavior. Deviations from the plan: selectors and the source picker keep one shared layout (no separate fork-repo combobox — the source menu lists forks grouped own-first, matching the ordering requirement); IconName::GitBranch does not exist upstream so fork rows use the project's CustomIconName::GitBranch.
  • Step 7 — settings CheckoutRecord/CheckoutsSettings group + new signed_state::checkouts::CheckoutsStore global (observe settings / local scan / announcements; debounced, coalesced, Arc-swapped): scheme-insensitive same_repo_url, resolve_associations (remembered freshest-first scanned origin/EUC matches, dedup, mirror-cache paths excluded), record(), per-repo request_statuses/statuses_of with CheckoutStatus (branch/head/base/ahead; dirty and detached checkouts never suggested; 15 s poll while any PR list is open). 7 tests. Deviations: settings records store the address as a string (the settings crate stays free of nostr types); mirror exclusion uses the cache root (new GitCache::root()); statuses are computed per requested repo with the announced HEAD supplied by the open list panel rather than from a 30618 DB query.
  • Step 8 — New PR panel prefills the freshest associated checkout on construction (no folder dialog); apply_folder_path applies a given path; successful folder picks and header clones are recorded back; the Source menu lists associated checkouts (checked when applied) plus "Choose another folder…". Deviations: instead of a separate folder combobox, the alternatives live in the Source menu (fewer controls, same outcome); open_new_pull_panel needed no signature change because the panel reads the association store itself.
  • Step 9 — "ready to contribute" banner: RepoDetailView requests the statuses while the repository panel is open (re-requested when the announced HEAD lands or changes) and renders the banner under the repo header; the first ready checkout not covered by an open PR of the signed-in user (branch-name, fallback c-tag tip) and not dismissed (per-panel dismissal set) is offered with a Create button opening the prefilled panel. The dedupe predicate is the tested pr_proposes_checkout in signed_state::checkouts. Deviation from the plan: the surface is the repository panel (not the PR-list panel, as the user requested after v1; the PR list keeps only its error/warning banners), and there is no window-focus trigger (no precedent in the codebase; the 15 s poll plus open/rescan/settings triggers cover the plan's "done when" cases).
  • Step 10 — deferred (v2, optional), per plan; the banner is the v1 surface.
  • Step 11docs/PR_FLOW.md rewritten for the current panel flow (fork import, GRASP-06 hosting, suggestions, deferred items explicit); docs/TODO.md updated; this log added. Manual e2e (§9) not yet run against a live GRASP-06 server.