Shared-Workdir Scratch-File Collision — Not a Race, Not Data Loss
Applies to: Anyone reading a stray reply.md, an unfamiliar nested clone, or an unpushed local commit inside a Multica crew agent's checkout and wondering whether something broke.
The trap
Multica crew agents used to pin a fixed, non-isolated local path (~/code/enkidu, Josh's own IDE checkout) as their working directory. When two or more agent runs landed on the same host in quick succession, they wrote into that one shared directory instead of each getting its own — racing on its single git HEAD/index/working tree and on fixed-relative scratch files, chiefly ./reply.md (written by the comment-formatting convention before every issue comment add).
During the v4.15 hotfix chain (OBJ-1911), this produced exactly that: a stale reply.md from a different task/branch, an untracked nested clone at ~/code/enkidu/enkidu/, and a two-week trail of ad-hoc scratch directories agents had invented to dodge the collision. It was first escalated as a suspected race condition with confirmed data loss. Neither framing survived investigation:
- No commit was ever lost. The commit in question (
63a68fc95f1787b17d427e439aeeb18145b9090f) was sitting unpushed in one checkout while a different checkout'sorigin/master..mastersweep — with its own bare repo and ~100 attached worktrees — looked for it and, unsurprisingly, didn't find it there. That's ordinary git, not a race: an unpushed local branch in one clone is invisible to a sweep running in another clone by construction. The commit shipped as PR #1897 and is live onorigin/master. - It wasn't a race in the concurrency sense either — no two writers were corrupting the same byte at the same instant. It was two separable, boring problems: agents on one host colliding on a shared scratch file, and a finished-but-unpushed commit being invisible to whichever checkout looked for it next.
What was actually real
- Intra-host scratch collision. Multiple agent runtimes on one machine writing a fixed-relative
./reply.mdand sharing a single checkout'sHEAD. Annoying (a wrongreply.mdbody could in principle get posted as an agent's actual comment) but not destructive on its own. - Handoff durability gap. An agent commits locally, doesn't push, and hands off — to an agent whose run lands in a different checkout or host. The commit isn't lost, but it's invisible to that next checkout until it reaches real
origin, which is exactly the ambiguity that manufactured the false data-loss alarm here. This is the same failure family asrepo-checkout-origin-only-gotcha.md(multica repo checkout --ref <branch>also only resolves throughrefs/remotes/origin/<branch>) — different entry point, same root cause: a commit that hasn't crossed theoriginboundary yet.
Where the fixed path actually lived — look here first
The binding was not in any crew agent's runtime brief, not in a .claude/skills/** file, not in a Multica project resource (local_directory), and not in any agent's runtime_config. All of those were audited and came back clean. It lived in exactly one place: the Multica workspace context field, injected verbatim into every agent's runtime brief. That's the first place to check if a "shared path" symptom ever resurfaces — not the agent definitions themselves.
The fix
Two independent changes closed this:
- The workspace
contextfield no longer names a fixed path. Agents now provision their own isolated workdir per task and populate it withmultica repo checkout <url>— the same isolation the platform already provided, just not previously the convention agents were told to use. Scratch files (reply drafts, temp output, notes) go inside that task-scoped workdir, never a fixed relative path like./reply.md, since a sibling runtime on the same host has no way to know it's there. - "Push before you hand off" is now non-negotiable. The
multica-handoff-protocolworkspace skill puts pushing the branch to realoriginas step 1 of the final-action order, ahead of commenting, status, and assignment — seemultica-workspace-skills.md. An agent that can't push says so in its handoff comment and leaves the issue with Maggie rather than handing off silently.
Symptom if you hit this anyway
A checkout shows a reply.md, a nested clone, or a scratch directory that doesn't belong to your task — or a sweep for unpushed commits comes back suspicious. Before treating any of it as a race or as lost work:
- Confirm which host you're actually looking at. Evidence from different hosts merged into one narrative is what turned two mild hygiene problems into a P1 here — name the host per observation.
- Check whether the commit in question has already reached
originand shipped (git log origin/<branch>..<branch>on the checkout in question, and check whether a PR merged it). An unpushed-but-real commit is not data loss. - If the working directory is a fixed shared path rather than a task-provisioned one, that's the real bug — flag it, don't just work around it.
Related
- OBJ-1915 — this investigation. Dave's original diagnosis (comment
a38cd725-f8df-4242-8065-0ddab3dd11d8); Josh's correction of the record (comment225a5c0a-08d6-44a6-a57a-44f520f22d53) is the authoritative account of what was and wasn't real. - OBJ-1911 — the v4.15 hotfix chain where the collision was first observed.
repo-checkout-origin-only-gotcha.md— the sibling gotcha for the same "hasn't crossedoriginyet" root cause, hit from the checkout side instead of the handoff side.shared-mirror-remote-pollution-gotcha.md— sibling gotcha, same "shared host-level git state outlives a single task" root cause, hit from thegit remote addside instead of the scratch-file/fixed-path side.multica-workspace-skills.md— themultica-handoff-protocolskill body, including the push-before-handoff rule.multica-agent-crew.md— the crew instructions this convention lives in.
Last updated: 2026-08-12 (OBJ-2477: added a reciprocal Related link to the new shared-mirror-remote-pollution-gotcha.md)
Previously: 2026-07-31 (OBJ-1915: document the shared-workdir scratch collision + handoff-durability gap, corrected from an initial race-condition/data-loss framing that didn't hold up)