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 @@ -15,6 +15,7 @@ function makeProject(
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeListItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function makeProject(id: string, title: string): EnvironmentProject {
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function makeProject(
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ export function buildHomeThreadGroups(input: {
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: pendingTask.message.createdAt,
updatedAt: pendingTask.message.createdAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const projects: ReadonlyArray<EnvironmentProject> = [
repositoryIdentity: null,
defaultModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-07-16T08:00:00.000Z",
updatedAt: "2026-07-16T08:00:00.000Z",
Expand All @@ -28,6 +29,7 @@ const projects: ReadonlyArray<EnvironmentProject> = [
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-07-16T08:00:00.000Z",
updatedAt: "2026-07-16T08:00:00.000Z",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
repositoryIdentity: null,
defaultModelSelection: editingPendingTask.modelSelection ?? null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: editingPendingTask.createdAt,
updatedAt: editingPendingTask.createdAt,
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/features/threads/use-project-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export function useCreateProjectThread() {
branch: input.branch,
worktreePath: input.worktreePath,
startFromOrigin: input.startFromOrigin ?? false,
worktreeBranchName: buildTemporaryWorktreeBranchName(randomHex),
worktreeBranchName: buildTemporaryWorktreeBranchName(
randomHex,
input.project.worktreeBranchPrefix,
),
}),
});
if (AsyncResult.isFailure(result)) {
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/lib/repositoryGroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function makeProject(
repositoryIdentity: null,
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/state/use-thread-outbox-drain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export function useThreadOutboxDrain(): void {
branch: creation.branch,
worktreePath: creation.worktreePath,
startFromOrigin: creation.startFromOrigin ?? false,
// Queued creations don't carry the project's prefix; this is only the
// temporary branch, which the server renames to the project's
// configured `<prefix>/<slug>` on the first turn.
worktreeBranchName: buildTemporaryWorktreeBranchName(randomHex),
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe("OrchestrationEngine", () => {
model: "gpt-5-codex",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-03-03T00:00:00.000Z",
updatedAt: "2026-03-03T00:00:01.000Z",
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
workspaceRoot: event.payload.workspaceRoot,
defaultModelSelection: event.payload.defaultModelSelection,
gitHubAccount: event.payload.gitHubAccount ?? null,
worktreeBranchPrefix: event.payload.worktreeBranchPrefix ?? null,
scripts: event.payload.scripts,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
Expand All @@ -522,6 +523,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
...(event.payload.gitHubAccount !== undefined
? { gitHubAccount: event.payload.gitHubAccount }
: {}),
...(event.payload.worktreeBranchPrefix !== undefined
? { worktreeBranchPrefix: event.payload.worktreeBranchPrefix }
: {}),
...(event.payload.scripts !== undefined ? { scripts: event.payload.scripts } : {}),
updatedAt: event.payload.updatedAt,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
model: "gpt-5-codex",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [
{
id: "script-1",
Expand Down Expand Up @@ -383,6 +384,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
model: "gpt-5-codex",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [
{
id: "script-1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ function mapProjectShellRow(
repositoryIdentity,
defaultModelSelection: row.defaultModelSelection,
gitHubAccount: row.gitHubAccount,
worktreeBranchPrefix: row.worktreeBranchPrefix,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -311,6 +312,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
workspace_root AS "workspaceRoot",
default_model_selection_json AS "defaultModelSelection",
github_account_json AS "gitHubAccount",
worktree_branch_prefix AS "worktreeBranchPrefix",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -685,6 +687,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
workspace_root AS "workspaceRoot",
default_model_selection_json AS "defaultModelSelection",
github_account_json AS "gitHubAccount",
worktree_branch_prefix AS "worktreeBranchPrefix",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand All @@ -708,6 +711,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
workspace_root AS "workspaceRoot",
default_model_selection_json AS "defaultModelSelection",
github_account_json AS "gitHubAccount",
worktree_branch_prefix AS "worktreeBranchPrefix",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -1190,6 +1194,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
repositoryIdentity: repositoryIdentities.get(row.projectId) ?? null,
defaultModelSelection: row.defaultModelSelection,
gitHubAccount: row.gitHubAccount,
worktreeBranchPrefix: row.worktreeBranchPrefix,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1317,6 +1322,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
workspaceRoot: row.workspaceRoot,
defaultModelSelection: row.defaultModelSelection,
gitHubAccount: row.gitHubAccount,
worktreeBranchPrefix: row.worktreeBranchPrefix,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1766,6 +1772,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
repositoryIdentity,
defaultModelSelection: option.value.defaultModelSelection,
gitHubAccount: option.value.gitHubAccount,
worktreeBranchPrefix: option.value.worktreeBranchPrefix,
scripts: option.value.scripts,
createdAt: option.value.createdAt,
updatedAt: option.value.updatedAt,
Expand Down
18 changes: 12 additions & 6 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type RuntimeMode,
type TurnId,
} from "@t3tools/contracts";
import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";
import { isTemporaryWorktreeBranch, sanitizeWorktreeBranchPrefix } from "@t3tools/shared/git";
import * as Cache from "effect/Cache";
import * as Cause from "effect/Cause";
import * as Crypto from "effect/Crypto";
Expand Down Expand Up @@ -166,15 +166,16 @@ function stalePendingRequestDetail(
return `Stale pending ${requestKind} request: ${requestId}. Provider callback state does not survive app restarts or recovered sessions. Restart the turn to continue.`;
}

function buildGeneratedWorktreeBranchName(raw: string): string {
function buildGeneratedWorktreeBranchName(raw: string, prefix?: string | null): string {
const resolvedPrefix = sanitizeWorktreeBranchPrefix(prefix);
const normalized = raw
.trim()
.toLowerCase()
.replace(/^refs\/heads\//, "")
.replace(/['"`]/g, "");

const withoutPrefix = normalized.startsWith(`${WORKTREE_BRANCH_PREFIX}/`)
? normalized.slice(`${WORKTREE_BRANCH_PREFIX}/`.length)
const withoutPrefix = normalized.startsWith(`${resolvedPrefix}/`)
? normalized.slice(`${resolvedPrefix}/`.length)
: normalized;

const branchFragment = withoutPrefix
Expand All @@ -186,7 +187,7 @@ function buildGeneratedWorktreeBranchName(raw: string): string {
.replace(/[./_-]+$/g, "");

const safeFragment = branchFragment.length > 0 ? branchFragment : "update";
return `${WORKTREE_BRANCH_PREFIX}/${safeFragment}`;
return `${resolvedPrefix}/${safeFragment}`;
}

const make = Effect.gen(function* () {
Expand Down Expand Up @@ -677,6 +678,7 @@ const make = Effect.gen(function* () {
readonly threadId: ThreadId;
readonly branch: string | null;
readonly worktreePath: string | null;
readonly worktreeBranchPrefix?: string | null;
readonly messageText: string;
readonly attachments?: ReadonlyArray<ChatAttachment>;
}) {
Expand Down Expand Up @@ -708,7 +710,10 @@ const make = Effect.gen(function* () {
});
if (!generated) return;

const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);
// Prefer the project's prefix, then the global default setting; an empty
// string or null resolves to the built-in default inside the builder.
const effectivePrefix = input.worktreeBranchPrefix ?? settings.worktreeBranchPrefix;
const targetBranch = buildGeneratedWorktreeBranchName(generated.branch, effectivePrefix);
if (targetBranch === oldBranch) return;

const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch });
Expand Down Expand Up @@ -822,6 +827,7 @@ const make = Effect.gen(function* () {
threadId: event.payload.threadId,
branch: thread.branch,
worktreePath: thread.worktreePath,
worktreeBranchPrefix: project?.worktreeBranchPrefix ?? null,
...generationInput,
}).pipe(Effect.forkScoped);

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration/commandInvariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const readModel: OrchestrationReadModel = {
model: "gpt-5-codex",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: now,
updatedAt: now,
Expand All @@ -50,6 +51,7 @@ const readModel: OrchestrationReadModel = {
model: "gpt-5-codex",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: now,
updatedAt: now,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
workspaceRoot: command.workspaceRoot,
defaultModelSelection: command.defaultModelSelection ?? null,
gitHubAccount: command.gitHubAccount ?? null,
worktreeBranchPrefix: command.worktreeBranchPrefix ?? null,
scripts: [],
createdAt: command.createdAt,
updatedAt: command.createdAt,
Expand Down Expand Up @@ -289,6 +290,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
? { defaultModelSelection: command.defaultModelSelection }
: {}),
...(command.gitHubAccount !== undefined ? { gitHubAccount: command.gitHubAccount } : {}),
...(command.worktreeBranchPrefix !== undefined
? { worktreeBranchPrefix: command.worktreeBranchPrefix }
: {}),
...(command.scripts !== undefined ? { scripts: command.scripts } : {}),
updatedAt: occurredAt,
},
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export function projectEvent(
workspaceRoot: payload.workspaceRoot,
defaultModelSelection: payload.defaultModelSelection,
gitHubAccount: payload.gitHubAccount ?? null,
worktreeBranchPrefix: payload.worktreeBranchPrefix ?? null,
scripts: payload.scripts,
createdAt: payload.createdAt,
updatedAt: payload.updatedAt,
Expand Down Expand Up @@ -247,6 +248,9 @@ export function projectEvent(
...(payload.gitHubAccount !== undefined
? { gitHubAccount: payload.gitHubAccount }
: {}),
...(payload.worktreeBranchPrefix !== undefined
? { worktreeBranchPrefix: payload.worktreeBranchPrefix }
: {}),
...(payload.scripts !== undefined ? { scripts: payload.scripts } : {}),
updatedAt: payload.updatedAt,
}
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/persistence/Layers/ProjectionProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
workspace_root,
default_model_selection_json,
github_account_json,
worktree_branch_prefix,
scripts_json,
created_at,
updated_at,
Expand All @@ -48,6 +49,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
${row.workspaceRoot},
${row.defaultModelSelection !== null ? JSON.stringify(row.defaultModelSelection) : null},
${row.gitHubAccount !== null ? JSON.stringify(row.gitHubAccount) : null},
${row.worktreeBranchPrefix},
${JSON.stringify(row.scripts)},
${row.createdAt},
${row.updatedAt},
Expand All @@ -59,6 +61,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
workspace_root = excluded.workspace_root,
default_model_selection_json = excluded.default_model_selection_json,
github_account_json = excluded.github_account_json,
worktree_branch_prefix = excluded.worktree_branch_prefix,
scripts_json = excluded.scripts_json,
created_at = excluded.created_at,
updated_at = excluded.updated_at,
Expand All @@ -77,6 +80,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
workspace_root AS "workspaceRoot",
default_model_selection_json AS "defaultModelSelection",
github_account_json AS "gitHubAccount",
worktree_branch_prefix AS "worktreeBranchPrefix",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand All @@ -97,6 +101,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
workspace_root AS "workspaceRoot",
default_model_selection_json AS "defaultModelSelection",
github_account_json AS "gitHubAccount",
worktree_branch_prefix AS "worktreeBranchPrefix",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
model: "gpt-5.4",
},
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts: [],
createdAt: "2026-03-24T00:00:00.000Z",
updatedAt: "2026-03-24T00:00:00.000Z",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import Migration0032 from "./Migrations/032_AuthPairingProofKeyThumbprint.ts";
import Migration0033 from "./Migrations/033_ProjectionThreadsSettled.ts";
import Migration0034 from "./Migrations/034_ProjectionThreadsSnoozed.ts";
import Migration0035 from "./Migrations/035_ProjectionProjectsGitHubAccount.ts";
import Migration0036 from "./Migrations/036_ProjectionProjectsWorktreeBranchPrefix.ts";

/**
* Migration loader with all migrations defined inline.
Expand Down Expand Up @@ -95,6 +96,7 @@ export const migrationEntries = [
[33, "ProjectionThreadsSettled", Migration0033],
[34, "ProjectionThreadsSnoozed", Migration0034],
[35, "ProjectionProjectsGitHubAccount", Migration0035],
[36, "ProjectionProjectsWorktreeBranchPrefix", Migration0036],
] as const;

export const makeMigrationLoader = (throughId?: number) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Effect from "effect/Effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";

export default Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const columns = yield* sql<{ readonly name: string }>`
PRAGMA table_info(projection_projects)
`;

if (!columns.some((column) => column.name === "worktree_branch_prefix")) {
yield* sql`
ALTER TABLE projection_projects
ADD COLUMN worktree_branch_prefix TEXT
`;
}
});
1 change: 1 addition & 0 deletions apps/server/src/persistence/Services/ProjectionProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ProjectionProject = Schema.Struct({
workspaceRoot: Schema.String,
defaultModelSelection: Schema.NullOr(ModelSelection),
gitHubAccount: Schema.NullOr(GitHubAccountRef),
worktreeBranchPrefix: Schema.NullOr(Schema.String),
scripts: Schema.Array(ProjectScript),
createdAt: IsoDateTime,
updatedAt: IsoDateTime,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/project/ProjectSetupScriptRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationPro
workspaceRoot: "/repo/project",
defaultModelSelection: null,
gitHubAccount: null,
worktreeBranchPrefix: null,
scripts,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
Expand Down
Loading