IR-012 — cross-process WIP-cap lease, shipped opt-in behind `PETROVA_WIP_CAP_CROSS_PROCESS_LOCK`
Date: 2026-09-13 Status: closed Supersedes: none Superseded-by: none — current
Context
Section titled “Context”irina/tickets/IR-012-cross-runner-wip-cap-lock.xml (filed while scoping
IR-007’s real E2E test — docs/decisions/2026-09-13-ir-007-e2e-fixture-repo.md)
names the honest remaining gap in the fleet-wide WIP cap: withWipCapGuard
(host/src/tools/wip-cap.ts) only serializes check-then-act within one Node
process. Two separate irina-cycle.yml runs — two separate GitHub Actions
runners — have no shared memory to serialize against, so the same TOCTOU
race IR-007 fixed in-process can still happen across processes.
Decision
Section titled “Decision”Ship the fix (host/src/tools/wip-cap-lock.ts) as a leased file,
.petrova/wip-cap.lock, committed to this repo and acquired via the GitHub
Contents API’s optimistic-concurrency sha match — a second acquire against
a stale sha fails with 409/422, read as “lock held.” A lease carries its
own expiresAt (LEASE_TTL_MS = 5 minutes); a runner killed mid-hold does
not wedge the cap, because the next acquirer treats an expired lease as
free rather than waiting on the crashed holder to release it.
Chosen over the ticket’s other two candidates:
- A GitHub Deployments-API mutex — no material advantage over the Contents API here, and it’s a less-obvious primitive to future readers of this code.
- Accept-the-race-and-reconcile — cheaper to build, but it lets a real 4th PR exist (even briefly) before closing it, which is a worse failure mode than refusing it up front for a system whose whole point is the refusal.
withWipCapGuard calls it only when PETROVA_WIP_CAP_CROSS_PROCESS_LOCK=1
is set — the lock ships disabled by default. Flipping it unconditionally
on would make every act() call depend on GitHub App write access to this
repo’s .petrova/wip-cap.lock, for every existing caller (CLI, Fleet MCP
server, any future consumer), unconditionally, the same day this code
lands. That is a bigger blast radius than IR-012 asked for. The intended
rollout: enable the flag on irina-cycle.yml specifically first (the only
real multi-runner caller today), observe a burn-in period, then flip the
default in a follow-up PR once it has actually run concurrently against
live GitHub state without wedging.
What this does NOT do
Section titled “What this does NOT do”- Does not change the ratified cap value (3) or the single-process
serialization in
wip-cap.ts— additive only. - Does not (yet) wire
PETROVA_WIP_CAP_CROSS_PROCESS_LOCK=1intoirina-cycle.yml’s env — that’s the burn-in step above, deliberately left as a separate, reviewable change once this lands. - Does not include IR-012’s C1 as a real two-runner
workflow_dispatchE2E test (the IR-007 pattern, extended).host/tests/wip-cap-lock.test.tsproves C1/C2/C3 against a fake Contents-API double with the same optimistic-concurrency contract real GitHub enforces — real 405 proof against two live runners racing the real fixture repo is scoped out to the burn-in step, not fabricated here.
Consequences
Section titled “Consequences”- IR-004 (write-path wiring) is unblocked from IR-012’s specific dependency, but its own human-ruling-required gate is unchanged — this doc does not authorize starting IR-004.
- A future flip-the-default PR should cite this doc and the burn-in evidence, not re-litigate the design.
References
Section titled “References”irina/tickets/IR-012-cross-runner-wip-cap-lock.xmlhost/src/tools/wip-cap-lock.ts,host/src/tools/wip-cap.tshost/tests/wip-cap-lock.test.tsdocs/decisions/2026-09-13-ir-007-e2e-fixture-repo.md(the pattern this follows: real gap, honestly scoped, follow-up ticket for what’s deferred)
Sign-off
Section titled “Sign-off”- Subagent: claude-sonnet-5 (session_01488gmoyur1UAMfpNMgUAhC)
- Human: alex@devarno.com
Countersigned by human:alex@devarno.com on 2026-09-13 — commit 46bb0eba
(this file’s creation commit) is authored by the human directly, per
docs/decisions/2026-09-05-proxy-countersign-retired.md’s post-retirement
definition. Line added retroactively by the agent as scribe.