Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8416ecd
Add AI-generated worktree branch naming and safe branch rename
juliusmarminge Mar 1, 2026
06912c3
Move worktree branch auto-rename into provider turn reactor
juliusmarminge Mar 1, 2026
61e1bab
Scope upstream refresh in checkout effect lifecycle
juliusmarminge Mar 1, 2026
0df68cc
Remove git.renameBranch RPC and harden branch rename args
juliusmarminge Mar 1, 2026
9b0fe5e
Generate temporary worktree branch names from UUID tokens
juliusmarminge Mar 1, 2026
d3d0a08
Resolve persisted attachment paths across server pipelines
juliusmarminge Mar 1, 2026
44e409f
nit
juliusmarminge Mar 1, 2026
4bc43a0
data uri for app server
juliusmarminge Mar 1, 2026
ad79b5a
nit
juliusmarminge Mar 1, 2026
700579b
fine there
juliusmarminge Mar 1, 2026
0e146e2
Handle branch-name generation failures as typed errors
juliusmarminge Mar 1, 2026
e64ad4b
Use Effect Schema to decode Codex structured outputs
juliusmarminge Mar 1, 2026
e1a8ee1
Handle parameterized base64 image data URLs
juliusmarminge Mar 1, 2026
875e6ed
Inject test ServerConfig across orchestration and git layers
juliusmarminge Mar 1, 2026
cd732a2
fix: address PR review feedback
juliusmarminge Mar 1, 2026
5850315
Switch image attachments to persisted ID-based paths
juliusmarminge Mar 1, 2026
88dc6c9
Harden attachment ID resolution and missing-file handling
juliusmarminge Mar 1, 2026
fb92741
Read stdin before validating required image flag in codex test stub
juliusmarminge Mar 2, 2026
dadda0d
Reuse attachment thread segment sanitizer in projection pipeline
juliusmarminge Mar 2, 2026
afc0c78
Fix attachment cleanup to match exact thread segments
juliusmarminge Mar 2, 2026
39f0786
Normalize attachment thread segments to lowercase
juliusmarminge Mar 2, 2026
150e171
Remove unused attachment route path helpers
juliusmarminge Mar 2, 2026
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
17 changes: 17 additions & 0 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {

import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";
import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";
import { GitCore, type GitCoreShape } from "../src/git/Services/GitCore.ts";
import { TextGeneration, type TextGenerationShape } from "../src/git/Services/TextGeneration.ts";
import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts";
import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";
import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts";
Expand Down Expand Up @@ -227,8 +229,23 @@ export const makeOrchestrationIntegrationHarness = Effect.gen(function* () {
const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
);
const gitCoreLayer = Layer.succeed(
GitCore,
{
renameBranch: (input: Parameters<GitCoreShape["renameBranch"]>[0]) =>
Effect.succeed({ branch: input.newBranch }),
} as unknown as GitCoreShape,
);
const textGenerationLayer = Layer.succeed(
TextGeneration,
{
generateBranchName: () => Effect.succeed({ branch: null }),
} as unknown as TextGenerationShape,
);
const providerCommandReactorLayer = ProviderCommandReactorLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(gitCoreLayer),
Layer.provideMerge(textGenerationLayer),
);
const checkpointReactorLayer = CheckpointReactorLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
Expand Down
54 changes: 54 additions & 0 deletions apps/server/src/attachmentPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import path from "node:path";

import { ATTACHMENTS_ROUTE_PREFIX } from "./projectFaviconRoute.ts";

export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null {
const normalized = path.normalize(rawRelativePath).replace(/^[/\\]+/, "");
if (
normalized.length === 0 ||
normalized.startsWith("..") ||
normalized.includes("\0")
) {
return null;
}
return normalized.replace(/\\/g, "/");
}

export function attachmentRouteToRelativePath(dataUrl: string): string | null {
const prefix = `${ATTACHMENTS_ROUTE_PREFIX}/`;
if (!dataUrl.startsWith(prefix)) {
return null;
}
return normalizeAttachmentRelativePath(dataUrl.slice(prefix.length));
}

export function resolveAttachmentRelativePath(input: {
readonly stateDir: string;
readonly relativePath: string;
}): string | null {
const normalizedRelativePath = normalizeAttachmentRelativePath(input.relativePath);
if (!normalizedRelativePath) {
return null;
}

const attachmentsRoot = path.resolve(path.join(input.stateDir, "attachments"));
const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath));
if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) {
return null;
}
return filePath;
}

