Skip to main content
The reconciler is the system’s health monitor. It periodically runs build and test checks on the main branch, and when failures are detected, it generates targeted fix tasks.

Core Responsibilities

  1. Periodic health sweeps: Run build + tests on a schedule (default: 5 minutes, adaptive)
  2. Failure detection: Identify compiler errors, test failures, and merge conflict markers
  3. Fix task generation: Call LLM to produce targeted fix tasks from error output
  4. Adaptive intervals: Speed up when errors detected, slow down after consecutive green sweeps

Implementation

Location: packages/orchestrator/src/reconciler.ts

Class Structure

export class Reconciler {
  private config: OrchestratorConfig;
  private reconcilerConfig: ReconcilerConfig;
  private completeLLM: ReconcilerCompleteLLM;
  private runCommand: ReconcilerRunCommand;
  private mergeQueue: MergeQueue;
  private monitor: Monitor;
  private systemPrompt: string;
  private targetRepoPath: string;
  private tracer: Tracer | null = null;
  
  private timer: ReturnType<typeof setInterval> | null;
  private running: boolean;
  private fixCounter: number;
  
  // Adaptive interval state
  private consecutiveGreenSweeps: number;
  private readonly minIntervalMs: number;
  private readonly maxIntervalMs: number;
  private currentIntervalMs: number;
  
  // Deduplication
  private recentFixScopes: Set<string> = new Set();

  constructor(
    config: OrchestratorConfig,
    reconcilerConfig: ReconcilerConfig,
    taskQueue: TaskQueue,
    mergeQueue: MergeQueue,
    monitor: Monitor,
    systemPrompt: string,
    deps?: Partial<ReconcilerDeps>,
  ) { /* ... */ }

  async sweep(): Promise<SweepResult> { /* ... */ }
}

Sweep Execution

async sweep(): Promise<SweepResult> {
  logger.info("Starting reconciler sweep");
  const sweepSpan = this.tracer?.startSpan("reconciler.sweep", { agentId: "reconciler" });

  const mergeCountBefore = this.mergeQueue.getMergeStats().totalMerged;

  // 1. Run tsc --noEmit
  const tscResult = await this.runCommand("npx", ["tsc", "--noEmit"], this.targetRepoPath);
  const buildOutput = tscResult.stdout + tscResult.stderr;
  const buildNotConfigured = /no inputs were found|could not find a valid tsconfig/i.test(buildOutput);
  const buildOk = buildNotConfigured || (tscResult.code === 0 && !tscResult.stderr?.includes("error TS"));

  // 2. Run npm run build
  const buildRunResult = await this.runCommand(
    "npm",
    ["run", "build", "--if-present"],
    this.targetRepoPath,
  );
  const buildRunOutput = buildRunResult.stdout + buildRunResult.stderr;
  const buildRunOk = buildRunResult.code === 0;

  // 3. Run npm test
  const testResult = await this.runCommand("npm", ["test"], this.targetRepoPath);
  const testOutput = testResult.stdout + testResult.stderr;
  const testNotConfigured = /Missing script|no test specified/i.test(testOutput);
  const testsOk = testNotConfigured || (testResult.code === 0 && !testResult.stderr?.includes("FAIL"));

  // 4. Scan for merge conflict markers
  const conflictResult = await this.runCommand(
    "git",
    ["grep", "-rl", "<<<<<<<", "--", "*.ts", "*.tsx", "*.js", "*.json"],
    this.targetRepoPath,
  );
  const conflictFiles = conflictResult.stdout.trim().split("\n").filter(Boolean);
  const hasConflictMarkers = conflictFiles.length > 0;

  logger.info("Sweep check results", {
    buildOk,
    buildRunOk,
    testsOk,
    hasConflictMarkers,
    conflictFileCount: conflictFiles.length,
  });

  // 5. If all green, return early
  if (buildOk && buildRunOk && testsOk && !hasConflictMarkers) {
    this.consecutiveGreenSweeps++;
    this.recentFixScopes.clear();
    if (this.consecutiveGreenSweeps >= 3) {
      this.adjustInterval(this.maxIntervalMs);  // Slow down
    }
    return {
      buildOk: true,
      testsOk: true,
      hasConflictMarkers: false,
      buildOutput: "",
      testOutput: "",
      conflictFiles: [],
      fixTasks: [],
    };
  }

  // 6. Check if merges occurred during sweep (stale results)
  const mergeCountAfter = this.mergeQueue.getMergeStats().totalMerged;
  if (mergeCountAfter > mergeCountBefore) {
    logger.info("Merges occurred during sweep — discarding stale results");
    return {
      buildOk,
      testsOk,
      hasConflictMarkers,
      buildOutput: "",
      testOutput: "",
      conflictFiles: [],
      fixTasks: [],
    };
  }

  // 7. Build LLM prompt from error output
  const gitResult = await this.runCommand(
    "git",
    ["log", "--oneline", "-10"],
    this.targetRepoPath,
  );
  const recentCommits = gitResult.stdout.trim();

  let userMessage = "";

  if (hasConflictMarkers) {
    userMessage += `## Merge Conflict Markers Found\nFiles with unresolved conflict markers:\n`;
    for (const f of conflictFiles.slice(0, 20)) {
      userMessage += `- ${f}\n`;
    }
  }

  if (!buildOk) {
    userMessage += `## Build Output (tsc --noEmit)\n\`\`\`\n${buildOutput.slice(0, 8000)}\n\`\`\`\n\n`;
  }

  if (!buildRunOk) {
    userMessage += `## Build Output (npm run build)\n\`\`\`\n${buildRunOutput.slice(0, 8000)}\n\`\`\`\n\n`;
  }

  if (!testsOk) {
    userMessage += `## Test Output (npm test)\n\`\`\`\n${testOutput.slice(0, 8000)}\n\`\`\`\n\n`;
  }

  userMessage += `## Recent Commits\n${recentCommits}\n\n`;

  if (this.recentFixScopes.size > 0) {
    userMessage += `## Pending Fix Scopes\nFix tasks already target these files — do NOT create duplicates: ${[...this.recentFixScopes].join(", ")}\n\n`;
  }

  const messages: LLMMessage[] = [
    { role: "system", content: this.systemPrompt },
    { role: "user", content: userMessage },
  ];

  // 8. Call LLM to generate fix tasks
  let rawTasks: ReturnType<typeof parseLLMTaskArray>;
  try {
    const response = await this.completeLLM(messages, undefined, sweepSpan);
    this.monitor.recordTokenUsage(response.usage.totalTokens);
    rawTasks = parseLLMTaskArray(response.content);
  } catch (llmError) {
    logger.warn("LLM unreachable for reconciler — skipping fix task generation");
    return {
      buildOk,
      testsOk,
      hasConflictMarkers,
      buildOutput: buildOk ? "" : buildOutput.slice(0, 8000),
      testOutput: testsOk ? "" : testOutput.slice(0, 8000),
      conflictFiles,
      fixTasks: [],
    };
  }

  // 9. Cap at maxFixTasks and deduplicate by scope
  const capped = rawTasks.slice(0, this.reconcilerConfig.maxFixTasks);
  const tasks: Task[] = [];
  for (const raw of capped) {
    const scope = raw.scope || [];
    const allScopesCovered = scope.length > 0 && scope.every((f) => this.recentFixScopes.has(f));
    if (allScopesCovered) {
      logger.debug("Skipping duplicate fix task (scope already covered)", { scope });
      continue;
    }

    this.fixCounter++;
    const id = raw.id || `fix-${String(this.fixCounter).padStart(3, "0")}`;
    tasks.push({
      id,
      description: raw.description,
      scope,
      acceptance: raw.acceptance || "tsc --noEmit returns 0 and npm test returns 0",
      branch: raw.branch || `${this.config.git.branchPrefix}${id}-${slugifyForBranch(raw.description)}`,
      status: "pending",
      createdAt: this.now(),
      priority: 1,  // Fixes are high priority
    });

    for (const f of scope) {
      this.recentFixScopes.add(f);
    }
  }

  logger.info(`Created ${tasks.length} fix tasks`, { taskIds: tasks.map((t) => t.id) });

  // 10. Adaptive interval adjustment
  this.consecutiveGreenSweeps = 0;
  this.adjustInterval(this.minIntervalMs);  // Speed up

  return {
    buildOk,
    testsOk,
    hasConflictMarkers,
    buildOutput: buildOk ? "" : buildOutput.slice(0, 8000),
    testOutput: testsOk ? "" : testOutput.slice(0, 8000),
    conflictFiles,
    fixTasks: tasks,
  };
}

Adaptive Interval Adjustment

