import { VALID_REVIEW_POLICY_STAGE_VALUES, VALID_SUBAGENT_RUNNERS, VALID_TICKET_BOUNDARY_MODES, type OrchestratorConfig, type ReviewPolicyStageValue, type TicketBoundaryMode, } from './config'; import type { OrchestratorOptions } from './types'; export type BaselineValue = 'orchestrator' | 'run-policy'; export const VALID_BASELINE_VALUES: BaselineValue[] = [ 'orchestrator', 'run-policy', ]; export type ParsedCliArgs = { command: string; positionals: string[]; flags: Set; planPath?: string; prNumber?: number; boundaryMode?: TicketBoundaryMode; subagentReviewPolicy?: ReviewPolicyStageValue; prReviewPolicy?: ReviewPolicyStageValue; subagent?: 'claude-cli' | 'codex-cli' | 'cursor-cli'; primary?: string; subagentModel?: string; subagentEffort?: string; baseline?: BaselineValue; promptFile?: string; ackReconciliation?: 'patched' | 'deferred' | 'clean'; commitSha?: string; reason?: string; dispositionsPath?: string; }; export const STANDALONE_TRIAGE_COMMAND = 'triage-standalone'; export const TICKET_TRIAGE_COMMAND = 'triage-ticket'; export function normalizeDeliveryCommand(command: string): string { if (command === 'ai-review') { return STANDALONE_TRIAGE_COMMAND; } if (command === 'reconcile-late-review') { return TICKET_TRIAGE_COMMAND; } return command; } export function isStandaloneTriageCommand(command: string): boolean { return normalizeDeliveryCommand(command) === STANDALONE_TRIAGE_COMMAND; } /** * Resolve runtime policy overrides from parsed CLI args onto the raw config. * CLI flags take precedence over config file values; absent flags preserve config. * Does not mutate `rawConfig` or `orchestrator.config.json`. */ export function resolveRuntimePolicyOverrides( parsed: Pick< ParsedCliArgs, 'boundaryMode' | 'subagentReviewPolicy' | 'prReviewPolicy' >, rawConfig: OrchestratorConfig, ): OrchestratorConfig { const mergedReviewPolicy = { ...rawConfig.reviewPolicy, ...(parsed.subagentReviewPolicy !== undefined ? { subagentReview: parsed.subagentReviewPolicy } : {}), ...(parsed.prReviewPolicy !== undefined ? { prReview: parsed.prReviewPolicy } : {}), }; const effectivePrReview = mergedReviewPolicy.prReview; if ( parsed.prReviewPolicy !== undefined && effectivePrReview !== 'disabled' && (rawConfig.prReviewAgents === undefined || rawConfig.prReviewAgents.length === 0) ) { throw new Error( `--pr-review-policy ${parsed.prReviewPolicy} requires prReviewAgents in orchestrator.config.json. Add a prReviewAgents array or use --pr-review-policy disabled.`, ); } return { ...rawConfig, ticketBoundaryMode: parsed.boundaryMode ?? rawConfig.ticketBoundaryMode, reviewPolicy: mergedReviewPolicy, }; } function isValidBoundaryMode(mode: unknown): mode is TicketBoundaryMode { return VALID_TICKET_BOUNDARY_MODES.includes(mode as TicketBoundaryMode); } export function getUsage(runDeliverInvocation: string): string { return [ `Usage: ${runDeliverInvocation} [--plan ] `, '', 'Commands:', ' triage-standalone [--pr ]', ' sync', ' status', ' repair-state', ' triage-advisory-observations --dispositions ', ' start [ticket-id]', ' post-red [ticket-id]', ' post-verify [ticket-id] [clean|patched] [patch-commit-sha ...]', ' write-subagent-adversarial-review [ticket-id] [--prompt-file ]', ' subagent-review [ticket-id] [clean|patched ] [--force] [--subagent ] [--primary ]', ' subagent-review record-deferred --reason "" [ticket-id]', ' reconcile-subagent-review [ticket-id]', ' write-subagent-refactor-review [ticket-id] --prompt-file ', ' subagent-refactor-review [ticket-id] [clean|patched ] [--force] [--subagent ] [--primary ]', ' subagent-refactor-review record-deferred --reason "" [ticket-id]', ' reconcile-subagent-refactor-review [ticket-id]', ' open-pr [ticket-id] [--ack-reconciliation ] [--commit ] [--reason ""]', ' poll-review [ticket-id]', ' triage-ticket ', ' record-review [note]', ' advance', ' restack [ticket-id]', '', 'Aliases:', ' ai-review -> triage-standalone', ' reconcile-late-review -> triage-ticket', '', 'Options:', ' --boundary-mode ', ' --subagent-review-policy ', ' --pr-review-policy ', ' --subagent ', ' --primary ', ' --subagent-model (applies only to the explicitly requested runner)', ' --subagent-effort (applies only to the explicitly requested runner; claude-cli/codex-cli only)', ' --ack-reconciliation ', ' --commit ', ' --reason ""', ' --dispositions ', ' --baseline ', ].join('\n'); } export function parseCliArgs(argv: string[], usage: string): ParsedCliArgs { let planPath: string | undefined; let prNumber: number | undefined; let boundaryMode: ParsedCliArgs['boundaryMode']; let subagentReviewPolicy: ParsedCliArgs['subagentReviewPolicy']; let prReviewPolicy: ParsedCliArgs['prReviewPolicy']; let subagent: ParsedCliArgs['subagent']; let primary: ParsedCliArgs['primary']; let subagentModel: ParsedCliArgs['subagentModel']; let subagentEffort: ParsedCliArgs['subagentEffort']; let baseline: ParsedCliArgs['baseline']; let promptFile: ParsedCliArgs['promptFile']; let ackReconciliation: ParsedCliArgs['ackReconciliation']; let commitSha: ParsedCliArgs['commitSha']; let reason: ParsedCliArgs['reason']; let dispositionsPath: ParsedCliArgs['dispositionsPath']; const flags = new Set(); const positionals: string[] = []; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === '--plan') { planPath = argv[index + 1]; index += 1; continue; } if (value === '--pr') { const rawNumber = argv[index + 1]; if (!rawNumber || Number.isNaN(Number(rawNumber))) { throw new Error('Pass --pr .'); } prNumber = Number(rawNumber); index += 1; continue; } if (value === '--boundary-mode') { const rawMode = argv[index + 1]; if (rawMode === undefined) { throw new Error( `Missing value for --boundary-mode; pass one of <${VALID_TICKET_BOUNDARY_MODES.join('|')}>.`, ); } if (!isValidBoundaryMode(rawMode)) { throw new Error( `Pass --boundary-mode <${VALID_TICKET_BOUNDARY_MODES.join('|')}>.`, ); } boundaryMode = rawMode; index += 1; continue; } if (value === '--subagent-review-policy') { const raw = argv[index + 1]; if ( raw === undefined || !VALID_REVIEW_POLICY_STAGE_VALUES.includes( raw as ReviewPolicyStageValue, ) ) { throw new Error( `Pass --subagent-review-policy <${VALID_REVIEW_POLICY_STAGE_VALUES.join('|')}>.`, ); } subagentReviewPolicy = raw as ReviewPolicyStageValue; index += 1; continue; } if (value === '--pr-review-policy') { const raw = argv[index + 1]; if ( raw === undefined || !VALID_REVIEW_POLICY_STAGE_VALUES.includes( raw as ReviewPolicyStageValue, ) ) { throw new Error( `Pass --pr-review-policy <${VALID_REVIEW_POLICY_STAGE_VALUES.join('|')}>.`, ); } prReviewPolicy = raw as ReviewPolicyStageValue; index += 1; continue; } if (value === '--subagent') { const raw = argv[index + 1]; if ( raw === undefined || raw.startsWith('--') || !(VALID_SUBAGENT_RUNNERS as readonly string[]).includes(raw) ) { throw new Error( `Pass --subagent <${VALID_SUBAGENT_RUNNERS.join('|')}>.`, ); } subagent = raw as ParsedCliArgs['subagent']; index += 1; continue; } if (value === '--primary') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error( 'Pass --primary (free-form, e.g. claude, codex, cursor, composer).', ); } primary = raw.trim(); index += 1; continue; } if (value === '--subagent-model') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error('Pass --subagent-model .'); } subagentModel = raw.trim(); index += 1; continue; } if (value === '--subagent-effort') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error('Pass --subagent-effort .'); } subagentEffort = raw.trim(); index += 1; continue; } if (value === '--preferred-runner') { throw new Error( '--preferred-runner has been removed. Pass --subagent or set `subagentRunner` in orchestrator.config.json.', ); } if (value === '--red-commit-sha') { throw new Error( '--red-commit-sha has been removed. Either author a `[red]` commit before continuing, or declare `Red: skip` in the ticket metadata if the ticket has no testable behavior.', ); } if (value === '--prompt-file') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error('Pass --prompt-file .'); } promptFile = raw; index += 1; continue; } if (value === '--baseline') { const raw = argv[index + 1]; if ( raw === undefined || !VALID_BASELINE_VALUES.includes(raw as BaselineValue) ) { throw new Error( `Pass --baseline <${VALID_BASELINE_VALUES.join('|')}>.`, ); } baseline = raw as BaselineValue; index += 1; continue; } if (value === '--ack-reconciliation') { const raw = argv[index + 1]; const VALID = ['patched', 'deferred', 'clean'] as const; if ( raw === undefined || raw.startsWith('--') || !(VALID as readonly string[]).includes(raw) ) { throw new Error(`Pass --ack-reconciliation <${VALID.join('|')}>.`); } ackReconciliation = raw as 'patched' | 'deferred' | 'clean'; index += 1; continue; } if (value === '--commit') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error('Pass --commit .'); } commitSha = raw.trim(); index += 1; continue; } if (value === '--reason') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--')) { throw new Error( 'Pass --reason "" (the rationale is captured on the ledger for audit).', ); } reason = raw; index += 1; continue; } if (value === '--dispositions') { const raw = argv[index + 1]; if (raw === undefined || raw.startsWith('--') || raw.trim() === '') { throw new Error('Pass --dispositions .'); } dispositionsPath = raw; index += 1; continue; } if (value === '--phase') { throw new Error( '--phase has been removed. Pass --plan instead.', ); } if (value?.startsWith('--')) { flags.add(value.slice(2)); continue; } positionals.push(value ?? ''); } const [command, ...rest] = positionals; if (!command) { throw new Error(usage); } return { command: normalizeDeliveryCommand(command), positionals: rest, flags, planPath, prNumber, boundaryMode, subagentReviewPolicy, prReviewPolicy, subagent, primary, subagentModel, subagentEffort, baseline, promptFile, ackReconciliation, commitSha, reason, dispositionsPath, }; } export async function resolveOptionsForCommand(input: { cwd: string; command: string; planPath?: string; createOptions: (input: { planPath?: string }) => OrchestratorOptions; inferPlanPathFromBranch: (cwd: string, branch: string) => Promise; readCurrentBranch: (cwd: string) => string; }): Promise { const { command, createOptions, cwd, inferPlanPathFromBranch, planPath, readCurrentBranch, } = input; if (planPath) { return createOptions({ planPath }); } if (command !== 'restack') { throw new Error( 'Pass --plan . Phase aliases are no longer supported.', ); } const branch = readCurrentBranch(cwd); const inferredPlanPath = await inferPlanPathFromBranch(cwd, branch); return createOptions({ planPath: inferredPlanPath }); }