export function resolveAttachmentRoutePath(input: {
readonly stateDir: string;
readonly dataUrl: string;
}): string | null {
const relativePath = attachmentRouteToRelativePath(input.dataUrl);
if (!relativePath) {
return null;
}
return resolveAttachmentRelativePath({
stateDir: input.stateDir,
relativePath,
});
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
226 changes: 225 additions & 1 deletion apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,28 @@ import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, Path } from "effect";
import { expect } from "vitest";

import { type ServerConfigShape, ServerConfig } from "../../config.ts";
import { CodexTextGenerationLive } from "./CodexTextGeneration.ts";
import { TextGenerationError } from "../Errors.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";

function makeTestServerConfig(stateDir: string): ServerConfigShape {
return {
mode: "web",
port: 0,
host: undefined,
cwd: process.cwd(),
keybindingsConfigPath: "",
stateDir,
staticDir: undefined,
devUrl: undefined,
noBrowser: true,
authToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
};
}

function makeFakeCodexBinary(dir: string) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand All @@ -21,13 +39,36 @@ function makeFakeCodexBinary(dir: string) {
"#!/bin/sh",
'output_path=""',
"while [ $# -gt 0 ]; do",
' if [ "$1" = "--image" ]; then',
" shift",
' if [ -n "$1" ]; then',
' seen_image="1"',
" fi",
" continue",
" fi",
' if [ "$1" = "--output-last-message" ]; then',
" shift",
' output_path="$1"',
" fi",
" shift",
"done",
"cat >/dev/null",
'if [ "$T3_FAKE_CODEX_REQUIRE_IMAGE" = "1" ] && [ "$seen_image" != "1" ]; then',
' printf "%s\\n" "missing --image input" >&2',
" exit 2",
"fi",
'stdin_content="$(cat)"',
'if [ -n "$T3_FAKE_CODEX_STDIN_MUST_CONTAIN" ]; then',
' printf "%s" "$stdin_content" | grep -F -- "$T3_FAKE_CODEX_STDIN_MUST_CONTAIN" >/dev/null || {',
' printf "%s\\n" "stdin missing expected content" >&2',
" exit 3",
" }",
"fi",
'if [ -n "$T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN" ]; then',
' if printf "%s" "$stdin_content" | grep -F -- "$T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN" >/dev/null; then',
' printf "%s\\n" "stdin contained forbidden content" >&2',
" exit 4",
" fi",
"fi",
'if [ -n "$T3_FAKE_CODEX_STDERR" ]; then',
' printf "%s\\n" "$T3_FAKE_CODEX_STDERR" >&2',
"fi",
Expand All @@ -48,6 +89,9 @@ function withFakeCodexEnv<A, E, R>(
output: string;
exitCode?: number;
stderr?: string;
requireImage?: boolean;
stdinMustContain?: string;
stdinMustNotContain?: string;
},
effect: Effect.Effect<A, E, R>,
) {
Expand All @@ -60,6 +104,9 @@ function withFakeCodexEnv<A, E, R>(
const previousOutput = process.env.T3_FAKE_CODEX_OUTPUT_B64;
const previousExitCode = process.env.T3_FAKE_CODEX_EXIT_CODE;
const previousStderr = process.env.T3_FAKE_CODEX_STDERR;
const previousRequireImage = process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;
const previousStdinMustContain = process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;
const previousStdinMustNotContain = process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;

yield* Effect.sync(() => {
process.env.PATH = `${binDir}:${previousPath ?? ""}`;
Expand All @@ -76,13 +123,34 @@ function withFakeCodexEnv<A, E, R>(
} else {
delete process.env.T3_FAKE_CODEX_STDERR;
}

if (input.requireImage) {
process.env.T3_FAKE_CODEX_REQUIRE_IMAGE = "1";
} else {
delete process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;
}

if (input.stdinMustContain !== undefined) {
process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN = input.stdinMustContain;
} else {
delete process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;
}

if (input.stdinMustNotContain !== undefined) {
process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN = input.stdinMustNotContain;
} else {
delete process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;
}
});

return {
previousPath,
previousOutput,
previousExitCode,
previousStderr,
previousRequireImage,
previousStdinMustContain,
previousStdinMustNotContain,
};
}),
() => effect,
Expand All @@ -107,6 +175,24 @@ function withFakeCodexEnv<A, E, R>(
} else {
process.env.T3_FAKE_CODEX_STDERR = previous.previousStderr;
}