private adjustInterval(targetMs: number): void {
  if (this.currentIntervalMs === targetMs) return;
  this.currentIntervalMs = targetMs;

  if (this.timer) {
    clearInterval(this.timer);
    this.timer = setInterval(async () => {
      try {
        const result = await this.sweep();
        for (const cb of this.sweepCompleteCallbacks) {
          cb(result);
        }
      } catch (error) {
        // Error handling
      }
    }, this.currentIntervalMs);
  }

  logger.info("Adjusted sweep interval", {
    newIntervalMs: this.currentIntervalMs,
    consecutiveGreen: this.consecutiveGreenSweeps,
  });
}
Adaptive behavior:
  • Start: maxIntervalMs (5 minutes)
  • On error detection: Drop to minIntervalMs (1 minute)
  • After 3 consecutive green sweeps: Return to maxIntervalMs

Prompt Engineering

Location: prompts/reconciler.md

1. Core Identity

You keep the main branch green. You analyze build and test failures, then produce targeted 
fix tasks. You do not write code. You run periodically as a health check.

2. Context Received

- **Merge conflict markers** — files containing unresolved `<<<<<<<` / `=======` / `>>>>>>>` markers
- **Build output** — Full build output from `npm run build`
- **Compiler output** — TypeScript compiler errors from `tsc --noEmit`
- **Test output** — Test failures from `npm test`
- **Recent commit log** — Last 10-20 commits

3. Workflow

1. **Conflict markers first.** If any files contain `<<<<<<<` markers, these are the highest priority.
2. Parse build output (`npm run build`). Build failures often reveal integration issues.
3. Parse compiler output (`tsc --noEmit`). Extract exact error messages with `file:line` references.
4. Parse test output (`npm test`). Identify failing tests and the assertion or runtime error.
5. Classify root cause: type errors from merges, missing imports, interface mismatches, broken tests.
6. Group related errors sharing a single root cause into one task.
7. Identify the minimal set of files (max 3) needed to fix each issue.
8. Emit JSON array of fix tasks.

4. Non-Negotiable Constraints

- **NEVER create more than 5 fix tasks per sweep.** Focus on the most critical errors first.
- **NEVER create tasks for warnings, linting issues, or non-error diagnostics.** Errors only.
- **NEVER create one task per compiler error line.** Find the root cause and fix it once.
- **NEVER create duplicate tasks** for errors that already have pending fix tasks.
- **NEVER add features or enhancements.** Fix only what is broken.
- **ALWAYS cite the exact error message** in each task description.
- **ALWAYS use verifiable acceptance criteria**: `tsc --noEmit returns 0` and/or `npm test returns 0`.
- **ALWAYS prefix fix task IDs with `fix-`.**
- **ALWAYS set priority to 1** — fixes land before feature work.
- **ALWAYS scope to specific files** from compiler/test output (max 3 files per task).
- If build passes and tests pass, output `[]`.
- Output ONLY the JSON array. No explanations, no surrounding text.

5. Error Grouping

- **Same file, same cause** — 5 errors from a renamed type → 1 task
- **Import chain** — File A can't find export from File B → 1 task scoped to both files
- **Interface mismatch** — Type changed in A, callers in B and C break → 1 task for the smaller fix

6. Error Priority

When multiple error types coexist, fix in this order:

1. **Merge conflict markers** — Nothing works until these are resolved
2. **Build failures** — The project cannot be used if it doesn't build
3. **Compiler errors** — Type safety violations compound quickly
4. **Test failures** — Functional regressions need targeted fixes

NEVER create tasks for multiple priority levels in the same sweep. Fix the highest-priority 
category first. Lower-priority errors often resolve as side effects.

7. Examples

Merge Conflict Markers

[{
  "id": "fix-001",
  "description": "Resolve merge conflict markers in renderer.ts and camera.ts. Open each file, find <<<<<<< / ======= / >>>>>>> blocks, resolve by keeping the correct version based on surrounding code context. Remove all conflict markers.",
  "scope": ["src/engine/renderer.ts", "src/engine/camera.ts"],
  "acceptance": "No <<<<<<< markers in either file. npm run build returns 0 and tsc --noEmit returns 0.",
  "branch": "worker/fix-001",
  "priority": 1
}]

Type Error

[{
  "id": "fix-001",
  "description": "Fix type error in renderer.ts line 42: TS2345 Argument of type 'string' is not assignable to parameter of type 'number'. The setViewport call passes a string width but expects number.",
  "scope": ["src/engine/renderer.ts"],
  "acceptance": "npm run build returns 0 and tsc --noEmit returns 0 with no errors in renderer.ts",
  "branch": "worker/fix-001",
  "priority": 1
}]

