Skip to content

Orchestrator Services

Services in @stoneforge/smithy provide the agent layer: dispatch, assignment, merge, sessions, pools, and steward scheduling.


TaskAssignmentService

Comprehensive task assignment management — assign, unassign, start, complete, handoff.

import { createTaskAssignmentService } from '@stoneforge/smithy';
const assignmentService = createTaskAssignmentService(api, mergeRequestProvider?);

Assignment lifecycle

// Assign
const task = await assignmentService.assignToAgent(taskId, agentId, {
branch: 'custom/branch',
worktree: '.stoneforge/.worktrees/custom',
sessionId: 'session-123',
markAsStarted: true,
});
// Unassign
await assignmentService.unassignTask(taskId);
// Start
await assignmentService.startTask(taskId, 'session-456');
// Complete
await assignmentService.completeTask(taskId);

Handoff

When a worker can’t finish a task, it hands off — the task returns to the pool with context notes.

await assignmentService.handoffTask(taskId, {
sessionId: 'session-123',
message: 'Completed API integration, needs CORS fix',
branch: 'agent/worker-1/abc123-implement-login',
worktree: '.stoneforge/.worktrees/worker-1-implement-login',
});

The message is appended as [AGENT HANDOFF NOTE] to the task description. Handoff history is recorded in metadata.

Workload queries

const tasks = await assignmentService.getAgentTasks(agentId);
const workload = await assignmentService.getAgentWorkload(agentId);
// workload.totalTasks, workload.inProgressCount, workload.awaitingMergeCount
const hasCapacity = await assignmentService.agentHasCapacity(agentId);

Status queries

const unassigned = await assignmentService.getUnassignedTasks();
const inProgress = await assignmentService.getTasksByAssignmentStatus('in_progress');
const awaitingMerge = await assignmentService.getTasksAwaitingMerge();
const assignments = await assignmentService.listAssignments({
agentId,
taskStatus: 'in_progress',
mergeStatus: 'pending',
});

DispatchService

Combines assignment with notification — dispatches tasks to agents and sends them messages.

import { createDispatchService } from '@stoneforge/smithy';
const dispatchService = createDispatchService(api, assignmentService, registry);
// Dispatch a task to an agent
const result = await dispatchService.dispatch(taskId, agentId, {
branch: 'custom/branch',
priority: 10,
restart: true,
markAsStarted: true,
notificationMessage: 'Custom message',
dispatchedBy: senderEntityId,
});
// result.task, result.agent, result.notification, result.channel
// Batch dispatch
const results = await dispatchService.dispatchBatch(
[taskId1, taskId2], agentId, { priority: 5 },
);
// Notify without assignment
await dispatchService.notifyAgent(
agentId,
'restart-signal', // 'task-assignment' | 'task-reassignment' | 'restart-signal'
'Please restart your session',
{ reason: 'configuration change' },
);

DispatchDaemon

The continuously-running background process that coordinates all agent activity.

import { createDispatchDaemon } from '@stoneforge/smithy';
const daemon = createDispatchDaemon(
api, agentRegistry, sessionManager, dispatchService,
worktreeManager, taskAssignment, stewardScheduler,
inboxService, { pollIntervalMs: 5000 },
poolService, settingsService,
);
await daemon.start();
await daemon.stop();
daemon.updateConfig({ pollIntervalMs: 10000 });

Polling loops

LoopPurpose
Orphan RecoveryResume workers with assigned tasks but no active session
Worker AvailabilityMatch idle ephemeral workers with highest-priority ready tasks
Inbox PollingDeliver messages to agents, spawn sessions when needed
Steward TriggersCheck conditions and create workflows from playbooks
Workflow TasksAssign workflow tasks to available stewards
Closed-Unmerged ReconciliationMove CLOSED tasks with unmerged branches back to REVIEW
Plan Auto-CompletionComplete plans when all tasks are closed
Stuck-Merge RecoveryDetect and recover stalled merge operations

Worker dispatch behavior

  1. Find ephemeral workers without an active session
  2. For each: query ready, unassigned tasks via api.ready()
  3. Assign highest-priority task to worker
  4. Send dispatch message to worker’s inbox
  5. Spawn worker in task worktree

Inbox routing

Agent typeActive sessionNo session
Ephemeral workerLeave unreadAccumulate for triage batch
Persistent workerForward as user inputWait
StewardLeave unreadAccumulate for triage batch
DirectorForward as user input (idle debounce)Messages accumulate

MergeStewardService

Automated branch integration — runs tests, merges, and creates fix tasks on failure. All merge operations run in a temporary worktree to avoid corrupting the main repository’s HEAD.

