#!/usr/bin/env node import fs from 'fs'; function fail(code, message) { process.stderr.write(`${code}: ${message}\n`); process.exit(1); } function parseArgs(argv) { const args = { input: '', pretty: true }; for (let i = 2; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--input') { const value = argv[i + 1]; if (!value || value.startsWith('--')) fail('CLI_ERROR', '--input requires a value'); args.input = value; i += 1; } else if (arg === '--compact') { args.pretty = false; } else { fail('CLI_ERROR', `unknown argument: ${arg}`); } } return args; } function readInput(path) { if (!path || path === '-') return fs.readFileSync(0, 'utf8'); return fs.readFileSync(path, 'utf8'); } function parseJson(raw) { try { return JSON.parse(raw); } catch { fail('INVALID_JSON', 'input must be valid JSON'); } } function hasNonEmptyString(value) { return typeof value === 'string' && value.trim().length > 0; } function hasEvidenceObject(value) { if (!value) return false; if (hasNonEmptyString(value)) return true; if (Array.isArray(value)) return value.length > 0; if (typeof value === 'object') return Object.keys(value).length > 0; return false; } function normalizedAction(value) { return hasNonEmptyString(value) ? value.trim() : ''; } function evaluatePlan(input) { const gateStatus = normalizedAction(input?.gateStatus); const actorStage = normalizedAction(input?.actorStage); const requiredNextAction = normalizedAction(input?.requiredNextAction || input?.concreteNextAction || input?.nextStep); const reviewOutcome = normalizedAction(input?.reviewOutcome).toLowerCase(); const blocker = normalizedAction(input?.blocker); const executionEvidence = input?.executionEvidence; const reviewEvidence = input?.reviewEvidence; const blockerEvidence = input?.blockerEvidence; if (gateStatus !== 'pass') { return { plannerStatus: 'blocked_by_gate', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'gateStatus must pass before auto-chain planning can proceed', requiredEvidence: ['gateStatus=pass'], autoChainAllowed: false, }; } if (!requiredNextAction) { return { plannerStatus: 'none', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'no concrete next action available for auto-chain planning', requiredEvidence: ['concreteNextAction'], autoChainAllowed: false, }; } if (actorStage === 'implementer_result' && requiredNextAction === 'request_spec_review') { if (!hasEvidenceObject(executionEvidence)) { return { plannerStatus: 'blocked_by_evidence', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'implementation evidence missing for review-required next action', requiredEvidence: ['executionEvidence'], autoChainAllowed: false, }; } return { plannerStatus: 'pass', derivedAction: 'dispatch_spec_review', dispatchMode: 'dry_run_dispatch', reason: 'implementation evidence present; derived spec review dispatch in dry-run mode', requiredEvidence: ['executionEvidence'], autoChainAllowed: true, }; } if (actorStage === 'spec_review' && reviewOutcome === 'pass' && requiredNextAction === 'request_code_quality_review') { if (!hasEvidenceObject(reviewEvidence)) { return { plannerStatus: 'blocked_by_evidence', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'review pass evidence missing for code quality review transition', requiredEvidence: ['reviewEvidence'], autoChainAllowed: false, }; } return { plannerStatus: 'pass', derivedAction: 'dispatch_code_quality_review', dispatchMode: 'dry_run_dispatch', reason: 'review pass evidence present; derived code quality review dispatch in dry-run mode', requiredEvidence: ['reviewEvidence'], autoChainAllowed: true, }; } if (requiredNextAction === 'fix_review_findings' || hasNonEmptyString(blocker)) { if (!hasEvidenceObject(blockerEvidence)) { return { plannerStatus: 'blocked_by_evidence', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'blocker evidence missing for retry/fix transition', requiredEvidence: ['blockerEvidence'], autoChainAllowed: false, }; } return { plannerStatus: 'pass', derivedAction: 'dispatch_fix_slice', dispatchMode: 'dry_run_dispatch', reason: 'blocker evidence present; derived retry/fix dispatch in dry-run mode', requiredEvidence: ['blockerEvidence'], autoChainAllowed: true, }; } return { plannerStatus: 'none', derivedAction: 'none', dispatchMode: 'no_dispatch', reason: 'no concrete next action matched a dry-run auto-chain transition', requiredEvidence: ['matchedTransitionEvidence'], autoChainAllowed: false, }; } function main() { const args = parseArgs(process.argv); const raw = readInput(args.input); const input = parseJson(raw); const output = evaluatePlan(input); process.stdout.write(JSON.stringify(output, null, args.pretty ? 2 : 0) + '\n'); } export { evaluatePlan }; const isDirectRun = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url)); if (isDirectRun) { try { main(); } catch (error) { fail('CLI_ERROR', error && error.message ? error.message : 'unexpected error'); } }