arial
GitHub

State Management

Arial uses a file-based state system to track workstream progress across runs. This document covers the state schema, storage, and operations.

Overview

State is stored in .arial/state.json and managed through pure functions in lib/state.ts. The design prioritizes:

  • Atomicity - No partial writes
  • Simplicity - Plain JSON, easy to inspect
  • Recoverability - State survives crashes

Schema

ArialState

interface ArialState {
  version: 2
  specsDir: string
  baseBranch: string
  status: RunStatus
  createdAt: string      // ISO 8601 timestamp
  updatedAt: string      // ISO 8601 timestamp
  workstreams: Workstream[]
}

type RunStatus = 'planning' | 'running' | 'completed' | 'failed'

Workstream

interface Workstream {
  id: string              // Derived from spec filename
  specFile: string        // Relative path to spec
  title: string           // Human-readable title
  description: string     // What it accomplishes
  status: WorkstreamStatus
  branch: string          // Git branch name
  worktreePath: string    // Path to Git worktree
  pid?: number            // Process ID if running
  startedAt?: string      // ISO timestamp
  completedAt?: string    // ISO timestamp
  retryCount: number
  lastError?: string
  summary?: string        // Completion summary
  adapter?: string        // Override default adapter
  requiredCapabilities?: AdapterCapability[]
}

type WorkstreamStatus = 'pending' | 'running' | 'done' | 'failed'

Storage Layout

.arial/
├── state.json           # Main state file
├── worktrees/           # Git worktrees per workstream
│   ├── auth-system/
│   ├── payment-flow/
│   └── ...
└── logs/                # Per-workstream logs
    ├── auth-system.log
    ├── payment-flow.log
    └── ...

State Operations

Initialization

function initArial(repoRoot: string): Result<void>

Creates the .arial/ directory structure:

.arial/
├── worktrees/
└── logs/

Creating State

function createState(
  specsDir: string,
  baseBranch: string
): ArialState

Creates a new run state with no workstreams.

Loading State

function loadState(repoRoot: string): Result<ArialState>

Loads and validates state from .arial/state.json.

Saving State

function saveState(
  repoRoot: string,
  state: ArialState
): Result<void>

Atomic write using temp file + rename:

// Pseudocode
const tempPath = `${statePath}.tmp`
writeFileSync(tempPath, JSON.stringify(state))
renameSync(tempPath, statePath)  // Atomic on POSIX

This prevents corruption from interrupted writes.

Query Functions

// Check if Arial is initialized
function isInitialized(repoRoot: string): boolean

// Check if there's an active run
function hasActiveRun(repoRoot: string): boolean

// Get workstreams by status
function getWorkstreamsByStatus(
  state: ArialState,
  status: WorkstreamStatus
): Workstream[]

Mutation Functions

// Add a new workstream
function addWorkstream(
  state: ArialState,
  repoRoot: string,
  input: WorkstreamInput
): ArialState

// Update workstream status
function updateWorkstreamStatus(
  state: ArialState,
  id: string,
  status: WorkstreamStatus,
  extra?: Partial<Workstream>
): ArialState

// Derive run status from workstreams
function updateRunStatus(state: ArialState): ArialState

All mutation functions return a new state object (immutable).

Status Transitions

Run Status

planning ──> running ──> completed
                    └──> failed
  • planning - Initial state during arial plan
  • running - Set when arial run starts
  • completed - All workstreams done
  • failed - Any workstream failed (without recovery)

Workstream Status

pending ──> running ──> done
                   └──> failed
  • pending - Created but not started
  • running - Executor is active
  • done - Completed and merged
  • failed - Error occurred

State Derivation

Run status is derived from workstream statuses:

function updateRunStatus(state: ArialState): ArialState {
  const statuses = state.workstreams.map(w => w.status)

  if (statuses.every(s => s === 'done')) {
    return { ...state, status: 'completed' }
  }

  if (statuses.some(s => s === 'failed')) {
    return { ...state, status: 'failed' }
  }

  if (statuses.some(s => s === 'running')) {
    return { ...state, status: 'running' }
  }

  return state
}

Recovery

If Arial crashes during execution:

  1. State file preserves last known status
  2. arial status shows current state
  3. arial run can resume pending workstreams
  4. arial cleanup removes orphaned resources

Version Migration

State includes a version field for schema changes:

interface ArialState {
  version: 2  // Current version
  // ...
}

Future versions can include migration logic:

function loadState(repoRoot: string): Result<ArialState> {
  const raw = readStateFile(repoRoot)

  if (raw.version === 1) {
    return migrateV1toV2(raw)
  }

  return { ok: true, value: raw }
}

Example State

{
  "version": 2,
  "specsDir": "./specs",
  "baseBranch": "main",
  "status": "running",
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T10:35:00Z",
  "workstreams": [
    {
      "id": "auth-system",
      "specFile": "specs/auth-system.md",
      "title": "Implement Authentication",
      "description": "Add user authentication with JWT",
      "status": "done",
      "branch": "arial/auth-system",
      "worktreePath": ".arial/worktrees/auth-system",
      "startedAt": "2024-01-15T10:30:05Z",
      "completedAt": "2024-01-15T10:32:00Z",
      "retryCount": 0,
      "summary": "Added JWT auth with login/logout endpoints"
    },
    {
      "id": "payment-flow",
      "specFile": "specs/payment-flow.md",
      "title": "Payment Integration",
      "description": "Integrate Stripe payments",
      "status": "running",
      "branch": "arial/payment-flow",
      "worktreePath": ".arial/worktrees/payment-flow",
      "startedAt": "2024-01-15T10:30:05Z",
      "retryCount": 0
    }
  ]
}