Multi-step migration workflows and project analysis helpers for the Angular Modernization Platform.
The package provides two entry points:
WorkflowEngine - runs a workflow of tool calls with dependency management, sequential or parallel scheduling, retries, checkpoints and resume. The orchestrate-migration MCP tool is built on it.analyzeProject() - loads a project with the analyzer plugin and runs its analysis rules.The engine does not know any tools itself. Each step names a tool and its arguments, and a ToolExecutor supplied by the caller runs it. In the MCP adapter the executor dispatches to the MCP tools (transform-code, run-build, ...).
pnpm add @angular-modernizer/orchestration
Caller (orchestrate-migration MCP tool, own code)
|
WorkflowEngine (scheduling, retries, checkpoints)
|
ToolExecutor (supplied by the caller) CheckpointManager (optional, supplied by the caller)
|
Tools (transform-code, run-build, ...)
interface WorkflowDefinition {
id: string;
name: string;
description: string;
steps: WorkflowStep[];
strategy?: 'sequential' | 'parallel' | 'auto'; // default 'auto'
config?: {
maxParallelSteps?: number; // default 3
autoCheckpoint?: boolean; // checkpoint after every fifth completed step
autoRollback?: boolean;
timeoutMs?: number; // not enforced by the engine
};
metadata?: {
createdBy?: string;
source?: 'plan-migration' | 'manual' | 'learn-patterns';
migrationPlan?: unknown;
};
}
interface WorkflowStep {
id: string;
name: string;
description: string;
tool: string; // passed to ToolExecutor.executeTool
args: Record<string, unknown>;
dependencies: string[]; // may be omitted by JSON callers, treated as []
estimatedDuration?: number; // used by dry runs (default 100 ms)
checkpoint?: boolean; // create a checkpoint after this step
optional?: boolean; // a failure is recorded as 'skipped'
retry?: { maxAttempts: number; delayMs: number };
}
import {
WorkflowEngine,
type ToolExecutor,
type WorkflowDefinition,
} from '@angular-modernizer/orchestration';
const executor: ToolExecutor = {
executeTool: async (toolName, args) => {
// Dispatch to the tool implementation
return { toolName, args };
},
};
const workflow: WorkflowDefinition = {
id: 'inject-migration',
name: 'Inject migration',
description: 'Transform one service, then build',
strategy: 'sequential',
steps: [
{
id: 'transform',
name: 'Transform',
description: 'Constructor injection to inject()',
tool: 'transform-code',
args: {
filePath: 'src/app/user.service.ts',
transformation: 'constructor-to-inject',
},
dependencies: [],
retry: { maxAttempts: 2, delayMs: 1000 },
},
{
id: 'build',
name: 'Build',
description: 'Check the build',
tool: 'run-build',
args: { rootPath: '/path/to/project' },
dependencies: ['transform'],
checkpoint: true,
},
],
};
const engine = new WorkflowEngine(executor);
const result = await engine.execute(workflow, {
onStepComplete: (step) => console.log(step.stepId, step.status),
});
console.log(result.status); // 'completed' | 'partial' | 'failed'
console.log(result.metrics); // totalSteps, successfulSteps, failedSteps, skippedSteps, checkpointsCreated
execute() does not throw for step failures. They are reported in the result: status is completed when no step failed, partial when some steps failed and at least one completed, failed otherwise. errors lists the error of each failed step; an error no step reported (for example a stuck workflow) is listed with the step ID workflow.
| Strategy | Behavior |
|---|---|
sequential |
Steps in definition order. A step whose dependencies have not completed is left out. |
parallel |
Every step whose dependencies have completed starts, up to config.maxParallelSteps at a time. When nothing runs and no pending step can start (circular dependencies, a dependency that was skipped), the workflow fails. |
auto (default) |
parallel when some step has dependencies and the steps form more than one dependency level, otherwise sequential. |
retry is tried again up to retry.maxAttempts times after the first failure, waiting retry.delayMs before each retry.skipped; the workflow goes on.A checkpoint records the results of all completed steps. The engine creates one after each step with checkpoint: true and, with config.autoCheckpoint, after every fifth completed step. With a CheckpointManager each checkpoint is saved, and a later run can resume from it:
import {
WorkflowEngine,
type CheckpointManager,
type WorkflowCheckpoint,
} from '@angular-modernizer/orchestration';
const store = new Map<string, WorkflowCheckpoint>();
const checkpoints: CheckpointManager = {
saveCheckpoint: async (checkpoint) => {
store.set(checkpoint.id, checkpoint);
},
loadCheckpoint: async (id) => store.get(id) ?? null,
listCheckpoints: async (workflowId) =>
[...store.values()].filter((c) => c.workflowId === workflowId),
};
const engine = new WorkflowEngine(executor, checkpoints);
const first = await engine.execute(workflow);
if (first.status !== 'completed' && first.finalCheckpoint) {
await engine.execute(workflow, {
resumeFromCheckpoint: first.finalCheckpoint.id,
});
}
Steps contained in the checkpoint are not run again. Without a CheckpointManager, resumeFromCheckpoint is ignored. An unknown checkpoint ID fails the workflow.
config.autoRollback triggers a rollback to the last checkpoint when the workflow fails. The engine currently only logs it; files are not restored. Use the rollback-changes MCP tool or the rollback support in @angular-modernizer/core to restore files.
| Option | Effect |
|---|---|
resumeFromCheckpoint |
Checkpoint ID to resume from (needs a CheckpointManager) |
dryRun |
No tool is called; each step waits its estimatedDuration and completes with { dryRun: true } |
onProgress |
Called with the WorkflowExecutionState after each completed step |
onStepComplete |
Called with the StepExecutionResult of each completed step |
onCheckpoint |
Called with each created WorkflowCheckpoint |
analyzeProject() creates a Kernel with the AnalyzerPlugin, collects the analysis rules of all registered plugins and runs them concurrently.
import { analyzeProject } from '@angular-modernizer/orchestration';
const results = await analyzeProject({
projectPath: '/path/to/tsconfig.json',
config: {},
options: { deep: true, maxDepth: 3 },
});
for (const result of results) {
console.log(result.ruleId, result.filePath, result.message);
}
projectPath is the path to the project's tsconfig.json; an empty path or a project without source files throws.sourceFile is the first source file of the project; rules that look at the whole project use context.project.options is passed to each rule as context.options.Rule execution failed: ... and metadata.error: true; the other rules still run.| Export | Kind |
|---|---|
WorkflowEngine |
class |
analyzeProject |
function |
ToolExecutor, CheckpointManager |
interfaces implemented by the caller |
WorkflowDefinition, WorkflowStep, WorkflowStepStatus, WorkflowExecutionStrategy |
workflow input types |
WorkflowExecutionOptions, WorkflowExecutionState, WorkflowExecutionResult, StepExecutionResult, WorkflowCheckpoint |
execution types |
AnalyzeProjectOptions |
input of analyzeProject |
packages/orchestration/
src/
index.ts # public exports
types.ts # workflow types
workflow-engine.ts # WorkflowEngine, ToolExecutor, CheckpointManager
project-analyzer.ts # analyzeProject
__tests__/
workflow-engine.test.ts
package.json
tsconfig.json
jest.config.js
README.md
pnpm --filter @angular-modernizer/orchestration test
The tests cover sequential, parallel and auto execution, checkpoints and resume, retries, dry runs, progress callbacks, result building and steps without dependencies.
@angular-modernizer/core - Kernel@angular-modernizer/api - createPublicApi@angular-modernizer/plugin-system - ContextFactory, plugin contracts@angular-modernizer/plugin-analyzer - AnalyzerPlugin used by analyzeProject
Workflow engine and project analysis helpers for the Angular Modernizer.
orchestrate-migrationMCP tool is built on it.