Skip to content

A repo cloned on a box no longer shows as a second project card - #218

Merged
pallaoro merged 1 commit into
mainfrom
taskwindow-was-here-locally-then-i
Sep 5, 2026
Merged

pallaoro merged 1 commit into
mainfrom
taskwindow-was-here-locally-then-i

Conversation

@pallaoro

@pallaoro pallaoro commented Sep 5, 2026

Copy link
Copy Markdown
Member

TaskWindow existed locally; a new project called TaskWindow was then created from the iOS app on hetzner-devbox and the repo cloned into it. The desktop board came back showing them as two separate projects.

Cause

The board merges a repo's copies across engines into one card by (githubOwner, githubName) (unify.ts), and that identity was written once at registration and never again (dispatcher.ts, the only two writers). The phone's New project calls register(path, { init: true }), which creates the folder, git inits it and registers it before anything is cloned in, so detectGithubRepo had no origin to read and the row was born with no identity.

The timestamps on the box confirm it: the project row was created at 21:22:25Z, and ~/TaskWindow/.git/config was written at 21:42:03Z, 20 minutes later.

This is not cosmetic. Because the box's copy isn't a member of the local card, picking Run on hetzner-devbox takes the else branch in the composer and clones clawnify/taskwindow to ~/taskwindow, beside the ~/TaskWindow the box already has, plus a third project row.

Fix

  • Identity comes from the origin URL, not gh repo view. gh needs to be installed, authenticated and online, so a box missing any of the three silently lost its identity. With no argument it also resolves through BaseRepo(), which reports a fork's parent, so two engines could derive two different identities for the same clone. One derivation, offline and instant, used by both the register and repair paths. cloneRepo now reuses the same parser instead of its own inline copy of the regex.
  • projects:list repairs the identity before answering, so a repo that gains a remote later heals itself on the next board load. Measured at 30ms for 11 real projects, on a call made at board mount and on connect and never polled, which keeps it well inside the aggregate's 10s merge-read budget. That budget is why detection must not touch the network.
  • The repair writes through a new repo.updateProject, not upsertProject, which sets lastOpenedAt and would have reshuffled the sidebar (listProjects orders by it) on every board load.
  • The card key is lowercased. Two clones of one repo really do disagree: clawnify/taskwindow locally against clawnify/TaskWindow on the box. GitHub treats those as one repo.

Verification

  • Both new tests fail against the old code and pass against the new one, checked by temporarily restoring the old key and the old list handler.
  • The repair path was run against a copy of the real desktop database: 11 projects, 30ms, exactly one repair (contaktly gaining Contaktly/contaktly), with row order and every lastOpenedAt unchanged.
  • Card keys recomputed from the real origin URLs on the Mac and both boxes produce the intended board: TaskWindow, contaktly and Clawnify each merge Local with hetzner-devbox, Ateam spans all three engines, and the two repos that genuinely have no origin stay honestly separate.
  • bun test 400 pass, bun run typecheck clean. The three lint errors in the touched files are byte-identical at main; no new ones, and the unrelated reformats biome wanted were reverted.

Repairs run per engine, so a box's rows heal once that box's server is on a build containing this. Documented in the troubleshooting table.

The board merges a repo's copies across engines by (githubOwner, githubName),
but that identity was captured once at registration and never again. The
phone's "New project" git-inits an empty folder and registers it BEFORE
anything is cloned into it, so the row is born with no identity and never
regains one: the repo shows twice, and "Run on <box>" re-clones a copy the box
already has, into a second directory.

Read the identity from the repo's `origin` URL instead of `gh repo view`, and
re-derive it on every projects:list so a repo that gains a remote later repairs
itself. `gh repo view` needed gh installed, authed and online, so a box missing
any of the three silently lost its identity; with no argument it also resolves
through BaseRepo(), which reports a fork's PARENT, so two engines could derive
two different identities for the same clone. The repair writes through a new
repo.updateProject rather than upsertProject, which would have bumped
lastOpenedAt and reshuffled the sidebar on every board load.

The card key is now lowercased: two clones of one repo really do disagree on
casing (clawnify/taskwindow here against clawnify/TaskWindow on a box) and
GitHub treats those as the same repo.
@pallaoro
pallaoro enabled auto-merge (squash) September 5, 2026 08:13
@pallaoro
pallaoro merged commit 55c4564 into main Sep 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant