) without rewriting the board logic.
2. State Machine over Logs: Agent progress is modeled as discrete states (backlog, in_progress, awaiting_review, blocked, completed, failed). The orchestrator polls or subscribes to harness events and updates card positions accordingly. This replaces log parsing with deterministic state transitions.
3. Intervention Routing: When an agent requires input, the orchestrator flags the card and pauses execution until the developer responds. This prevents silent blocking and ensures human attention is directed precisely where needed.
4. Repository Isolation: Each harness session should operate in a dedicated working directory or git worktree. The orchestrator manages path allocation to prevent concurrent sessions from overwriting each otherâs changes.
Implementation Example (TypeScript)
The following module demonstrates a minimal orchestrator structure that abstracts the harness layer, manages board state, and routes interventions.
// types.ts
export type TaskState =
| 'backlog'
| 'in_progress'
| 'awaiting_review'
| 'blocked'
| 'completed'
| 'failed';
export interface TaskCard {
id: string;
title: string;
state: TaskState;
harnessId: string;
interventionRequired: boolean;
lastUpdated: Date;
}
export interface HarnessAdapter {
startSession(taskId: string, workdir: string): Promise<string>;
streamOutput(sessionId: string): AsyncIterable<string>;
sendInput(sessionId: string, input: string): Promise<void>;
getSessionState(sessionId: string): Promise<TaskState>;
terminateSession(sessionId: string): Promise<void>;
}
// orchestrator.ts
import { EventEmitter } from 'events';
import { TaskCard, TaskState, HarnessAdapter } from './types';
export class KanbanOrchestrator extends EventEmitter {
private cards: Map<string, TaskCard> = new Map();
private harness: HarnessAdapter;
private workdirRoot: string;
constructor(harness: HarnessAdapter, workdirRoot: string) {
super();
this.harness = harness;
this.workdirRoot = workdirRoot;
}
async createTask(title: string): Promise<TaskCard> {
const id = crypto.randomUUID();
const workdir = `${this.workdirRoot}/${id}`;
const card: TaskCard = {
id,
title,
state: 'backlog',
harnessId: '',
interventionRequired: false,
lastUpdated: new Date()
};
this.cards.set(id, card);
this.emit('task.created', card);
return card;
}
async dispatchTask(taskId: string): Promise<void> {
const card = this.cards.get(taskId);
if (!card || card.state !== 'backlog') return;
const workdir = `${this.workdirRoot}/${taskId}`;
const sessionId = await this.harness.startSession(taskId, workdir);
card.harnessId = sessionId;
card.state = 'in_progress';
card.lastUpdated = new Date();
this.cards.set(taskId, card);
this.emit('task.dispatched', card);
this.monitorSession(sessionId, taskId);
}
private async monitorSession(sessionId: string, taskId: string): Promise<void> {
try {
for await (const chunk of this.harness.streamOutput(sessionId)) {
const currentState = await this.harness.getSessionState(sessionId);
const card = this.cards.get(taskId)!;
if (currentState !== card.state) {
card.state = currentState;
card.lastUpdated = new Date();
this.cards.set(taskId, card);
this.emit('task.state_changed', card);
}
if (chunk.includes('[INTERVENTION_REQUIRED]')) {
card.interventionRequired = true;
card.state = 'blocked';
this.cards.set(taskId, card);
this.emit('task.blocked', card);
break;
}
}
} catch (error) {
const card = this.cards.get(taskId)!;
card.state = 'failed';
card.lastUpdated = new Date();
this.cards.set(taskId, card);
this.emit('task.failed', card, error);
}
}
async resolveIntervention(taskId: string, userInput: string): Promise<void> {
const card = this.cards.get(taskId);
if (!card || !card.interventionRequired) return;
await this.harness.sendInput(card.harnessId, userInput);
card.interventionRequired = false;
card.state = 'in_progress';
card.lastUpdated = new Date();
this.cards.set(taskId, card);
this.emit('task.resumed', card);
}
getBoardState(): TaskCard[] {
return Array.from(this.cards.values()).sort((a, b) =>
b.lastUpdated.getTime() - a.lastUpdated.getTime()
);
}
}
Why This Structure Works
- Decoupled Harness Layer: The
HarnessAdapter interface ensures the orchestrator remains runtime-agnostic. Swapping Claude Code for another agent only requires implementing the adapter, not rewriting board logic.
- Event-Driven State Sync: Using
EventEmitter decouples state updates from the UI. Frontend components can subscribe to task.state_changed or task.blocked without polling.
- Explicit Intervention Handling: The
[INTERVENTION_REQUIRED] marker demonstrates how agents can signal human dependency. The orchestrator pauses execution and routes the card to a blocked state, preventing silent stalls.
- Deterministic State Machine: Transitions are explicit. Agents cannot jump from
backlog to completed without passing through in_progress, reducing race conditions and state corruption.
Pitfall Guide
-
Conflating Harness and Orchestrator
- Explanation: Treating the agent CLI as the management layer leads to tight coupling. When the agent updates its output format or requires new flags, the entire workflow breaks.
- Fix: Maintain a strict adapter boundary. The orchestrator should only interact with standardized interfaces, never raw CLI strings.
-
Ignoring Asynchronous Error Propagation
- Explanation: Agents frequently fail due to context limits, network timeouts, or malformed prompts. If errors arenât surfaced immediately, cards remain stuck in
in_progress indefinitely.
- Fix: Implement heartbeat checks and explicit error states. Route failures to a dedicated
failed column with attached logs for debugging.
-
Over-Engineering Board States
- Explanation: Adding excessive columns (
research, drafting, testing, deploying) creates ceremony overhead. Developers spend more time dragging cards than supervising agents.
- Fix: Start with four states:
backlog, in_progress, blocked, completed. Add states only when workflow friction proves theyâre necessary.
-
Running Unvetted Orchestrators on Production Repositories
- Explanation: Early-stage tools (like Agetor at v0.0.1) lack stability guarantees. Concurrent agents may overwrite files, corrupt git history, or trigger unintended deployments.
- Fix: Always isolate orchestrator sessions in throwaway branches or dedicated worktrees. Never point a pre-1.0 orchestrator at a mainline repository.
-
Assuming Linear Progress
- Explanation: AI agents often loop, retry failed approaches, or require multiple clarification rounds. Expecting straight-line completion leads to premature intervention or abandoned sessions.
- Fix: Implement timeout thresholds and retry limits. Allow agents to self-correct within bounds before escalating to human review.
-
Neglecting Resource and Token Limits
- Explanation: Running multiple agents concurrently multiplies API costs and compute usage. Without rate limiting, a single workflow can exhaust budgets or trigger provider throttling.
- Fix: Configure per-session token caps, implement queue backpressure, and monitor usage metrics at the orchestrator level.
-
Hardcoding Agent-Specific Output Parsing
- Explanation: Relying on regex matches against raw terminal output creates fragile state detection. Minor CLI updates break the entire board.
- Fix: Use structured output formats (JSON, SSE, or protocol buffers) from the harness. If the agent doesnât support it, build a lightweight wrapper that normalizes output before it reaches the orchestrator.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single agent, simple tasks | Terminal multiplexer | Low overhead, no setup required | Minimal (baseline API costs) |
| Sequential pipeline, strict ordering | Task queue with state machine | Guarantees execution order, easy to audit | Low (queue infrastructure) |
| Parallel agents, frequent human review | Visual Kanban orchestrator | Maximizes concurrency while routing interventions efficiently | Medium (orchestrator setup + monitoring) |
| Multi-agent research, unstructured output | Log aggregation + AI summarizer | Handles noisy output, extracts insights without rigid states | High (LLM summarization costs) |
Configuration Template
// orchestrator.config.ts
import { KanbanOrchestrator } from './orchestrator';
import { ClaudeCodeAdapter } from './adapters/claude-code';
export const orchestratorConfig = {
workdirRoot: './agent-workspaces',
harness: new ClaudeCodeAdapter({
model: 'claude-sonnet-4-20250514',
maxTokens: 8192,
timeoutMs: 300000,
interventionMarker: '[INTERVENTION_REQUIRED]',
stateEndpoint: '/api/session/status'
}),
boardColumns: ['backlog', 'in_progress', 'blocked', 'completed', 'failed'],
pollingInterval: 2000,
maxConcurrentSessions: 4,
retryPolicy: {
maxAttempts: 3,
backoffMs: 5000,
retryOnStates: ['failed', 'blocked']
},
logging: {
level: 'info',
outputDir: './logs/orchestrator',
retainDays: 7
}
};
export const board = new KanbanOrchestrator(
orchestratorConfig.harness,
orchestratorConfig.workdirRoot
);
Quick Start Guide
-
Initialize a sandbox repository: Create a temporary git repo or worktree. This isolates agent output from your main codebase.
mkdir -p ~/agent-sandbox && cd ~/agent-sandbox
git init
-
Install and configure the orchestrator: Clone the project, install dependencies, and apply the configuration template above. Ensure the harness adapter points to your preferred agent runtime.
git clone <orchestrator-repo>
cd orchestrator
npm install
cp orchestrator.config.example.ts orchestrator.config.ts
-
Launch the board and spawn a test task: Start the orchestrator process, open the Kanban interface, and create a simple task (e.g., "Add README.md with project description"). Dispatch it to verify state transitions and intervention routing.
npm run start
# Open http://localhost:3000/board
# Create task â Dispatch â Monitor state changes
-
Validate error handling and cleanup: Force a failure by providing an invalid prompt or disconnecting the network. Confirm the card transitions to failed, logs are captured, and the harness session terminates cleanly. Remove the sandbox workspace when testing concludes.
The orchestration layer is no longer optional for teams running parallel AI coding sessions. By decoupling the harness from the coordination surface, adopting explicit state machines, and routing human intervention deliberately, developers can supervise multiple agents without sacrificing clarity or control. Early tools like Agetor prove the concept; production-ready implementations will require rigorous harness abstraction, resource governance, and state reconciliation. Treat the orchestrator as infrastructure, not a convenience, and the workflow scales predictably.