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
// Assignconst task = await assignmentService.assignToAgent(taskId, agentId, { branch: 'custom/branch', worktree: '.stoneforge/.worktrees/custom', sessionId: 'session-123', markAsStarted: true,});
// Unassignawait assignmentService.unassignTask(taskId);
// Startawait assignmentService.startTask(taskId, 'session-456');
// Completeawait 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.awaitingMergeCountconst 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 agentconst 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 dispatchconst results = await dispatchService.dispatchBatch( [taskId1, taskId2], agentId, { priority: 5 },);
// Notify without assignmentawait 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
| Loop | Purpose |
|---|---|
| Orphan Recovery | Resume workers with assigned tasks but no active session |
| Worker Availability | Match idle ephemeral workers with highest-priority ready tasks |
| Inbox Polling | Deliver messages to agents, spawn sessions when needed |
| Steward Triggers | Check conditions and create workflows from playbooks |
| Workflow Tasks | Assign workflow tasks to available stewards |
| Closed-Unmerged Reconciliation | Move CLOSED tasks with unmerged branches back to REVIEW |
| Plan Auto-Completion | Complete plans when all tasks are closed |
| Stuck-Merge Recovery | Detect and recover stalled merge operations |
Worker dispatch behavior
- Find ephemeral workers without an active session
- For each: query ready, unassigned tasks via
api.ready() - Assign highest-priority task to worker
- Send dispatch message to worker’s inbox
- Spawn worker in task worktree
Inbox routing
| Agent type | Active session | No session |
|---|---|---|
| Ephemeral worker | Leave unread | Accumulate for triage batch |
| Persistent worker | Forward as user input | Wait |
| Steward | Leave unread | Accumulate for triage batch |
| Director | Forward 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 taskconst result = await mergeSteward.processTask(taskId, { skipTests: false, forceMerge: false, mergeCommitMessage: 'Custom message',});
// Process all pendingconst batch = await mergeSteward.processAllPending();
// Individual operationsconst 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 failureconst 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):
- Creates a detached HEAD worktree at
origin/<target> - Runs
git merge --squashandgit commit - Pushes
HEAD:<target>from the worktree - 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 taskconst result = await workerTaskService.completeTask(taskId, { summary: 'Implemented login feature', commitHash: 'abc123', runTests: false,});
// Build task context promptconst prompt = await workerTaskService.buildTaskContextPrompt(taskId, workerId);
// Cleanup after merge/abandonawait 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,});// Lifecycleawait scheduler.start();await scheduler.stop();
// Steward managementawait scheduler.registerSteward(stewardId);await scheduler.unregisterSteward(stewardId);await scheduler.refreshSteward(stewardId);const count = await scheduler.registerAllStewards();
// Manual executionconst result = await scheduler.executeSteward(stewardId, context?);
// Event publishingconst triggered = await scheduler.publishEvent('task_completed', { task: data });
// Statusconst 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);// Createconst 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,});
// Queryconst roleDef = await roleDefService.getRoleDefinition(roleDefId);const prompt = await roleDefService.getSystemPrompt(roleDefId);const defaultDir = await roleDefService.getDefaultRoleDefinition('director');const all = await roleDefService.listRoleDefinitions({ role: 'worker' });
// Updateawait roleDefService.updateRoleDefinition(roleDefId, { name: 'Senior Frontend Developer', systemPrompt: 'Updated prompt...', behaviors: { onError: 'New error handling' },});
// Deleteawait 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 documentationconst result = await docsSteward.scanAll();// result.issues, result.filesScanned, result.durationMs
// Individual verificationsawait docsSteward.verifyFilePaths();await docsSteward.verifyInternalLinks();await docsSteward.verifyExports();await docsSteward.verifyCliCommands();await docsSteward.verifyTypeFields();await docsSteward.verifyApiMethods();
// Session lifecycleconst 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 complexity | Examples | Action |
|---|---|---|
low | Typos, broken links, stale paths | Self-fix |
medium | Outdated exports, API changes | Self-fix |
high | Ambiguous product decisions | Escalate 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 pluginconst result = await executor.execute(plugin);
// Batch executionconst batch = await executor.executeBatch(plugins);// batch.total, batch.succeeded, batch.failed, batch.allSucceeded
// Validationconst validation = executor.validate(plugin);// { valid: boolean, errors: string[] }
// Built-in pluginsconst 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 settingsconst 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-limitedtracker.markLimited('claude', resetTime);
// Check if a provider is currently limitedtracker.isLimited('claude'); // boolean
// Find an available executable from a listconst exec = tracker.getAvailableExecutable(['claude', 'opencode']);
// Check if all providers are limitedtracker.isAllLimited(['claude', 'opencode']); // boolean
// Get soonest reset time across all tracked providersconst resetAt = tracker.getSoonestResetTime();
// Clear all rate limit statetracker.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 locallyconst local = createLocalMergeProvider();
// GitHub provider — creates PRs via the `gh` CLIconst 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-transitionsblockedCache.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], ); },});