feat(KB-130): allow dependent tasks to start from in-review dep branches

- Relax scheduler dep gate to treat in-review tasks as satisfied
- Add resolveBaseBranch() to detect in-review deps with unmerged worktrees
- Executor creates worktrees from dep branch instead of HEAD when baseBranch is set
- Extend file-overlap tracking to include in-review tasks with worktrees
- Add baseBranch field to Task type and store update logic
- Add scheduler and executor tests for in-review dependency handling
This commit is contained in:
Dustin Byrne
2026-03-27 02:15:20 -04:00
parent dd746ba904
commit 69dca3e11f
7 changed files with 507 additions and 27 deletions

View File

@@ -274,13 +274,16 @@ export class TaskExecutor {
let acquiredFromPool = false;
const settings = await this.store.getSettings();
// Resolve the base branch — set by the scheduler when a dep is in-review
const baseBranch = task.baseBranch || null;
if (!isResume) {
// Try acquiring a warm worktree from the pool
if (this.options.pool && settings.recycleWorktrees) {
const pooled = this.options.pool.acquire();
if (pooled) {
this.options.pool.prepareForTask(pooled, branchName);
this.options.pool.prepareForTask(pooled, branchName, baseBranch ?? undefined);
worktreePath = pooled;
acquiredFromPool = true;
executorLog.log(`Acquired worktree from pool: ${pooled}`);
@@ -291,9 +294,14 @@ export class TaskExecutor {
// Fall through to fresh worktree creation if pool had nothing
if (!acquiredFromPool) {
this.createWorktree(branchName, worktreePath);
this.createWorktree(branchName, worktreePath, baseBranch ?? undefined);
await this.store.updateTask(task.id, { worktree: worktreePath });
await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`);
if (baseBranch) {
await this.store.logEntry(task.id, `Worktree created at ${worktreePath} (based on ${baseBranch})`);
} else {
await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`);
}
// Run worktree init command for fresh worktrees (skip for pooled — caches are warm)
if (settings.worktreeInitCommand) {
@@ -713,13 +721,24 @@ export class TaskExecutor {
// ── Worktree management ────────────────────────────────────────────
private createWorktree(branch: string, path: string): void {
/**
* Create a git worktree at `path` on a new branch.
*
* @param branch — Branch name (e.g., `kb/kb-042`)
* @param path — Absolute worktree directory path
* @param startPoint — Optional git ref to branch from (e.g., `kb/kb-041`).
* When provided, the worktree starts from that ref instead of HEAD.
*/
private createWorktree(branch: string, path: string, startPoint?: string): void {
if (existsSync(path)) {
executorLog.log(`Worktree already exists: ${path}`);
return;
}
try {
execSync(`git worktree add -b "${branch}" "${path}"`, { cwd: this.rootDir, stdio: "pipe" });
const cmd = startPoint
? `git worktree add -b "${branch}" "${path}" "${startPoint}"`
: `git worktree add -b "${branch}" "${path}"`;
execSync(cmd, { cwd: this.rootDir, stdio: "pipe" });
} catch {
try {
execSync(`git worktree add "${path}" "${branch}"`, { cwd: this.rootDir, stdio: "pipe" });
@@ -727,7 +746,7 @@ export class TaskExecutor {
throw new Error(`Failed to create worktree: ${e.message}`);
}
}
executorLog.log(`Worktree created: ${path}`);
executorLog.log(`Worktree created: ${path}${startPoint ? ` (from ${startPoint})` : ""}`);
}
/**