Repository navigation
feat(mirror): project files can stay on the client while agents run on the host - #6157
malekelkssas wants to merge 35 commits into
Conversation
…n the host In remote mode the provider CLI runs on the host, but project files also had to live there. This adds project mirroring: the host materializes a git-backed mirror of a folder that lives on another environment (the origin), agents run against the mirror at full local speed, and a sync protocol keeps both sides in agreement over the existing connection layer. - Contracts: ProjectOrigin on projects, mirror.* RPCs, mirror:sync scope, projectMirroring capability, t3.json mirror.include allowlist. - Server: GitSync snapshot/bundle/three-way-apply primitives, signed GET/PUT bundle route, MirrorService (host broker + sync state machine, restart-safe queued apply-backs), MirrorAgent (origin side), turn-start sync gate with an explicit stale-run override, post-checkpoint apply-back. - Web: capability-gated "Files live on another machine" creation flow and a mirror status chip with Sync now and conflict listing. - Docs: internals section in remote.md, user guide, glossary entries. Implemented by Claude Fable 5 via Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mirror.createPeerCredential required access:write, which only admin-scoped (startup) pairings carry, so a normally paired client could not finish mirror setup. The endpoint mints a token scoped to mirror:sync only, so orchestration:operate - the scope that already permits running agents - is the right gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirroring is built on git, but the folder itself didn't need to be a repository already — a plain folder now gets one initialized in place on attach instead of being refused outright, with GitSync's seed/apply paths fixed to handle the resulting unborn-HEAD, no-branches state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Settings → General → Developer tools → React Grab overlay, letting
contributors hover T3 Code's own UI and copy component stack/source
locations for an agent. Gated so it can never reach a shipped build:
the loader script is injected only by vite dev (apply: "serve"), the
package stays a devDependency, and both the settings row and the hook
bail outside import.meta.env.DEV — enforced by reactGrabBoundary.test.ts.
The loader (reactGrabDevEntry.ts) has to run before react-dom evaluates
so React Grab's fiber instrumentation can see React mount, which a
same-module dynamic import can't guarantee; a separate injected
<script type="module"> ahead of main.tsx's own tag does. It also works
around react-grab's init({enabled:false}) returning a permanently inert
stub instead of a toggleable instance, and installs a live stylesheet
so turning the setting off fully hides the overlay instead of leaving
react-grab's own collapsed-toolbar affordance in the page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ving empty gitlink stubs Gitlinks (mode 160000) were never followed by the mirror's bundle/read-tree pipeline, so submodules and unregistered nested repos always mirrored as empty directories. Detect gitlinks via `git ls-tree`, and mirror each one through the same seed/sync/checkout primitives already used for the superproject, whether or not the path is registered in `.gitmodules`. A gitlink with no local copy anywhere on the origin is a per-path warning, not a failed sync. Includes a fix so a plain resync also picks up gitlinks that predate this feature (previously only newly-changed gitlinks were retried), plus two git-behavior bugs found along the way: isRepository misdetecting an uninitialized submodule path as already a repo, and `git fetch` triggering unwanted submodule auto-recursion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Setup Worktree script depends on .env and infra/relay/.env, which are gitignored and so are skipped by mirror sync by default.
This reverts commit cc4aae9.
Adds a "Sync gitignored env files" setting in project settings for mirrored projects, storing a mirrorIncludeIgnoredFiles flag alongside the existing defaultThreadEnvMode per-project override. The host sends the resolved extra include patterns (.env, .env.local, .env.*.local) to the origin on mirror.connect; both sides union them into their sync-snapshot include paths so no t3.json edit is required. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mirror.connect stream is long-lived, so a project's mirrorIncludeIgnoredFiles toggle only reached the origin at the next reconnect, not the next sync. Send a settings-updated event with the freshly resolved extra include paths right before every directive, so toggling the setting takes effect on the very next sync/apply-back (root or submodule) without needing the connection to drop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`git add` is fatal on an unmatched pathspec and then adds NONE of its arguments, so one missing entry (.env.local when only .env exists) silently dropped every include path from the snapshot — gitignored env files never synced. Add each include path in its own git add invocation so a missing pattern only skips itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plain `.env` pathspecs only match at the repo root, but monorepos keep their env files in per-app subfolders (apps/api/.env), so the toggle synced nothing for exactly the projects that need it most. Switch the patterns to `:(glob)**/...` pathspecs and exclude node_modules matches from every include force-add, since dependency trees are never synced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A stale mirror link (host project deleted or detached) made the origin agent retry mirror.connect every 15 seconds forever, flooding the log with MirrorProjectNotMirroredError. That answer is definitive, so stop the agent instead and log a single warning. The link row is kept; a real re-attach restarts the agent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Deleting a mirrored project now sends link-revoked to a connected origin (previously the directive had no producer), so the origin drops its mirror_links row and token instead of leaving a stale leftover; the host's sync watermark is cleared too. - New mirror.listLinks RPC plus a "Shared folders" section in Connections settings: see every folder a device shares to a host and remove leftovers via the existing mirror.detach. - Seed/sync transfer progress: the bundle upload route counts received bytes (with Content-Length totals) into MirrorBundleTransfer, the status stream republishes once a second while a transfer is in flight, and the mirror chip shows "Seeding · 30%" with a progress bar in the popover. - Mirror chip popover shows copyable origin and host mirror paths. - Project settings shows a "Mirrored project" info row (origin machine, path, live state); the Danger-zone copy explains deletion also disconnects the origin. - Sidebar thread rows show a mirror glyph on mirrored projects and the hover tooltip names the machine holding the files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Effect service conventions review of the new apps/server/src/mirror/* services and the packages/contracts/src/mirror.ts error contracts. Service definition shape (Context.Service with inline interface, make, layer, Foo["Service"] annotations, subpath namespace imports) looks consistent with the conventions.
Findings, all in the error-modelling and dependency-acquisition rules:
MirrorSyncFailedErrormodels failures as one unstructureddetailstring, derivesmessagefrom it, and has nocause; every production wrap setsdetail: cause.message/detail: String(cause), dropping the underlying error chain.MirrorAgent's transfer wraps stringify the HTTP failure intodetail, which carries the HMAC-signed single-use bundle URL into a caller-visible error.MirrorService.syncFailedis a pass-through error constructor helper, andfailPendingfabricatesprojectId: "" as ProjectIdwhen the real project id is in scope.Effect.catchTagused instead ofEffect.catchTags.SubmoduleSyncandmirrorIncludetake service instances as parameters instead of acquiring them from the Effect environment, hiding those requirements from the types.
Posted via Macroscope — Effect Service Conventions
ApprovabilityVerdict: Skipped Macroscope did not run approvability analysis for this PR. Diff is too large for automated approval analysis, so this PR cannot be approved automatically. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
# Conflicts: # apps/web/src/components/chat/ChatHeader.tsx # apps/web/src/components/settings/ProjectSettingsPanel.tsx
…ctness, and consistency Fixes raised by Macroscope's review of pingdotgg#6157: Security: - Bind mirror.connect/mirror.respond to the caller's authenticated peer subject so a peer token minted for one project can't act as the origin for another (ws.ts, MirrorService.ts). - Reject project ids that would let a mirrored project.create escape mirrorsDir via path traversal (Normalizer.ts). - Reject submodule directive paths that escape the project root on the origin agent (MirrorAgent.ts). - Stop leaking the signed single-use bundle URL into user-visible error detail on transport failures (MirrorAgent.ts). - Check the bundle transfer token's direction before consuming it, so a wrong-method request can't burn the one legitimate use (MirrorBundleTransfer.ts, http.ts). - Require the stale-run retry to resend the exact failed message, not just any message on the same thread within the offer window (ProviderCommandReactor.ts). Correctness: - Stage (snapshot + persist) a post-turn apply-back synchronously under the project lock before turn completion is signaled; only the network delivery to the origin is backgrounded. Fixes a race where a fast-following turn's ensureFresh could overwrite the mirror before the completed turn's edits were captured (CheckpointReactor.ts, MirrorService.ts). - Fix reconnect teardown ordering so a superseded connection's in-flight requests are failed immediately instead of leaking until their timeout (MirrorService.ts). - Remove a staged submodule bundle on every non-success exit, not just the success path, to stop bundle files accumulating in staging (MirrorService.ts). - Parse git ls-tree/merge-tree output with -z so paths with special characters aren't mishandled during conflict resolution, and handle submodule gitlink entries (mode 160000) alongside blobs (GitSync.ts). - Propagate branch-update failures during apply instead of ignoring them while still marking the apply successful (MirrorAgent.ts). - Guard against a double Enter-key submission creating duplicate mirrored projects (CommandPalette.tsx). - Return early on every detach failure, not just non-interrupted ones, so an interrupted failure doesn't show a false success toast (MirrorLinksSection.tsx). Other: - Align the bundle transfer token TTL with the 30-minute seed timeout. - Use Effect.catchTags consistently for single-tag recovery. - Remove a docs contradiction about non-git folders.
…ork-log warnings The claude-agent-sdk build running here emits a command_lifecycle message type ahead of what the pinned SDK's TypeScript types declare, so it fell through the adapter's exhaustiveness guard into an opaque "Claude SDK message 'command_lifecycle' — command_uuid: ... · state: ..." runtime-warning row in the thread's work log. It's internal bookkeeping with no T3 surface, so consume it deliberately like prompt_suggestion.
There was a problem hiding this comment.
One new finding on error payload boundedness in GitSync.ts. The other convention issues I flagged in the previous run (unstructured MirrorSyncFailedError, the syncFailed/gitFailed construction helpers, GitSync["Service"]/FileSystem+Path injection in SubmoduleSync.ts and mirrorInclude.ts) are still open in this revision; I have not re-posted those threads.
Posted via Macroscope — Effect Service Conventions
Fixes raised by Macroscope's follow-up review of pingdotgg#6157: Security: - Revoke any prior mirror-peer session for a project before issuing a new peer credential, so a stale token from a previous origin can't reconnect and displace the live connection (ws.ts). Correctness: - Fast-forward the checked-out branch via applyBranchUpdatesToCurrent in ingestBranchUpdates instead of just resetting the index — it was left on the old commit while the working tree had already moved (MirrorService.ts). - Fail on staging-directory creation instead of swallowing it, so the bundle-transfer layer doesn't silently start in a broken state (MirrorBundleTransfer.ts). - Delete the mirror link row at the point link-revoked is detected, not after the (now-failed) stream returns — the failure was short-circuiting past that check, so a revoked link retried with its dead token forever (MirrorAgent.ts). - Show a failed-to-load state on shared folders instead of an indistinguishable empty state (MirrorLinksSection.tsx). - Derive the deletion marker's OID width from a real oid in the same repo instead of assuming SHA-1, so SHA-256 repos don't break conflict resolution (GitSync.ts). - Thread priorState through processGitlink's recursive call so nested submodules-of-submodules resolve their real prior oid instead of always re-seeding from scratch (MirrorService.ts). - Base isMirrored on any checkout in the project group, not just the representative, so a mirrored non-representative checkout isn't hidden (ProjectSettingsPanel.tsx). - Use monotonic time (not wall-clock) for the stale-run offer window so a clock correction can't extend or collapse it (ProviderCommandReactor.ts). - Roll back the host-side project on any mirror setup failure after creation, so a broken origin folder doesn't leave an orphaned project behind on every retry (CommandPalette.tsx). Documented as an accepted trade-off rather than fixed: the bundle-transfer single-use token registry is in-memory only, so a restart within the TTL window allows one replay of an already-consumed token.
…aw argv/stderr args can carry a remote URL with embedded credentials (git remote add/set-url), and this error's message flows into MirrorSyncFailedError.detail and from there the RPC boundary, status, and logs. Keep args/stderr as structured fields but derive message from bounded attributes (subcommand, root, exit code, stderr length) only.
There was a problem hiding this comment.
One new finding on error modeling in the mirror service code; the findings from the previous runs on this PR (unstructured MirrorSyncFailedError.detail, the syncFailed construction helper in MirrorService.ts, GitSync["Service"] passed as a parameter in SubmoduleSync.ts, and readMirrorIncludePaths closing over FileSystem/Path instead of yielding them from the environment) are still open and are not re-posted here.
Posted via Macroscope — Effect Service Conventions
Keeps ProviderCommandReactor.test.ts within the no-manual-effect-runtime-in-tests legacy baseline (70 occurrences), fixing the failing CI lint check on PR #1.
# Conflicts: # apps/web/src/AppRoot.tsx # apps/web/src/components/settings/settingsSearch.ts # packages/contracts/src/environment.ts
- Move mirror-link revocation off project delete into a MirrorProjectDeletionReactor triggered by the project.deleted domain event, so it covers every dispatch transport (WS, HTTP, offline CLI), not just the WebSocket handler. It now also revokes the mirror-peer auth session so a disconnected origin's credential can't outlive the project. - Fix mirrorCreatePeerCredential to issue the replacement session before revoking the old one, so a failed issue doesn't strand the origin without any working credential. - Gate the mirror freshness check before first-turn branch rename/title generation side effects, so they can't run against a stale mirror. - Resolve mirror-add's explicit relative path against the origin browse cwd instead of null, and await the host-project rollback (surfacing a toast on failure) instead of firing it and forgetting. - Bound GitSyncCommandError's serializable fields (subcommand, stderrLength) instead of carrying raw argv/stderr, which could embed remote credentials. - Close MirrorAgent's manager scope via a finalizer, and add the missing gap/motion-reduce classes flagged on ChatHeader and MirrorStatusChip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Two Effect service-convention findings on the new MirrorProjectDeletionReactor. Previously reported items that are unchanged (e.g. syncFailed, SubmoduleSync/mirrorInclude service-instance parameters, MirrorNotARepositoryError.detail) are not repeated here.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
One finding: the new "Shared folders" settings section is not gated by the projectMirroring capability the way the rest of the mirroring UI is, so it surfaces (and can render a permanent error row) on environments whose server doesn't support mirroring.
The previously flagged breadcrumb gap and the reduced-motion guard on the transfer progress bar are both resolved.
Posted via Macroscope — UI Consistency
- Fix listRemotes' regex to allow remote URLs/paths containing spaces (e.g. a mirror workspace under "My Project"), which previously fell through unmatched and caused setRemotes to re-add an existing remote. - Resolve symlinks before the submodule path containment check, so a symlink inside the working tree can't be used to point a submodule seed/apply/sync operation outside the project root. - Thread MirrorService/EnvironmentAuth as Effect requirements in revokeMirrorLinkAndCredentials instead of passing service instances as plain parameters, and mock them via Layer.mock in the test instead of `as unknown as` casts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oring MirrorLinksSection queried mirror.listLinks against every saved environment, including ones running an older server without the RPC method, which rendered a permanent "unavailable" row and broke the version-skew contract CommandPalette already honors elsewhere. Filter to environments that advertise projectMirroring before handing them to the section. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
One finding in the new mirror UI. The previously raised point about MirrorLinksSection querying mirror.listLinks on environments that never advertised projectMirroring is still unaddressed, but I'm not re-posting it here.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
UI consistency review of the changed web surfaces (mirror chip/settings rows, sidebar mirror glyph, command palette mirror flow, React Grab settings row). Earlier findings (breadcrumb gap, reduced-motion guard on the transfer bar, capability-gated MirrorLinksSection) are resolved on this head. One minor labeling nit left.
Posted via Macroscope — UI Consistency
…gg#6157 - Retry mirror-link/credential revocation a few times with backoff before falling back to a logged best-effort skip, since it's consumed exactly once off a hot domain-event stream and a transient failure previously dropped the revocation for good. - Treat merge-tree exit code 1 with no listed conflict paths as a failure instead of silently applying a tree that may still contain raw conflict markers. - Give the two path-copy buttons in MirrorStatusChip's popover distinct aria-labels (origin vs. mirror) instead of both saying "Copy link". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI's Check job flagged two no-manual-effect-runtime-in-tests lint errors in the new reactor test. Switch to @effect/vitest's it.effect, matching the convention every other Effect test file in this repo uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Sync - ls-tree now runs with -r so a conflict path that's a directory on the preferred side (a file replaced by a folder of the same name) expands to every blob/gitlink beneath it, instead of being silently dropped by the blob/commit-only match and leaving merge-tree's synthetic subtree in the final result. Clears any bare-file entry at the conflict path first, since an index can't hold both a blob and nested entries there. - ls-tree and update-index --index-info both run with -z, so a conflicted path containing a literal tab or newline round-trips correctly instead of being quoted/escaped or split across records. - Added a test covering a file-vs-directory type conflict end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applyBranchUpdates used allowNonZeroExit on `git worktree list --porcelain` and never checked for truncated output, so a failing or truncated discovery silently degraded to "no other worktrees have this branch checked out" instead of blocking the ref update — the opposite of the guarantee this check exists to provide. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
diffGitlinks/discoverAllGitlinks took GitSync as a plain parameter instead of acquiring it from the environment, so the git dependency was invisible in their returned effects' requirements. Callers already hold a resolved instance, so they provide it locally instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
One finding: the new "Shared folders" settings section renders its heading and intro paragraph with no body for users who have no mirror links, which is the default state for everyone. Everything else in the changed web UI (mirror status chip, sidebar glyph/tooltip, command-palette mirror flow, project mirroring row, developer-tools row) follows the existing primitives and settings-layout conventions, and the earlier findings on this PR (copy-button accessible names, breadcrumb gap, reduced-motion guard on the transfer bar, capability filtering for mirror links) are addressed.
Posted via Macroscope — UI Consistency
…otARepositoryError Both errors derive their message from a detail string that production call sites built as `cause.message`, discarding the underlying error entirely. Added an optional cause: Schema.Defect() field (the same pattern already used elsewhere in packages/contracts) and threaded the real cause through at every call site that had one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Follow-up on the "Effect Service Conventions" check's retained-findings summary (commit bbec5bd): Fixed:
Deliberately not changed, with reasoning:
Happy to take another pass at the |
revokeLink deleted mirror_sync_runtime without holding the same per-project lock every sync/apply-back core runs under, so an in-flight operation's later saveRuntime call could recreate the row right after it was cleared — letting a subsequent relink reuse stale state instead of reseeding cleanly. fix(web): show an explicit empty state for Shared folders MirrorLinksSection rendered its heading and intro paragraph whenever any environment advertised mirroring, even when every environment had zero links, leaving a bodyless heading — the common case. Track per-environment content status and render a "No shared folders" row once every environment has reported none, matching the empty-state convention used elsewhere on this page (AccessRows). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g path agentStagingPath interpolated directive.syncId directly into a file path; since it's host-controlled input (untrusted, per this module's own trust boundary) with no character restriction, a compromised host could set it to a traversal sequence and make this origin-side agent write or delete files outside stagingDir. Percent-encode it first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| yield* hooks.afterMirrorChanged({ projectId, workspaceRoot: root }); | ||
| // The mirror moved under any queued apply-back; recompute it so a | ||
| // stale snapshot can never regress the origin working copy. | ||
| const pending = runtime.pendingApplyJson; |
There was a problem hiding this comment.
🔴 Critical mirror/MirrorService.ts:1222
A following ensureFresh can overwrite host edits captured by queueApplyBack, permanently losing the queued apply-back. The pending state is checked only after applySnapshot runs with conflictPreference: "target", so freshness can win the project lock, replace the working tree, and then clear or restage the pending apply from the already-overwritten tree; deliver or merge the persisted pending snapshot before applying the new origin snapshot.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mirror/MirrorService.ts around line 1222:
A following `ensureFresh` can overwrite host edits captured by `queueApplyBack`, permanently losing the queued apply-back. The pending state is checked only after `applySnapshot` runs with `conflictPreference: "target"`, so freshness can win the project lock, replace the working tree, and then clear or restage the pending apply from the already-overwritten tree; deliver or merge the persisted pending snapshot before applying the new origin snapshot.
| for (const link of targetLinks) { | ||
| if (diffedPaths.has(link.path)) continue; | ||
| const fullPath = pathPrefix === "" ? link.path : `${pathPrefix}/${link.path}`; | ||
| if (priorState[fullPath] !== undefined) continue; |
There was a problem hiding this comment.
🟠 High mirror/MirrorService.ts:931
Uncommitted edits inside an existing submodule are never mirrored when its superproject gitlink OID is unchanged, so manual and turn-start syncs silently miss those changes. The unchanged-gitlink pass skips every path present in priorState; remove that skip so known submodules are polled for submodule-sync-requested responses.
- if (priorState[fullPath] !== undefined) continue;🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mirror/MirrorService.ts around line 931:
Uncommitted edits inside an existing submodule are never mirrored when its superproject gitlink OID is unchanged, so manual and turn-start syncs silently miss those changes. The unchanged-gitlink pass skips every path present in `priorState`; remove that skip so known submodules are polled for `submodule-sync-requested` responses.
There was a problem hiding this comment.
One finding on the new mirror settings section; everything else in the changed web UI (mirror status chip, sidebar glyph, command palette flow, project settings rows, dev-tools switch) follows the existing primitive and settings-layout conventions, and the issues raised in earlier runs (empty state, copy-button labels, capability filtering, breadcrumb gap, reduced-motion guard) are resolved at this head.
Posted via Macroscope — UI Consistency
…nks warning className="text-warning" on SettingsRow had no visible effect since the row's own children (h3, description p) set their color explicitly. Wrap the error text in a span instead, matching the pattern used by ConnectionsSettings' WSL failure row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Note 🤖 GPT-5.6 Sol responding on behalf of Theo Closing this PR after an automated pass over open pull requests. Introduces client-host filesystem mirroring and changes project ownership across 85 files. |
What Changed
Adds project mirroring: in remote mode the provider CLI runs on the host, but project files can now stay on another environment (the origin). The host materializes a git-backed mirror of the origin's folder, agents run against the mirror at full local speed, and a sync protocol keeps both sides in agreement over the existing connection layer.
ProjectOriginon projects,mirror.*RPCs,mirror:syncscope,projectMirroringcapability,t3.jsonmirror.includeallowlist.GitSyncsnapshot/bundle/three-way-apply primitives, signed GET/PUT bundle route,MirrorService(host broker + sync state machine, restart-safe queued apply-backs),MirrorAgent(origin side), turn-start sync gate with an explicit stale-run override, post-checkpoint apply-back.remote.md, user guide, glossary entries.Why
Lets a project's real working copy stay on a laptop (uncommitted edits, untracked files, gitignored-but-allowlisted files like
.env) while an always-on machine runs the agents, syncing automatically before and after every turn instead of requiring the files to live on the host.UI Changes
Adds a "Files live on another machine" project creation option (shown only when both environments support mirroring) and a mirror status chip on the project header (Seeding/Syncing/Synced/Offline/Conflicts) with a manual "Sync now" action and conflict listing.
Test plan
Verified end-to-end with a two-device setup (host + origin): pairing, project seeding (dirty edits, untracked files,
mirror.includeentries), pre-turn push, post-turn apply-back, manual sync, origin-offline stale-run fallback with queued apply-back replay on reconnect, and conflict handling (user's local edit wins, agent's version stays reachable in git history). Also covers regression checks: gitignored files don't leak beyond the allowlist, non-git folders are rejected at setup, and non-mirrored projects behave unchanged.Automated suites:
pnpm vp test run src/mirror(GitSync + MirrorService end-to-end) andpnpm vp test run src/orchestration/Layers/ProviderCommandReactor.test.ts(turn gate).Checklist
Note
Add project mirroring to keep files on client, agents on host
MirrorServiceorchestrates seed/sync/apply via a long-livedMirrorAgentconnection, using git snapshots and HMAC-signed bundle transfers over new HTTP endpoints (http.ts, MirrorService.ts, MirrorAgent.ts, GitSync.ts)MirrorService.ensureFreshat turn start; if the origin is offline, the turn is blocked with a 10-minute stale-run offer window allowing a resend of the same message to run against last-synced files (ProviderCommandReactor.ts)CheckpointReactorqueues a mirror apply-back to push results to the origin working copy; project deletion triggersMirrorProjectDeletionReactorto revoke mirror links and peer sessions (CheckpointReactor.ts, MirrorProjectDeletionReactor.ts)mirrorAttach,mirrorConnect,mirrorRespond,subscribeMirrorStatus, etc.) with a newmirror:syncauth scope, client runtime atoms, and UI surfaces includingMirrorStatusChip, sidebar mirror glyphs, a command-palette mirror-project flow, and settings panels for link management and include-ignored toggle (ws.ts, rpc.ts, mirror.ts, MirrorStatusChip.tsx, CommandPalette.tsx)origin_json,mirror_include_ignored_files,mirror_sync_runtime, andmirror_linkstables;Normalizerforces mirrored projects to use a server-managed directory undermirrorsDirwith path-traversal validation (Migrations.ts, Normalizer.ts)ensureFreshgate inProviderCommandReactor.makeand the stale-run offer Map keyed bythreadIdare the primary places to check for regressions in turn dispatch.normalizeDispatchCommandresolvesworkspaceRootto a mirror directory — verifyprojectId-derived paths cannot escapemirrorsDir. New RPC scopeAuthMirrorSyncScopeinRpcAuthorization.tsmust be correctly mapped or peer connections will fail auth.Macroscope summarized 58807c0.