fix: Ensure phase branches exist before task dispatch

Task dispatch computed baseBranch as the phase branch name but never
ensured it existed in the git clone. When phases weren't dispatched
through the PhaseDispatchManager (which creates branches), the
git worktree add failed with "fatal: invalid reference".

Now DefaultDispatchManager calls ensureBranch for both the initiative
and phase branches before spawning, matching what PhaseDispatchManager
already does.
This commit is contained in:
Lukas May
2026-03-05 20:49:21 +01:00
parent d81e0864f7
commit 50aea7e6f1
2 changed files with 32 additions and 0 deletions

View File

@@ -212,6 +212,9 @@ export async function createContainer(options?: ContainerOptions): Promise<Conta
repos.phaseRepository,
repos.agentRepository,
repos.pageRepository,
repos.projectRepository,
branchManager,
workspaceRoot,
);
const phaseDispatchManager = new DefaultPhaseDispatchManager(
repos.phaseRepository,

View File

@@ -21,10 +21,13 @@ import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { PageRepository } from '../db/repositories/page-repository.js';
import type { ProjectRepository } from '../db/repositories/project-repository.js';
import type { Task, Phase } from '../db/schema.js';
import type { PageForSerialization } from '../agent/content-serializer.js';
import type { BranchManager } from '../git/branch-manager.js';
import type { DispatchManager, QueuedTask, DispatchResult } from './types.js';
import { phaseBranchName, taskBranchName, isPlanningCategory, generateInitiativeBranch } from '../git/branch-naming.js';
import { ensureProjectClone } from '../git/project-clones.js';
import { buildExecutePrompt } from '../agent/prompts/index.js';
import { createModuleLogger } from '../logger/index.js';
@@ -68,6 +71,9 @@ export class DefaultDispatchManager implements DispatchManager {
private phaseRepository?: PhaseRepository,
private agentRepository?: AgentRepository,
private pageRepository?: PageRepository,
private projectRepository?: ProjectRepository,
private branchManager?: BranchManager,
private workspaceRoot?: string,
) {}
/**
@@ -332,6 +338,29 @@ export class DefaultDispatchManager implements DispatchManager {
} catch {
// Non-fatal: fall back to default branching
}
// Ensure branches exist in project clones before spawning worktrees
if (baseBranch && this.projectRepository && this.branchManager && this.workspaceRoot) {
try {
const initiative = await this.initiativeRepository.findById(task.initiativeId);
const initBranch = initiative?.branch;
if (initBranch) {
const projects = await this.projectRepository.findProjectsByInitiativeId(task.initiativeId);
for (const project of projects) {
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
// Ensure initiative branch exists (from project defaultBranch)
await this.branchManager.ensureBranch(clonePath, initBranch, project.defaultBranch);
// Ensure phase branch exists (from initiative branch)
const phBranch = phaseBranchName(initBranch, (await this.phaseRepository?.findById(task.phaseId!))?.name ?? '');
if (phBranch) {
await this.branchManager.ensureBranch(clonePath, phBranch, initBranch);
}
}
}
} catch (err) {
log.warn({ taskId: task.id, err }, 'failed to ensure branches for task dispatch');
}
}
}
// Gather initiative context for the agent's input files