#!/usr/bin/env node /** * enforce-parallel-session-lock — PreToolUse wrapper around the pure * parallel-session-lock module (router-gate v4 Stream H Task 7). * * Prevents two Claude sessions on the same workspace from concurrently * mutating files. When session B tries a mutating tool while session A * holds a fresh (non-stale) lock, B is blocked with a message naming A's * pid for human triage. * * Activation: settings.json registration is deferred to Phase H-α/H-β * batch step. main() is a no-op (exit 0) until then. */ import { acquire, release, refresh, computeWorkspaceHash } from './parallel-session-lock.mjs'; /** * Pure decision: given an acquire() result, decide block/allow. * * @param {object} args * @param {object|null|undefined} args.acquireResult - from parallel-session-lock.acquire() * @param {string} args.sessionId - current session id * @returns {{block: boolean, reason?: string}} */ export function decide({ acquireResult, sessionId }) { // Fail-open if no acquire result (treat as internal error — never lockout). if (!acquireResult || typeof acquireResult !== 'object') return { block: false }; if (acquireResult.acquired) return { block: false }; const holder = acquireResult.holder || {}; return { block: true, reason: `parallel session lock held by ${holder.session_id || 'unknown'} (pid ${holder.pid || '?'}) — wait or close that session first`, }; } async function main() { // No-op until settings.json registration + Stop-hook release wiring lands // in the deferred Phase H-α/H-β batch step. Activating this hook before // the release pathway is wired would lock the user out of their own // session on first abnormal exit. let input = ''; for await (const chunk of process.stdin) input += chunk; process.exit(0); } if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || (process.argv[1] || '').endsWith('enforce-parallel-session-lock.mjs')) { main().catch(() => process.exit(0)); }