ADR-0025: Concurrent write safety for agent-driven parallel tasks
Status: accepted | Date: 2026-02-15
References: RFC-0002, ADR-0020
Context
When an agent (e.g. Cursor, Claude Code) runs multiple tasks in parallel that each invoke govctl to create or modify RFCs, ADRs, or work items, concurrent writes to the same files or to the same directory can cause:
- File corruption — Two processes write to the same file; interleaved writes or truncate-then-write races produce partial or invalid content.
- ID collision — Work item creation uses
find_max_sequence(work_dir, id_prefix)then writes a new file. Two processes can read the same max, both write the same ID or overwrite the same path (same date-slug). - Lost updates — Read-modify-write (e.g. edit, set, bump) without coordination: one process overwrites the other’s write.
This is distinct from ADR-0020, which addresses ID collision across branches (merge-time). Here the scenario is same repository, multiple concurrent processes (e.g. multiple agent tasks in one workspace).
Constraints: govctl is a CLI; no daemon. Implementation must work across processes. No network or external services. Must remain portable (Unix/macOS/Windows where feasible).
Decision
Use process-level filesystem locking so that only one govctl process mutates the governance tree at a time.
-
Scope of locking
- Any command that modifies
gov/or writes todocs/(render, new, set, add, edit, tick, bump, finalize, advance, accept, move, etc.) MUST acquire a lock before performing mutations and release it when done. - Read-only commands (list, get, check, status, show) do NOT need to hold the lock.
- Any command that modifies
-
Lock mechanism
- A single gov-root lock file (e.g.
gov/.govctl.lockor a lock in a well-known location under gov root). One lock for the entire gov tree. - Acquire: exclusive (write) lock on that file (e.g.
flock(LOCK_EX)on Unix; equivalent on Windows). - Blocking: if lock is held by another process, wait with optional timeout; on timeout, fail with a clear error instructing the user to retry or avoid parallel govctl writes.
- A single gov-root lock file (e.g.
-
Granularity
- Coarse-grained (one lock per gov root) is chosen over per-artifact or per-directory locks to avoid deadlock and to keep implementation and behavior simple. Parallel agents serialize at the gov root; throughput is traded for correctness and simplicity.
Consequences
Positive
- Prevents file corruption and ID collision under concurrent agent tasks.
- Single lock file: no deadlock, no lock ordering, easy to reason about.
- Portable: file locking is available on all supported platforms (flock/cfg with fallbacks).
Negative
- Parallel govctl write commands serialize; one task may block until another finishes. Mitigation: agents can be designed to queue writes or run write commands sequentially; CLI documents the locking behavior.
- Stale lock if process crashes without releasing. Mitigation: lock is process-scoped (OS releases on exit); optional timeout + clear error message for “stuck” waiters.
- Slight complexity in CLI entrypoint: acquire lock early for write commands, release on all exit paths.
Neutral
- Render (writing to docs/) is included in the lock scope so that render + new/edit from two processes do not interleave.