Core Responsibilities
- Periodic health sweeps: Run build + tests on a schedule (default: 5 minutes, adaptive)
- Failure detection: Identify compiler errors, test failures, and merge conflict markers
- Fix task generation: Call LLM to produce targeted fix tasks from error output
- 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,
});
}
- 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);
}
});
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();
}
## 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
}
- Passed to
planner.setLastSweepResult(result)so the planner sees build health in follow-up messages - Used by orchestrator to inject fix tasks via
planner.injectTask() - Logged for observability
Best Practices
1. Group Related Errors
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" }
]
[
{
"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
- Root Planner Agent — See how fix tasks are injected into planning
- Worker Agent — Understand how fix tasks are executed
- Core Concepts — Learn about the agent system