Skip to content

feat(SYNC): team presence — teammates' worktrees in the sidebar via git-ref sync#214

Open
btucker wants to merge 19 commits into
mainfrom
multi-user
Open

feat(SYNC): team presence — teammates' worktrees in the sidebar via git-ref sync#214
btucker wants to merge 19 commits into
mainfrom
multi-user

Conversation

@btucker

@btucker btucker commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of the team worktree sharing design (spec, plan): teammates' worktrees appear in the sidebar alongside your own, synced through the repo's existing git remote — zero new infrastructure.

  • Presence documents over git refs — each user publishes a small JSON doc (worktree names, branches, running/idle state) to refs/graftty/presence/<slug> on origin. The JSON rides in the commit message of an empty-tree commit (no working-tree writes, no stdin plumbing); slug derives from git config user.email.
  • Publish on change + 10-min heartbeat, 60s poll; fetched docs older than 30 min are treated as stale and omitted.
  • Opt-in per repo via the repo context menu ("Share Worktrees with Team"); default off (SYNC-4.1); disabling deletes your ref from origin and clears local state immediately (race-guarded against in-flight ticks).
  • Ambient sidebar rows — teammates' worktrees render read-only inside the repo section (dim person icon, dimmed text, italic running hint, trailing owner badge — no saturated colors, no interactivity).

Specs

SYNC-1.1..1.3 (document/slug), SYNC-2.1..2.3 (ref publish/fetch/delete), SYNC-3.1..3.4 (service tick: opt-in gate, own-doc exclusion, heartbeat, staleness), SYNC-4.1 (persisted flag), SYNC-5.1 (sidebar rendering, inventory). SPECS.md regenerated; generate-specs.py --check clean.

Test plan

  • 35 new tests incl. real-git integration (two clones exchanging presence through a bare upstream, end-to-end with production defaults) and a regression test for the leave-vs-tick interleave.
  • Full suite: 2040 tests, 0 failures.
  • Manual smoke: enable sharing on a repo with a remote, verify git ls-remote origin 'refs/graftty/presence/*'; second clone with different user.email publishes → dimmed row appears within ~60s; disable → ref deleted, rows gone.

Phase 2+ (consent-gated live view/control, history search) builds on this per the design doc.

🤖 Generated with Claude Code

btucker and others added 19 commits June 5, 2026 18:33
… history search over WebRTC)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JSON

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace `state: String` in `PresenceDocument.Worktree` with a nested
  `State` enum (`running`/`idle`); update `build()` and tests to use
  the typed cases instead of raw string literals.
- Rephrase the SYNC-1.1 `@Test` title to avoid escaped double-quotes,
  which caused `generate-specs.py` to emit a dangling backslash and
  truncate the sentence in SPECS.md.
- Add `/// Format version` doc comment on `version`, and one-line `///`
  comments on `user`, `email`, and `updatedAt` matching WorktreeEntry style.
- Regenerate SPECS.md; `--check` passes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PresenceIdentity with slug derivation (lowercase + non-alphanumeric runs → hyphen) and async load() that probes git config user.name/user.email; throws IdentityError.missingEmail when email is absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… over origin

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends fetchDecodesAllPeerDocs to push a non-JSON commit to
refs/graftty/presence/legacy-tool from cloneB before fetching,
confirming fetchAll mirrors the ref but skips it during decode
and returns only the valid doc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the N+1 subprocess loop (for-each-ref + per-ref git show) in
fetchAll with a single `git for-each-ref --format=%(refname)%09%(contents)`
call. Extract the namespace string into `refPrefix` and derive refName,
the fetch refspec, and the for-each-ref pattern from it. Add a comment
on the best-effort local delete in `delete`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and observable store

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ublish-on-change test

- Log a debug-level message when identityProvider fails so a missing
  git config user.email is greppable in Console instead of silently
  disabling sync for the repo.
- Add isTicking reentrancy guard (MainActor-isolated, race-free) so a
  hung git subprocess cannot stack a second tick's git work on the
  same refs; the next ticker fire retries cleanly.
- Add 6th test: publish fires immediately on worktree-set change
  without waiting for the heartbeat interval.
- Soften start() doc comment: uses the same ticker-injection seam as
  WorktreeStatsStore, does not implement stop().
- Hoist tickNow = now() and per-doc slug computation to avoid
  repeated calls inside the hot path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add block comment explaining why the suite has no @SPEC ID (behavioral
specs live on SYNC-1.x/2.x/3.x unit suites; duplication is prohibited).
Annotate @mainactor and local git-config rationale inline.
Replace ben@btucker.net with ben@example.com so fixtures use no real addresses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ring toggle

Renders teammates' worktrees as read-only ambient rows inside each repo's
sidebar section (dim person icon, dimmed branch, italic running hint, owner
badge — no buttons/drag/menu/selection per the ambient-cue design rule).

Adds an opt-in repo context-menu toggle ("Share/Stop Sharing Worktrees with
Team", git-tracked repos only). Enabling pulses the presence ticker for an
immediate publish/fetch; disabling deletes our presence ref from origin
(SYNC-2.3, best-effort).

Adds the minimal Task-8 seam: TeamPresenceSyncStore + teamPresenceTicker on
AppServices, threaded into MainWindow/SidebarView.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt delete failures

- RemoteWorktreeRow: append ", running" to accessibilityLabel when presence.state == .running so the only dynamic cue on the row is exposed to VoiceOver.
- MainWindow: add import os + static presenceLogger (Logger subsystem/category matching TeamPresenceSync); restructure toggleTeamSharing's disable branch from nested try? calls into a single do/catch that logs a debug message when identity load or ref delete fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ave/pulse, store change-guards

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eave

A @mainactor TeamPresenceSync.tick(repos:) could interleave with
leave(repoPath:) at its await suspension points (publisher/fetcher
subprocess calls). If leave ran while tick was suspended after passing the
presenceSharingEnabled guard, the resumed tick wrote the stale fetch result
back via store.update — repopulating rows the user just removed until the
next 60s tick.

Track repos whose leave() ran mid-tick in a MainActor-serialized
pendingLeaves set; the resumed tick skips the lastPublished and store writes
for those repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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