import { createMergeStewardService } from '@stoneforge/smithy';
const mergeSteward = createMergeStewardService(
api, taskAssignmentService, dispatchService, agentRegistry,
{
workspaceRoot: '/project',
mergeStrategy: 'squash', // 'squash' (default) or 'merge'
autoPushAfterMerge: true,
autoCleanup: true,
deleteBranchAfterMerge: true,
testCommand: 'npm test',
testTimeoutMs: 300000,
targetBranch: 'main', // Auto-detected if omitted
},
worktreeManager,
);

MergeStewardService Options

Parameter Type Default Description
workspaceRoot required string Workspace root directory (git repo).
mergeStrategy string squash Merge strategy: 'squash' or 'merge'.
autoPushAfterMerge boolean true Push target branch to remote after merge.
autoCleanup boolean true Remove task worktree after successful merge.
deleteBranchAfterMerge boolean true Delete source branch (local + remote) after merge.
testCommand string npm test Command to run tests.
testTimeoutMs number 300000 Test timeout in milliseconds.
autoMerge boolean true Automatically merge when tests pass.
targetBranch string Branch to merge into (auto-detected).
// Process a single task
const result = await mergeSteward.processTask(taskId, {
skipTests: false,
forceMerge: false,
mergeCommitMessage: 'Custom message',
});
// Process all pending
const batch = await mergeSteward.processAllPending();
// Individual operations
const testResult = await mergeSteward.runTests(taskId);
const mergeResult = await mergeSteward.attemptMerge(taskId, 'Commit message');
await mergeSteward.cleanupAfterMerge(taskId, true);
await mergeSteward.updateMergeStatus(taskId, 'merged');
// Create fix task on failure
const fixTaskId = await mergeSteward.createFixTask(taskId, {
type: 'test_failure', // 'test_failure' | 'merge_conflict' | 'general'
errorDetails: 'Test output...',
affectedFiles: ['src/file.ts'],
});

Merge strategy

Squash merge (default):

  1. Creates a detached HEAD worktree at origin/<target>
  2. Runs git merge --squash and git commit
  3. Pushes HEAD:<target> from the worktree
  4. Cleans up temp worktree, then syncs local target branch

Standard merge: Same flow but creates a merge commit preserving branch history.

On failure

  • Temp merge worktree is always cleaned up (via finally)
  • Task’s worktree and branch are NOT cleaned up (worker needs them for fixes)
  • A fix task is created with tags: ['fix'] and assigned to the original agent

WorkerTaskService

Complete worker-task lifecycle — dispatch, worktree creation, worker spawning, and completion.

import { createWorkerTaskService } from '@stoneforge/smithy';
const workerTaskService = createWorkerTaskService(
api, taskAssignment, agentRegistry, dispatchService,
spawnerService, sessionManager, worktreeManager,
);
// Start a worker on a task (full lifecycle)
const result = await workerTaskService.startWorkerOnTask(taskId, agentId, {
branch: 'custom/branch',
baseBranch: 'main',
additionalPrompt: 'Focus on test coverage',
skipWorktree: false,
});
// result.task, result.agent, result.dispatch, result.worktree, result.session
// Complete a task
const result = await workerTaskService.completeTask(taskId, {
summary: 'Implemented login feature',
commitHash: 'abc123',
runTests: false,
});
// Build task context prompt
const prompt = await workerTaskService.buildTaskContextPrompt(taskId, workerId);
// Cleanup after merge/abandon
await workerTaskService.cleanupTask(taskId, deleteBranch?);

WorktreeManager

Manages git worktrees for agent sessions.

import { createWorktreeManager } from '@stoneforge/smithy';
const worktreeManager = createWorktreeManager({
workspaceRoot: '/project',
worktreeDir: '.stoneforge/.worktrees',
defaultBaseBranch: 'master',
});
// Create read-only worktree (for triage sessions)
const result = await worktreeManager.createReadOnlyWorktree({
agentName: 'worker-alice',
purpose: 'triage',
});
// Detached HEAD at tip of default branch — no new Git branch created
// Path: .stoneforge/.worktrees/{agent-name}-{purpose}/

StewardScheduler

Executes stewards on cron schedules or in response to events.

import { createStewardScheduler } from '@stoneforge/smithy';
const scheduler = createStewardScheduler(agentRegistry, executor, {
maxHistoryPerSteward: 100,
defaultTimeoutMs: 300000,
startImmediately: false,
});
// Lifecycle
await scheduler.start();
await scheduler.stop();
// Steward management
await scheduler.registerSteward(stewardId);
await scheduler.unregisterSteward(stewardId);
await scheduler.refreshSteward(stewardId);
const count = await scheduler.registerAllStewards();
// Manual execution
const result = await scheduler.executeSteward(stewardId, context?);
// Event publishing
const triggered = await scheduler.publishEvent('task_completed', { task: data });
// Status
const jobs = scheduler.getScheduledJobs(stewardId?);
const subs = scheduler.getEventSubscriptions(stewardId?);
const history = scheduler.getExecutionHistory({
stewardId, success: true, limit: 10,
});
const stats = scheduler.getStats();

