Skip to content

feat(sync): cross-device GitHub sync — v3 lean rebuild#3

Merged
filocosta46 merged 37 commits into
mainfrom
worktree-feat+github-sync
May 22, 2026
Merged

feat(sync): cross-device GitHub sync — v3 lean rebuild#3
filocosta46 merged 37 commits into
mainfrom
worktree-feat+github-sync

Conversation

@filocosta46
Copy link
Copy Markdown
Owner

Summary

Adds dotaios sync — mirrors the local ~/aios/ folder to a private GitHub repo so a phone-side AI can read the same memory. Two-way: phone writes land in memory/inbox/, and the process-inbox skill files them on the next desktop session.

This is v3 of the feature. A v2 build (~16 tasks) was reviewed and found overbuilt; v3 cuts the overbuild:

  • Auth — dropped the custom GitHub App + device-flow OAuth. The user pastes a classic repo-scope PAT. No infrastructure DotAIOS owns, no app verification, no single point of failure, no unhandled token expiry.
  • Trigger — dropped the 3-platform background daemon (launchd / systemd / schtasks). Sync fires from the existing per-command hook plus an AGENTS.md rule — agent-agnostic, zero per-vendor code.
  • Tickcommit → pull --rebase → push. Branch-and-reset is a fallback for a genuine same-file conflict only, not the default for every divergence (v2 silently hard-reset local work).

Zero new dependencies; packages/core stays zero-dep. Net result: the feature is smaller than v2.

Design docs

  • Spec: docs/superpowers/specs/2026-05-21-github-sync-v3.md
  • Plan: docs/superpowers/plans/2026-05-21-github-sync-v3.md

Verification

  • Full test suite: 335 pass / 0 fail / 1 skip.
  • Live smoke test against real GitHub (throwaway account, isolated repo): setup → repo create + initial push; edit + tick → commit; phone inbox write → pull → process-inbox files it → inbox cleared; two-device divergence → clean rebase, no orphan branch, no conflict markers.
  • 5 bugs found during prep/smoke and 5 from an independent code audit — all fixed on-branch (see fix(sync): ... commits).

Known limitation

A same-file two-device conflict parks the local edit on a local-<ts> branch that sync status does not surface. Near-unreachable for the one-desktop-plus-phone ICP; documented in the v3 spec.

Test plan

  • CI: node --test tests/**/*.test.mjs green
  • Manual dotaios sync setup end-to-end on a fresh machine with no global git identity

🤖 Generated with Claude Code

filocosta46 and others added 30 commits May 19, 2026 17:15
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Supersedes v1/v2: cuts the custom GitHub App and the 3-platform
heartbeat. Token-paste auth, rebase-model tick, agent-agnostic
sync trigger via sync-hook + an AGENTS.md rule. Finishes the
phone-write inbox path v2 left unbuilt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces ffPull (which treated any divergence as an emergency and
hard-reset local work to origin) with a rebase model. Tick now
commits local changes first, then git pull --rebase: with the inbox
pattern, disjoint-file commits from two devices rebase cleanly with
no orphan branch. The branch-and-reset escape hatch is kept ONLY for
a genuine same-file rebase conflict, logged as sync-conflict.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops the custom GitHub App entirely — no App registration, no
verification, no privacy-policy obligation, no single point of
failure, no unhandled 8-hour token expiry. Setup now opens a
pre-filled github.com token link; the user pastes a classic
repo-scope PAT, which is validated via GET /user.

Also removes the placeholder-client-id hard-fail in runSetup —
the cause of the bug where 'dotaios setup' exited 1 on success.
The setup wizard now closes its own readline before the paste
flow opens one, avoiding two readline interfaces on one stdin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deletes heartbeat.mjs (launchd / systemd / schtasks installers) and
its tests, plus the heartbeatPlistPath / heartbeatUnitDir path
helpers. The OS-level installers were the most fragile part of the
feature, and the 5-minute timer's only unique job — pulling while
the machine is idle — has no user value.

