From ac8a72dd3ed53c9ff91f094dd7ef1be646a67fdf Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:01:41 -0700 Subject: [PATCH 01/15] test: cover Claude cross-directory fork transcript seeding --- .../CMUXCLIForkVerbRegressionTests.swift | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index 2f20fe005630..6ac0cd5384aa 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -13,6 +13,61 @@ import CMUXAgentLaunch @Suite(.serialized) struct CMUXCLIForkVerbRegressionTests { private final class BundleToken {} + + @Test + func claudeForkSeedsTranscriptWhenDestinationCwdDiffers() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-claude-fork-seed-\(UUID().uuidString)") + let config = root.appendingPathComponent("claude-config") + let source = root.appendingPathComponent("source") + let destination = root.appendingPathComponent("destination") + let sessionID = "seed-session" + let sourceProject = config.appendingPathComponent("projects/-tmp-source") + try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) + let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") + try Data(#"{"type":"user"} +"#.utf8).write(to: sourceTranscript) + try FileManager.default.createDirectory(at: sourceProject.appendingPathComponent(sessionID), withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + + let record = CMUXCLI.RestoreRecord( + mode: AgentRestoreRequestMode.forkAgent.rawValue, + kind: "claude", + checkpointID: sessionID, + source: nil, + workingDirectory: source.path, + environment: ["CLAUDE_CONFIG_DIR": config.path], + launchCommand: AgentLaunchCommand( + launcher: "claude", + executablePath: nil, + arguments: ["claude", "--resume", sessionID], + workingDirectory: source.path, + environment: [:] + ), + preparedArguments: nil, + preparedArgumentsWorkingDirectory: nil, + forkArguments: nil, + forkArgumentsWorkingDirectory: nil, + permissionMode: nil, + legacyCommand: nil, + legacyForkCommand: nil, + continuationPrompt: nil + ) + + try CMUXCLI.seedClaudeTranscriptForForkIfNeeded( + record: record, + targetWorkingDirectory: destination.path, + homeDirectory: root.path + ) + + let targetTranscript = config.appendingPathComponent("projects/-tmp-destination/\(sessionID).jsonl") + #expect(FileManager.default.fileExists(atPath: targetTranscript)) + #expect(Data(contentsOf: targetTranscript) == Data(contentsOf: sourceTranscript)) + #expect(FileManager.default.fileExists(atPath: targetTranscript.replacingOccurrences(of: ".jsonl", with: ""))) + } + @Test func snapshotForkVerbUsesNativeAndRegistrationForkArgv() throws { let sessionID = "fork-session" From 72a18e8d1505b80c9a0e1fd4f078d2666c5e01b0 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:01:51 -0700 Subject: [PATCH 02/15] fix: seed Claude transcripts before cross-directory forks --- CLI/CMUXCLI+Fork.swift | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index af0174bf1a22..b1353cc719ae 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -189,6 +189,10 @@ extension CMUXCLI { } else { nil } + try seedClaudeTranscriptForForkIfNeeded( + record: record, + targetWorkingDirectory: effectiveWorkingDirectory + ) let request = AgentRestoreRequest( mode: .forkAgent, kind: record.kind, @@ -289,6 +293,73 @@ extension CMUXCLI { ) } + /// Claude resolves `--resume` from the project directory for the current + /// cwd. Forking into another workspace therefore needs a copy of the source + /// transcript in that project directory before Claude starts. + static func seedClaudeTranscriptForForkIfNeeded( + record: RestoreRecord, + targetWorkingDirectory: String?, + fileManager: FileManager = .default, + homeDirectory: String = NSHomeDirectory() + ) throws { + guard record.kind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "claude", + let sessionID = record.checkpointID?.trimmingCharacters(in: .whitespacesAndNewlines), + sessionID.range(of: #"[\\/]"#, options: .regularExpression) == nil, + !sessionID.isEmpty, + let targetWorkingDirectory = targetWorkingDirectory?.trimmingCharacters(in: .whitespacesAndNewlines), + !targetWorkingDirectory.isEmpty else { return } + + let launchEnvironment = record.launchCommand?.environment ?? [:] + let rawConfigRoot = record.environment["CLAUDE_CONFIG_DIR"] + ?? launchEnvironment["CLAUDE_CONFIG_DIR"] + ?? ((homeDirectory as NSString).appendingPathComponent(".claude")) + let configRoot = ClaudeConfigDirectoryPath.preferredPath( + rawConfigRoot, + fileManager: fileManager, + homeDirectory: homeDirectory + ) + let projectsRoot = (configRoot as NSString).appendingPathComponent("projects") + let targetProject = (projectsRoot as NSString).appendingPathComponent( + encodeClaudeProjectDirectory(targetWorkingDirectory) + ) + let targetTranscript = (targetProject as NSString).appendingPathComponent("\(sessionID).jsonl") + guard !fileManager.fileExists(atPath: targetTranscript) else { return } + + let sourceWorkingDirectory = record.launchCommand?.workingDirectory ?? record.workingDirectory + let sourceProject: String? = sourceWorkingDirectory.map { + (projectsRoot as NSString).appendingPathComponent(encodeClaudeProjectDirectory($0)) + } + let sourceTranscript: String? = { + if let sourceProject { + let candidate = (sourceProject as NSString).appendingPathComponent("\(sessionID).jsonl") + if fileManager.fileExists(atPath: candidate) { return candidate } + } + guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } + return projectNames.sorted().lazy.map { + (projectsRoot as NSString).appendingPathComponent($0) + }.map { + ($0 as NSString).appendingPathComponent("\(sessionID).jsonl") + }.first { fileManager.fileExists(atPath: $0) } + }() + guard let sourceTranscript else { return } + + try fileManager.createDirectory(atPath: targetProject, withIntermediateDirectories: true) + try fileManager.copyItem(atPath: sourceTranscript, toPath: targetTranscript) + + let sourceSidecar = (sourceTranscript as NSString).deletingPathExtension + let targetSidecar = (targetTranscript as NSString).deletingPathExtension + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: sourceSidecar, isDirectory: &isDirectory), isDirectory.boolValue, + !fileManager.fileExists(atPath: targetSidecar) { + try fileManager.copyItem(atPath: sourceSidecar, toPath: targetSidecar) + } + } + + private static func encodeClaudeProjectDirectory(_ path: String) -> String { + path.replacingOccurrences(of: "/", with: "-") + .replacingOccurrences(of: ".", with: "-") + } + private func legacyForkCommand(for record: RestoreRecord) -> String? { if let explicit = record.legacyForkCommand?.trimmingCharacters(in: .whitespacesAndNewlines), !explicit.isEmpty { From 6f64735ffd01b0b1ffdf474a65a828774ca44042 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:12:43 -0700 Subject: [PATCH 03/15] test: fix fork transcript fixture literal --- cmuxTests/CMUXCLIForkVerbRegressionTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index 6ac0cd5384aa..939dce5edc23 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -27,8 +27,7 @@ struct CMUXCLIForkVerbRegressionTests { try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") - try Data(#"{"type":"user"} -"#.utf8).write(to: sourceTranscript) + try Data("{\"type\":\"user\"}\n".utf8).write(to: sourceTranscript) try FileManager.default.createDirectory(at: sourceProject.appendingPathComponent(sessionID), withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } From 2ba78996ed456ce95d040dbf9316227f3f600871 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:09:06 -0700 Subject: [PATCH 04/15] fix: harden Claude fork transcript seeding --- CLI/CMUXCLI+Fork.swift | 74 ++++--------- .../ClaudeTranscriptForkSeeder.swift | 104 ++++++++++++++++++ .../ClaudeTranscriptForkSeederTests.swift | 52 +++++++++ .../CMUXCLIForkVerbRegressionTests.swift | 58 +++++----- 4 files changed, 201 insertions(+), 87 deletions(-) create mode 100644 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift create mode 100644 Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index b1353cc719ae..d85d5cdbd6a5 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -189,7 +189,7 @@ extension CMUXCLI { } else { nil } - try seedClaudeTranscriptForForkIfNeeded( + try await seedClaudeTranscriptForForkIfNeeded( record: record, targetWorkingDirectory: effectiveWorkingDirectory ) @@ -293,18 +293,13 @@ extension CMUXCLI { ) } - /// Claude resolves `--resume` from the project directory for the current - /// cwd. Forking into another workspace therefore needs a copy of the source - /// transcript in that project directory before Claude starts. - static func seedClaudeTranscriptForForkIfNeeded( + /// Prepares provider state required for a Claude fork without exposing filesystem details to users. + private func seedClaudeTranscriptForForkIfNeeded( record: RestoreRecord, - targetWorkingDirectory: String?, - fileManager: FileManager = .default, - homeDirectory: String = NSHomeDirectory() - ) throws { + targetWorkingDirectory: String? + ) async throws { guard record.kind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "claude", let sessionID = record.checkpointID?.trimmingCharacters(in: .whitespacesAndNewlines), - sessionID.range(of: #"[\\/]"#, options: .regularExpression) == nil, !sessionID.isEmpty, let targetWorkingDirectory = targetWorkingDirectory?.trimmingCharacters(in: .whitespacesAndNewlines), !targetWorkingDirectory.isEmpty else { return } @@ -312,54 +307,25 @@ extension CMUXCLI { let launchEnvironment = record.launchCommand?.environment ?? [:] let rawConfigRoot = record.environment["CLAUDE_CONFIG_DIR"] ?? launchEnvironment["CLAUDE_CONFIG_DIR"] - ?? ((homeDirectory as NSString).appendingPathComponent(".claude")) - let configRoot = ClaudeConfigDirectoryPath.preferredPath( - rawConfigRoot, - fileManager: fileManager, - homeDirectory: homeDirectory - ) - let projectsRoot = (configRoot as NSString).appendingPathComponent("projects") - let targetProject = (projectsRoot as NSString).appendingPathComponent( - encodeClaudeProjectDirectory(targetWorkingDirectory) + ?? ((NSHomeDirectory() as NSString).appendingPathComponent(".claude")) + let configRoot = ClaudeConfigDirectoryPath.preferredPath(rawConfigRoot) + let request = ClaudeTranscriptForkSeedRequest( + sessionID: sessionID, + sourceWorkingDirectory: record.launchCommand?.workingDirectory ?? record.workingDirectory, + targetWorkingDirectory: targetWorkingDirectory, + configDirectory: configRoot ) - let targetTranscript = (targetProject as NSString).appendingPathComponent("\(sessionID).jsonl") - guard !fileManager.fileExists(atPath: targetTranscript) else { return } - - let sourceWorkingDirectory = record.launchCommand?.workingDirectory ?? record.workingDirectory - let sourceProject: String? = sourceWorkingDirectory.map { - (projectsRoot as NSString).appendingPathComponent(encodeClaudeProjectDirectory($0)) - } - let sourceTranscript: String? = { - if let sourceProject { - let candidate = (sourceProject as NSString).appendingPathComponent("\(sessionID).jsonl") - if fileManager.fileExists(atPath: candidate) { return candidate } - } - guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } - return projectNames.sorted().lazy.map { - (projectsRoot as NSString).appendingPathComponent($0) - }.map { - ($0 as NSString).appendingPathComponent("\(sessionID).jsonl") - }.first { fileManager.fileExists(atPath: $0) } - }() - guard let sourceTranscript else { return } - - try fileManager.createDirectory(atPath: targetProject, withIntermediateDirectories: true) - try fileManager.copyItem(atPath: sourceTranscript, toPath: targetTranscript) - - let sourceSidecar = (sourceTranscript as NSString).deletingPathExtension - let targetSidecar = (targetTranscript as NSString).deletingPathExtension - var isDirectory: ObjCBool = false - if fileManager.fileExists(atPath: sourceSidecar, isDirectory: &isDirectory), isDirectory.boolValue, - !fileManager.fileExists(atPath: targetSidecar) { - try fileManager.copyItem(atPath: sourceSidecar, toPath: targetSidecar) + do { + try await ClaudeTranscriptForkSeeder.seed(request) + } catch { + throw loggedForkError( + .providerSetupFailed, + stage: "provider.transcript-seed", + detail: String(reflecting: type(of: error)) + ) } } - private static func encodeClaudeProjectDirectory(_ path: String) -> String { - path.replacingOccurrences(of: "/", with: "-") - .replacingOccurrences(of: ".", with: "-") - } - private func legacyForkCommand(for record: RestoreRecord) -> String? { if let explicit = record.legacyForkCommand?.trimmingCharacters(in: .whitespacesAndNewlines), !explicit.isEmpty { diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift new file mode 100644 index 000000000000..aaa400a1040e --- /dev/null +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -0,0 +1,104 @@ +import Foundation + +/// The filesystem inputs needed to make a Claude transcript resumable in a new project directory. +public struct ClaudeTranscriptForkSeedRequest: Sendable { + public let sessionID: String + public let sourceWorkingDirectory: String? + public let targetWorkingDirectory: String + public let configDirectory: String + + public init( + sessionID: String, + sourceWorkingDirectory: String?, + targetWorkingDirectory: String, + configDirectory: String + ) { + self.sessionID = sessionID + self.sourceWorkingDirectory = sourceWorkingDirectory + self.targetWorkingDirectory = targetWorkingDirectory + self.configDirectory = configDirectory + } +} + +/// Copies Claude's transcript and sidecar into a destination project before a fork launches. +public enum ClaudeTranscriptForkSeeder { + /// Performs discovery and copying off the caller's executor, and repairs a missing sidecar on retry. + public static func seed(_ request: ClaudeTranscriptForkSeedRequest) async throws { + try await Task.detached(priority: .userInitiated) { + try seedSynchronously(request) + }.value + } + + private static func seedSynchronously(_ request: ClaudeTranscriptForkSeedRequest) throws { + guard !request.sessionID.isEmpty, + request.sessionID.range(of: #"[\\/]"#, options: .regularExpression) == nil, + !request.targetWorkingDirectory.isEmpty else { return } + + let fileManager = FileManager.default + let projectsRoot = (request.configDirectory as NSString).appendingPathComponent("projects") + let targetProject = (projectsRoot as NSString).appendingPathComponent( + encodeProjectDirectory(request.targetWorkingDirectory) + ) + let targetTranscript = (targetProject as NSString).appendingPathComponent("\(request.sessionID).jsonl") + let targetSidecar = (targetTranscript as NSString).deletingPathExtension + let sourceTranscript = findSourceTranscript( + sessionID: request.sessionID, + sourceWorkingDirectory: request.sourceWorkingDirectory, + projectsRoot: projectsRoot, + fileManager: fileManager + ) + guard let sourceTranscript else { return } + + let sourceSidecar = (sourceTranscript as NSString).deletingPathExtension + var sourceSidecarIsDirectory: ObjCBool = false + let hasSourceSidecar = fileManager.fileExists( + atPath: sourceSidecar, + isDirectory: &sourceSidecarIsDirectory + ) && sourceSidecarIsDirectory.boolValue + let hasTargetTranscript = fileManager.fileExists(atPath: targetTranscript) + let hasTargetSidecar = fileManager.fileExists(atPath: targetSidecar) + guard !hasTargetTranscript || (hasSourceSidecar && !hasTargetSidecar) else { return } + + try fileManager.createDirectory(atPath: targetProject, withIntermediateDirectories: true) + if !hasTargetTranscript { + try copyAtomically(sourceTranscript, to: targetTranscript, fileManager: fileManager) + } + if hasSourceSidecar && !hasTargetSidecar { + try copyAtomically(sourceSidecar, to: targetSidecar, fileManager: fileManager) + } + } + + private static func findSourceTranscript( + sessionID: String, + sourceWorkingDirectory: String?, + projectsRoot: String, + fileManager: FileManager + ) -> String? { + if let sourceWorkingDirectory { + let sourceProject = (projectsRoot as NSString).appendingPathComponent( + encodeProjectDirectory(sourceWorkingDirectory) + ) + let candidate = (sourceProject as NSString).appendingPathComponent("\(sessionID).jsonl") + if fileManager.fileExists(atPath: candidate) { return candidate } + } + guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } + for projectName in projectNames { + let candidate = ((projectsRoot as NSString).appendingPathComponent(projectName) as NSString) + .appendingPathComponent("\(sessionID).jsonl") + if fileManager.fileExists(atPath: candidate) { return candidate } + } + return nil + } + + private static func copyAtomically(_ source: String, to destination: String, fileManager: FileManager) throws { + let temporaryDestination = "\(destination).tmp-\(UUID().uuidString)" + defer { try? fileManager.removeItem(atPath: temporaryDestination) } + try fileManager.copyItem(atPath: source, toPath: temporaryDestination) + try fileManager.moveItem(atPath: temporaryDestination, toPath: destination) + } + + private static func encodeProjectDirectory(_ path: String) -> String { + path.replacingOccurrences(of: "/", with: "-") + .replacingOccurrences(of: ".", with: "-") + } +} diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift new file mode 100644 index 000000000000..4ba60cd3a2fa --- /dev/null +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift @@ -0,0 +1,52 @@ +import Foundation +import Testing +@testable import CMUXAgentLaunch + +@Suite +struct ClaudeTranscriptForkSeederTests { + @Test + func copiesTranscriptAndRepairsMissingSidecar() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-claude-seeder-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: root) } + let config = root.appendingPathComponent("config") + let source = root.appendingPathComponent("source") + let destination = root.appendingPathComponent("destination") + let sessionID = "seed-session" + let encodedSource = source.path.replacingOccurrences(of: "/", with: "-") + .replacingOccurrences(of: ".", with: "-") + let encodedDestination = destination.path.replacingOccurrences(of: "/", with: "-") + .replacingOccurrences(of: ".", with: "-") + let sourceProject = config.appendingPathComponent("projects").appendingPathComponent(encodedSource) + try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) + let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") + try Data("{\"type\":\"user\"}\n".utf8).write(to: sourceTranscript) + let sourceSidecar = sourceProject.appendingPathComponent(sessionID) + try FileManager.default.createDirectory(at: sourceSidecar, withIntermediateDirectories: true) + try Data("{\"state\":\"fixture\"}".utf8) + .write(to: sourceSidecar.appendingPathComponent("state.json")) + + let request = ClaudeTranscriptForkSeedRequest( + sessionID: sessionID, + sourceWorkingDirectory: source.path, + targetWorkingDirectory: destination.path, + configDirectory: config.path + ) + try await ClaudeTranscriptForkSeeder.seed(request) + let targetProject = config.appendingPathComponent("projects").appendingPathComponent(encodedDestination) + let targetTranscript = targetProject.appendingPathComponent("\(sessionID).jsonl") + let targetSidecarFile = targetProject.appendingPathComponent(sessionID).appendingPathComponent("state.json") + let copiedTranscript = try Data(contentsOf: targetTranscript) + let sourceTranscriptData = try Data(contentsOf: sourceTranscript) + let copiedSidecar = try Data(contentsOf: targetSidecarFile) + let sourceSidecarData = try Data(contentsOf: sourceSidecar.appendingPathComponent("state.json")) + #expect(copiedTranscript == sourceTranscriptData) + #expect(copiedSidecar == sourceSidecarData) + + try FileManager.default.removeItem(at: targetProject.appendingPathComponent(sessionID)) + try await ClaudeTranscriptForkSeeder.seed(request) + #expect(FileManager.default.fileExists(atPath: targetSidecarFile.path)) + } +} diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index 939dce5edc23..4ba524370922 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -15,56 +15,48 @@ struct CMUXCLIForkVerbRegressionTests { private final class BundleToken {} @Test - func claudeForkSeedsTranscriptWhenDestinationCwdDiffers() throws { + func claudeForkSeedsTranscriptWhenDestinationCwdDiffers() async throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("cmux-claude-fork-seed-\(UUID().uuidString)") let config = root.appendingPathComponent("claude-config") let source = root.appendingPathComponent("source") let destination = root.appendingPathComponent("destination") let sessionID = "seed-session" - let sourceProject = config.appendingPathComponent("projects/-tmp-source") + let sourceProject = config.appendingPathComponent("projects").appendingPathComponent( + source.path.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ".", with: "-") + ) try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") try Data("{\"type\":\"user\"}\n".utf8).write(to: sourceTranscript) - try FileManager.default.createDirectory(at: sourceProject.appendingPathComponent(sessionID), withIntermediateDirectories: true) + let sourceSidecar = sourceProject.appendingPathComponent(sessionID) + try FileManager.default.createDirectory(at: sourceSidecar, withIntermediateDirectories: true) + let sourceSidecarFile = sourceSidecar.appendingPathComponent("state.json") + try Data("{\"state\":\"fixture\"}".utf8).write(to: sourceSidecarFile) defer { try? FileManager.default.removeItem(at: root) } - let record = CMUXCLI.RestoreRecord( - mode: AgentRestoreRequestMode.forkAgent.rawValue, - kind: "claude", - checkpointID: sessionID, - source: nil, - workingDirectory: source.path, - environment: ["CLAUDE_CONFIG_DIR": config.path], - launchCommand: AgentLaunchCommand( - launcher: "claude", - executablePath: nil, - arguments: ["claude", "--resume", sessionID], - workingDirectory: source.path, - environment: [:] - ), - preparedArguments: nil, - preparedArgumentsWorkingDirectory: nil, - forkArguments: nil, - forkArgumentsWorkingDirectory: nil, - permissionMode: nil, - legacyCommand: nil, - legacyForkCommand: nil, - continuationPrompt: nil + let targetProject = config.appendingPathComponent("projects").appendingPathComponent( + destination.path.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ".", with: "-") ) - - try CMUXCLI.seedClaudeTranscriptForForkIfNeeded( - record: record, + try await ClaudeTranscriptForkSeeder.seed(ClaudeTranscriptForkSeedRequest( + sessionID: sessionID, + sourceWorkingDirectory: source.path, targetWorkingDirectory: destination.path, - homeDirectory: root.path - ) + configDirectory: config.path + )) - let targetTranscript = config.appendingPathComponent("projects/-tmp-destination/\(sessionID).jsonl") + let targetTranscript = targetProject.appendingPathComponent("\(sessionID).jsonl") #expect(FileManager.default.fileExists(atPath: targetTranscript)) - #expect(Data(contentsOf: targetTranscript) == Data(contentsOf: sourceTranscript)) - #expect(FileManager.default.fileExists(atPath: targetTranscript.replacingOccurrences(of: ".jsonl", with: ""))) + let copiedTranscript = try Data(contentsOf: targetTranscript) + let sourceTranscriptData = try Data(contentsOf: sourceTranscript) + #expect(copiedTranscript == sourceTranscriptData) + let targetSidecar = targetTranscript.deletingPathExtension() + #expect(FileManager.default.fileExists(atPath: targetSidecar.path)) + let targetSidecarFile = targetSidecar.appendingPathComponent("state.json") + let copiedSidecar = try Data(contentsOf: targetSidecarFile) + let sourceSidecarData = try Data(contentsOf: sourceSidecarFile) + #expect(copiedSidecar == sourceSidecarData) } @Test From 40217515a64af5110e2770b93a239db5c5bd5d08 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:13:14 -0700 Subject: [PATCH 05/15] fix: make Claude transcript seeder a service --- CLI/CMUXCLI+Fork.swift | 2 +- .../CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift | 7 ++++--- .../ClaudeTranscriptForkSeederTests.swift | 4 ++-- cmuxTests/CMUXCLIForkVerbRegressionTests.swift | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index d85d5cdbd6a5..8564f86824b8 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -316,7 +316,7 @@ extension CMUXCLI { configDirectory: configRoot ) do { - try await ClaudeTranscriptForkSeeder.seed(request) + try await ClaudeTranscriptForkSeeder().seed(request) } catch { throw loggedForkError( .providerSetupFailed, diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift index aaa400a1040e..550848611904 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -21,11 +21,12 @@ public struct ClaudeTranscriptForkSeedRequest: Sendable { } /// Copies Claude's transcript and sidecar into a destination project before a fork launches. -public enum ClaudeTranscriptForkSeeder { +public struct ClaudeTranscriptForkSeeder: Sendable { + public init() {} /// Performs discovery and copying off the caller's executor, and repairs a missing sidecar on retry. - public static func seed(_ request: ClaudeTranscriptForkSeedRequest) async throws { + public func seed(_ request: ClaudeTranscriptForkSeedRequest) async throws { try await Task.detached(priority: .userInitiated) { - try seedSynchronously(request) + try Self.seedSynchronously(request) }.value } diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift index 4ba60cd3a2fa..a3067de43e0a 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift @@ -34,7 +34,7 @@ struct ClaudeTranscriptForkSeederTests { targetWorkingDirectory: destination.path, configDirectory: config.path ) - try await ClaudeTranscriptForkSeeder.seed(request) + try await ClaudeTranscriptForkSeeder().seed(request) let targetProject = config.appendingPathComponent("projects").appendingPathComponent(encodedDestination) let targetTranscript = targetProject.appendingPathComponent("\(sessionID).jsonl") let targetSidecarFile = targetProject.appendingPathComponent(sessionID).appendingPathComponent("state.json") @@ -46,7 +46,7 @@ struct ClaudeTranscriptForkSeederTests { #expect(copiedSidecar == sourceSidecarData) try FileManager.default.removeItem(at: targetProject.appendingPathComponent(sessionID)) - try await ClaudeTranscriptForkSeeder.seed(request) + try await ClaudeTranscriptForkSeeder().seed(request) #expect(FileManager.default.fileExists(atPath: targetSidecarFile.path)) } } diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index 4ba524370922..c77553a724a0 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -39,7 +39,7 @@ struct CMUXCLIForkVerbRegressionTests { let targetProject = config.appendingPathComponent("projects").appendingPathComponent( destination.path.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ".", with: "-") ) - try await ClaudeTranscriptForkSeeder.seed(ClaudeTranscriptForkSeedRequest( + try await ClaudeTranscriptForkSeeder().seed(ClaudeTranscriptForkSeedRequest( sessionID: sessionID, sourceWorkingDirectory: source.path, targetWorkingDirectory: destination.path, From 383d482b3995926fcc95587cf9b4210e8060dd7a Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:20:22 -0700 Subject: [PATCH 06/15] fix: support Claude nested transcript layouts --- .../ClaudeTranscriptForkSeeder.swift | 60 +++++++++++++------ .../ClaudeTranscriptForkSeederTests.swift | 44 ++++++++++++-- .../CMUXCLIForkVerbRegressionTests.swift | 45 -------------- 3 files changed, 82 insertions(+), 67 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift index 550848611904..facad51a45e9 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -38,10 +38,8 @@ public struct ClaudeTranscriptForkSeeder: Sendable { let fileManager = FileManager.default let projectsRoot = (request.configDirectory as NSString).appendingPathComponent("projects") let targetProject = (projectsRoot as NSString).appendingPathComponent( - encodeProjectDirectory(request.targetWorkingDirectory) + ClaudeProjectSlug().slug(forWorkingDirectory: request.targetWorkingDirectory) ) - let targetTranscript = (targetProject as NSString).appendingPathComponent("\(request.sessionID).jsonl") - let targetSidecar = (targetTranscript as NSString).deletingPathExtension let sourceTranscript = findSourceTranscript( sessionID: request.sessionID, sourceWorkingDirectory: request.sourceWorkingDirectory, @@ -49,8 +47,10 @@ public struct ClaudeTranscriptForkSeeder: Sendable { fileManager: fileManager ) guard let sourceTranscript else { return } + let targetTranscript = (targetProject as NSString).appendingPathComponent(sourceTranscript.relativePath) + let targetSidecar = (targetProject as NSString).appendingPathComponent(request.sessionID) - let sourceSidecar = (sourceTranscript as NSString).deletingPathExtension + let sourceSidecar = (sourceTranscript.projectPath as NSString).appendingPathComponent(request.sessionID) var sourceSidecarIsDirectory: ObjCBool = false let hasSourceSidecar = fileManager.fileExists( atPath: sourceSidecar, @@ -61,12 +61,12 @@ public struct ClaudeTranscriptForkSeeder: Sendable { guard !hasTargetTranscript || (hasSourceSidecar && !hasTargetSidecar) else { return } try fileManager.createDirectory(atPath: targetProject, withIntermediateDirectories: true) - if !hasTargetTranscript { - try copyAtomically(sourceTranscript, to: targetTranscript, fileManager: fileManager) - } if hasSourceSidecar && !hasTargetSidecar { try copyAtomically(sourceSidecar, to: targetSidecar, fileManager: fileManager) } + if !hasTargetTranscript && !fileManager.fileExists(atPath: targetTranscript) { + try copyAtomically(sourceTranscript.path, to: targetTranscript, fileManager: fileManager) + } } private static func findSourceTranscript( @@ -74,32 +74,56 @@ public struct ClaudeTranscriptForkSeeder: Sendable { sourceWorkingDirectory: String?, projectsRoot: String, fileManager: FileManager - ) -> String? { + ) -> TranscriptLocation? { if let sourceWorkingDirectory { let sourceProject = (projectsRoot as NSString).appendingPathComponent( - encodeProjectDirectory(sourceWorkingDirectory) + ClaudeProjectSlug().slug(forWorkingDirectory: sourceWorkingDirectory) ) - let candidate = (sourceProject as NSString).appendingPathComponent("\(sessionID).jsonl") - if fileManager.fileExists(atPath: candidate) { return candidate } + if let location = transcriptLocation( + projectPath: sourceProject, sessionID: sessionID, fileManager: fileManager + ) { return location } } guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } for projectName in projectNames { - let candidate = ((projectsRoot as NSString).appendingPathComponent(projectName) as NSString) - .appendingPathComponent("\(sessionID).jsonl") - if fileManager.fileExists(atPath: candidate) { return candidate } + let projectPath = (projectsRoot as NSString).appendingPathComponent(projectName) + if let location = transcriptLocation( + projectPath: projectPath, sessionID: sessionID, fileManager: fileManager + ) { return location } } return nil } + private static func transcriptLocation( + projectPath: String, sessionID: String, fileManager: FileManager + ) -> TranscriptLocation? { + let candidates = [ + "\(sessionID).jsonl", + "\(sessionID)/messages/\(sessionID).jsonl" + ] + for relativePath in candidates { + let path = (projectPath as NSString).appendingPathComponent(relativePath) + var isDirectory: ObjCBool = false + guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue, + (try? fileManager.attributesOfItem(atPath: path)[.type] as? FileAttributeType) == .typeRegular + else { continue } + return TranscriptLocation(path: path, projectPath: projectPath, relativePath: relativePath) + } + return nil + } + + private struct TranscriptLocation { + let path: String + let projectPath: String + let relativePath: String + } + private static func copyAtomically(_ source: String, to destination: String, fileManager: FileManager) throws { + let destinationDirectory = (destination as NSString).deletingLastPathComponent + try fileManager.createDirectory(atPath: destinationDirectory, withIntermediateDirectories: true) let temporaryDestination = "\(destination).tmp-\(UUID().uuidString)" defer { try? fileManager.removeItem(atPath: temporaryDestination) } try fileManager.copyItem(atPath: source, toPath: temporaryDestination) try fileManager.moveItem(atPath: temporaryDestination, toPath: destination) } - private static func encodeProjectDirectory(_ path: String) -> String { - path.replacingOccurrences(of: "/", with: "-") - .replacingOccurrences(of: ".", with: "-") - } } diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift index a3067de43e0a..b07b73126a2e 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift @@ -13,10 +13,8 @@ struct ClaudeTranscriptForkSeederTests { let source = root.appendingPathComponent("source") let destination = root.appendingPathComponent("destination") let sessionID = "seed-session" - let encodedSource = source.path.replacingOccurrences(of: "/", with: "-") - .replacingOccurrences(of: ".", with: "-") - let encodedDestination = destination.path.replacingOccurrences(of: "/", with: "-") - .replacingOccurrences(of: ".", with: "-") + let encodedSource = ClaudeProjectSlug().slug(forWorkingDirectory: source.path) + let encodedDestination = ClaudeProjectSlug().slug(forWorkingDirectory: destination.path) let sourceProject = config.appendingPathComponent("projects").appendingPathComponent(encodedSource) try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) @@ -50,3 +48,41 @@ struct ClaudeTranscriptForkSeederTests { #expect(FileManager.default.fileExists(atPath: targetSidecarFile.path)) } } + +extension ClaudeTranscriptForkSeederTests { + @Test + func findsNestedTranscriptWithClaudeSlugAndRejectsDirectoryCandidate() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-claude-seeder-nested-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: root) } + let config = root.appendingPathComponent("config") + let source = root.appendingPathComponent("source folder/é") + let destination = root.appendingPathComponent("destination folder/é") + let sessionID = "nested-session" + let sourceProject = config.appendingPathComponent("projects") + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: source.path)) + let nestedDirectory = sourceProject.appendingPathComponent(sessionID).appendingPathComponent("messages") + try FileManager.default.createDirectory(at: nestedDirectory, withIntermediateDirectories: true) + try FileManager.default.createDirectory( + at: sourceProject.appendingPathComponent("\(sessionID).jsonl"), withIntermediateDirectories: true + ) + try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) + let sourceTranscript = nestedDirectory.appendingPathComponent("\(sessionID).jsonl") + try Data("{\"type\":\"nested\"}\n".utf8).write(to: sourceTranscript) + + try await ClaudeTranscriptForkSeeder().seed(ClaudeTranscriptForkSeedRequest( + sessionID: sessionID, + sourceWorkingDirectory: source.path, + targetWorkingDirectory: destination.path, + configDirectory: config.path + )) + + let targetTranscript = config.appendingPathComponent("projects") + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: destination.path)) + .appendingPathComponent(sessionID).appendingPathComponent("messages/\(sessionID).jsonl") + let copied = try Data(contentsOf: targetTranscript) + let sourceData = try Data(contentsOf: sourceTranscript) + #expect(copied == sourceData) + } +} diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index c77553a724a0..d812eeed4e66 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -14,51 +14,6 @@ import CMUXAgentLaunch struct CMUXCLIForkVerbRegressionTests { private final class BundleToken {} - @Test - func claudeForkSeedsTranscriptWhenDestinationCwdDiffers() async throws { - let root = FileManager.default.temporaryDirectory - .appendingPathComponent("cmux-claude-fork-seed-\(UUID().uuidString)") - let config = root.appendingPathComponent("claude-config") - let source = root.appendingPathComponent("source") - let destination = root.appendingPathComponent("destination") - let sessionID = "seed-session" - let sourceProject = config.appendingPathComponent("projects").appendingPathComponent( - source.path.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ".", with: "-") - ) - try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) - let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") - try Data("{\"type\":\"user\"}\n".utf8).write(to: sourceTranscript) - let sourceSidecar = sourceProject.appendingPathComponent(sessionID) - try FileManager.default.createDirectory(at: sourceSidecar, withIntermediateDirectories: true) - let sourceSidecarFile = sourceSidecar.appendingPathComponent("state.json") - try Data("{\"state\":\"fixture\"}".utf8).write(to: sourceSidecarFile) - defer { try? FileManager.default.removeItem(at: root) } - - let targetProject = config.appendingPathComponent("projects").appendingPathComponent( - destination.path.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ".", with: "-") - ) - try await ClaudeTranscriptForkSeeder().seed(ClaudeTranscriptForkSeedRequest( - sessionID: sessionID, - sourceWorkingDirectory: source.path, - targetWorkingDirectory: destination.path, - configDirectory: config.path - )) - - let targetTranscript = targetProject.appendingPathComponent("\(sessionID).jsonl") - #expect(FileManager.default.fileExists(atPath: targetTranscript)) - let copiedTranscript = try Data(contentsOf: targetTranscript) - let sourceTranscriptData = try Data(contentsOf: sourceTranscript) - #expect(copiedTranscript == sourceTranscriptData) - let targetSidecar = targetTranscript.deletingPathExtension() - #expect(FileManager.default.fileExists(atPath: targetSidecar.path)) - let targetSidecarFile = targetSidecar.appendingPathComponent("state.json") - let copiedSidecar = try Data(contentsOf: targetSidecarFile) - let sourceSidecarData = try Data(contentsOf: sourceSidecarFile) - #expect(copiedSidecar == sourceSidecarData) - } - @Test func snapshotForkVerbUsesNativeAndRegistrationForkArgv() throws { let sessionID = "fork-session" From 5e3d6987fcc5ea496675f4543774349c2fc127fd Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:39:05 -0700 Subject: [PATCH 07/15] fix: close Claude seeder review gaps --- CLI/CMUXCLI+Fork.swift | 7 +++++-- .../CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index 8564f86824b8..423d70194462 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -191,7 +191,8 @@ extension CMUXCLI { } try await seedClaudeTranscriptForForkIfNeeded( record: record, - targetWorkingDirectory: effectiveWorkingDirectory + targetWorkingDirectory: effectiveWorkingDirectory, + processEnvironment: processEnvironment ) let request = AgentRestoreRequest( mode: .forkAgent, @@ -296,7 +297,8 @@ extension CMUXCLI { /// Prepares provider state required for a Claude fork without exposing filesystem details to users. private func seedClaudeTranscriptForForkIfNeeded( record: RestoreRecord, - targetWorkingDirectory: String? + targetWorkingDirectory: String?, + processEnvironment: [String: String] ) async throws { guard record.kind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "claude", let sessionID = record.checkpointID?.trimmingCharacters(in: .whitespacesAndNewlines), @@ -307,6 +309,7 @@ extension CMUXCLI { let launchEnvironment = record.launchCommand?.environment ?? [:] let rawConfigRoot = record.environment["CLAUDE_CONFIG_DIR"] ?? launchEnvironment["CLAUDE_CONFIG_DIR"] + ?? processEnvironment["CLAUDE_CONFIG_DIR"] ?? ((NSHomeDirectory() as NSString).appendingPathComponent(".claude")) let configRoot = ClaudeConfigDirectoryPath.preferredPath(rawConfigRoot) let request = ClaudeTranscriptForkSeedRequest( diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift index facad51a45e9..60362ba998be 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -31,8 +31,8 @@ public struct ClaudeTranscriptForkSeeder: Sendable { } private static func seedSynchronously(_ request: ClaudeTranscriptForkSeedRequest) throws { - guard !request.sessionID.isEmpty, - request.sessionID.range(of: #"[\\/]"#, options: .regularExpression) == nil, + guard request.sessionID.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil, + request.configDirectory.hasPrefix("/"), !request.targetWorkingDirectory.isEmpty else { return } let fileManager = FileManager.default @@ -84,7 +84,7 @@ public struct ClaudeTranscriptForkSeeder: Sendable { ) { return location } } guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } - for projectName in projectNames { + for projectName in projectNames.sorted() { let projectPath = (projectsRoot as NSString).appendingPathComponent(projectName) if let location = transcriptLocation( projectPath: projectPath, sessionID: sessionID, fileManager: fileManager From 5b5f5392aa49de167f22ed3baf1b9a954bc92a69 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:21:43 -0700 Subject: [PATCH 08/15] test: cover Claude fork CLI wiring --- .../CMUXCLIForkVerbRegressionTests.swift | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index d812eeed4e66..989d701e12cc 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -294,6 +294,67 @@ struct CMUXCLIForkVerbRegressionTests { ) } + @Test + func cliForkVerbSeedsClaudeTranscriptBeforeLaunching() throws { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-fork-claude-wire-(UUID().uuidString)", isDirectory: true) + let config = root.appendingPathComponent("claude-config", isDirectory: true) + let source = root.appendingPathComponent("source", isDirectory: true) + let destination = root.appendingPathComponent("destination", isDirectory: true) + let executable = root.appendingPathComponent("claude", isDirectory: false) + let marker = root.appendingPathComponent("claude-output", isDirectory: false) + let sessionID = "cli-fork-session" + try fileManager.createDirectory(at: source, withIntermediateDirectories: true) + try fileManager.createDirectory(at: destination, withIntermediateDirectories: true) + let sourceProject = config.appendingPathComponent("projects", isDirectory: true) + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: source.path), isDirectory: true) + try fileManager.createDirectory(at: sourceProject, withIntermediateDirectories: true) + try Data("{\"type\":\"user\"}\n".utf8) + .write(to: sourceProject.appendingPathComponent("\(sessionID).jsonl")) + let sourceSidecar = sourceProject.appendingPathComponent(sessionID, isDirectory: true) + try fileManager.createDirectory(at: sourceSidecar, withIntermediateDirectories: true) + try Data("{\"state\":\"fixture\"}".utf8) + .write(to: sourceSidecar.appendingPathComponent("state.json")) + try "#!/bin/sh\nprintf 'launched\\n' > \"$FORK_TEST_MARKER\"\n" + .write(to: executable, atomically: true, encoding: .utf8) + try fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: executable.path) + defer { try? fileManager.removeItem(at: root) } + + let surfaceID = UUID().uuidString.lowercased() + let responseData = try JSONSerialization.data(withJSONObject: [ + "ok": true, + "result": [ + "restore_record": [ + "mode": "resumeAgent", "kind": "claude", "checkpoint_id": sessionID, + "working_directory": source.path, + "environment": ["CLAUDE_CONFIG_DIR": config.path], + "launch_command": [ + "arguments": [executable.path], "executable_path": executable.path, + "working_directory": source.path, "environment": [:] + ], + "prepared_fork_arguments": [executable.path, "--resume", sessionID, "--fork-session"], + "fork_arguments_working_directory": destination.path + ] + ] + ]) + let socketPath = "/tmp/cmux-fork-claude-wire-(UUID().uuidString.prefix(8)).sock" + let responder = try UnixSocketResponder(path: socketPath, response: String(decoding: responseData, as: UTF8.self)) + defer { responder.stop() } + let home = try isolatedCLIHome() + defer { try? fileManager.removeItem(at: home) } + var environment = isolatedCLIEnvironment(socketPath: socketPath, home: home) + environment["FORK_TEST_MARKER"] = marker.path + environment["PATH"] = "/usr/bin:/bin" + let result = try runCLI(arguments: ["fork", "--surface", surfaceID, "claude", sessionID], environment: environment) + #expect(result.status == 0, Comment(rawValue: result.description)) + #expect(fileManager.fileExists(atPath: marker.path)) + let targetProject = config.appendingPathComponent("projects", isDirectory: true) + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: destination.path), isDirectory: true) + #expect(fileManager.fileExists(atPath: targetProject.appendingPathComponent("\(sessionID).jsonl").path)) + #expect(fileManager.fileExists(atPath: targetProject.appendingPathComponent(sessionID).appendingPathComponent("state.json").path)) + } + @Test func cliForkVerbExecutesStructuredForkArguments() throws { let fileManager = FileManager.default From ae7f3163aa0e0d1c9e9380d6fbe5d9cae6c63a49 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:19:26 -0700 Subject: [PATCH 09/15] fix: correct Claude fork fixture interpolation --- cmuxTests/CMUXCLIForkVerbRegressionTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift index 989d701e12cc..8a103758c663 100644 --- a/cmuxTests/CMUXCLIForkVerbRegressionTests.swift +++ b/cmuxTests/CMUXCLIForkVerbRegressionTests.swift @@ -298,7 +298,7 @@ struct CMUXCLIForkVerbRegressionTests { func cliForkVerbSeedsClaudeTranscriptBeforeLaunching() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory - .appendingPathComponent("cmux-fork-claude-wire-(UUID().uuidString)", isDirectory: true) + .appendingPathComponent("cmux-fork-claude-wire-\(UUID().uuidString)", isDirectory: true) let config = root.appendingPathComponent("claude-config", isDirectory: true) let source = root.appendingPathComponent("source", isDirectory: true) let destination = root.appendingPathComponent("destination", isDirectory: true) @@ -338,7 +338,7 @@ struct CMUXCLIForkVerbRegressionTests { ] ] ]) - let socketPath = "/tmp/cmux-fork-claude-wire-(UUID().uuidString.prefix(8)).sock" + let socketPath = "/tmp/cmux-fork-claude-wire-\(UUID().uuidString.prefix(8)).sock" let responder = try UnixSocketResponder(path: socketPath, response: String(decoding: responseData, as: UTF8.self)) defer { responder.stop() } let home = try isolatedCLIHome() From 787a1b8e21dd682c2da41885d2df757e1e1a729e Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:25:44 -0700 Subject: [PATCH 10/15] Fix Claude fork transcript destination --- CLI/CMUXCLI+Fork.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index 423d70194462..1c9bef117aba 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -189,9 +189,12 @@ extension CMUXCLI { } else { nil } + let transcriptTargetWorkingDirectory = normalizedRestoreWorkingDirectory( + record.forkArgumentsWorkingDirectory + ) ?? effectiveWorkingDirectory try await seedClaudeTranscriptForForkIfNeeded( record: record, - targetWorkingDirectory: effectiveWorkingDirectory, + targetWorkingDirectory: transcriptTargetWorkingDirectory, processEnvironment: processEnvironment ) let request = AgentRestoreRequest( From a1298895cdaa75cead4f1f77754f1029adbb066f Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:39:19 -0700 Subject: [PATCH 11/15] fix Claude forks across config directories --- CLI/CMUXCLI+Fork.swift | 5 ++- .../ClaudeTranscriptForkSeeder.swift | 24 ++++++++++---- .../ClaudeTranscriptForkSeederTests.swift | 32 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CLI/CMUXCLI+Fork.swift b/CLI/CMUXCLI+Fork.swift index 1c9bef117aba..f6ec1fbb826f 100644 --- a/CLI/CMUXCLI+Fork.swift +++ b/CLI/CMUXCLI+Fork.swift @@ -319,7 +319,10 @@ extension CMUXCLI { sessionID: sessionID, sourceWorkingDirectory: record.launchCommand?.workingDirectory ?? record.workingDirectory, targetWorkingDirectory: targetWorkingDirectory, - configDirectory: configRoot + configDirectory: configRoot, + sourceConfigDirectories: [ + ((NSHomeDirectory() as NSString).appendingPathComponent(".claude")) + ] ) do { try await ClaudeTranscriptForkSeeder().seed(request) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift index 60362ba998be..645f9687a2b0 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -6,17 +6,20 @@ public struct ClaudeTranscriptForkSeedRequest: Sendable { public let sourceWorkingDirectory: String? public let targetWorkingDirectory: String public let configDirectory: String + public let sourceConfigDirectories: [String] public init( sessionID: String, sourceWorkingDirectory: String?, targetWorkingDirectory: String, - configDirectory: String + configDirectory: String, + sourceConfigDirectories: [String] = [] ) { self.sessionID = sessionID self.sourceWorkingDirectory = sourceWorkingDirectory self.targetWorkingDirectory = targetWorkingDirectory self.configDirectory = configDirectory + self.sourceConfigDirectories = sourceConfigDirectories } } @@ -40,12 +43,19 @@ public struct ClaudeTranscriptForkSeeder: Sendable { let targetProject = (projectsRoot as NSString).appendingPathComponent( ClaudeProjectSlug().slug(forWorkingDirectory: request.targetWorkingDirectory) ) - let sourceTranscript = findSourceTranscript( - sessionID: request.sessionID, - sourceWorkingDirectory: request.sourceWorkingDirectory, - projectsRoot: projectsRoot, - fileManager: fileManager - ) + let sourceRoots = ([request.configDirectory] + request.sourceConfigDirectories) + .filter { $0.hasPrefix("/") } + .reduce(into: [String]()) { roots, root in + if !roots.contains(root) { roots.append(root) } + } + let sourceTranscript = sourceRoots.lazy.compactMap { root in + findSourceTranscript( + sessionID: request.sessionID, + sourceWorkingDirectory: request.sourceWorkingDirectory, + projectsRoot: (root as NSString).appendingPathComponent("projects"), + fileManager: fileManager + ) + }.first guard let sourceTranscript else { return } let targetTranscript = (targetProject as NSString).appendingPathComponent(sourceTranscript.relativePath) let targetSidecar = (targetProject as NSString).appendingPathComponent(request.sessionID) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift index b07b73126a2e..f7fee68f40e8 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeTranscriptForkSeederTests.swift @@ -50,6 +50,38 @@ struct ClaudeTranscriptForkSeederTests { } extension ClaudeTranscriptForkSeederTests { + @Test + func copiesFromFallbackConfigDirectoryIntoLaunchConfig() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-claude-seeder-fallback-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: root) } + let sourceConfig = root.appendingPathComponent("source-config") + let launchConfig = root.appendingPathComponent("launch-config") + let source = root.appendingPathComponent("source") + let destination = root.appendingPathComponent("destination") + let sessionID = "fallback-session" + let sourceProject = sourceConfig.appendingPathComponent("projects") + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: source.path)) + try FileManager.default.createDirectory(at: sourceProject, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true) + let sourceTranscript = sourceProject.appendingPathComponent("\(sessionID).jsonl") + try Data("{\"type\":\"user\"}\n".utf8).write(to: sourceTranscript) + + try await ClaudeTranscriptForkSeeder().seed(ClaudeTranscriptForkSeedRequest( + sessionID: sessionID, + sourceWorkingDirectory: source.path, + targetWorkingDirectory: destination.path, + configDirectory: launchConfig.path, + sourceConfigDirectories: [sourceConfig.path] + )) + + let targetTranscript = launchConfig.appendingPathComponent("projects") + .appendingPathComponent(ClaudeProjectSlug().slug(forWorkingDirectory: destination.path)) + .appendingPathComponent("\(sessionID).jsonl") + #expect(try Data(contentsOf: targetTranscript) == Data(contentsOf: sourceTranscript)) + } + @Test func findsNestedTranscriptWithClaudeSlugAndRejectsDirectoryCandidate() async throws { let root = FileManager.default.temporaryDirectory From e24a1fc40c754aebfd2f8b896cdedb7ee400a667 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:39:44 -0700 Subject: [PATCH 12/15] avoid sorting Claude project fallback scan --- .../Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift index 645f9687a2b0..daa715cab7f4 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptForkSeeder.swift @@ -94,7 +94,7 @@ public struct ClaudeTranscriptForkSeeder: Sendable { ) { return location } } guard let projectNames = try? fileManager.contentsOfDirectory(atPath: projectsRoot) else { return nil } - for projectName in projectNames.sorted() { + for projectName in projectNames { let projectPath = (projectsRoot as NSString).appendingPathComponent(projectName) if let location = transcriptLocation( projectPath: projectPath, sessionID: sessionID, fileManager: fileManager From e3696c97227917a7f3f0c44ef6bd835ded2ede6f Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:05:50 -0700 Subject: [PATCH 13/15] fix nested Codex fork watcher propagation --- CLI/cmux.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index f240a6a4a268..e916e1a6f39a 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -31561,6 +31561,21 @@ struct CMUXCLI { "--session", sessionId, ] + // A cmux-created Codex fork carries its parent and launch claim in the + // environment. Forward them to the detached monitor so it can watch + // the owner process for the child rollout and publish the child hook + // binding. Without this, the first fork may render, but a fork of that + // child has no durable parent association to discover. + if let forkParent = env[CodexForkSessionWatcher.parentSessionEnvironmentKey], + !forkParent.isEmpty { + monitorArgs += ["--fork-parent", forkParent] + if let launchID = env[CodexForkSessionWatcher.launchIDEnvironmentKey], !launchID.isEmpty { + monitorArgs += ["--fork-launch-id", launchID] + } + if let ownerPID = env["CMUX_CODEX_PID"], !ownerPID.isEmpty { + monitorArgs += ["--fork-owner-pid", ownerPID] + } + } if let surfaceId, !surfaceId.isEmpty { monitorArgs += ["--surface", surfaceId] } From adada745224aeb04f531b22206f5265f28647b93 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:06:36 -0700 Subject: [PATCH 14/15] test Codex nested fork monitor handoff --- CLI/cmux.swift | 24 ++++++++------- cmuxTests/CodexForkMonitorArgumentTests.swift | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 cmuxTests/CodexForkMonitorArgumentTests.swift diff --git a/CLI/cmux.swift b/CLI/cmux.swift index e916e1a6f39a..4b74ec7474da 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -31566,16 +31566,7 @@ struct CMUXCLI { // the owner process for the child rollout and publish the child hook // binding. Without this, the first fork may render, but a fork of that // child has no durable parent association to discover. - if let forkParent = env[CodexForkSessionWatcher.parentSessionEnvironmentKey], - !forkParent.isEmpty { - monitorArgs += ["--fork-parent", forkParent] - if let launchID = env[CodexForkSessionWatcher.launchIDEnvironmentKey], !launchID.isEmpty { - monitorArgs += ["--fork-launch-id", launchID] - } - if let ownerPID = env["CMUX_CODEX_PID"], !ownerPID.isEmpty { - monitorArgs += ["--fork-owner-pid", ownerPID] - } - } + monitorArgs += Self.codexForkMonitorArguments(environment: env) if let surfaceId, !surfaceId.isEmpty { monitorArgs += ["--surface", surfaceId] } @@ -31604,6 +31595,19 @@ struct CMUXCLI { } } + static func codexForkMonitorArguments(environment: [String: String]) -> [String] { + guard let forkParent = environment[CodexForkSessionWatcher.parentSessionEnvironmentKey], + !forkParent.isEmpty else { return [] } + var arguments = ["--fork-parent", forkParent] + if let launchID = environment[CodexForkSessionWatcher.launchIDEnvironmentKey], !launchID.isEmpty { + arguments += ["--fork-launch-id", launchID] + } + if let ownerPID = environment["CMUX_CODEX_PID"], !ownerPID.isEmpty { + arguments += ["--fork-owner-pid", ownerPID] + } + return arguments + } + /// Watches the Codex rollout until the turn settles. /// /// Returns the Stop replay for a healthy completion instead of running it, diff --git a/cmuxTests/CodexForkMonitorArgumentTests.swift b/cmuxTests/CodexForkMonitorArgumentTests.swift new file mode 100644 index 000000000000..460ca14aef18 --- /dev/null +++ b/cmuxTests/CodexForkMonitorArgumentTests.swift @@ -0,0 +1,30 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +struct CodexForkMonitorArgumentTests { + @Test + func forwardsForkParentClaimToDetachedMonitor() { + let arguments = CMUXCLI.codexForkMonitorArguments(environment: [ + "CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session", + "CMUX_AGENT_FORK_LAUNCH_ID": "launch-id", + "CMUX_CODEX_PID": "1234", + ]) + + #expect(arguments == [ + "--fork-parent", "parent-session", + "--fork-launch-id", "launch-id", + "--fork-owner-pid", "1234", + ]) + } + + @Test + func omitsForkArgumentsForNormalCodexMonitor() { + #expect(CMUXCLI.codexForkMonitorArguments(environment: [:]).isEmpty) + } +} From b6b24e29c6f5ed9624852d1d49cb3f331d8e2b14 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:35:40 -0700 Subject: [PATCH 15/15] Wire Codex fork monitor tests into Xcode --- cmux.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index b2187f3b3015..aa6732efd794 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1552,6 +1552,7 @@ 1606FB10C21C603790FEE7D3 /* CodexAutoNamingArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62542679E9A9BFDD99A7BC6 /* CodexAutoNamingArgumentsTests.swift */; }; B6F143F6E02543F3899F1E02 /* CodexAutoresumeChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F143F6E02543F3899F1E01 /* CodexAutoresumeChainTests.swift */; }; C8711B000000000000000002 /* CodexCodeModeRolloutIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8711B000000000000000001 /* CodexCodeModeRolloutIdentityTests.swift */; }; + A0EF24A521C245FBB99B8B32 /* CodexForkMonitorArgumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA4140BCE8D49BBAE4E8234 /* CodexForkMonitorArgumentTests.swift */; }; 9167FFA2759E9AD60F49616C /* CodexHookCapturedSocketCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */; }; C1175700000000000000004A /* CodexHookCapturedSocketCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */; }; 918100000000000000000051 /* CodexHookFailureCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000050 /* CodexHookFailureCandidate.swift */; }; @@ -5822,6 +5823,7 @@ A62542679E9A9BFDD99A7BC6 /* CodexAutoNamingArgumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexAutoNamingArgumentsTests.swift; sourceTree = ""; }; B6F143F6E02543F3899F1E01 /* CodexAutoresumeChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexAutoresumeChainTests.swift; sourceTree = ""; }; C8711B000000000000000001 /* CodexCodeModeRolloutIdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexCodeModeRolloutIdentityTests.swift; sourceTree = ""; }; + 5FA4140BCE8D49BBAE4E8234 /* CodexForkMonitorArgumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodexForkMonitorArgumentTests.swift"; sourceTree = ""; }; D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodexHookCapturedSocketCommands.swift"; sourceTree = ""; }; 918100000000000000000050 /* CodexHookFailureCandidate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexHookFailureCandidate.swift; sourceTree = ""; }; 918100000000000000000040 /* CodexHookFailureSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexHookFailureSummary.swift; sourceTree = ""; }; @@ -12971,6 +12973,7 @@ C851974591E32B011E9704A7 /* ClaudeHookWorkStateTests.swift */, 2D3917E88FC3A4BFAD8ED176 /* SidebarAgentWorkStateTests.swift */, EA6E81B689749BFCBB162559 /* AutoNamingOpenCodeArgumentsTests.swift */, + 5FA4140BCE8D49BBAE4E8234 /* CodexForkMonitorArgumentTests.swift */, ); path = cmuxTests; sourceTree = ""; @@ -17127,6 +17130,7 @@ A9E040000000000000000002 /* CodexAppServerSessionTests.swift in Sources */, B6F143F6E02543F3899F1E02 /* CodexAutoresumeChainTests.swift in Sources */, C8711B000000000000000002 /* CodexCodeModeRolloutIdentityTests.swift in Sources */, + A0EF24A521C245FBB99B8B32 /* CodexForkMonitorArgumentTests.swift in Sources */, 9167FFA2759E9AD60F49616C /* CodexHookCapturedSocketCommands.swift in Sources */, 256E3FBC057F233E8849DF79 /* CodexRestoreHookEvidenceTests.swift in Sources */, 7FF4F2EA73F8883D21912965 /* CodexRestoreOwnerGenerationTests.swift in Sources */,