feat: support operator-routed external task checkouts (#3398)

## Summary
- add an explicit API route that persists one clean external Git
checkout for task execution and enforced review
- fence execution to the checkout's persisted branch and fail closed
when the route becomes invalid
- allow completion invariants to validate explicitly routed checkouts
outside the project worktree directory

## Test plan
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/external-execution-checkout.test.ts
src/__tests__/review-checkout.test.ts
src/__tests__/engine-no-blocking-shellout.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/executor-fast-mode-workflows.test.ts -t 'prepares a
persisted external execution checkout' --silent=passed-only
--reporter=dot`
- `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest
run src/__tests__/routes-tasks-near-duplicate.test.ts -t 'PATCH
external-checkout' --project dashboard-api --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm --filter @fusion/dashboard exec tsc --noEmit`
- `pnpm verify:fast`
- `pnpm test:gate` (605 non-PostgreSQL tests pass; local PostgreSQL
suites cannot authenticate because the configured client returns an
empty password)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for routing task execution and review through
operator-selected external Git checkouts.
* External checkouts are validated for valid Git repositories, attached
branches, clean status, and branch consistency.
  * Tasks can clear previously configured external checkout routing.
* Valid routed checkouts are used directly without creating a separate
worktree.

* **Bug Fixes**
* Invalid, incomplete, dirty, or mismatched checkout configurations now
fail early with clear validation errors.
  * Missing tasks return the appropriate not-found response.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
Phil Larson
2026-08-09 16:49:06 -07:00
committed by GitHub
parent ccebe5c5cf
commit 477f3faf0a
8 changed files with 438 additions and 14 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Allow operators to route task execution and review through one validated external Git checkout.
category: feature
dev: PATCH /api/tasks/:id/external-checkout persists a clean checkout path and branch fence in task source metadata.

View File

@@ -2,6 +2,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import express from "express";
import * as core from "@fusion/core";
import * as engine from "@fusion/engine";
import type { Column, Task, TaskStore } from "@fusion/core";
import { registerTaskWorkflowRoutes } from "../routes/register-task-workflow-routes.js";
import { request as performRequest } from "../test-request.js";
@@ -62,6 +63,7 @@ function buildApp(seed: Task[]) {
tasks[index] = next;
return next;
}),
logEntry: vi.fn().mockResolvedValue(undefined),
recordActivity: vi.fn().mockResolvedValue(undefined),
};
@@ -339,4 +341,59 @@ describe("routes /api/tasks near duplicate", () => {
nearDuplicateDismissed: true,
});
});
it("PATCH external-checkout persists one clean Git checkout for execution and review", async () => {
const inspection = vi.spyOn(engine, "inspectExternalGitCheckout").mockResolvedValue({
valid: true,
checkoutPath: "/tmp/external-runtime",
branch: "local/runtime-fixes",
});
const seeded = mkTask({
id: "FN-6097",
title: "External runtime task",
description: "Implement in a supported external checkout",
column: "todo",
});
const { app, tasks } = buildApp([seeded]);
const res = await performRequest(
app,
"PATCH",
"/api/tasks/FN-6097/external-checkout",
JSON.stringify({ checkoutPath: "/tmp/external-runtime" }),
{ "content-type": "application/json" },
);
expect(res.status, JSON.stringify(res.body)).toBe(200);
expect((res.body as Task).sourceMetadata).toMatchObject({
externalExecutionCheckout: "/tmp/external-runtime",
externalExecutionBranch: "local/runtime-fixes",
externalReviewCheckout: "/tmp/external-runtime",
});
expect(tasks[0]?.sourceMetadata).toMatchObject((res.body as Task).sourceMetadata ?? {});
inspection.mockResolvedValueOnce({ valid: false, reason: "checkoutPath must be clean before routing" });
const dirty = await performRequest(
app,
"PATCH",
"/api/tasks/FN-6097/external-checkout",
JSON.stringify({ checkoutPath: "/tmp/external-runtime" }),
{ "content-type": "application/json" },
);
expect(dirty.status).toBe(400);
const cleared = await performRequest(
app,
"PATCH",
"/api/tasks/FN-6097/external-checkout",
JSON.stringify({ checkoutPath: null }),
{ "content-type": "application/json" },
);
expect(cleared.status).toBe(200);
expect((cleared.body as Task).sourceMetadata).toMatchObject({
externalExecutionCheckout: null,
externalExecutionBranch: null,
externalReviewCheckout: null,
});
});
});