if (previous.previousRequireImage === undefined) {
delete process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;
} else {
process.env.T3_FAKE_CODEX_REQUIRE_IMAGE = previous.previousRequireImage;
}

if (previous.previousStdinMustContain === undefined) {
delete process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;
} else {
process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN = previous.previousStdinMustContain;
}

if (previous.previousStdinMustNotContain === undefined) {
delete process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;
} else {
process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN = previous.previousStdinMustNotContain;
}
}),
);
}
Expand Down Expand Up @@ -170,6 +256,144 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
),
);

it.effect("generates branch names and normalizes branch fragments", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
branch: " Feat/Session ",
}),
stdinMustNotContain: "Image attachments supplied to the model",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateBranchName({
cwd: process.cwd(),
message: "Please update session handling.",
});

expect(generated.branch).toBe("feat/session");
}),
),
);

it.effect("omits attachment metadata section when no attachments are provided", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
branch: "fix/session-timeout",
}),
stdinMustNotContain: "Attachment metadata:",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateBranchName({
cwd: process.cwd(),
message: "Fix timeout behavior.",
});

expect(generated.branch).toBe("fix/session-timeout");
}),
),
);

it.effect("passes image attachments through as codex image inputs", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
branch: "fix/ui-regression",
}),
requireImage: true,
stdinMustContain: "Attachment metadata:",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateBranchName({
cwd: process.cwd(),
message: "Fix layout bug from screenshot.",
attachments: [
{
type: "image",
name: "bug.png",
mimeType: "image/png",
sizeBytes: 5,
dataUrl: "data:image/png;base64,SGVsbG8=",
},
],
});

expect(generated.branch).toBe("fix/ui-regression");
}),
),
);

it.effect("resolves persisted attachment URLs to files for codex image inputs", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
branch: "fix/ui-regression",
}),
requireImage: true,
},
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const stateDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-state-textgen-" });
const imagePath = path.join(
stateDir,
"attachments",
"thread-1",
"message-1-0.png",
);
yield* fs.makeDirectory(path.join(stateDir, "attachments", "thread-1"), {
recursive: true,
});
yield* fs.writeFile(imagePath, Buffer.from("hello"));

const textGeneration = yield* TextGeneration;
const generated = yield* textGeneration
.generateBranchName({
cwd: process.cwd(),
message: "Fix layout bug from screenshot.",
attachments: [
{
type: "image",
name: "bug.png",
mimeType: "image/png",
sizeBytes: 5,
dataUrl: "/attachments/thread-1/message-1-0.png",
},
],
})
.pipe(Effect.provideService(ServerConfig, makeTestServerConfig(stateDir)));

expect(generated.branch).toBe("fix/ui-regression");
}),
),
);

it.effect("returns null branch when codex returns the wrong object shape", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
title: "This is not a branch payload",
}),
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateBranchName({
cwd: process.cwd(),
message: "Fix websocket reconnect flake",
});

expect(generated.branch).toBeNull();
}),
),
);

it.effect("returns typed TextGenerationError when codex exits non-zero", () =>
withFakeCodexEnv(
{
Expand Down
Loading