#!/usr/bin/env ts-node import * as path from 'path'; import { promises as fs } from 'fs'; import { SequentialOrchestrator } from '../src/backend/services/SequentialOrchestrator.ts'; import type { WorkflowStage, WorkflowContext } from '../src/backend/services/SequentialOrchestrator.ts'; import { WorkflowStatusManager } from '../src/backend/services/WorkflowStatusManager.ts'; type RunWorkflowOptions = { mode?: 'start' | 'resume' | 'restart'; checkpointPath?: string; statusPath?: string; progressLogPath?: string; maxRetriesPerPhase?: number; now?: Date; }; type CliArgs = { mode: 'start' | 'resume' | 'restart'; checkpointPath?: string; statusPath?: string; progressLogPath?: string; maxRetriesPerPhase?: number; }; const DEFAULT_CHECKPOINT_PATH = path.join(process.cwd(), 'status', 'workflow-checkpoint.json'); const DEFAULT_STATUS_PATH = path.join(process.cwd(), 'status', 'workflow-status.json'); const DEFAULT_PROGRESS_LOG_PATH = path.join(process.cwd(), 'status', 'workflow-progress.jsonl'); function parseCliArgs(argv: string[]): CliArgs { const out: CliArgs = { mode: 'resume' }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; const next = argv[i + 1]; if (arg === '--mode' && next) { if (next === 'start' || next === 'resume' || next === 'restart') { out.mode = next; i += 1; } continue; } if (arg === '--checkpoint' && next) { out.checkpointPath = next; i += 1; continue; } if (arg === '--status' && next) { out.statusPath = next; i += 1; continue; } if (arg === '--progress-log' && next) { out.progressLogPath = next; i += 1; continue; } if (arg === '--max-retries' && next) { const parsed = Number.parseInt(next, 10); if (Number.isFinite(parsed) && parsed >= 0) { out.maxRetriesPerPhase = parsed; } i += 1; continue; } } return out; } function createTimestampedSummary(label: string, now: Date): string { return `${label} @ ${now.toISOString()}`; } function createStages(now: Date): WorkflowStage[] { return [ { id: 'review-plan', name: 'Review TODO and roadmap', execute: async (_context: WorkflowContext) => { const todoPath = path.join(process.cwd(), 'TODO.md'); const roadmapPath = path.join(process.cwd(), 'ROADMAP.md'); const todo = await fs.readFile(todoPath, 'utf8'); const roadmap = await fs.readFile(roadmapPath, 'utf8'); const todoLines = todo.split(/\r?\n/).length; const roadmapLines = roadmap.split(/\r?\n/).length; return { summary: createTimestampedSummary(`Loaded planning docs (TODO: ${todoLines} lines, ROADMAP: ${roadmapLines} lines)`, now), metadata: { todoLines, roadmapLines }, nextAction: 'Proceed to implementation stage.', }; }, }, { id: 'verify-build', name: 'Verify TypeScript build', execute: async (_context: WorkflowContext) => { // Lightweight validation step to keep workflow deterministic in tests. const packageJsonPath = path.join(process.cwd(), 'package.json'); const pkgRaw = await fs.readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(pkgRaw) as { scripts?: Record }; const hasBuildScript = Boolean(pkg.scripts?.build); if (!hasBuildScript) { throw new Error('Missing required npm script: build'); } return { summary: createTimestampedSummary('Validated project scripts and build entrypoint', now), metadata: { hasBuildScript }, nextAction: 'Proceed to tests stage.', }; }, }, { id: 'verify-tests', name: 'Verify test command availability', execute: async (_context: WorkflowContext) => { const packageJsonPath = path.join(process.cwd(), 'package.json'); const pkgRaw = await fs.readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(pkgRaw) as { scripts?: Record }; const hasTestScript = Boolean(pkg.scripts?.test); if (!hasTestScript) { throw new Error('Missing required npm script: test'); } return { summary: createTimestampedSummary('Validated test script presence', now), metadata: { hasTestScript }, nextAction: 'Workflow checks complete.', }; }, }, ]; } export async function runWorkflow(options: RunWorkflowOptions = {}): Promise { const checkpointPath = options.checkpointPath ?? DEFAULT_CHECKPOINT_PATH; const statusPath = options.statusPath ?? DEFAULT_STATUS_PATH; const progressLogPath = options.progressLogPath ?? DEFAULT_PROGRESS_LOG_PATH; const mode = options.mode ?? 'resume'; const maxRetriesPerPhase = options.maxRetriesPerPhase ?? 1; const now = options.now ?? new Date(); const workflow = new SequentialOrchestrator({ checkpointPath, maxRetriesPerPhase, statusManager: new WorkflowStatusManager(statusPath), progressLogPath, }); const initialContext: WorkflowContext = { runId: `workflow-${now.toISOString()}`, startedAt: now.toISOString(), metadata: { initiatedBy: 'scripts/run-workflow.ts', mode, }, }; const stages = createStages(now); if (mode === 'restart' || mode === 'start') { await workflow.start(stages, initialContext); return; } await workflow.resume(stages, initialContext); } if (import.meta.url === `file://${process.argv[1]}`) { const args = parseCliArgs(process.argv.slice(2)); runWorkflow({ mode: args.mode, checkpointPath: args.checkpointPath, statusPath: args.statusPath, progressLogPath: args.progressLogPath, maxRetriesPerPhase: args.maxRetriesPerPhase, }).catch((error) => { const message = error instanceof Error ? error.message : String(error); console.error(`[workflow:run] failed: ${message}`); process.exitCode = 1; }); }