View File

@@ -102,6 +102,7 @@ import {
prepareRevertPrBranch,
prepareWorkspaceRevertPrBranches,
isInReviewMissingWorktreeSessionStartFailure,
inspectExternalGitCheckout,
// FN-8004 follow-up: shared with SelfHealingManager.recoverStaleMergingStatus so the manual
// Retry gate and the automatic sweep agree on when a merge-active stamp is orphaned.
isStaleMergeActiveStatus,
@@ -6411,6 +6412,54 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
}
});
// Persist one operator-validated external checkout for both implementation
// and enforced review. Keep filesystem routing out of the user-defined
// workflow custom-field schema.
router.patch("/tasks/:id/external-checkout", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const checkoutPath = (req.body as { checkoutPath?: unknown } | undefined)?.checkoutPath;
await scopedStore.getTask(req.params.id);
if (checkoutPath === null) {
const task = await scopedStore.updateTask(req.params.id, {
sourceMetadataPatch: {
externalExecutionCheckout: null,
externalExecutionBranch: null,
externalReviewCheckout: null,
},
});
await scopedStore.logEntry(req.params.id, "External execution/review checkout routing cleared by operator");
res.json(task);
return;
}
const inspection = await inspectExternalGitCheckout(checkoutPath, { requireClean: true });
if (!inspection.valid || !inspection.checkoutPath || !inspection.branch) {
throw badRequest(`Invalid external checkout: ${inspection.reason ?? "unknown error"}`);
}
const task = await scopedStore.updateTask(req.params.id, {
sourceMetadataPatch: {
externalExecutionCheckout: inspection.checkoutPath,
externalExecutionBranch: inspection.branch,
externalReviewCheckout: inspection.checkoutPath,
},
});
await scopedStore.logEntry(
req.params.id,
`External execution/review checkout routed to ${inspection.checkoutPath} (${inspection.branch}) by operator`,
);
res.json(task);
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
if (isTaskLookupMiss(err) || (err instanceof Error ? err.message : String(err)).includes("not found")) {
throw notFound(err instanceof Error ? err.message : String(err));
}
rethrowAsApiError(err);
}
});
// Patch a task's custom field values (U13/KTD-14). Delegates to the single
// store write authority (`updateTaskCustomFields`), which validates the patch
// against the task's workflow field schema. A typed rejection surfaces as a

View File

