Appearance
Flip a task from queued to in_progress in a single kernel-atomic write — so two polling triggers racing the same task produce one winner, one loser, never two duplicate spawns.
Pattern
Work Claim
The shape
One task file line. Four possible states. One winning writer.
Before
[ ]Add OAuth2 provider{opus, adw_feature}
/mark_in_progress · adw_id=cc73faf1
Claimed
🟡cc73faf1Add OAuth2 providerclaimed_at=<ts>
terminal state · release
Success
✅a3f91ccc73faf1
Failure
❌cc73faf1// Failed: <reason>
The claim carries who (adw_id) and when (timestamp). Release writes the terminal outcome back to the same line.
Why two triggers can't both win
POSIX writes under PIPE_BUF are kernel-atomic.
One write() syscall, <4 KB. The kernel serializes; one trigger replaces the line, the other sees "already claimed" and moves on.
Trigger A · t=0ms
read → status: queued
write → status: in_progress
✓ claim granted
write()
ATOMIC · < PIPE_BUF
4096 B · POSIX
Trigger B · t=0ms
read → status: queued
write → status: in_progress
✗ re-read: already claimed → skip
macOS, Linux, BSD guarantee it. Windows is weaker but usable at small scale. NFS is where the guarantee dies.
If you skip atomicity, both triggers succeed at "claim" — and the task list itself ends up corrupted, not just double-spawned.
The graduation ladder
Markdown now. Row lock later. Not the other way around.
MARKDOWN FILE
NFS DEAD ZONE
DB / LOCK / QUEUE
1 hostN hosts
✓ Markdown file
Kernel-atomic, debuggable with cat, costs nothing to operate.
single machine
⚠ NFS
Caches aggressively, writes don't propagate synchronously — atomicity is lost.
don't do this
→ Row lock / advisory / MQ
UPDATE … WHERE status='queued', pg_advisory_lock, or a queue.
multi-host
Graduate when multi-host polling is actually required. Until then, the markdown file is cheaper, simpler, debuggable.
What the claim doesn't solve
Work Claim alone wedges on the first crash.
A claim never expires on its own. One crashed agent = one permanent lock unless something reaps it.
💥
Agent crashes
mid-task, claim still in file
→
🔒
Claim stays
no release, no TTL, forever in_progress
→
♻
Orphan Reaper
TTL check; reverts stale claim to queued
Work Claim without [[Orphan Reaper]] — one crash per worktree eventually blocks the whole worktree. The claim is the lock; the reaper is the key-under-the-mat.
If you ship Work Claim without Orphan Reaper, you have a correctness bug waiting on your next SIGKILL.
The discipline
Transition to in-progress is one atomic write — or it's a race.
Read-then-write without kernel-level atomicity is a double-spawn waiting on a poll collision. Small scale: markdown file under PIPE_BUF. Larger scale: row lock. Either way — pair it with an Orphan Reaper or the first crash wedges the worktree.