fix(#1884): surface planning-lock mkdir failures, not a phantom timeout (#3472)

* test(#1884): reproduce phantom lock timeout from swallowed mkdir failure

withPlanningLock swallows a platformEnsureDir failure at src/planning-workspace.cts:210,
so an EACCES/ENOSPC/EROFS creating .planning/ is misreported as a 10s "held by a live
process" lock-contention timeout instead of the real filesystem error. These tests pin
the corrected contract and currently fail against the unfixed source (RED). Also proves
the pre-existing Docker overlay-fs lock-write retry race is unaffected by this change.

* fix(#1884): surface mkdir failures instead of a phantom lock timeout

withPlanningLock swallowed platformEnsureDir failures (EACCES/ENOSPC/EROFS/EMFILE)
creating .planning/, so the subsequent lock write failed with ENOENT (parent
missing), which is retryable (PLANNING_LOCK_RETRY_ERRNOS, added for a Docker
overlay-fs race). The loop then spun the full 10s budget and threw a phantom
"held by a live process" contention error pointing at a nonexistent holder.

The mkdir failure now propagates immediately with its real errno and message.
The Docker overlay-fs ENOENT lock-write race (directory present) is unaffected,
as is every path where .planning/ already exists or is creatable.

Per docs/adr/1411-resolution-provenance.md's 2026-07-26 amendment, this is the
one site in epic #1879 that legitimately throws (ADR-227's genuinely-fatal
carve-out) -- its defect was throwing the wrong error after swallowing the
real one, not that it threw at all.

* chore(#1884): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 11:04:55 -04:00
committed by GitHub
parent dbc8b4077a
commit 0c43d853e2
3 changed files with 154 additions and 2 deletions

View File

@@ -206,8 +206,15 @@ function withPlanningLock<T>(cwd: string, fn: () => T, clock?: Clock): T {
const deadmanCeilingMs = 60000;
const start = clock.now();
// Ensure .planning/ exists
try { platformEnsureDir(planningDir(cwd)); } catch { /* ok */ }
// Ensure .planning/ exists. A genuine failure here (EACCES/ENOSPC/EROFS/EMFILE)
// MUST surface immediately: the prior `catch { /* ok */ }` swallowed it, the lock
// write below then failed with ENOENT (parent dir missing), and ENOENT is retryable
// (PLANNING_LOCK_RETRY_ERRNOS — added for a Docker overlay-fs race), so the loop
// spun the full 10s budget and reported a PHANTOM "held by a live process"
// contention pointing at a nonexistent holder (epic #1879 / F16, #1884).
// `mkdirSync(recursive:true)` does not throw on an existing dir, so the normal
// path (dir already present) is unaffected; only real creation failures propagate.
platformEnsureDir(planningDir(cwd));
function acquireLock(): void {
// Atomic create — fails if file exists