Sync now fires from two agent-agnostic mechanisms instead: the
per-command hook (sync-hook.mjs) and an AGENTS.md rule (added in a
following commit). Updates help text and stale comments accordingly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rules

Completes the two-way design v2 left unbuilt. Adds the process-inbox
skill: a local agent reads notes that arrived from another device in
memory/inbox/, files each into the right vault/context location, then
removes the inbox file. Two rules added to the AGENTS.md template —
one to run process-inbox when memory/inbox/ has files, one to run
'dotaios sync tick' at session start/end. AGENTS.md is read by every
agent, so the sync trigger is vendor-agnostic with no hook code.

inbox.mjs (a JS helper listed in the v2 file map) is intentionally
omitted: the skill is markdown an agent runs with its own file tools,
so a helper module with no caller would be dead code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents dotaios sync: the one-time PAT setup, reading the repo
from a phone, the memory/inbox phone-write path, and the broad-scope
classic-token tradeoff with the fine-grained alternative. Adds the
sync command to the commands table. No daemon, no GitHub App.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two bugs found by inspection before the live smoke test:

1. Exit-code leak (the bug class behind Task 16, only partly fixed).
   Removing the placeholder block killed one trigger, not the leak:
   a genuine sync failure inside the 'dotaios setup' wizard still set
   process.exitCode=1, so the whole wizard exited 1 even though setup
   otherwise succeeded. runSetup now THROWS on failure and never
   touches process.exitCode. 'dotaios sync setup' (the dispatcher)
   catches and exits non-zero — correct for a standalone command. The
   wizard catches, logs, and lets setup finish 0 — correct for an
   optional step.

2. 'dotaios sync setup' ignored --path. runSetup now parses --path
   like 'sync tick' does, so a non-default AIOS folder can be synced
   (and smoke-tested) without touching ~/aios.

runSetup gains an injectable orchestrate override for testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dotaios init copies the templates/ tree into ~/aios/, which dragged
templates/sync-gitignore.template in as a stray literal file. That
template is a build-time resource read by 'dotaios sync setup' to
write the synced repo's .gitignore — it has no place in the user's
folder. Excluded it from the init copy, alongside aios.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
filocosta46 and others added 7 commits May 22, 2026 09:05
Step 1 prints the full token URL but step 2 only said 'github.com/new
(pre-filled)'. If the browser-open lands on the wrong window the user
had no URL to copy. Now both steps print their full URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two cosmetic inaccuracies in the last_push_sha status field, both
found during the live smoke test:

- After 'sync setup', the field stayed null: the initial mirror push
  runs via initialMirrorPush, which bypasses tick, so tick's first
  run had nothing to push. initialMirrorPush now returns the commit
  sha and setup records it.
- After a tick whose rebase replayed commits, the field held
  commitAll's PRE-rebase sha, which no longer exists in history once
  the rebase rewrites it. Tick now reads HEAD after the push.

Both verified live: last_push_sha matches the GitHub HEAD sha.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ser config

A non-technical user's machine often has no global git user.name /
user.email. Without them 'git commit' fails 'Author identity unknown'
and 'dotaios sync setup' dies at the initial-push step. The stubbed
tests and the smoke test (on an already-configured machine) both
missed this. createGit now sets GIT_AUTHOR_* / GIT_COMMITTER_* in the
spawn env on every git call, so sync is self-sufficient.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The token is embedded in the origin remote URL, so it also lives in
<aios>/.git/config at mode 0644 — but 'dotaios sync logout' only
removed ~/.dotaios/sync.json and printed 'Signed out', leaving a
valid token on disk. logout now also removes the origin remote and
tells the user to revoke the token on GitHub (the only way to kill
it server-side).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…error

GitHub's create-repo page has 'Initialize this repository' options
(README / .gitignore / license). If the user ticks one, the repo has
a commit and the initial mirror's 'git push' is rejected
non-fast-forward — setup died with an unreadable git error at step 3.

