- MCP create-backlog-item for natural language Plan/Epic/Task authoring - Worker non-code: optional worktree + per-harness allowlist + doc_review→published - Harness registry: harness_providers/role_harness_routes, WorkerAdapter (minimal|crewai|langgraph|remote-ag-ui), AG-UI POST /api/ag-ui, Settings → Harnesses with Test - Plans tree: backlog.listTree + createPlan/Epic/Task + Start→Convoy, web CRUD without CLI - Fleet: /fleet rollup across workspaces + SSE /api/fleet/events Decision: docs/decisions/2026-08-25-second-brain-os-pivot.md Convoy: .convoys/second-brain-os-v01 Co-authored-by: orchestrator
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
import { access } from "node:fs/promises";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
export type WorktreeContext = {
|
|
repoRoot: string;
|
|
worktreePath: string;
|
|
branch: string;
|
|
};
|
|
|
|
async function pathExists(path: string): Promise<boolean> {
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Reuse an existing worktree directory when present (crash resume without re-clone). */
|
|
export async function reuseWorktreeIfPresent(
|
|
repoRoot: string,
|
|
worktreePath: string,
|
|
branch: string | null,
|
|
): Promise<WorktreeContext | null> {
|
|
if (!(await pathExists(worktreePath))) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
cwd: worktreePath,
|
|
});
|
|
if (stdout.trim() !== worktreePath && !(await pathExists(join(worktreePath, ".git")))) {
|
|
return null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
let resolvedBranch = branch ?? "HEAD";
|
|
if (!branch) {
|
|
try {
|
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
cwd: worktreePath,
|
|
});
|
|
resolvedBranch = stdout.trim() || "HEAD";
|
|
} catch {
|
|
resolvedBranch = "HEAD";
|
|
}
|
|
}
|
|
|
|
return { repoRoot, worktreePath, branch: resolvedBranch };
|
|
}
|
|
|
|
export async function ensureWorktree(
|
|
repoRoot: string,
|
|
worktreeBase: string,
|
|
worktreeKey: string,
|
|
branchSuffix: string,
|
|
opts?: { needsWorktree?: boolean },
|
|
): Promise<WorktreeContext | null> {
|
|
if (opts?.needsWorktree === false) {
|
|
return null;
|
|
}
|
|
const branch = `worker/${branchSuffix}`.replace(/[^a-zA-Z0-9/_-]/g, "-");
|
|
const worktreePath = join(worktreeBase, worktreeKey);
|
|
await mkdir(worktreeBase, { recursive: true });
|
|
|
|
const existing = await reuseWorktreeIfPresent(repoRoot, worktreePath, branch);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
try {
|
|
await execFileAsync("git", ["worktree", "add", "-B", branch, worktreePath], {
|
|
cwd: repoRoot,
|
|
});
|
|
} catch {
|
|
await execFileAsync("git", ["worktree", "add", worktreePath, branch], {
|
|
cwd: repoRoot,
|
|
});
|
|
}
|
|
|
|
return { repoRoot, worktreePath, branch };
|
|
}
|
|
|
|
export async function removeWorktree(repoRoot: string, worktreePath: string): Promise<void> {
|
|
try {
|
|
await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
|
|
cwd: repoRoot,
|
|
});
|
|
} catch {
|
|
// Best-effort cleanup; operator can prune stale worktrees manually.
|
|
}
|
|
}
|