import type { EventBus } from '../kernel/events.js'; /** * Lifecycle of a single worktree handle. * * allocating → active → committing → merging → merged * └─→ needs-review (conflict, kept) * (any) → failed */ export type WorktreeStatus = 'allocating' | 'active' | 'committing' | 'merging' | 'merged' | 'needs-review' | 'failed'; export interface WorktreeHandle { /** Stable id (== slug). Used as the event `handleId`. */ id: string; /** Caller-supplied owner (a phase id in Goal). */ ownerId: string; /** Human label for the owner (phase name). */ ownerLabel: string; slug: string; /** Absolute path to the worktree checkout. */ dir: string; /** Branch checked out in the worktree (`wstack/ap/`). */ branch: string; /** Branch the worktree was forked from and merges back into. */ baseBranch: string; status: WorktreeStatus; createdAt: number; updatedAt: number; /** Diff stats from the last commit. */ insertions: number; deletions: number; files: number; sha?: string | undefined; lastError?: string | undefined; conflictFiles?: string[] | undefined; } export interface AllocateOpts { /** Friendly basis for the slug/branch (e.g. the phase name). */ slugHint?: string | undefined; ownerLabel?: string | undefined; /** Override the detected base branch. */ baseBranch?: string | undefined; } export interface MergeOpts { squash?: boolean | undefined; message?: string | undefined; /** * Optional conflict resolver. Invoked when the squash-merge conflicts, with * the conflicted paths and the base working tree (`cwd`). It must resolve the * conflict markers in place and return `true` when done. If it returns `true` * and no conflict markers remain, the merge is committed and `merge()` returns * `{ ok: true, resolved: true }`. Otherwise the merge is aborted (hard reset) * and the handle is parked `needs-review` — exactly as if no resolver were * provided, so the base tree is never left dirty. */ resolve?: (info: { conflictFiles: string[]; cwd: string; }) => Promise; } export interface MergeResult { ok: boolean; conflict?: boolean | undefined; conflictFiles?: string[] | undefined; stderr?: string | undefined; /** True when an initial conflict was successfully resolved by `opts.resolve`. */ resolved?: boolean | undefined; } export interface RunResult { code: number; stdout: string; stderr: string; } export interface WorktreeManagerOptions { projectRoot: string; events?: EventBus | undefined; sessionId?: string | (() => string | undefined) | undefined; gitBin?: string | undefined; /** * Test seam. When provided, replaces the real `git` spawn so the manager's * sequencing/arg vectors can be asserted without touching a repo. */ run?: (args: string[], cwd: string) => Promise; } /** * Owns the git-worktree lifecycle for isolated, parallel work units. Shells out * to `git` directly (never via the `git` *tool*) so it can target arbitrary * worktree directories without the tool's permission gate or `findGitDir` * resolution, and so `@wrongstack/core` keeps no dependency on `@wrongstack/tools`. */ export declare class WorktreeManager { private readonly projectRoot; private readonly events?; private readonly sessionIdSource; private readonly gitBin; private readonly runGit; /** Keyed by ownerId. */ private readonly handles; private readonly usedSlugs; constructor(opts: WorktreeManagerOptions); /** Create a fresh worktree + branch forked from the current base branch. */ allocate(ownerId: string, opts?: AllocateOpts): Promise; /** Stage everything and commit inside the worktree. */ commitAll(handle: WorktreeHandle, message: string): Promise<{ committed: boolean; }>; /** Merge the worktree branch back into the base branch (squash by default). */ merge(handle: WorktreeHandle, opts?: MergeOpts): Promise; /** * Current tip SHA of a handle's base branch (without checking it out). Capture * this before a merge so a regressed merge can be reverted to exactly this * commit — unambiguous even when a squash produced no diff. Returns null on * failure (caller then skips the revert). */ baseHead(handle: WorktreeHandle): Promise; /** * Hard-reset the base branch back to `sha` (a value previously returned by * {@link baseHead}). Used to undo a squash-merge whose integrated result failed * re-verification, so an auto-resolved-but-broken merge never sticks on base. * Safe because SDD merges are serialized — no other commit lands in between. */ revertBaseTo(handle: WorktreeHandle, sha: string): Promise; /** * Current base branch + tip SHA, captured WITHOUT a handle. The SDD run calls * this once at start so a later rollback knows which branch the run's squash * commits landed on. Returns null when not in a usable git state. */ currentBase(): Promise<{ branch: string; sha: string; } | null>; /** * Force-remove EVERY managed worktree + branch this project owns, without * relying on the in-memory `handles` map — so it works post-run (a fresh * manager can clean up a previous run's leftovers). Enumerates * `git worktree list --porcelain`, removes every checkout living under the * `.wrongstack/worktrees` root, deletes every `wstack/ap/*` branch, then prunes. * Returns the number of worktrees removed. Never throws — best-effort cleanup. */ cleanupAllManaged(): Promise<{ removed: number; }>; /** * Read-only inventory of this project's managed git artifacts: every worktree * checkout under the `.wrongstack/worktrees` root (with its branch, when the * checkout has one) plus every `wstack/ap/*` branch. Unlike `cleanupStale` / * `cleanupAllManaged` this removes nothing — it powers the WebUI worktree panel * so the user can SEE orphans before deciding to clean them. Never throws. */ listManaged(): Promise<{ worktrees: Array<{ dir: string; branch?: string | undefined; }>; branches: string[]; }>; /** * Force-remove ONE managed worktree + (optionally) its branch — the targeted * counterpart to `cleanupAllManaged`. Used by the WebUI worktree panel's * per-row Remove/Discard. Path-guarded so only checkouts under the project's * worktrees root can be removed. Best-effort; never throws. */ removeOne(dir: string, branch?: string | undefined): Promise<{ removed: boolean; }>; /** * Squash-merge an arbitrary `wstack/ap/*` branch into the base branch from the * main checkout — the handle-free counterpart to {@link merge}, used by the * WebUI worktree panel's per-row "Merge to base". On conflict it hard-resets * the base tree (never leaves it dirty) and reports the conflicted paths. * Refuses on a dirty base tree so uncommitted work is never clobbered. */ mergeBranch(branch: string, baseBranch?: string | undefined): Promise<{ ok: boolean; conflict?: boolean; conflictFiles?: string[]; reason?: string; }>; /** * Compact change summary for a worktree checkout: working-tree edits * (numstat vs HEAD) + commit count ahead of `baseBranch`. Powers the panel's * "View changes" without streaming a full diff. Never throws. */ diffSummary(dir: string, baseBranch?: string | undefined): Promise<{ files: Array<{ path: string; insertions: number; deletions: number; }>; insertions: number; deletions: number; commits: number; }>; /** * Detect and clean up stale worktrees left behind by crashed subagents. * * P2 #B6 (sprint2 audit): when a subagent crashes (OOM, SIGKILL), its * worktree checkout and branch remain on disk. `cleanupAllManaged()` * sweeps everything, but is never called automatically — only via * `/worktree clean`. This method runs on Director boot (or any other * entry point) to clean up leftovers from a previous crashed run. * * Unlike `cleanupAllManaged()`, this method is a no-op when no stale * worktrees are detected — it doesn't clear in-memory handles or emit * events unnecessarily. When stale worktrees ARE found, it delegates * to `cleanupAllManaged()` for the actual removal. * * Returns the number of stale worktrees removed (0 = clean). */ cleanupStale(): Promise<{ removed: number; detected: number; }>; /** * Undo a run's squash commits by reverting each (newest → oldest) on the base * branch — history-preserving, never a destructive reset. Refuses on a dirty * working tree (so uncommitted work is never clobbered) and aborts cleanly if a * revert conflicts, reporting which SHA. `shas` are the run commit SHAs in the * order they landed; this reverses them. Returns the count reverted. */ revertCommits(baseBranch: string, shas: string[]): Promise<{ ok: boolean; reverted: number; reason?: string; }>; /** * Run the caller-supplied resolver against a conflicted squash-merge, then * commit if it cleared every marker. Returns a successful `MergeResult` on a * clean resolution, or `null` to signal the caller should fall back to the * abort path. Never leaves the base tree committed-but-dirty: a partial or * failed resolution returns `null` and the caller hard-resets. */ private tryResolveConflict; /** * True when staged content still carries conflict markers. * * Primary probe: `git grep --cached` scans the STAGED blobs directly for a * full marker line (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) — exit 0 * means found. This is byte-level and independent of git version, locale, * and human-output phrasing. When the caller knows which files conflicted, * the scan is restricted to them so an unrelated `=======` underline in * some document can't false-positive. * * Fallback probe: `git diff --cached --check` prints a "leftover conflict * marker" line per survivor. Kept as belt-and-braces — it was the original * sole probe, but CI runners were observed to miss markers through it * (output parsing), which let a half-resolved merge commit. */ private hasConflictMarkers; /** * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`) * are left on disk for inspection. */ release(handle: WorktreeHandle, opts?: { keep?: boolean | undefined; }): Promise; get(ownerId: string): WorktreeHandle | undefined; list(): WorktreeHandle[]; private worktreesRoot; private detectBaseBranch; private makeSlug; private collectStats; /** * `git -c user.*` fallback so commits succeed on machines and CI runners * that have no global git identity configured. Returns `[]` when both * `user.name` and `user.email` are already set (the common case), so a real * user's identity is never overridden. The worktree branch commits are * squashed away on merge, so the fallback identity never reaches the base * branch history. */ private identityArgs; private unmergedFiles; private emitCommitted; private fail; private setStatus; private emit; private currentSessionId; private defaultRun; } /** * Extract conflicted paths from git merge output. Git prints one * `CONFLICT (): Merge conflict in ` line per conflicted file to * stdout — a portable signal that doesn't depend on the post-merge index * state (which `git diff --diff-filter=U` can report as empty on some runners * after a `--squash` conflict). */ export declare function parseConflictPaths(output: string): string[]; /** Throw if `dir` resolves outside `projectRoot`. */ export declare function assertSafePath(dir: string, projectRoot: string): void; //# sourceMappingURL=worktree-manager.d.ts.map