Events

scheduler.on('execution:started', (entry) => { /* ... */ });
scheduler.on('execution:completed', (entry) => { /* ... */ });
scheduler.on('execution:failed', (entry) => { /* ... */ });
scheduler.on('steward:registered', (stewardId) => { /* ... */ });
scheduler.on('steward:unregistered', (stewardId) => { /* ... */ });

AgentPoolService

Controls concurrent agent execution with pool-based limits.

import { createAgentPoolService } from '@stoneforge/smithy';
const poolService = createAgentPoolService(api, sessionManager, agentRegistry);

Create pool

const pool = await poolService.createPool({
name: 'default',
description: 'Default agent pool',
maxSize: 5,
agentTypes: [
{ role: 'worker', workerMode: 'ephemeral', priority: 100 },
{ role: 'worker', workerMode: 'persistent', priority: 50, maxSlots: 2 },
{ role: 'steward', stewardFocus: 'merge', priority: 80 },
],
enabled: true,
tags: ['production'],
createdBy: userEntityId,
});

Query and manage

const pool = await poolService.getPool(poolId);
const pool = await poolService.getPoolByName('default');
const pools = await poolService.listPools({ enabled: true, hasAvailableSlots: true });
await poolService.updatePool(poolId, { maxSize: 10, enabled: true });
await poolService.deletePool(poolId);

Pool status

const status = await poolService.getPoolStatus(poolId);
// status.activeCount, status.availableSlots, status.activeByType, status.activeAgentIds
await poolService.refreshAllPoolStatus();

Spawn decisions

The dispatch daemon uses these to check constraints before spawning:

const check = await poolService.canSpawn({
role: 'worker',
workerMode: 'ephemeral',
agentId: entityId,
});
if (check.canSpawn) {
// Safe to spawn
} else {
console.log(check.reason);
// e.g., "Pool 'default' is at capacity (5 agents)"
}

Lifecycle tracking

await poolService.onAgentSpawned(agentId);
await poolService.onAgentSessionEnded(agentId);

RoleDefinitionService

Manages reusable role definitions — stored prompts and behavioral configurations for agents.

import { createRoleDefinitionService } from '@stoneforge/smithy';
const roleDefService = createRoleDefinitionService(api);
// Create
const roleDef = await roleDefService.createRoleDefinition({
role: 'worker',
name: 'Frontend Developer',
description: 'Specialized in React and TypeScript',
systemPrompt: 'You are a frontend developer...',
maxConcurrentTasks: 1,
behaviors: {
onStartup: 'Check for existing work.',
onStuck: 'Break down the problem.',
onError: 'Log and notify the director.',
},
workerMode: 'persistent',
tags: ['frontend'],
createdBy: userEntityId,
});
// Query
const roleDef = await roleDefService.getRoleDefinition(roleDefId);
const prompt = await roleDefService.getSystemPrompt(roleDefId);
const defaultDir = await roleDefService.getDefaultRoleDefinition('director');
const all = await roleDefService.listRoleDefinitions({ role: 'worker' });
// Update
await roleDefService.updateRoleDefinition(roleDefId, {
name: 'Senior Frontend Developer',
systemPrompt: 'Updated prompt...',
behaviors: { onError: 'New error handling' },
});
// Delete
await roleDefService.deleteRoleDefinition(roleDefId);

AgentBehaviors

interface AgentBehaviors {
onStartup?: string; // Appended when agent starts
onTaskAssigned?: string; // Appended when task is assigned
onStuck?: string; // Appended when agent appears stuck
onHandoff?: string; // Appended before creating a handoff
onError?: string; // Appended when handling errors
}

DocsStewardService

Scans documentation for issues and applies automated fixes.

import { createDocsStewardService } from '@stoneforge/smithy';
const docsSteward = createDocsStewardService({
workspaceRoot: '/project',
docsDir: 'docs',
sourceDirs: ['packages', 'apps'],
autoPush: true,
});
// Scan all documentation
const result = await docsSteward.scanAll();
// result.issues, result.filesScanned, result.durationMs
// Individual verifications
await docsSteward.verifyFilePaths();
await docsSteward.verifyInternalLinks();
await docsSteward.verifyExports();
await docsSteward.verifyCliCommands();
await docsSteward.verifyTypeFields();
await docsSteward.verifyApiMethods();
// Session lifecycle
const worktree = await docsSteward.createSessionWorktree('d-steward-1');
await docsSteward.commitFix('Fix broken link', ['docs/README.md']);
await docsSteward.mergeAndCleanup(branchName, 'docs: fix issues');
await docsSteward.cleanupSession(worktreePath, branchName);
Issue complexityExamplesAction
lowTypos, broken links, stale pathsSelf-fix
mediumOutdated exports, API changesSelf-fix
highAmbiguous product decisionsEscalate to Director