@@ -9,6 +9,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import "./executor-test-helpers.js";
import { getBuiltinWorkflow } from "@fusion/core";
import { TaskExecutor } from "../executor.js";
import { resolveExternalExecutionCheckoutRoute } from "../execution/external-execution-checkout.js";
import { WorkflowGraphTaskRunner } from "../workflows/workflow-graph-task-runner.js";
import { FOREACH_ACTIVE_CONTEXT_KEY } from "../workflows/workflow-node-handlers.js";
import {
@@ -20,6 +21,12 @@ import {
resetExecutorMocks,
} from "./executor-test-helpers.js";
vi.mock("../execution/external-execution-checkout.js", () => ({
resolveExternalExecutionCheckoutRoute: vi.fn(async () => ({ configured: false })),
}));
const mockedResolveExternalExecutionCheckoutRoute = vi.mocked(resolveExternalExecutionCheckoutRoute);
const now = "2026-06-10T00:00:00.000Z";
function task(overrides: Record<string, unknown> = {}) {
@@ -70,6 +77,8 @@ function workflowResult() {
describe("fast mode workflow/runtime invariants", () => {
beforeEach(() => {
resetExecutorMocks();
mockedResolveExternalExecutionCheckoutRoute.mockReset();
mockedResolveExternalExecutionCheckoutRoute.mockResolvedValue({ configured: false });
mockedExistsSync.mockReturnValue(true);
});
@@ -248,6 +257,43 @@ describe("fast mode workflow/runtime invariants", () => {
});
});
it("prepares a persisted external execution checkout instead of the project task worktree", async () => {
const routedTask = task({
id: "FN-6097",
worktree: "/tmp/project-task-worktree",
branch: "fusion/fn-6097",
sourceMetadata: {
externalExecutionCheckout: "/tmp/external-runtime",
externalExecutionBranch: "local/runtime-fixes",
externalReviewCheckout: "/tmp/external-runtime",
},
});
const store = createMockStore();
store.getTask.mockResolvedValue(routedTask);
mockedResolveExternalExecutionCheckoutRoute.mockResolvedValueOnce({
configured: true,
valid: true,
checkoutPath: "/tmp/external-runtime",
branch: "local/runtime-fixes",
});
const executor = new TaskExecutor(store, "/tmp/project-root");
const result = await (executor as any)
.createAuthoritativeWorkflowPrimitives({ experimentalFeatures: { workflowGraphExecutor: true } })
.prepareWorktree(
{ run: { taskId: "FN-6097" }, node: { node: { id: "execute" }, context: {} } },
routedTask,
);
expect(result).toMatchObject({
outcome: "success",
data: {
worktreePath: "/tmp/external-runtime",
branchName: "local/runtime-fixes",
},
});
});
it("does not project a fresh graph step or capture its baseline before the executor creates its worktree", async () => {
let liveTask = task({
steps: [{ name: "Preflight", status: "pending" }],

View File

@@ -0,0 +1,96 @@
/*
* Persisted external checkout routing is an explicit operator contract. Execution
* must use the same validated checkout as review, and stale path/branch metadata
* must fail closed instead of silently falling back to the project task worktree.
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { execFileSync } from "node:child_process";
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { inspectExternalGitCheckout, resolveExternalExecutionCheckoutRoute } from "../execution/external-execution-checkout.js";
import { resolveReviewCheckoutCwd } from "../execution/review-checkout.js";
function makeGitCheckout(branch = "local/runtime-fixes"): string {
const dir = mkdtempSync(join(tmpdir(), "external-execution-checkout-"));
execFileSync("git", ["init", "-b", branch], { cwd: dir, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
execFileSync("git", ["config", "user.name", "Fusion Test"], { cwd: dir });
execFileSync("git", ["commit", "--allow-empty", "-m", "initial"], { cwd: dir, stdio: "ignore" });
return dir;
}
let checkout: string;
beforeAll(() => {
checkout = makeGitCheckout();
});
afterAll(() => {
rmSync(checkout, { recursive: true, force: true });
});
describe("resolveExternalExecutionCheckoutRoute", () => {
it("reports an absent route without widening to other task fields", async () => {
await expect(resolveExternalExecutionCheckoutRoute({
worktree: "/tmp/task-worktree",
customFields: { executionCheckoutPath: "/tmp/untrusted" },
})).resolves.toEqual({ configured: false });
await expect(resolveExternalExecutionCheckoutRoute({
sourceMetadata: {
externalExecutionCheckout: null,
externalExecutionBranch: null,
},
})).resolves.toEqual({ configured: false });
});
it("resolves a persisted path and branch and matches explicit review routing", async () => {
const realCheckout = realpathSync(checkout);
const task = {
sourceMetadata: {
externalExecutionCheckout: checkout,
externalExecutionBranch: "local/runtime-fixes",
externalReviewCheckout: checkout,
},
};
await expect(resolveExternalExecutionCheckoutRoute(task)).resolves.toEqual({
configured: true,
valid: true,
checkoutPath: realCheckout,
branch: "local/runtime-fixes",
});
expect(resolveReviewCheckoutCwd(task, "/tmp/task-worktree")).toBe(realCheckout);
});
it("fails closed when the persisted branch does not match the checkout", async () => {
await expect(resolveExternalExecutionCheckoutRoute({
sourceMetadata: {
externalExecutionCheckout: checkout,
externalExecutionBranch: "stale-branch",
},
})).resolves.toMatchObject({
configured: true,
valid: false,
reason: expect.stringContaining("branch mismatch"),
});
});
it("fails closed when a path is persisted without a branch fence", async () => {
await expect(resolveExternalExecutionCheckoutRoute({
sourceMetadata: { externalExecutionCheckout: checkout },
})).resolves.toMatchObject({
configured: true,
valid: false,
reason: expect.stringContaining("externalExecutionBranch"),
});
});
it("requires a clean checkout when an operator first persists the route", async () => {
writeFileSync(join(checkout, "dirty.txt"), "dirty");
await expect(inspectExternalGitCheckout(checkout, { requireClean: true })).resolves.toMatchObject({
valid: false,
reason: expect.stringContaining("must be clean"),
});
});
});

View File

@@ -0,0 +1,118 @@
import { execFile } from "node:child_process";
import { existsSync, realpathSync, statSync } from "node:fs";
import { isAbsolute } from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export interface ExternalGitCheckoutInspection {
valid: boolean;
checkoutPath?: string;
branch?: string;
reason?: string;
}
export type ExternalExecutionCheckoutResolution =
| { configured: false }
| ({ configured: true } & ExternalGitCheckoutInspection);
function readSourceMetadata(task: unknown): Record<string, unknown> | undefined {
if (!task || typeof task !== "object") return undefined;
const sourceMetadata = (task as Record<string, unknown>).sourceMetadata;
return sourceMetadata && typeof sourceMetadata === "object"
? sourceMetadata as Record<string, unknown>
: undefined;
}
export async function inspectExternalGitCheckout(
candidate: unknown,
options: { requireClean?: boolean } = {},
): Promise<ExternalGitCheckoutInspection> {
if (typeof candidate !== "string" || candidate.trim().length === 0) {
return { valid: false, reason: "checkoutPath must be a non-empty string" };
}
const checkoutPath = candidate.trim();
if (!isAbsolute(checkoutPath)) {
return { valid: false, reason: "checkoutPath must be absolute" };
}
try {
if (!existsSync(checkoutPath) || !statSync(checkoutPath).isDirectory()) {
return { valid: false, reason: `checkoutPath is not a directory: ${checkoutPath}` };
}
const canonicalCheckout = realpathSync(checkoutPath);
const { stdout: topLevelOutput } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
cwd: canonicalCheckout,
encoding: "utf-8",
timeout: 10_000,
});
const topLevel = topLevelOutput.trim();
const canonicalTopLevel = realpathSync(topLevel);
if (canonicalTopLevel !== canonicalCheckout) {
return {
valid: false,
reason: `checkoutPath must be the Git top-level (observed ${canonicalTopLevel})`,
};
}
const { stdout: branchOutput } = await execFileAsync("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
cwd: canonicalCheckout,
encoding: "utf-8",
timeout: 10_000,
});
const branch = branchOutput.trim();
if (!branch) {
return { valid: false, reason: "checkoutPath must have a checked-out branch" };
}
if (options.requireClean) {
const { stdout: statusOutput } = await execFileAsync("git", ["status", "--porcelain=v1"], {
cwd: canonicalCheckout,
encoding: "utf-8",
timeout: 10_000,
});
const status = statusOutput.trim();
if (status.length > 0) {
return { valid: false, reason: "checkoutPath must be clean before routing" };
}
}
return { valid: true, checkoutPath: canonicalCheckout, branch };
} catch (error) {
return {
valid: false,
reason: error instanceof Error ? error.message : String(error),
};
}
}
export async function resolveExternalExecutionCheckoutRoute(task: unknown): Promise<ExternalExecutionCheckoutResolution> {
const sourceMetadata = readSourceMetadata(task);
if (!sourceMetadata || !Object.prototype.hasOwnProperty.call(sourceMetadata, "externalExecutionCheckout")) {
return { configured: false };
}
if (sourceMetadata.externalExecutionCheckout == null) {
return { configured: false };
}
const inspection = await inspectExternalGitCheckout(sourceMetadata.externalExecutionCheckout);
if (!inspection.valid) {
return { configured: true, ...inspection };
}
const expectedBranch = sourceMetadata.externalExecutionBranch;
if (typeof expectedBranch !== "string" || expectedBranch.trim().length === 0) {
return {
configured: true,
valid: false,
reason: "sourceMetadata.externalExecutionBranch must be persisted with the checkout route",
};
}
const normalizedExpectedBranch = expectedBranch.trim();
if (inspection.branch !== normalizedExpectedBranch) {
return {
configured: true,
valid: false,
reason: `external execution checkout branch mismatch: observed ${inspection.branch}, expected ${normalizedExpectedBranch}`,
};
}
return { configured: true, ...inspection };
}

View File

@@ -36,6 +36,7 @@ import { WorkflowAgentCapacity } from "./agents/workflow-agent-capacity.js";
import { createExecutorColumnBoundaryHooks } from "./workflow-column-boundary-hooks.js";
import { ensureWorkflowCompletionSummary } from "./workflows/workflow-completion-summary.js";
import { createCodeNodeRunner } from "./execution/code-node-runner.js";
import { resolveExternalExecutionCheckoutRoute } from "./execution/external-execution-checkout.js";
import { getTaskReviewCheckoutPath, resolveReviewCheckoutCwd } from "./execution/review-checkout.js";
import { getActiveNotificationService } from "./util/notifier.js";
import type { ParseStepsHandlerDeps, CodeNodeRunner } from "./workflows/workflow-node-handlers.js";
@@ -9007,6 +9008,14 @@ export class TaskExecutor {
prepareWorktree: async (_ctx, task) => {
const live = await this.store.getTask(task.id).catch(() => null);
const liveTask = live?.id === task.id ? live : null;
const routedTask = liveTask ?? task;
const externalRoute = await resolveExternalExecutionCheckoutRoute(routedTask);
if (externalRoute.configured && !externalRoute.valid) {
return {
outcome: "failure",
value: `external-execution-checkout-invalid: ${externalRoute.reason ?? "unknown error"}`,
};
}
/*
FNXC:WorkflowExecution 2026-06-23-11:49:
The workflow execute node must not perform a second worktree acquisition ahead of the authoritative executor. Passing the repo root as a prepared worktree makes the inner execute() reject a valid fresh-worktree task as repo-root reuse; pass only an existing task worktree and let execute() acquire when none exists.
@@ -9015,8 +9024,12 @@ export class TaskExecutor {
Upgrade safety requires the graph primitive to tolerate older or minimal stores that return null or a mismatched row during startup/cutover. Only trust the live row when it is for the requested task; otherwise fall back to the runner snapshot.
*/
const prepared: PreparedWorktree = {
worktreePath: liveTask?.worktree || task.worktree || "",
branchName: liveTask?.branch || task.branch,
worktreePath: externalRoute.configured
? externalRoute.checkoutPath ?? ""
: liveTask?.worktree || task.worktree || "",
branchName: externalRoute.configured
? externalRoute.branch
: liveTask?.branch || task.branch,
};
return { outcome: "success", value: "worktree-ready", data: prepared };
},
@@ -14092,6 +14105,12 @@ export class TaskExecutor {
// Behavior-inert when nothing is customized (declaration defaults === legacy
// defaults; absent-default lanes never override).
const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings());
const externalExecutionRoute = await resolveExternalExecutionCheckoutRoute(task);
if (externalExecutionRoute.configured && !externalExecutionRoute.valid) {
const message = `Persisted external execution checkout is invalid: ${externalExecutionRoute.reason ?? "unknown error"}`;
await this.store.logEntry(task.id, message, undefined, this.getRunContextFor(task.id));
throw new Error(message);
}
// Keep runtime plugin workflow step templates synchronized into TaskStore.
// TaskStore resolves plugin-prefixed workflow IDs from this injected cache
@@ -14228,7 +14247,7 @@ export class TaskExecutor {
);
}
if (task.column === preflightWipLane && !task.worktree) {
if (task.column === preflightWipLane && !task.worktree && !externalExecutionRoute.configured) {
executorLog.error(
`${task.id}: drift detected — task is in-progress with no worktree. ` +
`Recovering by creating a fresh worktree. This usually indicates a partial ` +
@@ -14243,7 +14262,9 @@ export class TaskExecutor {
}
// Hoist worktreePath so it's accessible in the catch block for dep-abort cleanup
let worktreePath = task.worktree ?? "";
let worktreePath = externalExecutionRoute.configured
? externalExecutionRoute.checkoutPath ?? ""
: task.worktree ?? "";
// Set by stuck-abort handlers; the actual moveTask("todo") is deferred to
// the finally block so this.executing is cleared first (prevents re-dispatch race).
@@ -14333,7 +14354,7 @@ export class TaskExecutor {
}
}
const hadAssignedWorktree = Boolean(task.worktree);
const hadAssignedWorktree = Boolean(task.worktree) || externalExecutionRoute.configured;
const taskCommandAbortController = new AbortController();
this.registerConfiguredCommandController(task.id, taskCommandAbortController);
/*
@@ -14348,6 +14369,14 @@ export class TaskExecutor {
hydrated: true,
isResume: Boolean(task.sessionFile),
}
: externalExecutionRoute.configured
? {
worktreePath: externalExecutionRoute.checkoutPath ?? "",
branch: externalExecutionRoute.branch ?? "",
source: "existing",
hydrated: true,
isResume: Boolean(task.sessionFile),
}
: await (async () => {
try {
return await acquireTaskWorktree({
@@ -14443,7 +14472,7 @@ export class TaskExecutor {
FNXC:Workspace 2026-06-21-12:00:
KTD1 — every preflight below (base-commit capture, contamination check, worktree-liveness gate) runs git against `worktreePath`, which equals the non-git workspace root in workspace mode. They would all fail. Gate the whole block off in workspace mode; the per-repo equivalents return in Phase B (master U3) against each acquired sub-repo worktree. The non-workspace branch is unchanged.
*/
if (!this.workspaceConfig) {
if (!this.workspaceConfig && !externalExecutionRoute.configured) {
// Capture the base commit SHA for diff computation whenever a task
// starts with a newly assigned worktree.
if (!acquisition.isResume) {
@@ -18148,9 +18177,24 @@ export class TaskExecutor {
}
return { ok: true };
}
const branchName = resolveTaskWorkingBranch(task);
const externalExecutionRoute = await resolveExternalExecutionCheckoutRoute(task);
if (externalExecutionRoute.configured && !externalExecutionRoute.valid) {
return {
ok: false,
reason: "wrong_toplevel",
observed: externalExecutionRoute.reason ?? "invalid persisted external execution checkout",
expected: "valid persisted external execution checkout",
};
}
const branchName = externalExecutionRoute.configured
? externalExecutionRoute.branch ?? ""
: resolveTaskWorkingBranch(task);
// Non-workspace tasks hold a one-element set; fall back to its sole member to preserve the original singular resolution.
const worktreePath = worktreePathOverride ?? task.worktree ?? this.getActiveWorktreePaths(task.id)[0] ?? null;
const worktreePath = worktreePathOverride
?? (externalExecutionRoute.configured ? externalExecutionRoute.checkoutPath : undefined)
?? task.worktree
?? this.getActiveWorktreePaths(task.id)[0]
?? null;
if (!worktreePath) {
return {
@@ -18198,12 +18242,13 @@ export class TaskExecutor {
if (observedTopLevelRaw) {
const observedTopLevel = canonicalizePath(observedTopLevelRaw);
if (
observedTopLevel === expectedRoot ||
!isInsideWorktreesDir(this.rootDir, observedTopLevel, settings) ||
observedTopLevel !== expectedWorktreeRealpath
) {
if (allowReanchor && observedTopLevel !== expectedRoot && isInsideWorktreesDir(this.rootDir, observedTopLevel, settings)) {
const violatesCheckoutBoundary = externalExecutionRoute.configured
? observedTopLevel !== expectedWorktreeRealpath
: observedTopLevel === expectedRoot
|| !isInsideWorktreesDir(this.rootDir, observedTopLevel, settings)
|| observedTopLevel !== expectedWorktreeRealpath;
if (violatesCheckoutBoundary) {
if (!externalExecutionRoute.configured && allowReanchor && observedTopLevel !== expectedRoot && isInsideWorktreesDir(this.rootDir, observedTopLevel, settings)) {
const reanchor = await detectNestedWorktreeRoot(this.rootDir, worktreePath, settings);
if (reanchor.reanchored) {
await this.store.updateTask(task.id, { worktree: reanchor.root });

View File

@@ -482,6 +482,12 @@ export {
type SquashAuditRecentMainCommit,
} from "./merge/merger-squash-audit.js";
export { reviewStep, type ReviewType, type ReviewVerdict, type ReviewResult, type ReviewOptions } from "./execution/reviewer.js";
export {
inspectExternalGitCheckout,
resolveExternalExecutionCheckoutRoute,
type ExternalExecutionCheckoutResolution,
type ExternalGitCheckoutInspection,
} from "./execution/external-execution-checkout.js";
export { createFnAgent, promptWithFallback, describeModel, setHostExtensionPaths, getHostExtensionPaths, wrapToolsWithActionGate, type AgentOptions, type AgentResult } from "./pi.js";
export { resolveMcpServersForRuntime, resolveMcpServersForStore, type ResolvedMcpServersForRuntime } from "./mcp/mcp-resolution.js";
export { discoverMcpServers, type DiscoverMcpServersOptions, type DiscoverMcpServersResult } from "./mcp/mcp-discovery-service.js";