Missing Export

[{
  "id": "fix-002",
  "description": "Fix missing export: chunk.ts line 3 imports 'RenderContext' from renderer.ts but it is not exported (TS2305). Either export the type from renderer.ts or update the import in chunk.ts.",
  "scope": ["src/world/chunk.ts", "src/engine/renderer.ts"],
  "acceptance": "tsc --noEmit returns 0, no import errors in chunk.ts",
  "branch": "worker/fix-002",
  "priority": 1
}]

Configuration

export interface ReconcilerConfig {
  intervalMs: number;     // How often to sweep (ms)
  maxFixTasks: number;    // Max fix tasks created per sweep
}

export const DEFAULT_RECONCILER_CONFIG: ReconcilerConfig = {
  intervalMs: 300_000,  // 5 minutes
  maxFixTasks: 5,
};

Adaptive Interval Constants

this.minIntervalMs = Math.min(60_000, reconcilerConfig.intervalMs);  // 1 minute
this.maxIntervalMs = reconcilerConfig.intervalMs;                    // 5 minutes
this.currentIntervalMs = reconcilerConfig.intervalMs;                // Start at max

Integration with Orchestrator

The reconciler is started by the orchestrator and its results are fed back to the planner:
// Start reconciler
this.reconciler.start();

// Hook sweep results into planner
this.reconciler.onSweepComplete((result) => {
  // Update planner with build/test health
  this.planner.setLastSweepResult(result);

  // Inject fix tasks directly into planner
  for (const fixTask of result.fixTasks) {
    this.planner.injectTask(fixTask);
  }
});
Why injectTask() instead of normal planning:
  • Fix tasks are reactive (created from errors), not proactive (planned upfront)
  • They bypass the LLM planning cycle for faster response
  • They have priority 1 (land before feature work)

Scope Deduplication

Reconciler tracks recently created fix scopes to prevent duplicate tasks:
private recentFixScopes: Set<string> = new Set();

// During fix task creation:
for (const raw of capped) {
  const scope = raw.scope || [];
  const allScopesCovered = scope.length > 0 && scope.every((f) => this.recentFixScopes.has(f));
  if (allScopesCovered) {
    logger.debug("Skipping duplicate fix task (scope already covered)", { scope });
    continue;
  }

  // Create task and record scope
  tasks.push({ /* ... */ });
  for (const f of scope) {
    this.recentFixScopes.add(f);
  }
}

// Clear on green sweep
if (buildOk && testsOk && !hasConflictMarkers) {
  this.recentFixScopes.clear();
}
Prompt includes pending scopes:
## Pending Fix Scopes
Fix tasks already target these files — do NOT create duplicates: src/auth/token.ts, src/db/schema.ts

Sweep Result Interface

export interface SweepResult {
  buildOk: boolean;
  testsOk: boolean;
  hasConflictMarkers: boolean;
  buildOutput: string;         // Empty if buildOk
  testOutput: string;          // Empty if testsOk
  conflictFiles: string[];     // Empty if no conflicts
  fixTasks: Task[];            // Empty if all green
}
This result is:
  1. Passed to planner.setLastSweepResult(result) so the planner sees build health in follow-up messages
  2. Used by orchestrator to inject fix tasks via planner.injectTask()
  3. Logged for observability

Best Practices

Don’t create one task per error line: Bad:
[
  { "id": "fix-001", "description": "Fix error in file.ts line 10" },
  { "id": "fix-002", "description": "Fix error in file.ts line 15" },
  { "id": "fix-003", "description": "Fix error in file.ts line 22" }
]
Good:
[
  { 
    "id": "fix-001", 
    "description": "Fix type errors in file.ts (lines 10, 15, 22): All caused by renamed 'User' type to 'UserProfile'. Update all references.",
    "scope": ["file.ts"]
  }
]

2. Prioritize Conflict Markers

Conflict markers block all work:
if (hasConflictMarkers) {
  // Only create conflict resolution tasks — ignore other errors
  // because they'll likely resolve after conflicts are fixed
}

3. Cite Exact Errors

Include file:line references and error codes:
{
  "description": "Fix type error in renderer.ts line 42: TS2345 Argument of type 'string' is not assignable to parameter of type 'number'. The setViewport call passes a string width but expects number."
}

4. Use Verifiable Acceptance

Always specify build/test exit codes:
{
  "acceptance": "tsc --noEmit returns 0 and npm test returns 0"
}

Next Steps

Build docs developers (and LLMs) love