191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
#!/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<string, string> };
|
|
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<string, string> };
|
|
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<void> {
|
|
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;
|
|
});
|
|
}
|