#!/usr/bin/env node import fs from 'node:fs'; const LEGAL_TERMINAL_STATES = new Set(['waiting_user', 'blocked', 'pending_verification']); function isNonEmptyString(value) { return typeof value === 'string' && value.trim().length > 0; } function isObject(value) { return value != null && typeof value === 'object' && !Array.isArray(value); } function hasValidDispatchReceipt(receipt) { if (!isObject(receipt)) return false; if (!isNonEmptyString(receipt.planId)) return false; if (!isNonEmptyString(receipt.currentTask)) return false; if (!isObject(receipt.nextDerivedAction)) return false; if (!isNonEmptyString(receipt.dispatchedAt)) return false; return true; } function parseArgs(argv) { let inputPath = null; let compact = false; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--input') { inputPath = argv[i + 1] ?? null; i += 1; continue; } if (arg.startsWith('--input=')) { inputPath = arg.slice('--input='.length); continue; } if (arg === '--compact') { compact = true; continue; } } return { inputPath, compact }; } function readInput(inputPath) { if (!inputPath) { return { ok: false, error: 'missing_required_input', }; } try { const raw = fs.readFileSync(inputPath, 'utf8'); const parsed = JSON.parse(raw); return { ok: true, bytes: Buffer.byteLength(raw, 'utf8'), preview: raw.slice(0, 0), parsed, }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error), }; } } function evaluateContinuity(payload) { const taskComplete = payload?.taskState === 'complete'; const nextAction = payload?.nextDerivedAction ?? payload?.derivedAction ?? null; const nextActionKnown = nextAction != null; const explicitNextTaskKnown = payload?.nextTaskKnown === true; const sameApprovedPlan = payload?.sameApprovedPlan === true; const taskBoundaryStop = payload?.taskBoundaryStop === true; const highRiskStop = payload?.highRiskStop === true; const hasDispatchReceipt = hasValidDispatchReceipt(payload?.dispatchReceipt ?? null); const closureState = payload?.replyClosureState ?? null; const isLegalTerminalState = LEGAL_TERMINAL_STATES.has(closureState); const autoNextObligatory = taskComplete && explicitNextTaskKnown && sameApprovedPlan && taskBoundaryStop && !isLegalTerminalState && !highRiskStop; if (autoNextObligatory && !hasDispatchReceipt) { return { ok: false, status: 'continuity_failure', verdict: 'continuity_failure', reason: 'missing_auto_next_dispatch', }; } if (taskComplete && nextActionKnown && !hasDispatchReceipt && !isLegalTerminalState && !highRiskStop && !('sameApprovedPlan' in (payload ?? {}))) { return { ok: false, status: 'continuity_failure', verdict: 'continuity_failure', reason: 'missing_dispatch_receipt', }; } if (taskComplete && nextActionKnown && !hasDispatchReceipt && !isLegalTerminalState && !highRiskStop && sameApprovedPlan && !taskBoundaryStop && !explicitNextTaskKnown) { return { ok: false, status: 'continuity_failure', verdict: 'continuity_failure', reason: 'missing_dispatch_receipt', }; } return { ok: true, status: 'pass', verdict: 'pass', }; } const { inputPath, compact } = parseArgs(process.argv.slice(2)); const input = readInput(inputPath); const evaluation = input.ok ? evaluateContinuity(input.parsed) : { ok: false, status: 'input_error', verdict: 'input_error', }; const response = { ...evaluation, gate: 'approved_plan_continuity', compact, inputPath, input: { ok: input.ok, ...(input.ok ? { bytes: input.bytes, preview: input.preview, } : { error: input.error, }), }, }; process.stdout.write(`${JSON.stringify(response)} `);