Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,90 @@ describe("ProviderCommandReactor", () => {
expect(multiCall?.[1]).toMatchObject({
cwd: "/tmp/multi-workspace",
additionalRoots: ["/tmp/multi-workspace/backend", "/tmp/oss/frontend"],
repoRoots: ["/tmp/multi-workspace/backend", "/tmp/oss/frontend"],
});
}),
);

effectIt.effect(
"launches a multi-repo worktree run with every worktree, including the anchor",
() =>
Effect.gen(function* () {
const harness = yield* Effect.promise(() => createHarness());
const now = "2026-01-01T00:00:00.000Z";
const multiModelSelection = {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
};

yield* harness.engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-project-create-multi-wt"),
projectId: asProjectId("project-multi-wt"),
title: "Multi Repo Project",
workspaceRoot: "/tmp/multi-workspace",
repoRoots: ["/tmp/multi-workspace/backend", "/tmp/oss/frontend"],
defaultModelSelection: multiModelSelection,
createdAt: now,
});
yield* harness.engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-thread-create-multi-wt"),
threadId: ThreadId.make("thread-multi-wt"),
projectId: asProjectId("project-multi-wt"),
title: "Multi Worktree Thread",
modelSelection: multiModelSelection,
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
branch: "t3code/multi",
worktreePath: "/tmp/worktrees/thread-multi-wt/backend",
worktrees: [
{
repoRoot: "/tmp/multi-workspace/backend",
worktreePath: "/tmp/worktrees/thread-multi-wt/backend",
},
{
repoRoot: "/tmp/oss/frontend",
worktreePath: "/tmp/worktrees/thread-multi-wt/frontend",
},
],
createdAt: now,
});

yield* harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start-multi-wt"),
threadId: ThreadId.make("thread-multi-wt"),
message: {
messageId: asMessageId("user-message-multi-wt"),
role: "user",
text: "edit both repos",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
});

