Batch-state coexistence
When /ds-implement-ticket runs with 2 or more ticket IDs, a sibling file .agentic/batch-state.json tracks batch-level cursor alongside each ticket's own loop-state-$LOOP_KEY.json phase cursor.
Session ownership gate: both files carry a session_id field. Every write applies a per-write gate that aborts (with an operator-visible warning) if:
- The existing
session_id belongs to a different session whose liveness timestamp (last_updated for a keyed loop-state file, updated_at for batch-state.json) is within 10 minutes - and on batch-state.json ONLY, status is also active
- The existing
session_id is null or absent, regardless of status (legacy state - force-takeover eligible)
batch-state.json alone carries the status=active precondition: its terminal mark stamps a fresh timestamp on exit, so without it the gate would abort the first write of an approved resume. A keyed loop-state file skips it - a live session can hold a non-active one (the Phase 7 stall path continues to the next ticket), so adding the precondition there would let a foreign session clobber a live file.
This prevents orphan-session corruption uniformly across both files. Note that per-ticket keying removes different-ticket contention from this gate entirely - those sessions write different files - and leaves same-ticket contention, which is exactly what the gate is for.
Concurrency limits:
- Only one batch per project root is supported
- A second concurrent N>=2 invocation is refused at Phase 0a-pre
- N=1 invocations against an active foreign batch warn but do not refuse
- Single-ticket Trivial invocations never create
batch-state.json
The SessionEnd hook mirrors its loop-state terminal interrupted-mark write to batch-state.json via the same best-effort silent-fail discipline. The Stop hook's separate per-turn liveness refresh mirrors similarly.