| author | |
| committer | |
| log | 2afe87ea4bb94f4dbbd1660a556cbffefd7498a2 |
| tree | 86e3f4e85d1a4b16f50c39d600f07643e638033b |
| parent | cbb731beb36db2330c9a2632b11354315658ea21 |
| signature | Signed by PGP key B5690EEEBB952194 |
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: { |
| 48 | 48 | {props.mode === "static" ? ( |
| 49 | 49 | <View className="gap-2 px-4 py-3"> |
| 50 | 50 | <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. | |
| 52 | 52 | </Text> |
| 53 | 53 | <AppTextInput |
| 54 | 54 | 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", () => |
| 1104 | 1104 | yield* waitUntil(() => Effect.sync(() => harness.createWorktree.mock.calls.length === 1)); |
| 1105 | 1105 | assert.match( |
| 1106 | 1106 | harness.createWorktree.mock.calls[0]?.[0]?.newRefName ?? "", |
| 1107 | /^t3code\/[0-9a-f]{8}$/u, | |
| 1107 | /^t3\/[0-9a-f]{8}$/u, | |
| 1108 | 1108 | ); |
| 1109 | 1109 | yield* waitUntil(() => |
| 1110 | 1110 | threads |
| ... | ... | @@ -1115,7 +1115,7 @@ it.effect("names the worktree itself when the client provides no branch", () => |
| 1115 | 1115 | }), |
| 1116 | 1116 | ); |
| 1117 | 1117 | |
| 1118 | it.effect("renames a temporary t3code/<hash> branch off the provisioning critical path", () => | |
| 1118 | it.effect("renames a temporary t3/<hash> branch off the provisioning critical path", () => | |
| 1119 | 1119 | Effect.gen(function* () { |
| 1120 | 1120 | const branchNameStarted = yield* Deferred.make<void>(); |
| 1121 | 1121 | const allowBranchName = yield* Deferred.make<void>(); |
| ... | ... | @@ -1138,11 +1138,11 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica |
| 1138 | 1138 | command: "command:launch:temp-branch", |
| 1139 | 1139 | thread: "thread:launch:temp-branch", |
| 1140 | 1140 | message: "Build the feature", |
| 1141 | workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" }, | |
| 1141 | workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" }, | |
| 1142 | 1142 | }), |
| 1143 | 1143 | ); |
| 1144 | 1144 | 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"); | |
| 1146 | 1146 | yield* waitUntil(() => |
| 1147 | 1147 | threads |
| 1148 | 1148 | .getThreadProjection(launched.threadId) |
| ... | ... | @@ -1150,7 +1150,7 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica |
| 1150 | 1150 | ); |
| 1151 | 1151 | assert.equal( |
| 1152 | 1152 | (yield* threads.getThreadProjection(launched.threadId)).thread.branch, |
| 1153 | "t3code/abcd1234", | |
| 1153 | "t3/abcd1234", | |
| 1154 | 1154 | ); |
| 1155 | 1155 | yield* Deferred.succeed(allowBranchName, undefined); |
| 1156 | 1156 | yield* waitUntil(() => |
| ... | ... | @@ -1160,17 +1160,17 @@ it.effect("renames a temporary t3code/<hash> branch off the provisioning critica |
| 1160 | 1160 | ); |
| 1161 | 1161 | assert.deepEqual(harness.renameBranch.mock.calls[0]?.[0], { |
| 1162 | 1162 | cwd: "/repo-worktrees/temp", |
| 1163 | oldBranch: "t3code/abcd1234", | |
| 1163 | oldBranch: "t3/abcd1234", | |
| 1164 | 1164 | newBranch: "generated-branch", |
| 1165 | 1165 | }); |
| 1166 | 1166 | }).pipe(Effect.provide(harness.layer)); |
| 1167 | 1167 | }), |
| 1168 | 1168 | ); |
| 1169 | 1169 | |
| 1170 | it.effect("provisions under t3code-<hash> when a plain t3code branch blocks t3code/*", () => | |
| 1170 | it.effect("provisions under t3-<hash> when a plain t3 branch blocks t3/*", () => | |
| 1171 | 1171 | Effect.gen(function* () { |
| 1172 | 1172 | const harness = makeHarness({ |
| 1173 | hasCommit: (input) => Effect.succeed(input.refName === "refs/heads/t3code"), | |
| 1173 | hasCommit: (input) => Effect.succeed(input.refName === "refs/heads/t3"), | |
| 1174 | 1174 | createWorktree: (input) => |
| 1175 | 1175 | Effect.succeed({ |
| 1176 | 1176 | 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 |
| 1184 | 1184 | command: "command:launch:blocked-namespace", |
| 1185 | 1185 | thread: "thread:launch:blocked-namespace", |
| 1186 | 1186 | message: "Build the feature", |
| 1187 | workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" }, | |
| 1187 | workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" }, | |
| 1188 | 1188 | }), |
| 1189 | 1189 | ); |
| 1190 | 1190 | yield* waitUntil(() => |
| ... | ... | @@ -1192,8 +1192,8 @@ it.effect("provisions under t3code-<hash> when a plain t3code branch blocks t3co |
| 1192 | 1192 | .getThreadProjection(launched.threadId) |
| 1193 | 1193 | .pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")), |
| 1194 | 1194 | ); |
| 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"); | |
| 1197 | 1197 | }).pipe(Effect.provide(harness.layer)); |
| 1198 | 1198 | }), |
| 1199 | 1199 | ); |
| ... | ... | @@ -1235,11 +1235,11 @@ it.effect("keeps the temporary branch when branch generation fails", () => |
| 1235 | 1235 | command: "command:launch:branch-fallback", |
| 1236 | 1236 | thread: "thread:launch:branch-fallback", |
| 1237 | 1237 | message: "Build the feature", |
| 1238 | workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" }, | |
| 1238 | workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" }, | |
| 1239 | 1239 | }), |
| 1240 | 1240 | ); |
| 1241 | 1241 | 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"); | |
| 1243 | 1243 | yield* waitUntil(() => |
| 1244 | 1244 | threads |
| 1245 | 1245 | .getThreadProjection(launched.threadId) |
| ... | ... | @@ -1248,7 +1248,7 @@ it.effect("keeps the temporary branch when branch generation fails", () => |
| 1248 | 1248 | assert.equal(harness.renameBranch.mock.calls.length, 0); |
| 1249 | 1249 | assert.equal( |
| 1250 | 1250 | (yield* threads.getThreadProjection(launched.threadId)).thread.branch, |
| 1251 | "t3code/abcd1234", | |
| 1251 | "t3/abcd1234", | |
| 1252 | 1252 | ); |
| 1253 | 1253 | }).pipe(Effect.provide(harness.layer)); |
| 1254 | 1254 | }), |
| ... | ... | @@ -1267,8 +1267,8 @@ it.effect("renames a temporary branch on an existing worktree to a generated nam |
| 1267 | 1267 | message: "Build the feature", |
| 1268 | 1268 | workspace: { |
| 1269 | 1269 | type: "existing_worktree", |
| 1270 | worktreePath: "/repo-worktrees/t3code-abcd1234", | |
| 1271 | branch: "t3code/abcd1234", | |
| 1270 | worktreePath: "/repo-worktrees/t3-abcd1234", | |
| 1271 | branch: "t3/abcd1234", | |
| 1272 | 1272 | }, |
| 1273 | 1273 | }), |
| 1274 | 1274 | ); |
| ... | ... | @@ -1278,8 +1278,8 @@ it.effect("renames a temporary branch on an existing worktree to a generated nam |
| 1278 | 1278 | .pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")), |
| 1279 | 1279 | ); |
| 1280 | 1280 | 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", | |
| 1283 | 1283 | newBranch: "generated-branch", |
| 1284 | 1284 | }); |
| 1285 | 1285 | }).pipe(Effect.provide(harness.layer)); |
apps/server/src/orchestration-v2/ThreadLaunchService.ts+1-1| ... | ... | @@ -304,7 +304,7 @@ const make = Effect.gen(function* () { |
| 304 | 304 | }); |
| 305 | 305 | |
| 306 | 306 | // 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 | |
| 308 | 308 | // name generation, then rename in the background below. |
| 309 | 309 | const requestedBranch = input.workspaceStrategy.branch; |
| 310 | 310 | let branch: string | null; |
apps/web/src/components/settings/BranchNamingSettings.tsx+1-1| ... | ... | @@ -76,7 +76,7 @@ export function BranchNamingSettings() { |
| 76 | 76 | serverScoped |
| 77 | 77 | settingKeys={["branchNamePrefix"]} |
| 78 | 78 | 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." | |
| 80 | 80 | resetAction={ |
| 81 | 81 | prefixMixed || |
| 82 | 82 | 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. |
| 42 | 42 | |
| 43 | 43 | In **Settings → Source Control → Worktree branch naming**, choose a static prefix, |
| 44 | 44 | a model-selected semantic prefix such as `feat/` or `fix/`, or custom instructions |
| 45 | for the complete name. The static prefix defaults to `t3code/`; a trailing slash is | |
| 45 | for the complete name. The static prefix defaults to `t3/`; a trailing slash is | |
| 46 | 46 | optional, and an empty prefix adds nothing. Invalid characters in a static prefix |
| 47 | 47 | are replaced with hyphens. Custom instructions are appended to |
| 48 | 48 | the 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", () => { |
| 1085 | 1085 | }); |
| 1086 | 1086 | |
| 1087 | 1087 | describe("branch naming settings", () => { |
| 1088 | it("defaults existing settings to the t3code static prefix", () => { | |
| 1088 | it("defaults existing settings to the t3 static prefix", () => { | |
| 1089 | 1089 | expect(decodeServerSettings({})).toMatchObject({ |
| 1090 | 1090 | branchNamingMode: "static", |
| 1091 | branchNamePrefix: "t3code", | |
| 1091 | branchNamePrefix: "t3", | |
| 1092 | 1092 | branchNameInstructions: "", |
| 1093 | 1093 | }); |
| 1094 | 1094 | }); |
packages/contracts/src/settings.ts+1-1| ... | ... | @@ -1368,7 +1368,7 @@ export const ServerSettings = Schema.Struct({ |
| 1368 | 1368 | branchNamingMode: BranchNamingMode.pipe( |
| 1369 | 1369 | Schema.withDecodingDefault(Effect.succeed("static" as const)), |
| 1370 | 1370 | ), |
| 1371 | branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("t3code"))), | |
| 1371 | branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("t3"))), | |
| 1372 | 1372 | branchNameInstructions: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), |
| 1373 | 1373 | removeAgentCreditsOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 1374 | 1374 | sourceControlWritingStyle: SourceControlWritingStyleSettings.pipe( |
packages/shared/src/git.test.ts+17-14| ... | ... | @@ -190,34 +190,37 @@ describe("isTemporaryWorktreeBranch", () => { |
| 190 | 190 | ); |
| 191 | 191 | }); |
| 192 | 192 | |
| 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); | |
| 194 | 197 | expect( |
| 195 | 198 | isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12`), |
| 196 | ).toBe(true); | |
| 199 | ).toBe(false); | |
| 197 | 200 | }); |
| 198 | 201 | |
| 199 | 202 | it("rejects UUID-shaped refs that are not RFC 4122 v4", () => { |
| 200 | 203 | // 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); | |
| 204 | 205 | // 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); | |
| 208 | 207 | }); |
| 209 | 208 | |
| 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", () => { | |
| 211 | 210 | const flat = flattenTemporaryWorktreeBranchName(`${WORKTREE_BRANCH_PREFIX}/deadbeef`); |
| 212 | 211 | expect(flat).toBe(`${WORKTREE_BRANCH_PREFIX}-deadbeef`); |
| 213 | 212 | expect(isTemporaryWorktreeBranch(flat)).toBe(true); |
| 214 | 213 | expect(isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}-deadbeef-extra`)).toBe(false); |
| 215 | 214 | 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 | ); | |
| 221 | 224 | }); |
| 222 | 225 | |
| 223 | 226 | it("rejects non-temporary refName names", () => { |
packages/shared/src/git.ts+17-14| ... | ... | @@ -11,14 +11,18 @@ import * as Arr from "effect/Array"; |
| 11 | 11 | import * as Result from "effect/Result"; |
| 12 | 12 | import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; |
| 13 | 13 | |
| 14 | export 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. | |
| 14 | export 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. | |
| 21 | const TEMP_WORKTREE_HEX_TOKEN = "[0-9a-f]{8}"; | |
| 22 | const 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}"; | |
| 20 | 24 | const 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}))$`, | |
| 22 | 26 | ); |
| 23 | 27 | |
| 24 | 28 | /** |
| ... | ... | @@ -125,15 +129,14 @@ export function buildTemporaryWorktreeBranchName( |
| 125 | 129 | } |
| 126 | 130 | |
| 127 | 131 | /** |
| 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. | |
| 130 | 134 | */ |
| 131 | 135 | export 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); | |
| 137 | 140 | return `${WORKTREE_BRANCH_PREFIX}-${token}`; |
| 138 | 141 | } |
| 139 | 142 |