PluginExecutor

Executes steward plugins — playbooks, scripts, and commands for automated maintenance.

import { createPluginExecutor } from '@stoneforge/smithy';
const executor = createPluginExecutor({
api,
workspaceRoot: '/project',
});
// Execute a single plugin
const result = await executor.execute(plugin);
// Batch execution
const batch = await executor.executeBatch(plugins);
// batch.total, batch.succeeded, batch.failed, batch.allSucceeded
// Validation
const validation = executor.validate(plugin);
// { valid: boolean, errors: string[] }
// Built-in plugins
const plugin = executor.getBuiltIn('gc-ephemeral-tasks');
const names = executor.listBuiltIns();

Built-in plugins: gc-ephemeral-tasks, cleanup-stale-worktrees, gc-ephemeral-workflows, health-check-agents.


SettingsService

Manages workspace-wide runtime settings and agent defaults. These are stored in SQLite (not config.yaml) and managed via the dashboard or API.

import { createSettingsService } from '@stoneforge/smithy';
const settingsService = createSettingsService(storage);
// Get/set individual settings
const value = await settingsService.getSetting('defaultProvider');
await settingsService.setSetting('defaultProvider', 'claude-code');
// Agent defaults (provider, model, executable paths, fallback chain)
const defaults = await settingsService.getAgentDefaults();
// defaults.defaultProvider, defaults.defaultModels, defaults.defaultExecutablePaths, defaults.fallbackChain
await settingsService.setAgentDefaults({
defaultProvider: 'claude-code',
defaultModels: { 'claude-code': 'claude-sonnet-4-20250514' },
defaultExecutablePaths: { 'claude-code': '/usr/local/bin/claude' },
fallbackChain: ['claude', 'opencode'],
});

Agent defaults are used by the Create Agent dialog and the dispatch daemon when spawning agents without explicit provider/model configuration.


RateLimitTracker

In-memory tracker for provider rate limit state. Used by the dispatch daemon to avoid spawning agents on rate-limited providers.

import { createRateLimitTracker } from '@stoneforge/smithy';
const tracker = createRateLimitTracker();
// Mark a provider as rate-limited
tracker.markLimited('claude', resetTime);
// Check if a provider is currently limited
tracker.isLimited('claude'); // boolean
// Find an available executable from a list
const exec = tracker.getAvailableExecutable(['claude', 'opencode']);
// Check if all providers are limited
tracker.isAllLimited(['claude', 'opencode']); // boolean
// Get soonest reset time across all tracked providers
const resetAt = tracker.getSoonestResetTime();
// Clear all rate limit state
tracker.clear();

MergeRequestProvider

Interface for pluggable merge request backends. Stoneforge ships with two implementations:

import {
createLocalMergeProvider,
createGitHubMergeProvider,
} from '@stoneforge/smithy';
// Local provider (default) — no-op, merges happen locally
const local = createLocalMergeProvider();
// GitHub provider — creates PRs via the `gh` CLI
const github = createGitHubMergeProvider();
interface MergeRequestProvider {
createMergeRequest(params: {
title: string;
sourceBranch: string;
targetBranch: string;
description?: string;
}): Promise<{ url?: string }>;
}

The Local provider returns immediately with no side effects. The GitHub provider shells out to gh pr create and returns the PR URL.


Service integration pattern

Services are typically created together and wired up:

import {
createDependencyService,
createBlockedCacheService,
createPriorityService,
createInboxService,
createIdLengthCache,
} from '@stoneforge/quarry';
const depService = createDependencyService(storage);
const blockedCache = createBlockedCacheService(storage);
const priorityService = createPriorityService(storage);
const inboxService = createInboxService(storage);
const idLengthCache = createIdLengthCache(storage);
// Wire up auto-transitions
blockedCache.setStatusTransitionCallback({
onBlock: (elementId, previousStatus) => {
storage.run(
'UPDATE elements SET data = json_set(data, "$.status", ?) WHERE id = ?',
['blocked', elementId],
);
},
onUnblock: (elementId, statusToRestore) => {
storage.run(
'UPDATE elements SET data = json_set(data, "$.status", ?) WHERE id = ?',
[statusToRestore, elementId],
);
},
});