You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Claim work in flight so concurrent agents don't collide
Why
openspec-references-coordination solves cross-repo awareness: two repos, two
worktrees, "the other one exists and here is where". This proposal is the
neighbouring problem it does not cover: one repo, several agents, overlapping
files, at the same time.
Every item below is a real incident from a single day on androidand/brick-now,
with three agents (two coding, one recovering PRs) working the same repo.
1. Two agents wrote to the same worktree. Agent A was mid-task in brick-now-worktrees/step-source; Agent B committed and pushed to the same
branch, in the same directory. Nothing detected it. The only reason no work was
lost is that A happened to have a clean tree at that moment.
2. Uncommitted work was destroyed by a /tmp cleanup. A worktree lived in /tmp/bricknow-step-source; macOS cleaned it between sessions. A shared helper
module, 17 converted call sites and a test file were gone. The branch survived —
only the uncommitted delta died. Nothing warned that a worktree holding
uncommitted work sat in a volatile path.
3. Two agents independently implemented the same fix. Agent A had already
landed a plan→parts projection with a mutation-tested guard and corpus evidence.
Agent B, from the OpenSpec change alone, announced it was about to write "a
shared pure adapter over the canonical plan" — the same function. Both changes
were legitimately in scope for their respective changes; neither could see the
other's in-progress files. Discovering this needed a human relaying chat logs.
4. "Merge" commits that were squashes left PRs open forever. Master contained 8873c2a99 titled "merge: integrate into master" with one parent.
The content was in master; none of the branch's commits were ancestors. So git merge-base --is-ancestor said "not merged", GitHub kept the PR open, and an
agent nearly re-merged a branch that had since gone stale — merging it would
have regressed two files (569→175 and 206→134 lines).
5. A branch was behind master in content while looking mergeable. Same root
cause as (4): ancestry is not a reliable "is this landed?" signal once squashes
enter the history, and nothing recorded which change a squash represented.
6. --admin merges bypassed CI and left master red. Two PRs were merged with gh pr merge --squash --admin. The second broke three tests in the service the
first had added. The failure was only found because another agent happened to run
the backend suite. A "verified green" claim from an earlier PR was carried
forward past the change that invalidated it.
The common shape: specsync knows what work exists, but not what is being worked
on right now, by whom, or where on disk. A change's issue says "active" from the
moment it is proposed until it is archived — which is precisely the window in
which collisions happen.
What Changes
specsync claim — record that an agent is actively working a change:
worktree path, branch, agent identifier, and the file globs it expects to
touch. Stored in a gitignored .specsync/claims/ snapshot and projected onto
the issue (a claimed-by marker), so it is visible without repo access. specsync release clears it; claims carry a TTL so a crashed agent does not
hold a lock forever.
specsync claims — list active claims with their paths, branches and
globs. This is the "who is in this file right now" query that (1) and (3)
needed. Machine-readable via -json for planners.
Overlap detection on claim. Claiming globs that intersect another live
claim's globs fails loudly, naming the other change, agent and worktree. That
is (3) caught before duplicate work, not after.
Volatile-worktree warning. Claiming a worktree under a
system-temp path warns, and specsync claims flags any claim whose worktree
holds uncommitted changes. That is (2): the danger is uncommitted work in a
path the OS may reclaim.
Landed-detection that survives squashes. Record the merge/squash commit for
a change so "is this landed?" is answered from recorded fact, not ancestry.
Surface a warning when a change's issue is open but its content is present on
the default branch — the (4)/(5) signature.
Verification provenance. Let a sync attach the commit SHA a verification
claim was measured at. A "tests green" note pinned to a SHA that is no longer
the branch tip is shown as stale rather than as current — the (6) failure.
What This Does Not Do
It is not a lock. Claims are advisory and observable; they do not prevent
writes. The goal is for a collision to be visible, not impossible.
It does not manage worktrees or branches. Creating, moving and removing them
stays with git and the agent harness.
It does not replace CI. Verification provenance records when a claim was
measured; it does not run or gate anything.
Open Questions
Should claims live only in the gitignored snapshot, or also be committed? A
committed claim is visible to agents that never talk to GitHub, but it churns
the tree and will conflict — which is itself the problem being solved.
Is per-file-glob overlap the right granularity, or should it be per-change with
human-declared "surfaces" (backend / frontend / ingest)? Globs are precise but
need an agent to predict its own blast radius up front, and in incident (3)
neither agent would have predicted the shared module.
TTL default, and whether release-on-merge should be automatic.
Each phase is independently useful. Phase A alone would have caught incidents
(1) and (3); Phase C alone would have caught (4) and (5).
Phase A — Claims
A.1 specsync claim -change <id> [-worktree <path>] [-branch <b>] [-agent <name>] [-globs <a,b,...>] [-ttl <dur>]. Default worktree/branch
from the cwd's git state, agent from SPECSYNC_AGENT or hostname+pid.
A.2 Store in gitignored .specsync/claims/<change>.json. Confirm .specsync/ is already gitignored in consuming repos; the README says not
to commit it, but verify rather than assume.
A.3 specsync release -change <id>, plus TTL expiry so a crashed agent
does not hold a claim indefinitely.
A.5 Overlap detection: claiming globs intersecting a live claim fails and
names the holder. -force to override, because a human reassigning work
is legitimate.
Phase B — Worktree hygiene
B.1 Warn when a claimed worktree is under a system temp dir (/tmp, $TMPDIR, /private/tmp). Real loss: a helper module, 17 converted call
sites and a test file, deleted by a /tmp cleanup between sessions.
B.2 In specsync claims, flag claims whose worktree has uncommitted
changes, and show how long they have been uncommitted. Uncommitted work in
a volatile path is the actual hazard; either alone is survivable.
B.3 Flag two live claims sharing one worktree path — incident (1),
where two agents wrote to the same directory undetected.
Phase C — Landed detection that survives squashes
C.1 On sync, record the merge/squash commit for a change once its issue
closes, so "landed?" is a recorded fact rather than an ancestry query.
C.2 Warn when a change's issue is open but its content is already on the
default branch. Real case: master held a single-parent commit titled
"merge: integrate into master"; the PR stayed open indefinitely
and git merge-base --is-ancestor reported "not merged".
C.3 In that state, also warn that the branch may now be behind master —
re-merging the stale branch would have regressed two files (569→175 and
206→134 lines).
C.4 Nudge on discovery: a squash whose subject begins merge: is the
signature. Report it; do not try to fix history.
Phase D — Verification provenance
D.1 Allow a verification note to carry the commit SHA it was measured at.
D.2 Render it stale when that SHA is no longer the branch tip. Real case:
"8 instruction tests, 20 build-route tests pass" was true at one PR; the
next PR broke three tests in that same service and the claim was repeated
unchanged.
D.3 Note in the issue body when a PR merged with --admin (checks
bypassed), so a red default branch is attributable rather than mysterious.
Phase E — Validation
E.1 Unit tests for claim/release/TTL/overlap.
E.2 Replay each of the six incidents in the proposal as a test fixture and
assert the tooling reports it. An incident that cannot be replayed is not
evidence the feature works.
E.3 Dogfood on a real two-agent session before recommending it.
Notes
Claims are advisory, never locks. Making collisions visible is the goal;
a tool that blocks writes will be bypassed and then trusted wrongly.
Every incident here shares one shape: a failure that presents as success.
A squash labelled "merge:" looks merged. A green test claim looks current. An
optional call that silently no-ops looks like a working control. The value of
this change is turning that class of silence into a signal.
Claim work in flight so concurrent agents don't collide
Why
openspec-references-coordinationsolves cross-repo awareness: two repos, twoworktrees, "the other one exists and here is where". This proposal is the
neighbouring problem it does not cover: one repo, several agents, overlapping
files, at the same time.
Every item below is a real incident from a single day on
androidand/brick-now,with three agents (two coding, one recovering PRs) working the same repo.
1. Two agents wrote to the same worktree. Agent A was mid-task in
brick-now-worktrees/step-source; Agent B committed and pushed to the samebranch, in the same directory. Nothing detected it. The only reason no work was
lost is that A happened to have a clean tree at that moment.
2. Uncommitted work was destroyed by a
/tmpcleanup. A worktree lived in/tmp/bricknow-step-source; macOS cleaned it between sessions. A shared helpermodule, 17 converted call sites and a test file were gone. The branch survived —
only the uncommitted delta died. Nothing warned that a worktree holding
uncommitted work sat in a volatile path.
3. Two agents independently implemented the same fix. Agent A had already
landed a plan→parts projection with a mutation-tested guard and corpus evidence.
Agent B, from the OpenSpec change alone, announced it was about to write "a
shared pure adapter over the canonical plan" — the same function. Both changes
were legitimately in scope for their respective changes; neither could see the
other's in-progress files. Discovering this needed a human relaying chat logs.
4. "Merge" commits that were squashes left PRs open forever. Master contained
8873c2a99titled "merge: integrate into master" with one parent.The content was in master; none of the branch's commits were ancestors. So
git merge-base --is-ancestorsaid "not merged", GitHub kept the PR open, and anagent nearly re-merged a branch that had since gone stale — merging it would
have regressed two files (569→175 and 206→134 lines).
5. A branch was behind master in content while looking mergeable. Same root
cause as (4): ancestry is not a reliable "is this landed?" signal once squashes
enter the history, and nothing recorded which change a squash represented.
6.
--adminmerges bypassed CI and left master red. Two PRs were merged withgh pr merge --squash --admin. The second broke three tests in the service thefirst had added. The failure was only found because another agent happened to run
the backend suite. A "verified green" claim from an earlier PR was carried
forward past the change that invalidated it.
The common shape: specsync knows what work exists, but not what is being worked
on right now, by whom, or where on disk. A change's issue says "active" from the
moment it is proposed until it is archived — which is precisely the window in
which collisions happen.
What Changes
specsync claim— record that an agent is actively working a change:worktree path, branch, agent identifier, and the file globs it expects to
touch. Stored in a gitignored
.specsync/claims/snapshot and projected ontothe issue (a
claimed-bymarker), so it is visible without repo access.specsync releaseclears it; claims carry a TTL so a crashed agent does nothold a lock forever.
specsync claims— list active claims with their paths, branches andglobs. This is the "who is in this file right now" query that (1) and (3)
needed. Machine-readable via
-jsonfor planners.Overlap detection on claim. Claiming globs that intersect another live
claim's globs fails loudly, naming the other change, agent and worktree. That
is (3) caught before duplicate work, not after.
Volatile-worktree warning. Claiming a worktree under a
system-temp path warns, and
specsync claimsflags any claim whose worktreeholds uncommitted changes. That is (2): the danger is uncommitted work in a
path the OS may reclaim.
Landed-detection that survives squashes. Record the merge/squash commit for
a change so "is this landed?" is answered from recorded fact, not ancestry.
Surface a warning when a change's issue is open but its content is present on
the default branch — the (4)/(5) signature.
Verification provenance. Let a sync attach the commit SHA a verification
claim was measured at. A "tests green" note pinned to a SHA that is no longer
the branch tip is shown as stale rather than as current — the (6) failure.
What This Does Not Do
writes. The goal is for a collision to be visible, not impossible.
stays with git and the agent harness.
measured; it does not run or gate anything.
Open Questions
committed claim is visible to agents that never talk to GitHub, but it churns
the tree and will conflict — which is itself the problem being solved.
human-declared "surfaces" (backend / frontend / ingest)? Globs are precise but
need an agent to predict its own blast radius up front, and in incident (3)
neither agent would have predicted the shared module.
Related
Tasks
Tasks — Claim work in flight
Phase A — Claims
specsync claim -change <id> [-worktree <path>] [-branch <b>] [-agent <name>] [-globs <a,b,...>] [-ttl <dur>]. Default worktree/branchfrom the cwd's git state, agent from
SPECSYNC_AGENTor hostname+pid..specsync/claims/<change>.json. Confirm.specsync/is already gitignored in consuming repos; the README says notto commit it, but verify rather than assume.
specsync release -change <id>, plus TTL expiry so a crashed agentdoes not hold a claim indefinitely.
specsync claims [-json]— change, agent, branch, worktree, globs,age, expiry.
names the holder.
-forceto override, because a human reassigning workis legitimate.
Phase B — Worktree hygiene
/tmp,$TMPDIR,/private/tmp). Real loss: a helper module, 17 converted callsites and a test file, deleted by a
/tmpcleanup between sessions.specsync claims, flag claims whose worktree has uncommittedchanges, and show how long they have been uncommitted. Uncommitted work in
a volatile path is the actual hazard; either alone is survivable.
where two agents wrote to the same directory undetected.
Phase C — Landed detection that survives squashes
closes, so "landed?" is a recorded fact rather than an ancestry query.
default branch. Real case: master held a single-parent commit titled
"merge: integrate into master"; the PR stayed open indefinitely
and
git merge-base --is-ancestorreported "not merged".re-merging the stale branch would have regressed two files (569→175 and
206→134 lines).
merge:is thesignature. Report it; do not try to fix history.
Phase D — Verification provenance
"8 instruction tests, 20 build-route tests pass" was true at one PR; the
next PR broke three tests in that same service and the claim was repeated
unchanged.
--admin(checksbypassed), so a red default branch is attributable rather than mysterious.
Phase E — Validation
assert the tooling reports it. An incident that cannot be replayed is not
evidence the feature works.
Notes
a tool that blocks writes will be bypassed and then trusted wrongly.
A squash labelled "merge:" looks merged. A green test claim looks current. An
optional call that silently no-ops looks like a working control. The value of
this change is turning that class of silence into a signal.