feat: work-state ferrying across accounts#24
Merged
Conversation
First increment of work-state ferrying (plan 2026-07-18): project identity as the abcd-compatible root-SHA set, computed with an isolated git environment (an inherited GIT_DIR must never answer for another repo — the issue-#17 leak shape), refusing shallow clones, linked worktrees, and commit-less repos; and the data-driven cargo registry mapping the abcd layout and Claude Code harness to their work-state locations and receive policies. Not wired to any command yet — the work verbs land in later increments on this branch. Assisted-by: Claude:claude-fable-5
Second increment of work-state ferrying: the versioned cargo manifest (validated as untrusted input — path containment, hex identity, strict decode, newer-version refusal) and the per-account local state file (pack/receive baselines keyed by content hash, hash-pinned secret-gate acknowledgements, last-receive record for precise reversal, and the written-path set that ferry restore work will enumerate). Assisted-by: Claude:claude-fable-5
Third increment of work-state ferrying: the store layout (<store>/<root-sha>/<seq>-<hash>.ferrywork with O_CREAT|O_EXCL sequence allocation and fsynced bundle writes, safe on APFS/SMB/exFAT), the mechanical store-location guards (inside-a-git-worktree refusal, known sync-root refusal behind an explicit override), equal-seq fork listing, and per-account claim files — each written only by its owner, merged on read, so no file is ever written by two accounts. Assisted-by: Claude:claude-fable-5
Fourth increment of work-state ferrying: Pack collects the registry items (missing optional items tolerated and noted; the handover note required unless --allow-empty), passes every byte through the secret gate — a high-confidence finding aborts the whole pack, releasable only by --exclude or an acknowledgement pinned to file + content hash — builds the cargo zip (sorted members, sanitised modes, manifest stamped with the sequence the store actually allocated via the new WriteBundle build callback), appends the pack claim, updates the local baseline, and records the sidecar handover marker without mutating any cargo file. Symlinks anywhere in cargo are a refusal, never a silent skip. Assisted-by: Claude:claude-fable-5
The snapshot machinery was reachable only from inside restore flows. The work domain needs a snapshot per receive so `ferry work restore` can revert exactly that receive; Snapshot() exposes the existing snapshotCurrent unchanged. Assisted-by: Claude:claude-fable-5
Fifth increment of work-state ferrying: Receive lands the latest cargo (chosen by sequence, never mtime) behind the full guard set — bundle bytes verified against name hash and per-file manifest hashes, no unlisted members, root-SHA set intersection, equal-seq tie refusal naming both bundles and their claim owners, superseded (taken-back) refusal, the v1 project-under-home rule, guarded-overwrite divergence checks against the hash baseline (first-receive-into-populated refused; identical content is not divergence), union-merge for transcripts under the store's advisory lock, and memory-target verification. All writes are backup-first inside one journal run, preceded by a per-receive snapshot; WorkRestore reverts exactly that receive. Take-back on the packer's account clears the marker and restores nothing. LocateProject scans foreign store directories by manifest root-set intersection so a reordered rev-list cannot orphan cargo. Also fixes a bug caught by test: a receive plan failing after taking the transcript lock leaked the lock file (the release defer registered only after the error check). Assisted-by: Claude:claude-fable-5
Prune applies the retention policy on demand, refusing keep < 1; RemoveBundle deletes one bundle by hash — the targeted form the equal-seq tie message points users at. Assisted-by: Claude:claude-fable-5
Status assembles the read-only picture the status verb renders: cargo
list, unresolved equal-seq forks, merged claims, handover-marker
freshness ("handed over, not modified since" vs "modified after"),
divergence of guarded items against the local baseline, store size, and
other projects present in the store by manifest repo name.
AllWrittenPaths unions every project's written set — the enumeration
the `ferry restore work` domain form reverts. The machine config gains
the optional [work] table (store path, keep-last-N, sync-root opt-in);
hostname and repo stay the only required fields.
Assisted-by: Claude:claude-fable-5
The work domain is now reachable from the production entry point: `ferry work pack|receive|status|prune|restore`, self-wired like the bundle noun. The command layer resolves the project identity (with its guards), the [work] cargo store from the machine config (missing table gets setup guidance; --allow-sync-root gives a one-run override), the per-project state, and a sanitised user@host claim identity. Receive and work-restore hold the same engine lock apply holds so they never interleave with a reconcile. Pack applies keep-last-N retention after storing, and --acknowledge pins named secret findings to their current content before a single retry. `ferry restore work` fans the domain out to every work-written path, mirroring the agents expansion. Verified end-to-end against the real binary: pack → status → receive → work restore → take-back → superseded refusal across two fake homes sharing one store. Full unit + eval suites green. Assisted-by: Claude:claude-fable-5
The Diátaxis set for work-state ferrying: how-to "Hand work over to another account" (one-time shared-store setup, pack/receive walkthrough, refusal guidance), explanation "The work domain" (why baton-pass verbs and not a reconcile domain, why the store is not the config repo, the no-shared-writes store design, advisory claims, project identity), reference rows for the five verbs and the [work] machine-config table, regenerated CLI pages, the CHANGELOG entry, and companion ADR 0003 citing ADR 0002's durable/ephemeral split as precedent. Assisted-by: Claude:claude-fable-5
Two-sandbox evals driving the real binary through the plan's matrix: pack→receive round trip across two homes sharing one store (with the ~/.ssh tripwire armed), work-restore reverting a receive, divergence refusal with --force override, the secret-gate abort with both escape hatches (--acknowledge pinned to content, --exclude recorded in the manifest), shallow-clone and linked-worktree refusals, take-back plus the superseded refusal, and the store-inside-a-worktree guard. Assisted-by: Claude:claude-fable-5
Pack now gates file NAMES as well as content (parity with bundle export) — a secret-shaped path component aborts the pack and the refusal withholds the name rather than echoing it back. The cargo manifest validator refuses case-fold duplicate file paths within an item, which case-insensitive APFS would otherwise land last-writer-wins. Assisted-by: Claude:claude-fable-5
A mid-write receive failure left state.LastReceive nil (or pointing at the PREVIOUS receive's snapshot), so the advertised recovery — `ferry work restore` reverts the partial receive — was wrong exactly when it mattered. The record (snapshot id, bundle, affected paths) is now saved before the first write; the baseline still updates only on success. Found in the pre-PR review pass; regression-tested with a partial landing forced via an unwritable memory target. Assisted-by: Claude:claude-fable-5
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.
What
Implements the approved plan
.abcd/development/plans/2026-07-18-work-state-ferrying.md:ferry work pack|receive|status|prune|restorecarries a project's in-flight work state — the session handover note, run journal, per-project agent memory, and redacted transcript store — between accounts as an explicit baton pass through a shared cargo store.Design highlights (per the plan)
O_CREAT|O_EXCLsequence allocation, per-account claim files merged on read; equal-seq forks surfaced and refused at receive until named or pruned. Ordering is by sequence, never timestamps..ssh/VCS components, case-fold duplicates refused).--exclude(recorded in the manifest) and--acknowledge(pinned to file + content hash; re-aborts on change).ferry work restorereverts exactly that receive, including a partial one;ferry restore workreverts all work-verb writes.GIT_DIRshape), store-inside-a-worktree refusal, cloud-sync-root opt-in, project-under-home v1 rule, divergence-by-content-hash with--forceas the explicit override, take-back and superseded semantics.Verification
-racesuites for every package; evals drive the real binary through a two-home pack→receive round trip and the full refusal matrix (7 ACs), with the~/.sshtripwire armed.Notes for the reviewer
internal/work/validateCargoRelis deliberate kin ofinternal/bundle's privatecanonicalRel(strict, no cleaning — cargo paths are pack-built). Flagged as a consolidation candidate rather than exporting/reshaping the bundle API in this PR.cmd/root.gostill saysv0.8.0-dev; release ldflags override it at tag time, but a version-bump chore may be due.