yield* Effect.promise(() =>
waitFor(() =>
harness.startSession.mock.calls.some(
(call) => call[0] === ThreadId.make("thread-multi-wt"),
),
),
);
const call = harness.startSession.mock.calls.find(
(entry) => entry[0] === ThreadId.make("thread-multi-wt"),
);
// The session anchors in the first worktree, so `additionalRoots`
// leaves it out while `repoRoots` keeps it.
expect(call?.[1]).toMatchObject({
cwd: "/tmp/worktrees/thread-multi-wt/backend",
additionalRoots: ["/tmp/worktrees/thread-multi-wt/frontend"],
repoRoots: [
"/tmp/worktrees/thread-multi-wt/backend",
"/tmp/worktrees/thread-multi-wt/frontend",
],
});
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,8 @@ const make = Effect.gen(function* () {
projects: project ? [project] : [],
});
const additionalRoots = manifest ? manifestExtraRoots(manifest) : [];
const repoRoots =
manifest && additionalRoots.length > 0 ? manifest.roots.map((root) => root.path) : [];
const refreshWorkspaceSnapshot = effectiveCwd
? providerRegistry
.refreshWorkspaceSnapshot({ instanceId: desiredInstanceId, cwd: effectiveCwd })
Expand Down Expand Up @@ -752,6 +754,7 @@ const make = Effect.gen(function* () {
...(effectiveCwd ? { cwd: effectiveCwd } : {}),
...(sessionTitle ? { title: sessionTitle } : {}),
...(additionalRoots.length > 0 ? { additionalRoots } : {}),
...(repoRoots.length > 0 ? { repoRoots } : {}),
modelSelection: desiredModelSelection,
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
runtimeMode: desiredRuntimeMode,
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ProviderInteractionMode } from "@t3tools/contracts";
import type { V2TurnStartParams__AdditionalContextEntry } from "effect-codex-app-server/schema";
import { buildRuntimeInstructions } from "./RuntimeInstructions.ts";
import { buildRuntimeInstructions, type MultiRepoWorkspace } from "./RuntimeInstructions.ts";

const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `## T3 Code collaborative browser

Expand Down Expand Up @@ -188,7 +188,7 @@ export interface CodexRuntimeInfo {
readonly model: string;
readonly modelName?: string | undefined;
readonly reasoningEffort: string;
readonly multiRepo?: boolean | undefined;
readonly multiRepo?: MultiRepoWorkspace | undefined;
}

/** Mode prompt for `turn/start.collaborationMode.settings.developer_instructions`. */
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/Layers/AntigravityAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ import * as EffectAcpErrors from "effect-acp/errors";
import type * as EffectAcpSchema from "effect-acp/schema";

import { ServerConfig } from "../../config.ts";
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
import {
buildRuntimeInstructions,
multiRepoWorkspace,
type MultiRepoWorkspace,
} from "../RuntimeInstructions.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import type { AntigravityAuth } from "../AntigravityAuth.ts";
import {
Expand Down Expand Up @@ -192,8 +196,8 @@ interface TurnIntent {
interface SessionContext {
readonly threadId: ThreadId;
readonly cwd: string;
/** The session spans several repository roots. */
readonly multiRepo: boolean;
/** Set when the session has roots beyond its working directory. */
readonly multiRepo: MultiRepoWorkspace | undefined;
readonly nativeSessionId: string;
readonly scope: Scope.Closeable;
readonly runtime: Runtime;
Expand Down Expand Up @@ -867,7 +871,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
context = {
threadId: input.threadId,
cwd,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(input),
nativeSessionId: started.sessionId,
scope: sessionScope,
runtime,
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,17 @@ describe("ClaudeAdapterLive", () => {
provider: ProviderDriverKind.make("claudeAgent"),
cwd: "/workspace",
additionalRoots: ["/workspace/api", "/workspace/web"],
repoRoots: ["/workspace/api", "/workspace/web"],
runtimeMode: "full-access",
});

assert.deepEqual(harness.getLastCreateQueryInput()?.options.systemPrompt, {
type: "preset",
preset: "claude_code",
append: buildRuntimeInstructions({ harness: "Claude Code", multiRepo: true }),
append: buildRuntimeInstructions({
harness: "Claude Code",
multiRepo: { cwd: "/workspace", repoRoots: ["/workspace/api", "/workspace/web"] },
}),
});
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts";
import { claudeSignedOutMessage, makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import { planClaudeSkillDispatch } from "../Drivers/ClaudeSkillDispatch.ts";
import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts";
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
import { buildRuntimeInstructions, multiRepoWorkspace } from "../RuntimeInstructions.ts";
import {
BUNDLED_CLAUDE_MODEL_CATALOG,
type ClaudeModelCatalog,
Expand Down Expand Up @@ -4915,7 +4915,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
// Model and effort can change after this session-level prompt is set.
append: buildRuntimeInstructions({
harness: "Claude Code",
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(input),
}),
},
settingSources: [...CLAUDE_SETTING_SOURCES],
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
...(input.additionalRoots && input.additionalRoots.length > 0
? { additionalRoots: input.additionalRoots }
: {}),
...(input.repoRoots && input.repoRoots.length > 0 ? { repoRoots: input.repoRoots } : {}),
binaryPath: codexConfig.binaryPath,
...(options?.models ? { models: options.models } : {}),
launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment),
Expand Down
12 changes: 9 additions & 3 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
buildCodexAdditionalContext,
buildCodexDeveloperInstructions,
} from "../CodexDeveloperInstructions.ts";
import type { MultiRepoWorkspace } from "../RuntimeInstructions.ts";
import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";
import {
buildTurnStartParams,
Expand Down Expand Up @@ -297,7 +298,7 @@ describe("buildTurnStartParams", () => {

it.effect("asks a multi-repo session for absolute file paths", () =>
Effect.gen(function* () {
const instructions = function* (multiRepo: boolean) {
const instructions = function* (multiRepo: MultiRepoWorkspace | undefined) {
const params = yield* buildTurnStartParams({
threadId: "provider-thread-1",
runtimeMode: "full-access",
Expand All @@ -308,8 +309,13 @@ describe("buildTurnStartParams", () => {
return params.additionalContext?.t3_code_runtime?.value ?? "";
};

NodeAssert.match(yield* instructions(true), /<multi_repo_workspace>/);
NodeAssert.doesNotMatch(yield* instructions(false), /<multi_repo_workspace>/);
const multiRepo = yield* instructions({
cwd: "/work",
repoRoots: ["/work/api", "/work/web"],
});
NodeAssert.match(multiRepo, /<multi_repo_workspace>/);
NodeAssert.match(multiRepo, /- \/work\/api\n- \/work\/web/);
NodeAssert.doesNotMatch(yield* instructions(undefined), /<multi_repo_workspace>/);
}),
);

Expand Down
13 changes: 8 additions & 5 deletions apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
buildCodexDeveloperInstructions,
type T3CodeToolAvailability,
} from "../CodexDeveloperInstructions.ts";
import { multiRepoWorkspace, type MultiRepoWorkspace } from "../RuntimeInstructions.ts";
const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);

const PROVIDER = ProviderDriverKind.make("codex");
Expand Down Expand Up @@ -183,6 +184,8 @@ export interface CodexSessionRuntimeOptions {
* registered after thread open via `skills/extraRoots/set`.
*/
readonly additionalRoots?: ReadonlyArray<string>;
/** Every repo root the session works in, including `cwd` when it is one. */
readonly repoRoots?: ReadonlyArray<string>;
readonly runtimeMode: RuntimeMode;
readonly model?: string;
readonly serviceTier?: CodexServiceTier | undefined;
Expand Down Expand Up @@ -597,7 +600,7 @@ function buildCodexTurnInstructions(input: {
readonly modelName?: string;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;
readonly multiRepo?: boolean;
readonly multiRepo?: MultiRepoWorkspace | undefined;
}): Pick<CodexTurnStartParamsWithCollaborationMode, "collaborationMode" | "additionalContext"> {
if (input.interactionMode === undefined) {
return {};
Expand Down Expand Up @@ -640,8 +643,8 @@ export function buildTurnStartParams(input: {
readonly interactionMode?: ProviderInteractionMode;
/** Defaults to true so callers that predate the agent-access gate are unchanged. */
readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;
/** The session spans several repository roots. */
readonly multiRepo?: boolean;
/** Set when the session has roots beyond its working directory. */
readonly multiRepo?: MultiRepoWorkspace | undefined;
}): Effect.Effect<
CodexTurnStartParamsWithCollaborationMode,
CodexErrors.CodexAppServerProtocolParseError
Expand All @@ -664,7 +667,7 @@ export function buildTurnStartParams(input: {
...(input.modelName ? { modelName: input.modelName } : {}),
...(input.effort ? { effort: input.effort } : {}),
browserToolsAvailable: input.browserToolsAvailable ?? true,
...(input.multiRepo ? { multiRepo: true } : {}),
...(input.multiRepo ? { multiRepo: input.multiRepo } : {}),
});

return decodeCodexTurnStartParamsWithCollaborationMode({
Expand Down Expand Up @@ -2608,7 +2611,7 @@ export const makeCodexSessionRuntime = (
options.appServerArgs,
options.mcpCapabilities,
),
multiRepo: (options.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(options),
});
yield* Ref.set(lastAdditionalContextRef, params.additionalContext);
const rawResponse = yield* client.raw.request("turn/start", params);
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ import type * as EffectAcpSchema from "effect-acp/schema";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
import {
buildRuntimeInstructions,
multiRepoWorkspace,
type MultiRepoWorkspace,
} from "../RuntimeInstructions.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import {
ProviderAdapterProcessError,
Expand Down Expand Up @@ -138,8 +142,8 @@ interface CursorSessionContext {
session: ProviderSession;
readonly scope: Scope.Closeable;
readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"];
/** The session spans several repository roots. */
readonly multiRepo: boolean;
/** Set when the session has roots beyond its working directory. */
readonly multiRepo: MultiRepoWorkspace | undefined;
notificationFiber: Fiber.Fiber<void, never> | undefined;
readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;
Expand Down Expand Up @@ -795,7 +799,7 @@ export function makeCursorAdapter(
session,
scope: sessionScope,
acp,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(input),
notificationFiber: undefined,
pendingApprovals,
pendingUserInputs,
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import type * as EffectAcpSchema from "effect-acp/schema";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
import {
buildRuntimeInstructions,
multiRepoWorkspace,
type MultiRepoWorkspace,
} from "../RuntimeInstructions.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import {
ProviderAdapterProcessError,
Expand Down Expand Up @@ -138,8 +142,8 @@ interface GrokSessionContext {
session: ProviderSession;
readonly scope: Scope.Closeable;
readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"];
/** The session spans several repository roots. */
readonly multiRepo: boolean;
/** Set when the session has roots beyond its working directory. */
readonly multiRepo: MultiRepoWorkspace | undefined;
notificationFiber: Fiber.Fiber<void, never> | undefined;
readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;
Expand Down Expand Up @@ -1292,7 +1296,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
session,
scope: sessionScope,
acp,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(input),
notificationFiber: undefined,
pendingApprovals,
pendingUserInputs,
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/Layers/OpenCodeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import {
ProviderAdapterSessionNotFoundError,
ProviderAdapterValidationError,
} from "../Errors.ts";
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
import {
buildRuntimeInstructions,
multiRepoWorkspace,
type MultiRepoWorkspace,
} from "../RuntimeInstructions.ts";
import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";
import {
buildOpenCodePermissionRules,
Expand Down Expand Up @@ -341,8 +345,8 @@ interface OpenCodeSessionContext {
readonly client: OpencodeClient;
readonly server: OpenCodeServerConnection;
readonly directory: string;
/** The session spans several repository roots. */
readonly multiRepo: boolean;
/** Set when the session has roots beyond its working directory. */
readonly multiRepo: MultiRepoWorkspace | undefined;
openCodeSessionId: string;
readonly relatedSessionIds: Set<string>;
readonly resolvedRequestIds: Set<string>;
Expand Down Expand Up @@ -3010,7 +3014,7 @@ export function makeOpenCodeAdapter(
client: started.client,
server: started.server,
directory,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
multiRepo: multiRepoWorkspace(input),
openCodeSessionId: started.openCodeSession.id,
relatedSessionIds: new Set([started.openCodeSession.id]),
resolvedRequestIds: new Set(),
Expand Down
Loading
Loading