Overview
Merge stewards automate the code review and merge process. When a worker completes a task, a merge steward is automatically dispatched to review the changes, run tests, resolve conflicts, and merge to the main branch.
How It Works
Task Completion
Worker completes a task and runs: sf task complete < task-i d >
This:
Creates a merge request (PR) for the task’s branch
Sets task status to REVIEW
Sets mergeStatus to pending
Automatic Dispatch
The dispatch daemon detects tasks in REVIEW status with mergeStatus: pending and assigns them to an available merge steward. The steward is spawned in an isolated worktree on the task’s branch.
Sync and Review
The steward:
Syncs the branch with the base branch (usually main)
Resolves any merge conflicts
Reviews the changes (git diff origin/main..HEAD)
Verifies acceptance criteria are met
Checks for documentation updates
Test Execution
If tests are configured, the steward runs them in the worktree:
Tests pass : Proceed to merge
Tests fail : Create a fix task and assign it to the original worker
Merge or Reject
If approved :This squash-merges the branch, pushes to remote, cleans up the worktree/branch, and closes the task. If changes needed :sf task reject < task-i d > --reason "Tests failed" --message "Review feedback: ..."
Or hand off with context: sf task handoff < task-i d > --message "Review feedback: ..."
Merge Steward Configuration
Registering a Merge Steward
import { createOrchestratorAPI } from '@stoneforge/smithy' ;
const api = createOrchestratorAPI ( storage );
const steward = await api . registerSteward ({
name: 'm-steward-1' ,
stewardFocus: 'merge' ,
triggers: [
{ type: 'event' , event: 'task_completed' },
],
createdBy: directorId ,
maxConcurrentTasks: 1 , // How many reviews in parallel
});
Merge Service Configuration
Customize merge behavior with the MergeStewardService:
import {
createMergeStewardService ,
createTaskAssignmentService ,
createDispatchService ,
createAgentRegistry ,
} from '@stoneforge/smithy' ;
import { createWorktreeManager } from '@stoneforge/smithy/git' ;
const taskAssignment = createTaskAssignmentService ( api );
const dispatchService = createDispatchService ( api , taskAssignment , agentRegistry );
const agentRegistry = createAgentRegistry ( api );
const worktreeManager = createWorktreeManager ({
workspaceRoot: process . cwd (),
});
const mergeService = createMergeStewardService (
api ,
taskAssignment ,
dispatchService ,
agentRegistry ,
{
workspaceRoot: process . cwd (),
testCommand: 'npm run test:ci' , // Custom test command
testTimeoutMs: 5 * 60 * 1000 , // 5 minutes
autoMerge: true , // Auto-merge on passing tests
autoCleanup: true , // Auto-cleanup worktree after merge
deleteBranchAfterMerge: true , // Delete branch after merge
mergeStrategy: 'squash' , // 'squash' or 'merge'
autoPushAfterMerge: true , // Push to remote after merge
targetBranch: 'main' , // Default: auto-detect
stewardEntityId: stewardId , // For creating fix tasks
},
worktreeManager
);
Merge Strategies
Squash Merge (Default)
Merge Commit
Combines all commits from the task branch into a single commit on the target branch: Before: After:
main: A---B main: A---B---C'
\
feature: C1-C2-C3
Commit message: "Task Title (task-id)"
Pros :
Clean, linear history
One commit per task for easy reversion
Hides messy intermediate commits
Cons :
Loses granular commit history
Preserves all commits from the task branch: Before: After:
main: A---B main: A---B-----M
\ \
feature: C1-C2-C3 feature: C1-C2-C3
Commit message: "Merge branch 'feature' (Task: task-id)"
Pros :
Preserves full commit history
Shows evolution of the work
Cons :
Cluttered history with many small commits
Harder to revert (need to revert merge commit)
Review Criteria
Merge stewards verify the following before approving:
1. Acceptance Criteria Verification
Read the task description
Note all acceptance criteria listed in the task.
Cross-reference against changes
git diff origin/main..HEAD
For each acceptance criterion, confirm the diff contains changes that fulfill it.
Reject if criteria unmet
If any criterion is NOT satisfied: sf task reject < task-i d > --reason "Incomplete" \
--message "Missing: [specific criterion]. The task acceptance criteria state: ..."
2. Code Quality Checks
Style compliance : Follows project conventions
No obvious bugs : Logic errors, null pointer exceptions, etc.
Security : No hardcoded secrets, SQL injection, XSS vulnerabilities
Tests : All tests pass
3. Documentation Updates
If the PR changes documented behavior (APIs, config, CLI):
# Search for affected documentation
sf document search "keyword from changed area"
# Check if docs were updated
git diff origin/main..HEAD -- '*.md' 'docs/**'
If relevant docs exist but weren’t updated, include in review feedback:
sf task reject < task-i d > --reason "Documentation outdated" \
--message "Please update the API reference doc (el-doc-xxx) to reflect the new endpoint signature."
Conflict Resolution
Merge stewards resolve ALL conflicts themselves, including complex logic conflicts.
Sync Before Review
The daemon syncs the branch with the target branch before spawning the steward:
# Daemon runs this before dispatch
sf task sync < task-i d >
This:
Fetches latest origin/main
Merges origin/main into the task’s branch
Detects merge conflicts
Records sync result in task metadata
If conflicts are detected, the steward is spawned into a worktree with the conflicts already present (uncommitted).
Resolving Conflicts
The steward resolves conflicts and commits the resolution:
# See conflicted files
git status
# Resolve each file
vim src/file-with-conflict.ts
# Stage resolved files
git add .
# Commit the resolution
git commit -m "Resolve merge conflicts with main"
# Continue with review and merge
sf task merge < task-i d >
Common Conflict Patterns
Import ordering conflicts
Strategy : Keep both sets of imports, remove duplicates, sort alphabetically// Before (conflict markers)
<<<<<<< HEAD
import { foo } from './foo' ;
import { bar } from './bar' ;
=======
import { baz } from './baz' ;
import { foo } from './foo' ;
>>>>>>> origin / main
// After resolution
import { bar } from './bar' ;
import { baz } from './baz' ;
import { foo } from './foo' ;
Strategy : Delete and regeneraterm package-lock.json
npm install
git add package-lock.json
git commit -m "Regenerate package-lock.json"
Logic conflicts (both sides modify same function)
Strategy : Understand both changes, merge intent correctly// Branch A adds validation
function createUser ( data : UserData ) {
validate ( data );
return db . users . create ( data );
}
// Branch B adds logging
function createUser ( data : UserData ) {
logger . info ( 'Creating user' , data );
return db . users . create ( data );
}
// Resolution: combine both changes
function createUser ( data : UserData ) {
logger . info ( 'Creating user' , data );
validate ( data );
return db . users . create ( data );
}
When to Escalate
Only escalate conflicts in these cases:
Situation Action Multiple valid approaches, needs product decision Flag for human operator Resolution reveals task is incomplete (more work needed) Hand off: “Conflict resolution shows additional work needed: [details]“ Context window exhaustion Hand off with context for next steward
Test Execution
If testCommand is configured, stewards run tests before merging:
const testResult = await mergeService . runTests ( taskId );
if ( ! testResult . passed ) {
// Create fix task
const fixTaskId = await mergeService . createFixTask ( taskId , {
type: 'test_failure' ,
errorDetails: testResult . output . substring ( 0 , 2000 ),
});
// Update merge status
await mergeService . updateMergeStatus ( taskId , 'test_failed' , {
testResult: testResult . testResult ,
});
}
Fix Task Creation
When tests fail or conflicts can’t be resolved, stewards create fix tasks:
const fixTaskId = await mergeService . createFixTask ( originalTaskId , {
type: 'test_failure' , // or 'merge_conflict', 'general'
errorDetails: 'Test suite "auth" failed: Expected 200, got 401' ,
affectedFiles: [ 'src/auth/login.test.ts' ],
});
The fix task:
Has the same priority as the original task
Is assigned to the same worker (if available)
Links to the original task via metadata.originalTaskId
Includes detailed error information
Fix tasks prevent duplicate creation. If a fix task already exists for the same original task and fix type, the steward returns the existing fix task ID.
Merge Status Transitions
Tasks progress through merge statuses:
Optimistic Locking
To prevent race conditions with multiple stewards, status transitions use optimistic locking:
try {
// Only transition pending → testing if still pending
await mergeService . updateMergeStatus ( taskId , 'testing' , undefined , 'pending' );
} catch ( error ) {
if ( error instanceof MergeStatusConflictError ) {
// Another steward already claimed this task
console . log ( 'Task already claimed, skipping' );
return { status: 'skipped' , merged: false };
}
throw error ;
}
Programmatic Usage
Process a Single Task
const result = await mergeService . processTask ( taskId , {
skipTests: false , // Run tests
forceMerge: false , // Only merge if tests pass
mergeCommitMessage: 'Custom merge message' ,
performedBy: stewardEntityId ,
});
if ( result . merged ) {
console . log ( 'Merged successfully:' , result . mergeCommitHash );
} else if ( result . status === 'test_failed' ) {
console . log ( 'Tests failed, fix task created:' , result . fixTaskId );
} else if ( result . status === 'conflict' ) {
console . log ( 'Merge conflict, fix task created:' , result . fixTaskId );
}
Batch Process All Pending
const batchResult = await mergeService . processAllPending ({
skipTests: false ,
});
console . log ( `Processed ${ batchResult . totalProcessed } tasks in ${ batchResult . durationMs } ms` );
console . log ( `Merged: ${ batchResult . mergedCount } ` );
console . log ( `Test failures: ${ batchResult . testFailedCount } ` );
console . log ( `Conflicts: ${ batchResult . conflictCount } ` );
console . log ( `Errors: ${ batchResult . errorCount } ` );
Manual Merge Operations
// Run tests only
const testResult = await mergeService . runTests ( taskId );
// Attempt merge without tests
const mergeResult = await mergeService . attemptMerge ( taskId , 'Custom commit message' );
// Update merge status manually
await mergeService . updateMergeStatus ( taskId , 'merged' , {
testResult: { passed: true , completedAt: createTimestamp () },
});
// Cleanup after manual merge
await mergeService . cleanupAfterMerge ( taskId , true /* deleteBranch */ );
Handling Edge Cases
No Commits to Merge
If the task’s branch has no commits beyond the merge base (already merged or no work done):
# Verify no commits
git log origin/main..HEAD
# (empty output)
# Close with not_applicable
sf task merge-status < task-i d > not_applicable
This:
Closes the task without merging
Unblocks dependent tasks
Cleans up the branch/worktree
Already Merged Branches
If the branch was manually merged outside Stoneforge:
# Check if branch is already merged
git branch --contains HEAD --list main
# Output: main
# Mark as merged
sf task merge-status < task-i d > merged
Pre-existing Issues
If the steward discovers issues NOT caused by the PR (e.g., failing tests on main):
# Report to Director
sf message send --from < Steward I D > --to < Director I D > \
--content "Found pre-existing issue during review of <task-id>: Test 'auth.login' fails on main. Please create a task to fix this."
# Continue with PR review (don't block for pre-existing issues)
Best Practices
Resolve All Conflicts Stewards should resolve all conflicts, including complex logic conflicts. Only escalate when truly ambiguous or incomplete.
Verify Acceptance Criteria Always cross-reference the task description’s acceptance criteria against the changes. Don’t assume passing tests means all criteria are met.
Check Documentation Search for relevant docs and verify they were updated. Documentation drift causes confusion for future agents.
Create Specific Fix Tasks When creating fix tasks, include detailed error messages, affected files, and exact criteria that are unmet.
Git Worktrees How worktrees enable parallel agent work
Steward Types Overview of all steward focuses
Custom Agents Create specialized merge reviewers