Skip to content

Add a project to a box from GitHub, instead of git-initting an empty one - #220

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

pallaoro merged 3 commits into
mainfrom
taskwindow-was-here-locally-then-i

Conversation

@pallaoro

@pallaoro pallaoro commented Sep 5, 2026

Copy link
Copy Markdown
Member

Follow-up to #218, which fixed the symptom (a repo showing as two cards). This fixes what kept producing it.

Why

The phone's only prominent action on Add project was New, which creates a folder, git inits it and makes an initial commit. On a screen titled "Add project", people press it meaning add, then graft a real repo onto the empty one by hand. The box still shows the seam:

476b948  21:22:24  commit (initial): Initial commit
cf60b84  21:43:02  reset: moving to origin/main

What that leaves is a repo with no origin, which is the one state that can never be merged with its copies on other engines (unify.ts keys on owner/name read from origin) and can never be provisioned onto a second box at all (the composer refuses: "This repo has no git remote to clone onto a box"). Two of the three duplicate rows on my board came from this one button.

What changes

Ask the intent up front rather than "pick a folder, and if it isn't a repo, offer to init it":

Bring one over from GitHub Pick from the repos the box's gh can see; cloned onto the box and registered in one tap. Paste-a-URL fallback for anything gh won't list.
Use a repo already on the box The existing filesystem browser, unchanged.
Start something new Names a folder, git inits it, and offers to create the GitHub repo for it at the same time, so a new project is portable from birth instead of a dead end.

Design notes

gh api user/repos?affiliation=owner,organization_member rather than gh repo list. The latter takes a single owner and defaults to the authenticated user, so it would have silently omitted every org repo, which for most people is where the work is. Verified live: the list comes back org repos included, most recently pushed first.

Creating the remote is best-effort. If GitHub refuses, the project is still registered, and its null githubOwner is how the client knows it has no remote yet. Losing the folder because GitHub said no would be worse.

Clone and the repo list stay off AteamApi. They address a box, not an entity on one, so the desktop aggregate cannot route them and would fall back to the local Mac (aggregate.ts: an unrouted method "would fall back to the local engine"). They live on the phone's Connection alongside update(), which is off AteamApi for exactly the same reason. The desktop is not touched.

Both halves are v9 and gated. Since v7 skew is advisory rather than a refusal, the bump alone protects nobody: a v8 box answers the repo list with "Unknown method" and accepts createRemote and silently ignores it. FEATURE_MIN_VERSION.githubProjects is what hides them, the same lesson v8's followUps entry encodes. Older boxes get the paste-a-URL door, which works on anything from v2.

Verification

  • createGithubRepo run for real, against a name already taken so nothing could be created: gh parsed the exact argv, authenticated, reached GitHub and returned GraphQL: Name already exists on this account (createRepository). The local repo was left with no origin, which also proves the best-effort path.
  • listRemoteRepos run for real against the live API: org repos included, correct shape, sorted by push date.
  • Alert.prompt's signature read from the installed React Native source, not recalled.
  • Caught a bug in my own code before it shipped: "Start something new" straight from the menu defaulted the parent dir to a literal "~", which the engine would mkdirSync as a folder actually named ~. It now resolves the real path from fs.listDir().
  • 418 tests pass (12 new parseGithubRepo cases covering https, scp-style ssh, ssh://, trailing slash, both real casings, and non-GitHub hosts returning null). Workspace typecheck clean.
  • Mobile's separate tsc (not covered by scripts/typecheck.sh) has the same two errors as main and no others; both are pre-existing rot in demo.ts where the fixture never followed TaskDTO.prState (v6) and SessionDTO.agentSessionId (v7). Proved by typechecking HEAD's mobile files and comparing. Left alone.

Not covered by a test: the handler's catch-and-continue around createGithubRepo, because testing it needs GitHub reachable in CI. Verified by reading it plus the proven throw above.

The merge of origin/main resolved two conflicts, both the squash-reintroduction shape (an additive import of mine landing beside the region #218's squash rewrote). #217's changes to dispatcher.ts merged cleanly and its tests are in the 418.

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.
The phone's only prominent action on "Add project" was "New", which creates a
folder, git-inits it and makes an initial commit. People pressed it meaning
"add", then grafted a real repo onto the empty one by hand. The box still shows
the seam: TaskWindow's reflog is an initial commit at 21:22:24 followed by a
`reset --hard origin/main` twenty minutes later. What that leaves behind is a
repo with no origin, which is the one state that can never be merged with its
copies on other engines and can never be provisioned onto a second box.

Ask the intent up front instead. "Bring one over from GitHub" clones a repo the
box's gh can see and registers it in one tap; "Use a repo already on the box"
is the old browser; "Start something new" names a folder, inits it, and offers
to create the GitHub repo for it at the same time, so a new project is portable
from birth rather than a dead end.

The repo list is read with `gh api user/repos?affiliation=owner,organization_member`
rather than `gh repo list`, which takes a single owner and defaults to the
authenticated user: it would have silently omitted every org repo, which for
most people is where the work is. Creating the remote is best-effort — if
GitHub refuses, the project is still registered and its null githubOwner is how
the client knows it has no remote yet.

Both halves are v9. Clone and the repo list stay off AteamApi, on the phone's
Connection alongside update(), because they address a box rather than an entity
on one: the desktop aggregate can't route them and would fall back to the local
Mac. A pre-v9 box has neither, and since v7 skew is advisory rather than a
refusal, FEATURE_MIN_VERSION.githubProjects is what hides them — an older
engine accepts `createRemote` and silently ignores it. Those boxes get the
paste-a-URL door instead, which works on anything from v2.
…ocally-then-i

# Conflicts:
#	packages/git-core/src/index.ts
#	packages/server/src/dispatcher.ts
@pallaoro
pallaoro enabled auto-merge (squash) September 5, 2026 14:59
@pallaoro
pallaoro merged commit cf648b8 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