Skip to main content

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

1

Task Completion

Worker completes a task and runs:
sf task complete <task-id>
This:
  • Creates a merge request (PR) for the task’s branch
  • Sets task status to REVIEW
  • Sets mergeStatus to pending
2

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.
3

Sync and Review

The steward:
  1. Syncs the branch with the base branch (usually main)
  2. Resolves any merge conflicts
  3. Reviews the changes (git diff origin/main..HEAD)
  4. Verifies acceptance criteria are met
  5. Checks for documentation updates
4

Test Execution

If tests are configured, the steward runs them in the worktree:
npm test
  • Tests pass: Proceed to merge
  • Tests fail: Create a fix task and assign it to the original worker
5

Merge or Reject

If approved:
sf task merge <task-id>
This squash-merges the branch, pushes to remote, cleans up the worktree/branch, and closes the task.If changes needed:
sf task reject <task-id> --reason "Tests failed" --message "Review feedback: ..."
Or hand off with context:
sf task handoff <task-id> --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

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

Review Criteria

Merge stewards verify the following before approving:

1. Acceptance Criteria Verification

1

Read the task description

sf show <task-id>
Note all acceptance criteria listed in the task.
2

Cross-reference against changes

git diff origin/main..HEAD
For each acceptance criterion, confirm the diff contains changes that fulfill it.
3

Reject if criteria unmet

If any criterion is NOT satisfied:
sf task reject <task-id> --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-id> --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-id>
This:
  1. Fetches latest origin/main
  2. Merges origin/main into the task’s branch
  3. Detects merge conflicts
  4. 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-id>

Common Conflict Patterns

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 regenerate
rm package-lock.json
npm install
git add package-lock.json
git commit -m "Regenerate package-lock.json"
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:
SituationAction
Multiple valid approaches, needs product decisionFlag for human operator
Resolution reveals task is incomplete (more work needed)Hand off: “Conflict resolution shows additional work needed: [details]“
Context window exhaustionHand 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-id> 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-id> 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 ID> --to <Director ID> \
  --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

Build docs developers (and LLMs) love