arial
GitHub

Orchestration System

The orchestration system coordinates parallel workstream execution and sequential merging. This document covers the orchestrator and workstream executor.

Overview

Arial's orchestration follows a two-phase approach:

  1. Parallel execution - All workstreams run simultaneously in isolated Git worktrees
  2. Sequential merging - Completed workstreams merge one at a time to avoid conflicts

Orchestrator

The orchestrator (lib/orchestrator.ts) is the central coordination engine.

Interface

interface Orchestrator {
  executors: Map<string, WorkstreamExecutor>

  start(): Promise<void>
  stopAll(): Promise<void>
  wait(): Promise<boolean>
  addContext(workstreamId: string, context: string): Promise<void>
}

Lifecycle

// Create orchestrator
const orchestrator = createOrchestrator(state, {
  repoRoot,
  adapter,
  onStateChange,
  onWorkstreamEvent,
})

// Start all workstreams
await orchestrator.start()

// Wait for completion
const success = await orchestrator.wait()

// Or stop early
await orchestrator.stopAll()

Event Callbacks

The orchestrator emits events via callbacks:

interface OrchestratorOptions {
  onStateChange: (state: ArialState) => void
  onWorkstreamEvent: (event: WorkstreamEvent) => void
}

Events include:

  • workstream.started - Workstream execution began
  • workstream.completed - Workstream finished successfully
  • workstream.failed - Workstream encountered an error
  • merge.started - Merge operation began
  • merge.completed - Merge finished
  • merge.conflict - Merge conflict detected

Merge Queue

Completed workstreams are queued for merging:

// Pseudocode
const mergeQueue: Workstream[] = []

onWorkstreamComplete(workstream) {
  mergeQueue.push(workstream)
  processMergeQueue()
}

processMergeQueue() {
  while (mergeQueue.length > 0 && !isMerging) {
    const next = mergeQueue.shift()
    await mergeWorkstream(next)
  }
}

Sequential merging ensures:

  • No concurrent merge conflicts
  • Clear ordering of changes
  • Ability to retry failed merges

Workstream Executor

Each workstream has its own executor (lib/workstream-executor.ts).

Interface

interface WorkstreamExecutor {
  workstream: Workstream
  currentActivity: string
  outputBuffer: OutputBuffer

  start(): Promise<void>
  stop(): void
  isRunning(): boolean
  wait(): Promise<ExecutorResult>
}

Execution Flow

1. Resolve Adapter
   ├── Check workstream.adapter preference
   └── Fall back to default adapter

2. Validate Capabilities
   ├── Check requiredCapabilities from spec
   └── Fail if adapter doesn't support them

3. Create Git Worktree
   ├── Create branch: arial/{workstream-id}
   └── Create worktree: .arial/worktrees/{workstream-id}

4. Load Context
   ├── Read spec file content
   └── Load _context.md if present

5. Build Prompt
   ├── Apply execution.md template
   └── Inject spec, context, and instructions

6. Create Runner
   ├── Get runner from adapter
   └── Subscribe to events

7. Execute
   ├── Start runner
   ├── Stream tool events to TUI
   └── Wait for completion

8. Handle Result
   ├── Success: stage all, commit
   └── Failure: mark failed, save error

Output Buffering

The executor maintains an output buffer for the TUI:

interface OutputBuffer {
  lines: string[]
  maxLines: number

  add(line: string): void
  getRecent(count: number): string[]
  clear(): void
}

Error Handling

Executors use Result types for error handling:

type ExecutorResult =
  | { ok: true; summary: string }
  | { ok: false; error: string }

Errors are captured and stored in the workstream state:

workstream.status = 'failed'
workstream.lastError = error.message

Merge Logic

Merging is handled by lib/merge.ts.

Simple Merge

async function simpleMerge(
  repoRoot: string,
  branch: string
): Promise<MergeResult>

Attempts a fast-forward or recursive merge. Returns conflict information if merge fails.

Merge with Retry

async function mergeWithRetry(
  repoRoot: string,
  branch: string,
  adapter: Adapter,
  maxRetries: number
): Promise<MergeResult>

If conflicts occur:

  1. Get conflicted files
  2. Run conflict resolution agent
  3. Stage resolved files
  4. Complete merge
  5. Retry up to maxRetries times

AI Conflict Resolution

The conflict agent receives:

  • List of conflicted files
  • Conflict markers in each file
  • Context about the workstream

It produces:

  • Resolved file contents
  • Summary of resolutions made

Concurrency Model

Arial uses a simple concurrency model:

┌─────────────────────────────────────────────────────────┐
│                     Orchestrator                         │
│                                                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │
│  │Executor1│  │Executor2│  │Executor3│  ... (parallel) │
│  └────┬────┘  └────┬────┘  └────┬────┘                 │
│       │            │            │                       │
│       v            v            v                       │
│  ┌─────────────────────────────────────┐               │
│  │         Merge Queue (sequential)     │               │
│  └─────────────────────────────────────┘               │
└─────────────────────────────────────────────────────────┘

Key points:

  • Executors run in parallel (Promise.all)
  • Each executor has its own worktree (no file conflicts)
  • Merges happen sequentially (one at a time)
  • State updates are synchronized via callbacks

Graceful Shutdown

On Ctrl+C or error:

  1. Stop all running executors
  2. Wait for current operations to complete
  3. Save current state
  4. Report final status
process.on('SIGINT', async () => {
  await orchestrator.stopAll()
  saveState(repoRoot, state)
  process.exit(0)
})