setup step 2 now explicitly says to leave those options off, and
pollForRepoExists checks the repo is empty (GitHub returns 409 for an
empty repo's commits) — a non-empty repo now fails with a plain
'delete it and retry' message before any push is attempted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… line

- dotaios setup --path X now passes X to the sync sub-step (it was
  hardcoded to ~/aios).
- Remove inboxDir() from paths.mjs — orphaned when inbox.mjs was cut,
  referenced only by its own test.
- Drop the stale memory/.daemon.* line from sync-gitignore.template;
  the daemon was removed in v3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit finding 4: a same-file two-device conflict parks the local edit
on a local-<ts> branch that sync status does not surface. Deliberately
not built for v3 — the conflict path is near-unreachable for the
one-desktop-plus-phone ICP, and the event is already logged to
events.jsonl. Documented as a known limitation with the trigger for
revisiting it (multi-desktop users).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dotaios Ready Ready Preview, Comment May 22, 2026 8:28am

@filocosta46
Copy link
Copy Markdown
Owner Author

Pre-merge review pass — 3 bugs fixed, branch merge-ready

Closing out the feature. Three bugs from pre-merge review, verified real against current code (systematic-debugging), each fixed test-first:

Fix 1 — sync had no git identity (087ce18)
git.mjs shelled git commit/git rebase with no author/committer. On a machine where git is installed but user.name/user.email was never set globally — common for a non-technical user — git commit failed Author identity unknown and dotaios sync setup died at the initial push. Tests stub spawn, so CI never caught it; the live smoke test ran on an already-configured machine.
Fix: stamp GIT_AUTHOR_*/GIT_COMMITTER_* (DotAIOS Sync <sync@dotaios.local>) into the spawn env of every git call — sync no longer depends on the user's global git config.
Verified end-to-end with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null HOME=/dev/null: commit succeeds, identity is the stamped one, rebase exits 0.

Fix 2 — logout left the live PAT on disk (dbac315)
The repo-scope PAT is embedded in the origin remote URL, so it sat plaintext in ~/aios/.git/config (mode 0644). runLogout only deleted ~/.dotaios/sync.json and printed "Signed out" — the token stayed on disk and valid on GitHub.
Fix: runLogout now also runs git remote remove origin (best-effort) to strip the token from .git/config, and the message tells the user to revoke the token at github.com/settings/tokens.

Fix 3 — initial push failed on a non-empty repo (c78f86e)
If the user ticked "Add a README" (or .gitignore/license) on GitHub's new-repo page, the repo had a commit, the initial git push was rejected non-fast-forward, and setup threw a cryptic git error.
Fix: setup Step 2 now explicitly tells the user to leave every "Initialize this repository" option unchecked. Belt-and-suspenders: pollForRepoExists checks GET /repos/{}/commits (409 = empty as expected) and, if the repo already has commits, throws a plain-language message instead of letting a raw git error escape.

Also evaluated

  • #5 — dotaios setup --path X ignored by the sync sub-step (48cdf37): fixed — setup.mjs now threads --path into runSetup. Trivial. (Same commit drops dead inboxDir() + a stale .daemon gitignore line — confirmed 0 references.)
  • feat(onboarding): collapse the four-wall install funnel to one — agent-carried onboarding #4sync status does not surface a local-<ts> conflict branch: reported, not fixed. Surfacing it means sync status must shell git to list branches — a new feature surface, not a bug fix, and the conflict path is near-unreachable for the one-desktop-plus-phone ICP. Documented as an accepted v3 limitation in the spec (cb85eb3); revisit if DotAIOS targets multi-desktop users.

Verification
Full suite green after every commit: 336 tests, 335 pass, 0 fail, 1 skip. Working tree clean. Independent correctness review of the fix diff: no bugs found.

Merge decision left to @filocosta46 — not merging from automation.

@filocosta46 filocosta46 merged commit e98c9f9 into main May 22, 2026
4 checks passed
@filocosta46 filocosta46 deleted the worktree-feat+github-sync branch May 22, 2026 09:33
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