authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-05 15:34:37-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 15:34:37-07:00
log2afe87ea4bb94f4dbbd1660a556cbffefd7498a2
tree86e3f4e85d1a4b16f50c39d600f07643e638033b
parentcbb731beb36db2330c9a2632b11354315658ea21
signature Signed by PGP key B5690EEEBB952194

feat: new branches use the shorter t3/ prefix (#16220)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

9 files changed, 59 insertions(+), 53 deletions(-)

apps/mobile/src/features/settings/components/BranchNamingSettings.tsx+1-1
......@@ -48,7 +48,7 @@ export function BranchNamingSettings(props: {
4848 {props.mode === "static" ? (
4949 <View className="gap-2 px-4 py-3">
5050 <Text className="text-sm text-foreground-muted">
51 Use t3code or t3code/ for t3code/add-search. Leave empty for no prefix.
51 Use t3 or t3/ for t3/add-search. Leave empty for no prefix.
5252 </Text>
5353 <AppTextInput
5454 key={props.prefix}
apps/server/src/orchestration-v2/ThreadLaunchService.test.ts+18-18
......@@ -1104,7 +1104,7 @@ it.effect("names the worktree itself when the client provides no branch", () =>
11041104 yield* waitUntil(() => Effect.sync(() => harness.createWorktree.mock.calls.length === 1));
11051105 assert.match(
11061106 harness.createWorktree.mock.calls[0]?.[0]?.newRefName ?? "",
1107 /^t3code\/[0-9a-f]{8}$/u,
1107 /^t3\/[0-9a-f]{8}$/u,
11081108 );
11091109 yield* waitUntil(() =>
11101110 threads
......@@ -1115,7 +1115,7 @@ it.effect("names the worktree itself when the client provides no branch", () =>
11151115 }),
11161116);
11171117
1118it.effect("renames a temporary t3code/<hash> branch off the provisioning critical path", () =>
1118it.effect("renames a temporary t3/<hash> branch off the provisioning critical path", () =>
11191119 Effect.gen(function* () {
11201120 const branchNameStarted = yield* Deferred.make<void>();
11211121 const allowBranchName = yield* Deferred.make<void>();
......@@ -1138,11 +1138,11 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica
11381138 command: "command:launch:temp-branch",
11391139 thread: "thread:launch:temp-branch",
11401140 message: "Build the feature",
1141 workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" },
1141 workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" },
11421142 }),
11431143 );
11441144 yield* Deferred.await(branchNameStarted);
1145 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3code/abcd1234");
1145 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3/abcd1234");
11461146 yield* waitUntil(() =>
11471147 threads
11481148 .getThreadProjection(launched.threadId)
......@@ -1150,7 +1150,7 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica
11501150 );
11511151 assert.equal(
11521152 (yield* threads.getThreadProjection(launched.threadId)).thread.branch,
1153 "t3code/abcd1234",
1153 "t3/abcd1234",
11541154 );
11551155 yield* Deferred.succeed(allowBranchName, undefined);
11561156 yield* waitUntil(() =>
......@@ -1160,17 +1160,17 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica
11601160 );
11611161 assert.deepEqual(harness.renameBranch.mock.calls[0]?.[0], {
11621162 cwd: "/repo-worktrees/temp",
1163 oldBranch: "t3code/abcd1234",
1163 oldBranch: "t3/abcd1234",
11641164 newBranch: "generated-branch",
11651165 });
11661166 }).pipe(Effect.provide(harness.layer));
11671167 }),
11681168);
11691169
1170it.effect("provisions under t3code-<hash> when a plain t3code branch blocks t3code/*", () =>
1170it.effect("provisions under t3-<hash> when a plain t3 branch blocks t3/*", () =>
11711171 Effect.gen(function* () {
11721172 const harness = makeHarness({
1173 hasCommit: (input) => Effect.succeed(input.refName === "refs/heads/t3code"),
1173 hasCommit: (input) => Effect.succeed(input.refName === "refs/heads/t3"),
11741174 createWorktree: (input) =>
11751175 Effect.succeed({
11761176 worktree: { path: "/repo-worktrees/temp", refName: input.newRefName, headSha: "abc" },
......@@ -1184,7 +1184,7 @@ it.effect("provisions under t3code-<hash> when a plain t3code branch blocks t3co
11841184 command: "command:launch:blocked-namespace",
11851185 thread: "thread:launch:blocked-namespace",
11861186 message: "Build the feature",
1187 workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" },
1187 workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" },
11881188 }),
11891189 );
11901190 yield* waitUntil(() =>
......@@ -1192,8 +1192,8 @@ it.effect("provisions under t3code-<hash> when a plain t3code branch blocks t3co
11921192 .getThreadProjection(launched.threadId)
11931193 .pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")),
11941194 );
1195 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3code-abcd1234");
1196 assert.equal(harness.renameBranch.mock.calls[0]?.[0]?.oldBranch, "t3code-abcd1234");
1195 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3-abcd1234");
1196 assert.equal(harness.renameBranch.mock.calls[0]?.[0]?.oldBranch, "t3-abcd1234");
11971197 }).pipe(Effect.provide(harness.layer));
11981198 }),
11991199);
......@@ -1235,11 +1235,11 @@ it.effect("keeps the temporary branch when branch generation fails", () =>
12351235 command: "command:launch:branch-fallback",
12361236 thread: "thread:launch:branch-fallback",
12371237 message: "Build the feature",
1238 workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" },
1238 workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" },
12391239 }),
12401240 );
12411241 yield* waitUntil(() => Effect.sync(() => harness.generateBranchName.mock.calls.length === 1));
1242 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3code/abcd1234");
1242 assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, "t3/abcd1234");
12431243 yield* waitUntil(() =>
12441244 threads
12451245 .getThreadProjection(launched.threadId)
......@@ -1248,7 +1248,7 @@ it.effect("keeps the temporary branch when branch generation fails", () =>
12481248 assert.equal(harness.renameBranch.mock.calls.length, 0);
12491249 assert.equal(
12501250 (yield* threads.getThreadProjection(launched.threadId)).thread.branch,
1251 "t3code/abcd1234",
1251 "t3/abcd1234",
12521252 );
12531253 }).pipe(Effect.provide(harness.layer));
12541254 }),
......@@ -1267,8 +1267,8 @@ it.effect("renames a temporary branch on an existing worktree to a generated nam
12671267 message: "Build the feature",
12681268 workspace: {
12691269 type: "existing_worktree",
1270 worktreePath: "/repo-worktrees/t3code-abcd1234",
1271 branch: "t3code/abcd1234",
1270 worktreePath: "/repo-worktrees/t3-abcd1234",
1271 branch: "t3/abcd1234",
12721272 },
12731273 }),
12741274 );
......@@ -1278,8 +1278,8 @@ it.effect("renames a temporary branch on an existing worktree to a generated nam
12781278 .pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")),
12791279 );
12801280 assert.deepEqual(harness.renameBranch.mock.calls[0]?.[0], {
1281 cwd: "/repo-worktrees/t3code-abcd1234",
1282 oldBranch: "t3code/abcd1234",
1281 cwd: "/repo-worktrees/t3-abcd1234",
1282 oldBranch: "t3/abcd1234",
12831283 newBranch: "generated-branch",
12841284 });
12851285 }).pipe(Effect.provide(harness.layer));
apps/server/src/orchestration-v2/ThreadLaunchService.ts+1-1
......@@ -304,7 +304,7 @@ const make = Effect.gen(function* () {
304304 });
305305
306306 // The server owns worktree naming: without an explicit branch, provision
307 // under a temporary `t3code/<hash>` name so the worktree never waits on
307 // under a temporary `t3/<hash>` name so the worktree never waits on
308308 // name generation, then rename in the background below.
309309 const requestedBranch = input.workspaceStrategy.branch;
310310 let branch: string | null;
apps/web/src/components/settings/BranchNamingSettings.tsx+1-1
......@@ -76,7 +76,7 @@ export function BranchNamingSettings() {
7676 serverScoped
7777 settingKeys={["branchNamePrefix"]}
7878 title="Branch prefix"
79 description="For example, t3code or t3code/ produces t3code/add-search. Leave empty for no prefix."
79 description="For example, t3 or t3/ produces t3/add-search. Leave empty for no prefix."
8080 resetAction={
8181 prefixMixed ||
8282 settings.branchNamePrefix !== DEFAULT_SERVER_SETTINGS.branchNamePrefix ? (
docs/user/project-settings.md+1-1
......@@ -42,7 +42,7 @@ and other phone-only settings ignore the filter.
4242
4343In **Settings → Source Control → Worktree branch naming**, choose a static prefix,
4444a model-selected semantic prefix such as `feat/` or `fix/`, or custom instructions
45for the complete name. The static prefix defaults to `t3code/`; a trailing slash is
45for the complete name. The static prefix defaults to `t3/`; a trailing slash is
4646optional, and an empty prefix adds nothing. Invalid characters in a static prefix
4747are replaced with hyphens. Custom instructions are appended to
4848the naming prompt and can specify issue IDs, namespaces, and casing.
packages/contracts/src/settings.test.ts+2-2
......@@ -1085,10 +1085,10 @@ it("validates remote device hosts and rejects ambiguous host ids", () => {
10851085});
10861086
10871087describe("branch naming settings", () => {
1088 it("defaults existing settings to the t3code static prefix", () => {
1088 it("defaults existing settings to the t3 static prefix", () => {
10891089 expect(decodeServerSettings({})).toMatchObject({
10901090 branchNamingMode: "static",
1091 branchNamePrefix: "t3code",
1091 branchNamePrefix: "t3",
10921092 branchNameInstructions: "",
10931093 });
10941094 });
packages/contracts/src/settings.ts+1-1
......@@ -1368,7 +1368,7 @@ export const ServerSettings = Schema.Struct({
13681368 branchNamingMode: BranchNamingMode.pipe(
13691369 Schema.withDecodingDefault(Effect.succeed("static" as const)),
13701370 ),
1371 branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("t3code"))),
1371 branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("t3"))),
13721372 branchNameInstructions: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
13731373 removeAgentCreditsOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
13741374 sourceControlWritingStyle: SourceControlWritingStyleSettings.pipe(
packages/shared/src/git.test.ts+17-14
......@@ -190,34 +190,37 @@ describe("isTemporaryWorktreeBranch", () => {
190190 );
191191 });
192192
193 it("matches legacy UUID-shaped temporary worktree refs from older mobile builds", () => {
193 it("matches legacy t3code temporary worktree refs", () => {
194 expect(isTemporaryWorktreeBranch("t3code/deadbeef")).toBe(true);
195 expect(isTemporaryWorktreeBranch("t3code-deadbeef")).toBe(true);
196 expect(isTemporaryWorktreeBranch("t3code/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12")).toBe(true);
194197 expect(
195198 isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12`),
196 ).toBe(true);
199 ).toBe(false);
197200 });
198201
199202 it("rejects UUID-shaped refs that are not RFC 4122 v4", () => {
200203 // version nibble is not 4
201 expect(
202 isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-1d48-b4f2-9cf0aa54ab12`),
203 ).toBe(false);
204 expect(isTemporaryWorktreeBranch("t3code/f4ae4e0e-f971-1d48-b4f2-9cf0aa54ab12")).toBe(false);
204205 // variant nibble is not [89ab]
205 expect(
206 isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-c4f2-9cf0aa54ab12`),
207 ).toBe(false);
206 expect(isTemporaryWorktreeBranch("t3code/f4ae4e0e-f971-4d48-c4f2-9cf0aa54ab12")).toBe(false);
208207 });
209208
210 it("matches the flat fallback used when a plain t3code branch exists", () => {
209 it("matches the flat fallback used when a plain t3 branch exists", () => {
211210 const flat = flattenTemporaryWorktreeBranchName(`${WORKTREE_BRANCH_PREFIX}/deadbeef`);
212211 expect(flat).toBe(`${WORKTREE_BRANCH_PREFIX}-deadbeef`);
213212 expect(isTemporaryWorktreeBranch(flat)).toBe(true);
214213 expect(isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}-deadbeef-extra`)).toBe(false);
215214 expect(isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}-feature`)).toBe(false);
216 expect(
217 flattenTemporaryWorktreeBranchName(
218 `${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12`,
219 ),
220 ).toBe(`${WORKTREE_BRANCH_PREFIX}-f4ae4e0e`);
215 expect(flattenTemporaryWorktreeBranchName("t3code/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12")).toBe(
216 `${WORKTREE_BRANCH_PREFIX}-f4ae4e0e`,
217 );
218 expect(flattenTemporaryWorktreeBranchName("t3code-deadbeef")).toBe(
219 `${WORKTREE_BRANCH_PREFIX}-deadbeef`,
220 );
221 expect(flattenTemporaryWorktreeBranchName("t3code/deadbeef")).toBe(
222 `${WORKTREE_BRANCH_PREFIX}-deadbeef`,
223 );
221224 });
222225
223226 it("rejects non-temporary refName names", () => {
packages/shared/src/git.ts+17-14
......@@ -11,14 +11,18 @@ import * as Arr from "effect/Array";
1111import * as Result from "effect/Result";
1212import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts";
1313
14export const WORKTREE_BRANCH_PREFIX = "t3code";
15// Canonical form is `t3code/<8 hex>`. Older mobile builds generated `t3code/<uuid>`
16// via Crypto.randomUUID() (always RFC 4122 v4), so the matcher also accepts exactly
17// that shape — version nibble `4`, variant nibble `[89ab]` — to keep those threads
18// eligible for branch regeneration without loosening beyond what was ever generated.
19// `t3code-<8 hex>` is the fallback when a plain `t3code` branch blocks the namespace.
14export const WORKTREE_BRANCH_PREFIX = "t3";
15// Canonical form is `t3/<8 hex>`. `t3-<8 hex>` is the fallback when a plain `t3`
16// branch blocks the namespace. The matcher also accepts every legacy shape, so
17// existing threads stay eligible for branch regeneration: `t3code/<8 hex>` and
18// `t3code-<8 hex>` from before the prefix was shortened, and `t3code/<uuid>` from
19// older mobile builds that used Crypto.randomUUID() (always RFC 4122 v4, so version
20// nibble `4` and variant nibble `[89ab]`). Nothing looser than what was generated.
21const TEMP_WORKTREE_HEX_TOKEN = "[0-9a-f]{8}";
22const TEMP_WORKTREE_UUID_V4_TOKEN =
23 "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
2024const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(
21 `^${WORKTREE_BRANCH_PREFIX}(?:-[0-9a-f]{8}$|\\/(?:[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$)`,
25 `^(?:${WORKTREE_BRANCH_PREFIX}[-/]${TEMP_WORKTREE_HEX_TOKEN}|t3code(?:[-/]${TEMP_WORKTREE_HEX_TOKEN}|\\/${TEMP_WORKTREE_UUID_V4_TOKEN}))$`,
2226);
2327
2428/**
......@@ -125,15 +129,14 @@ export function buildTemporaryWorktreeBranchName(
125129}
126130
127131/**
128 * Git stores refs as paths, so a plain `t3code` branch makes every `t3code/<hex>`
129 * ref impossible. This moves a temporary name to the flat `t3code-<hex>` sibling.
132 * Git stores refs as paths, so a plain `t3` branch makes every `t3/<hex>`
133 * ref impossible. This moves a temporary name to the flat `t3-<hex>` sibling.
130134 */
131135export function flattenTemporaryWorktreeBranchName(refName: string): string {
132 // Keep only the canonical 8-hex token so legacy UUID names stay recognizable.
133 const token = refName
134 .trim()
135 .toLowerCase()
136 .slice(WORKTREE_BRANCH_PREFIX.length + 1, WORKTREE_BRANCH_PREFIX.length + 9);
136 // Keep only the canonical 8-hex token so legacy `t3code/` and UUID names map cleanly.
137 const normalized = refName.trim().toLowerCase();
138 const tokenStart = normalized.search(/[-/]/) + 1;
139 const token = normalized.slice(tokenStart, tokenStart + 8);
137140 return `${WORKTREE_BRANCH_PREFIX}-${token}`;
138141}
139142