Sync a worktree without a coding session by running the git sync in Treemon - #154
Merged
Conversation
…merged PR is never masked Preserve provider fetch order when deduplicating GitHub PRs by branch, add reused-branch fixture coverage, and checkpoint related specification updates.
Coalesce exact duplicate pending prompts in the server bridge and extension send queue while preserving later deliveries.
Add the persistent accepted-sync store, runtime wiring, tests, and spec updates.
…y-accepted revision Persist accepted sync revisions, suppress same-revision prompts during the retry window, and clear stale records at lifecycle boundaries.
…merged Disable merged branches during refresh, reject re-enable attempts, and cover eligibility races and cleanup with tests.
Add bounded branch sync outcomes, safe merge/abort verification, and scratch-repository tests.
…enness is explicit Introduce an explicit sync target DU, thread it through auto-sync delivery and scheduler dependencies, and update selection tests.
Add provider-routed live open-PR queries, response classification, and focused fixtures and tests.
Document that live GitHub detection must include pull requests originating from forks.
Add a bounded non-force current-branch push helper and scratch-repository coverage, and update the Branch Sync specification.
…e requeue Reuse pending-envelope coalescing when drain survivors merge with concurrent enqueues, with regression coverage and spec clarification.
Syncing onto main brought in the rename of hasLocalDiff to localComparisonContent and its widening from �ool to the tri-state ComparisonContent. syncWithBase was written against the old boolean, where a failed probe read as "not dirty" and let the merge proceed. Under the new type a probe that could not answer is Undetermined, not Clean, so the sync now refuses it as a command failure and refuses confirmed local content as dirty. Only a worktree proven clean is merged, which is what the spec already required of the mechanical path. The scheduler test fixtures pick up the same rename and the auto-sync store the branch added to SchedulerServices.
…e same server process Track claim state separately from delivery and retry accepted revisions after expiry without restarting.
…uring toggle Re-read PR status after enabling auto-sync, roll back merged branches, and add race coverage plus spec clarification.
…branch lookup
The GitHub open-PR query filtered by `{upstreamOwner}:{branch}`, so a pull request whose head branch lives in a fork answered an empty list - a confirmed absence that would finish a mechanical sync without pushing. The head owner now comes from the remote git records for that branch, the same remote the push sends to; an unresolvable or non-GitHub head remote leaves the state unknown.
Document that fork head ownership must come from the branch remote's effective push URL, including remotes with a distinct pushurl.
…urable records Publish accepted records before retrying expired claims, clear synchronized claim state after catch-up, and add concurrent-expiry regression coverage.
Document that a stale catch-up clear must not erase or retire a later accepted auto-sync generation.
…ceptance Give every acceptance a generation that both the durable record and the in-process claim carry, so a catch-up retires only the acceptance it read and can neither erase a later acceptance's record nor retire its claim.
…c race test Replace the two ref cells with method-scoped mutable locals and document the mailbox-loop impurity boundary without changing the deterministic interleaving or assertions.
… for GitHub head ownership Resolve GitHub PR head ownership from the branch remote's effective push URL and cover fork, unreadable, non-GitHub, and argument-list cases.
…n, prompt an agent only on failure Run guarded mechanical branch sync for worktrees without open sessions and fall back to an agent prompt only when the operation cannot complete.
Document that the live PR lookup and push must stay bound to the branch observed and mechanically synchronized.
…ne branch Guard mechanical sync and non-force push against checked-out branch changes, with scratch-repository coverage and updated fallback behavior.
…d branch Re-read the checked-out branch inside the Git sync itself, immediately before each merge command, so a checkout landing during the cleanliness probe or the fetch can no longer redirect the merge onto another branch. Cover the window with scratch-repository regression tests.
Reject configured push remotes beginning with a dash before building the Git push command, with focused tests and spec clarification.
…n open PR Match the GitHub PR head owner and branch, document the behavior, and add regression coverage.
…til its operation ends Keep active operation guards until AutoSync.trigger releases them, and add regression coverage for disappearing worktrees.
…t its edge Extract pure open-PR response classification and keep logging at the response boundary.
… branch bloated Move branch synchronization into GitBranchSync and scheduler state into SchedulerState, then requalify consumers and update architecture documentation.
Removes machinery this branch added that duplicates work already done elsewhere, with no change to observable sync behavior. Deduplication now rests on the durable accepted-revision record alone. AutoSync.trigger holds the per-worktree operation guard across an entire attempt, so two operations for one worktree cannot overlap; the in-process revision claim it was paired with (ClaimedRevision, ClaimReason, AcceptanceGeneration, AutoSyncTriggeredRevisions and its five StateMsg cases) only re-solved that same problem. The push decision reads PrInfo.IsOpen instead of a second provider query. The PR refresh already fetches open and closed pull requests separately and then discarded which was which, so carrying that through the reconciled map replaces PrOpenState, both queryOpenPrState implementations, and the fork head-owner resolution. A push decided from data one refresh old is self-correcting: it is non-force, and the next cycle catches up. Also removed: server-side pending-prompt coalescing (the extension queue covers every producer), merged-branch auto-disable and the toggle's write-then-reread-then-rollback (eligibility already refuses merged branches at every action boundary), two of five rev-parse HEAD probes (the merge and the push each re-read the branch themselves), durable-record pruning on worktree disappearance (records are revision-keyed and expire), and ~200 lines of rationale-essay comments. Net -1,944 lines: -577 production, -1,367 tests. Specs updated to describe the current system. Copilot-Session: 060af155-a294-49e5-88a8-c457e926131c
Resolved three conflicts and the breakage the merge caused: - GithubPrStatus.ParsedGithubPr: kept both new fields — this branch's IsOpen (open-PR classification for the auto-sync push gate) and main's AutoMergeEnabled (#153 auto-merge badge). - CliTests FormatPrTests: took main's basePr copy-and-update fixture, which the merged test bodies already call, and added IsOpen. - code-improvements.md: kept this branch's updated row 8 (state slice extracted to SchedulerState.fs) and honored main's deletion of row 9, whose ProcessRunner consolidation shipped in #150. - GitBranchSync: ported to main's Spawn API — runArgumentListWithTimeout is gone; CaptureLimits.small is the same 64 KB bound, Timeout keeps the 120 s budget. - AutoSyncTests/BranchSyncTests: PrInfo literals set AutoMergeEnabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
Security and correctness: - GitBranchSync: refuse the push when branch.<name>.merge names a different branch. Git does not require an upstream to share its branch name, and the open-PR gate is evaluated on the upstream name, so it passed precisely when the names differed - publishing local commits onto someone else's PR branch or onto the base branch. Refusal routes to the agent fallback. (F6) - Auto-sync now distinguishes "PR status not loaded yet" from "no pull request". PerRepoState.PrData is an option, ReadPrStatus returns an option, and a sessionless worktree defers to the next refresh instead of merging locally and recording an accepted revision it can never push. An open session is unaffected - it resolves PR state itself. (F8) - GithubPrStatus: a fetched PR counts only when its head repository is one this checkout could push to (configured remotes' push-URL owners plus the upstream owner). GitHub matches by bare head ref, so an outsider's fork PR could otherwise open a same-named local branch's push gate. (F7) - GitBranchSync: -- now precedes the fetch remote, so a dash-leading remote name cannot be parsed as a git option. (F5) Modelling and duplication: - PrInfo carries a three-state PrState (Open | Merged | ClosedUnmerged) instead of independent IsOpen/IsMerged booleans, so the illegal combinations are unrepresentable and each provider maps its single status fact once. (F11) - Delete AutoSyncLaunchesInFlight: it is nested inside the operation guard and could never refuse, so the operation guard is the only guard. (F9) - WorktreeApi reuses the injected ClearAcceptedRevision instead of duplicating AutoSyncStore.clear. (F10) Specs: - canvas-pane.md no longer claims server-queue coalescing that SessionBridge does not implement and the Decisions section denies. (F1) - worktree-monitor.md describes the real push behaviour instead of a "pushed on the next cycle" guarantee no code path provides. (F2) - Restore the GitWorktree.fs Key Files responsibilities the branch dropped (F4) and list src/Extension/send-queue.mjs (F3). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
There was a problem hiding this comment.
Pull request overview
Adds sessionless mechanical Git synchronization with durable deduplication, safer PR-state handling, and a serialized extension send queue.
Changes:
- Adds guarded fetch/merge/push synchronization with agent fallback.
- Persists accepted sync revisions and introduces explicit PR states.
- Extracts scheduler state and extension queue logic into dedicated modules.
Show a summary per file
| File | Description |
|---|---|
src/Tests/WorktreePathResolutionTests.fs |
Updates scheduler-state references and API arguments. |
src/Tests/UpstreamRemoteTests.fs |
Imports extracted scheduler state. |
src/Tests/Tests.fsproj |
Registers new sync/store tests. |
src/Tests/SessionActivityServiceTests.fs |
Updates scheduler mailbox types. |
src/Tests/ServerLifecycleTests.fs |
Uses extracted scheduler agent. |
src/Tests/SchedulerTests.fs |
Covers new stores and PR state. |
src/Tests/PrFilteringTests.fs |
Migrates PR assertions to PrState. |
src/Tests/OverviewSnapshotCaptureTests.fs |
Updates scheduler-state references. |
src/Tests/OverviewHistoryApiTests.fs |
Updates API construction. |
src/Tests/MergedPrStoreTests.fs |
Migrates merged-state fixtures. |
src/Tests/IdleDetectionTests.fs |
Imports extracted scheduler state. |
src/Tests/GithubFixtureTests.fs |
Tests PR owners and branch reuse. |
src/Tests/fixtures/worktrees.json |
Updates serialized PR state. |
src/Tests/fixtures/github/pr-list-fork.json |
Adds fork-owner fixture data. |
src/Tests/fixtures/github/pr-list-branch-reuse-open.json |
Adds open reused-branch fixture. |
src/Tests/fixtures/github/pr-list-branch-reuse-closed.json |
Adds closed reused-branch fixtures. |
src/Tests/DiffViewerTests.fs |
Updates scheduler-state references. |
src/Tests/DiffEndpointTests.fs |
Updates scheduler mailbox types. |
src/Tests/CodingToolSinceTests.fs |
Imports extracted scheduler state. |
src/Tests/CliTests.fs |
Migrates CLI fixtures to PrState. |
src/Tests/CanvasDocServerTests.fs |
Updates scheduler-state references. |
src/Tests/BranchSyncTests.fs |
Tests mechanical merge and push behavior. |
src/Tests/AzDoFixtureTests.fs |
Tests parsed Azure PR states. |
src/Tests/AutoSyncStoreTests.fs |
Tests persistence and corruption handling. |
src/Shared/Types.fs |
Introduces three-state PR status. |
src/Server/WorktreeApi.fs |
Wires auto-sync storage and cleanup. |
src/Server/SessionActivityService.fs |
Uses extracted scheduler protocol. |
src/Server/SessionActivityRuntime.fs |
Updates scheduler mailbox types. |
src/Server/SessionActivity.fs |
Updates scheduler documentation reference. |
src/Server/Server.fsproj |
Registers new server modules. |
src/Server/SchedulerState.fs |
Extracts dashboard state and mailbox agent. |
src/Server/RefreshScheduler.fs |
Orchestrates durable mechanical auto-sync. |
src/Server/PrStatus.fs |
Adds PR states and trusted-owner discovery. |
src/Server/Program.fs |
Creates and flushes runtime stores. |
src/Server/OverviewSnapshotCapture.fs |
Uses extracted scheduler state. |
src/Server/MergedPrStore.fs |
Migrates persisted PR reconstruction. |
src/Server/GithubPrStatus.fs |
Parses PR states and filters head owners. |
src/Server/GitBranchSync.fs |
Implements mechanical merge and push. |
src/Server/DemoFixture.fs |
Migrates demo PR fixtures. |
src/Server/CanvasDocServer.fs |
Updates scheduler mailbox types. |
src/Server/AutoSyncStore.fs |
Adds durable accepted-revision storage. |
src/Server/AutoSync.fs |
Adds target selection and mechanical fallback. |
src/Extension/session-prompt.test.mjs |
Tests typed prompt decoding. |
src/Extension/session-prompt.mjs |
Returns prompt kind with text. |
src/Extension/send-queue.test.mjs |
Tests queue serialization and coalescing. |
src/Extension/send-queue.mjs |
Implements serialized send queue. |
src/Extension/extension.mjs |
Integrates the extracted queue. |
src/Client/CardViews.fs |
Renders three-state PR data. |
src/Cli/Program.fs |
Formats merged state from PrState. |
docs/spec/worktree-monitor.md |
Documents mechanical auto-sync architecture. |
docs/spec/user-idle-detection.md |
Updates scheduler module references. |
docs/spec/session-status-push.md |
Documents sessionless sync behavior. |
docs/spec/overview-activity-history.md |
Updates scheduler state references. |
docs/spec/future/code-improvements.md |
Records scheduler-state extraction. |
docs/spec/canvas-pane.md |
Documents queue coalescing. |
docs/spec/canvas-interaction-routing.md |
Reformats queue behavior text. |
docs/spec/beads-overview-band.md |
Updates scheduler module references. |
Review details
- Files reviewed: 58/58 changed files
- Comments generated: 5
- Review effort level: Medium
- Run every mechanical-sync git command with repository hooks disabled. pre-merge-commit, post-merge and pre-push are project-controlled scripts, and this path runs unattended with no session watching it - verified that an unguarded merge does execute a repo's post-merge hook. Pointing core.hooksPath at a directory Treemon never creates disables them all; --no-verify would not, since it leaves post-merge running. - Prove the worktree clean twice: before the fetch and again immediately before the merge. The fetch reaches the network and may run for up to 120s, so the tree proven clean before it is not evidence about the tree being merged into. - Treat an accepted record stamped in the future as invalid. now - AcceptedAt is negative after a clock rollback or a hand-edited runtime file, which is trivially under the retry age, so the revision stayed suppressed until that future time instead of for one hour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
An open terminal was treated as an owner: any session seen inside the openness
window kept the sync on the agent path, so a worktree parked overnight with a
CLI still running never got synced. Waiting for the terminal to close is a
condition no user would guess.
Openness and activity are now separate questions. SyncTarget gains a case -
WorkingSession | IdleSession | NoOpenSession - and only a session mid-turn keeps
the agent path; idle and waiting-for-user are synced mechanically like a worktree
with no session at all. Because it is a DU, the compiler flagged every routing
decision the split had to revisit.
- Background agents count as work: the effective status already reports Working
while one runs, so a session that spawned one is protected between its turns.
- A failed mechanical sync prompts the idle session itself rather than launching
a second terminal.
- Accepted risk: idle is a turn status, not a machine status, so a clean tree with
a build running outside a background agent can still be merged under. The
clean-tree refusal covers everything with work in progress.
The auto-sync toggle now reports progress instead of only going dead while its
request is in flight: the two-arrow glyph spins, the tooltip reads "Syncing
with {base}...", and it stays undimmed so working is distinct from disabled.
prefers-reduced-motion drops the rotation and keeps the tooltip. This matters
more now that idle sessions sync mechanically - the awaited toggle reaches real
Git work in the common case rather than the rare one.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
selectTargetFromSessions had reimplemented pickActive's filter-sort-take-head shape with its own status predicate, which is the same selection rule written twice. Both meanings now come from one private core in SessionActivity, the module that owns session-status collapse: - pickActive - drop Idle, so a session waiting on its user still lights the dot. - pickWorking - only a session mid-turn, which is what owns a worktree for sync. The two rules differ by exactly one predicate and now say so, instead of an ad-hoc isWorking helper in AutoSync restating the pipeline. No behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
Six call sites passed ten positional arguments, and the optional stores read as a run of bare Nones - None None None roots None "1.0" None in the test helpers. Two of the ten are string option (TestFixtures and DeployBranch), so a transposition would have compiled and silently swapped them. WorktreeApiDependencies names each one. The body destructures the record in a single bind, so the change is the signature and the call sites rather than 800 lines of member code, and fixture-mode wiring now reads as what it configures. No behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
Reported from a real deploy: clicking auto-sync gave no feedback even though the request took 4 seconds and the sync happened. The cause was the spinner I added. It fed the whole in-flight state through an animation, and then disabled that animation under prefers-reduced-motion while also setting opacity back to 1 - which cancelled the dimming the disabled button used to provide. On a machine with Windows animations turned off the browser reports reduced motion, so the in-flight toggle rendered identically to the resting one: strictly less feedback than before the spinner existed. The state now reads statically - the palette's yellow in-progress tint on the glyph and border - and keeps the rotation where motion is allowed. That also separates "working" from the green "enabled" state instead of relying on motion alone to carry it. The regression test compares computed paint between the resting and syncing toggle, so an animation-only cue fails it. It samples after the button's 0.15s colour transition, since an immediate read returns the pre-transition value. Two existing assertions on the in-flight class string now expect the syncing marker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
Treemon's mechanical sync conflicted and aborted; resolved by hand. main added repository-declared diff categories (#155) and consolidated the dashboard font sizes (#156) while this branch moved the scheduler state slice into SchedulerState and reshaped worktreeApi's dependencies, so the two collided on five files: - WorktreeApi: kept both sides - main's diffCategoryReport helper and this branch's WorktreeApiDependencies record. The record already carries AutoSyncStore, which main's positional signature did not have. - CanvasDocServer: took main's resolveDiffTarget (it replaces the older getDiffComparisonContext) and retyped it to SchedulerState.StateMsg, dropping the stale duplicate resolver body this branch still had. - DiffEndpointTests: took main's deletion - those helpers now live in the new DiffEndpointTestHelpers.fs. This branch's only edits there were the module rename, which is reapplied below. - CanvasDocServerTests: kept main's new repoId binding with this branch's SchedulerState.createAgent. - worktree-monitor.md: kept this branch's GitWorktree/GitBranchSync rows and took main's TreemonConfig row, which now names the diffCategories read. main's new files still referenced RefreshScheduler for the state members this branch moved, so DiffEndpointTestHelpers.fs, DiffCategoryEndToEndTests.fs and one WorktreeApi call site were repointed at SchedulerState. Server, client and tests build clean; fast suite 1878 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
Durable state is split between SQLite (SessionActivityStore) and per-port JSON files behind PersistentStore + JsonStore (MergedPrStore, AutoSyncStore, CanvasDocOwnership). Each store's choice fits its shape, but two mechanisms is a standing cost, so record it as a candidate rather than leaving it as folklore. Notes the two things that would otherwise be re-litigated: durability is not the gap (JsonStore already writes temp-then-move), and all three stores have to move together or the converted one becomes the odd store out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f775f5-e421-45b9-acac-d4f981b3f691
A session mid-turn was asked to sync itself, which put the prompt into the CLI's own queue -- a queue Treemon cannot observe. When the agent stayed busy past the one-hour acceptance window, the same base revision was observed again and prompted a second time, leaving two identical messages queued and repeating every hour. A busy worktree is now waited for: nothing is delivered, nothing is mutated and nothing is recorded, so every sync runs through the single mechanical path and spends an agent only on what Git could not finish. That removes the failure mode rather than detecting it, and the sync lands sooner without an agent turn. "Idle" must be settled rather than sampled: status dips to idle for milliseconds between back-to-back turns, so an instantaneous reading would let a fetch and merge start under an agent about to resume. Over 3,267 real inter-turn gaps, 87% are under half a second and only 0.7% fall between two seconds and 30s, so settleWindow sits in an empty valley. With the working-session branch gone, SyncPlan has one case left and is replaced by the PR status it carried. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
- AutoSync.fs:202 - the settle check read UpdatedAt raw, but it is a last-write-wins ordering clock: the MinValue sentinel of a session whose only report is a title/intent hydration cleared a 30s window by two thousand years, and a stamp ahead of server time (reports are clamped only 5 min ahead) read as negative idleness and pinned the worktree busy. Both now go through hasSettled. - AutoSync.fs:343 - the fallback prompt reused the target selected before the fetch and merge, so a session that resumed meanwhile was still prompted - and a resuming agent is the likeliest cause of the dirty worktree that brings the sync there. The target is re-selected beside the existing eligibility re-read and the prompt is dropped when it is busy. - AutoSync.fs:378 - the eligibility gate (a config read under the process-wide lock plus a scheduler mailbox round-trip) was computed and discarded on every deferred observation. The target is read first. - Two spec surfaces still described the deleted working-session delivery path, and the unloaded-PR deferral is no longer sessionless-only. Left for a decision, per the review: SessionBusy living in the delivery address DU, an unbounded deferral when a session is stuck Working, the settle window applying to WaitingForUser, and the toggle returning Ok on a deferred worktree. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
SessionBusy was a case of SyncTarget, the same DU carried by DeliveryRequest.Target and read by deliver -- so "deliver into a worktree somebody is working in" was representable, and kept out only by an exhaustive match rather than by the type. SyncTarget.sessionId also had to answer None for two opposite meanings: "never deliver here" and "no retained identity, launch a new CLI". WorktreeOwnership = Busy | Free of SyncTarget now carries the verdict, and SyncTarget holds addresses only. Delivery can be reached solely by unwrapping Free, so the invariant is in the type instead of in a comment, and SyncTarget.sessionId is total and unambiguous. SelectTarget/selectTarget are renamed to ReadOwnership/readOwnership, since they answer who owns the worktree before they answer where to deliver. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
The in-flight spin was suppressed under prefers-reduced-motion, and Windows "animation effects: off" makes browsers report exactly that - so on such a machine a multi-second fetch and merge showed only a static yellow border, which reads as a disabled button rather than as work in progress. The suppression is dropped by decision: a 13px glyph turning in place is not the vestibular motion the media query exists to protect against, and it is the only cue that conveys "still running" rather than "stuck". The static yellow tint stays as the second, screenshot-readable half of the state. The regression test now pins both halves - paint must differ from the resting toggle AND the glyph must report a running sync-spin animation - so neither cue can be dropped silently again. It also drops a dead shadowed binding left in that test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
A completed mechanical sync left the card reporting "1 behind main" until the next scheduled RefreshGit came round, even though the sync had just cleared it. The existing ExpediteRefresh does not cover this: it only drops the RefreshWorktreeList stamp, which refreshes list membership rather than git data, and it still waits for the scheduler loop to come back around. RefreshGit's body becomes reloadGitData, shared with a new ReloadGitData dependency that AutoSync calls after - and only after - MechanicalSync returns Ok. The observation that was invalidated is refreshed by the code that invalidated it, with no scheduler timing in the path. A sync that stopped changes nothing, so it does not reload. tryFindOwningRepo moves above the dependency wiring that now resolves the repository for a worktree path; it is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
The toggle's glyph was two straight arrows - a swap symbol. Rotating it while a sync runs read as a symbol falling over rather than as work in progress, which is what made the newly-visible animation look wrong. The same two arrows, curved into a circle, keep the two-way sync meaning and make rotation the motion the shape already implies. Chosen from a live canvas mock of six candidates (single arrow, two arrows, arc spinner, stretching ring, dancing dots, breathing) compared at real 13px on a card row. It replaces the resting glyph too, so the button keeps one shape and simply starts turning when a sync begins, rather than swapping icons mid-state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
The spec says deleting a worktree clears its accepted-revision record, but only the API deletion path did. A worktree removed any other way - git worktree remove, a manual delete - never gets another observation, so nothing cleared its record and a worktree later recreated at that path could inherit the suppression and lose its first sync for up to an hour. The records also accumulated forever in the runtime file. Discovery already computes the removed set for its own pruning, so the clearing hangs off the same signal. It is guarded on the discovery having succeeded: listWorktrees answers None on a Git failure, which must never read as every worktree vanishing and wipe live records. removedWorktreePaths is a pure function so that guard is directly testable. Raised by the Copilot PR reviewer on #154 and deferred there as an owner decision; resolved in favour of the code matching the documented lifecycle rather than weakening the spec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e93e674-4f32-40a0-b019-1b439cf8a39a
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Auto-sync only worked when a coding session was open. The card toggle marked a branch for syncing, but the actual work — fetch, merge, push — was delegated to an agent in a live session. A worktree parked overnight with the toggle on stayed behind base until someone opened a session, which is exactly when nobody is watching.
docs/spec/worktree-monitor.mdrecorded this as a deliberate decision ("agent-driven auto-sync preference rather than a mechanical Git pipeline"), so the gap was by design rather than an oversight.Prompt delivery had no memory either: deduplication lived only in process state, so a restart re-prompted work already queued to an agent.
Changes
Mechanical sync when no session is open (
GitBranchSync.fs, new)Treemon now does the Git work itself and spends an agent only on what it could not finish. The sequence is deliberately narrow: merge only a worktree proven clean (a content probe that cannot answer refuses too), fetch
{upstreamRemote}/{baseBranch}, try a fast-forward and then a non-editing merge, abort conflicts, and verify the fetched base is an ancestor ofHEAD. The branch is re-read from the worktree immediately before each mutation, so a checkout landing mid-operation refuses instead of merging into a branch nobody observed. No project builds or tests run on this path.A successful sync is published only when the branch has an open pull request, as a non-force push with both halves of the refspec spelled out, so neither
push.defaultnorHEADpicks what moves. The push refuses an unconfigured upstream, an option-like remote name, an upstream naming a different branch, and a remote that has moved ahead. Every refusal, conflict, and command failure falls back to one agent prompt carrying only a closed structured reason — never raw Git output, paths, or commit text.An open session still owns its worktree and is only asked to sync, unchanged.
Restart-safe deduplication (
AutoSyncStore.fs, new)An accepted prompt — and only an accepted one — persists the worktree path, base revision, and acceptance time to port-scoped gitignored runtime state. The same revision stays suppressed for an hour, long enough to cover an in-progress sync; a different revision triggers immediately. A missing or corrupt record loads as empty, so an unusable record re-prompts rather than silently suppressing. Catching up, disabling the preference, and deleting the worktree all clear it.
PR status the push decision can rely on
PrInfonow carries a three-statePrState(Open | Merged | ClosedUnmerged) instead of independentIsOpen/IsMergedbooleans, because a merged flag alone cannot tell a closed-unmerged pull request from an open one. GitHub matches pull requests by bare head ref, so a fetched PR counts only when its head repository is one the checkout could push to — the configured remotes' push-URL owners plus the upstream owner. That keeps fork workflows working while stopping an outsider's fork PR from deciding a same-named local branch's push gate. A repository whose PR data has never loaded is treated as unknown rather than "no pull request", so a sessionless sync defers to the next refresh instead of merging locally and recording an acceptance whose push it could never make.Extension send queue (
send-queue.mjs, new)The serialized
session.sendqueue and its pending-duplicate coalescing moved out ofextension.mjsinto its own module with tests. A payload stops being pending the moment it is handed tosession.send, so suppression only ever removes a message that would repeat an undelivered one.Module split
RefreshScheduler.fshad grown past the repository's file-size limit, so theDashboardState/StateMsg/processMessagestate slice moved toSchedulerState.fs. Behaviour-preserving.Tests
Server, client, and test projects build clean (0 warnings). Fast suite: 1773 passed, 0 failed.
New coverage:
BranchSyncTests.fs(merge, abort, dirty refusal, branch-change refusal, push targeting and refusals),AutoSyncStoreTests.fs(durability, corruption, retry window), plus GitHub fixtures for fork head owners and branch-name reuse.