From e6d7f3d860ccb753edd84ee8175fa7b414c59d8d Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Tue, 17 Mar 2026 23:36:03 -0500 Subject: [PATCH 1/7] MAESTRO: fix worktree session Auto Run path resolution (#584) buildWorktreeSession() previously copied the parent session's autoRunFolderPath verbatim, causing worktree sessions to use the parent repo's Auto Run folder instead of their own. Added resolveWorktreeAutoRunPath() that rebases absolute paths under the parent cwd onto the worktree cwd, keeps relative and external absolute paths unchanged, and handles Windows backslash separators. --- .../renderer/utils/worktreeSession.test.ts | 77 +++++++++++++++++++ src/renderer/utils/worktreeSession.ts | 47 ++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/__tests__/renderer/utils/worktreeSession.test.ts b/src/__tests__/renderer/utils/worktreeSession.test.ts index 5fd253eb20..c0a14b5573 100644 --- a/src/__tests__/renderer/utils/worktreeSession.test.ts +++ b/src/__tests__/renderer/utils/worktreeSession.test.ts @@ -139,6 +139,83 @@ describe('buildWorktreeSession', () => { expect(session.sessionSshRemoteConfig).toEqual(sshConfig); }); + describe('autoRunFolderPath resolution', () => { + it('should rebase absolute path under parent cwd onto worktree cwd', () => { + const parent = createMockParentSession({ + cwd: '/projects/main', + autoRunFolderPath: '/projects/main/Auto Run Docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe('/worktrees/feature-x/Auto Run Docs'); + }); + + it('should keep relative path as-is', () => { + const parent = createMockParentSession({ + autoRunFolderPath: 'Auto Run Docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe('Auto Run Docs'); + }); + + it('should keep external absolute path as-is', () => { + const parent = createMockParentSession({ + cwd: '/projects/main', + autoRunFolderPath: '/shared/autorun-docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe('/shared/autorun-docs'); + }); + + it('should handle Windows backslash paths', () => { + const parent = createMockParentSession({ + cwd: 'C:\\Users\\Admin\\Software\\Maestro', + autoRunFolderPath: 'C:\\Users\\Admin\\Software\\Maestro\\Auto Run Docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: 'C:\\Users\\Admin\\Software\\Maestro-worktree', + name: 'worktree', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe( + 'C:\\Users\\Admin\\Software\\Maestro-worktree\\Auto Run Docs' + ); + }); + + it('should return undefined when parent has no autoRunFolderPath', () => { + const parent = createMockParentSession({ + autoRunFolderPath: undefined, + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBeUndefined(); + }); + }); + it('should create initial AI tab with correct settings', () => { const parent = createMockParentSession(); const session = buildWorktreeSession({ diff --git a/src/renderer/utils/worktreeSession.ts b/src/renderer/utils/worktreeSession.ts index e7e956879a..00be3938d6 100644 --- a/src/renderer/utils/worktreeSession.ts +++ b/src/renderer/utils/worktreeSession.ts @@ -33,6 +33,45 @@ export interface BuildWorktreeSessionParams { worktreeParentPath?: string; } +/** + * Resolves the Auto Run folder path for a worktree session. + * + * - Relative paths are kept as-is (they resolve from each session's own cwd). + * - Absolute paths under the parent's cwd are rebased onto the worktree's cwd. + * - Absolute paths outside the parent's cwd are kept as-is (intentionally external). + */ +function resolveWorktreeAutoRunPath( + parentAutoRunPath: string | undefined, + parentCwd: string, + worktreeCwd: string +): string | undefined { + if (!parentAutoRunPath) return undefined; + + // Normalize to forward slashes for comparison + const norm = (p: string) => p.replace(/\\/g, '/'); + const normalized = norm(parentAutoRunPath); + + // Check if the path is absolute (Unix / or Windows drive letter) + const isAbsolute = /^\/|^[A-Za-z]:[/\\]/.test(parentAutoRunPath); + if (!isAbsolute) return parentAutoRunPath; + + // Check if the absolute path is under the parent's cwd + const normalizedParentCwd = norm(parentCwd); + const prefix = normalizedParentCwd.endsWith('/') + ? normalizedParentCwd + : normalizedParentCwd + '/'; + + if (normalized === normalizedParentCwd || normalized.startsWith(prefix)) { + const relativePart = normalized.slice(normalizedParentCwd.length); + const result = norm(worktreeCwd) + relativePart; + // Preserve original separator style + return parentAutoRunPath.includes('\\') ? result.replace(/\//g, '\\') : result; + } + + // External absolute path - keep as-is + return parentAutoRunPath; +} + export function buildWorktreeSession(params: BuildWorktreeSessionParams): Session { const newId = generateId(); const initialTabId = generateId(); @@ -120,6 +159,12 @@ export function buildWorktreeSession(params: BuildWorktreeSessionParams): Sessio // New model inherits these; legacy does not customContextWindow: isLegacy ? undefined : params.parentSession.customContextWindow, nudgeMessage: isLegacy ? undefined : params.parentSession.nudgeMessage, - autoRunFolderPath: isLegacy ? undefined : params.parentSession.autoRunFolderPath, + autoRunFolderPath: isLegacy + ? undefined + : resolveWorktreeAutoRunPath( + params.parentSession.autoRunFolderPath, + params.parentSession.cwd, + params.path + ), } as Session; } From 56fe6e0c937cc32162ad730bcf532012641e2dea Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Tue, 17 Mar 2026 23:40:08 -0500 Subject: [PATCH 2/7] MAESTRO: fix batch run dispatch to use target session's Auto Run path When dispatching a batch run to a worktree session, look up the target session's autoRunFolderPath (resolved relative to its cwd) instead of always using the parent/active session's path. Falls back to the active session's path if the target doesn't have one set. Also verified useAutoRunDocumentLoader.ts - it already uses the active session's own autoRunFolderPath via selectActiveSession, so it's correct after the worktreeSession.ts fix. Fixes #584 --- .../hooks/batch/useAutoRunHandlers.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/renderer/hooks/batch/useAutoRunHandlers.ts b/src/renderer/hooks/batch/useAutoRunHandlers.ts index 88cae76bac..049573e3c1 100644 --- a/src/renderer/hooks/batch/useAutoRunHandlers.ts +++ b/src/renderer/hooks/batch/useAutoRunHandlers.ts @@ -395,14 +395,27 @@ export function useAutoRunHandlers( } } + // Resolve the Auto Run folder path for the target session. + // When dispatching to a worktree, use the target session's own + // autoRunFolderPath (resolved relative to its cwd by buildWorktreeSession). + // Fall back to activeSession's path if the target doesn't have one set. + let folderPath = activeSession.autoRunFolderPath; + if (targetSessionId !== activeSession.id) { + const targetSession = useSessionStore + .getState() + .sessions.find((s) => s.id === targetSessionId); + if (targetSession?.autoRunFolderPath) { + folderPath = targetSession.autoRunFolderPath; + } + } + window.maestro.logger.log('info', 'Starting batch run', 'AutoRunHandlers', { sessionId: targetSessionId, - folderPath: activeSession.autoRunFolderPath, + folderPath, isWorktreeTarget: targetSessionId !== activeSession.id, }); setBatchRunnerModalOpen(false); - // Documents stay with the parent session's autoRunFolderPath; execution targets the worktree agent - startBatchRun(targetSessionId, config, activeSession.autoRunFolderPath); + startBatchRun(targetSessionId, config, folderPath); }, [activeSession, startBatchRun, setBatchRunnerModalOpen] ); From 8d44bb92f48dac40fc2e5df063218926be1a12df Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Tue, 17 Mar 2026 23:42:39 -0500 Subject: [PATCH 3/7] MAESTRO: verify worktree Auto Run folder selection stores path independently Confirmed that handleAutoRunFolderSelected already stores the selected folder path directly on the active session with no parent-session resolution logic. Added clarifying comment documenting this behavior for worktree sessions. --- src/renderer/hooks/batch/useAutoRunHandlers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/hooks/batch/useAutoRunHandlers.ts b/src/renderer/hooks/batch/useAutoRunHandlers.ts index 049573e3c1..f33c0b06d0 100644 --- a/src/renderer/hooks/batch/useAutoRunHandlers.ts +++ b/src/renderer/hooks/batch/useAutoRunHandlers.ts @@ -218,7 +218,11 @@ export function useAutoRunHandlers( startBatchRun, } = deps; - // Handler for auto run folder selection from setup modal + // Handler for auto run folder selection from setup modal. + // Worktree sessions store their own independent Auto Run folder paths - + // the selected path is stored directly on activeSession (whichever session + // is active), with no parent-session resolution. This is correct because + // the user picks the folder relative to the worktree's own working directory. const handleAutoRunFolderSelected = useCallback( async (folderPath: string) => { if (!activeSession) return; From d39baeaf20039eb81c9bc672eb16cdc823767e86 Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Tue, 17 Mar 2026 23:58:43 -0500 Subject: [PATCH 4/7] fix: resolve Auto Run folder path relative to worktree cwd (#584) Worktree sessions now resolve their Auto Run folder path relative to their own working directory instead of inheriting the parent repo's absolute path. Batch run dispatch also now uses the target session's resolved path. Changes in this commit: - Add case-insensitive path comparison for Windows in resolveWorktreeAutoRunPath to handle mixed-case drive letters - Update stale comment in useBatchProcessor.ts to reflect that folderPath may come from the target worktree session - Add edge case tests: empty string path, path-equals-cwd, and case-insensitive Windows matching --- .../renderer/utils/worktreeSession.test.ts | 46 +++++++++++++++++++ src/renderer/hooks/batch/useBatchProcessor.ts | 4 +- src/renderer/utils/worktreeSession.ts | 7 ++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/__tests__/renderer/utils/worktreeSession.test.ts b/src/__tests__/renderer/utils/worktreeSession.test.ts index c0a14b5573..49a0da61f6 100644 --- a/src/__tests__/renderer/utils/worktreeSession.test.ts +++ b/src/__tests__/renderer/utils/worktreeSession.test.ts @@ -214,6 +214,52 @@ describe('buildWorktreeSession', () => { }); expect(session.autoRunFolderPath).toBeUndefined(); }); + + it('should treat empty string autoRunFolderPath as undefined', () => { + const parent = createMockParentSession({ + autoRunFolderPath: '', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBeUndefined(); + }); + + it('should rebase when autoRunFolderPath equals parent cwd exactly', () => { + const parent = createMockParentSession({ + cwd: '/projects/main', + autoRunFolderPath: '/projects/main', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe('/worktrees/feature-x'); + }); + + it('should handle case-insensitive matching on Windows-style paths', () => { + const parent = createMockParentSession({ + cwd: 'C:\\Users\\Admin\\Software\\Maestro', + autoRunFolderPath: 'c:\\users\\admin\\software\\Maestro\\Auto Run Docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: 'C:\\Users\\Admin\\Software\\Maestro-worktree', + name: 'worktree', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + expect(session.autoRunFolderPath).toBe( + 'C:\\Users\\Admin\\Software\\Maestro-worktree\\Auto Run Docs' + ); + }); }); it('should create initial AI tab with correct settings', () => { diff --git a/src/renderer/hooks/batch/useBatchProcessor.ts b/src/renderer/hooks/batch/useBatchProcessor.ts index 73dc679060..afa9e216b2 100644 --- a/src/renderer/hooks/batch/useBatchProcessor.ts +++ b/src/renderer/hooks/batch/useBatchProcessor.ts @@ -604,8 +604,8 @@ export function useBatchProcessor({ /** * Start a batch processing run for a specific session with multi-document support. * Note: sessionId and folderPath can belong to different sessions when running - * in a worktree — the parent session owns the Auto Run documents (folderPath) - * while the worktree agent (sessionId) executes the tasks. + * in a worktree. The folderPath is resolved from the target session's own + * autoRunFolderPath (rebased onto its cwd) or falls back to the parent's path. */ const startBatchRun = useCallback( async (sessionId: string, config: BatchRunConfig, folderPath: string) => { diff --git a/src/renderer/utils/worktreeSession.ts b/src/renderer/utils/worktreeSession.ts index 00be3938d6..24470122bc 100644 --- a/src/renderer/utils/worktreeSession.ts +++ b/src/renderer/utils/worktreeSession.ts @@ -56,12 +56,17 @@ function resolveWorktreeAutoRunPath( if (!isAbsolute) return parentAutoRunPath; // Check if the absolute path is under the parent's cwd + // Use case-insensitive comparison for Windows (drive letters, directory names) const normalizedParentCwd = norm(parentCwd); const prefix = normalizedParentCwd.endsWith('/') ? normalizedParentCwd : normalizedParentCwd + '/'; - if (normalized === normalizedParentCwd || normalized.startsWith(prefix)) { + const normalizedLower = normalized.toLowerCase(); + const parentCwdLower = normalizedParentCwd.toLowerCase(); + const prefixLower = prefix.toLowerCase(); + + if (normalizedLower === parentCwdLower || normalizedLower.startsWith(prefixLower)) { const relativePart = normalized.slice(normalizedParentCwd.length); const result = norm(worktreeCwd) + relativePart; // Preserve original separator style From 0662c9ebbe9830c87c24687f9a6fa969e3981744 Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Wed, 18 Mar 2026 18:30:15 -0500 Subject: [PATCH 5/7] fix: copy queued docs on worktree dispatch, migrate stale paths, add isPathUnderRoot helper - Extract shared `isPathUnderRoot()` and `normSep()` into worktreeSession.ts - Copy-on-dispatch: when dispatching from parent to worktree, copy only the queued docs to the worktree's Auto Run folder via Promise.all - Show toast on partial copy failure so users know if docs are missing - Add session restoration migration for pre-existing worktree sessions whose autoRunFolderPath still points at the parent directory - Platform-aware: Windows case-insensitive, Unix case-sensitive comparison - Add 6 direct unit tests for isPathUnderRoot, update dispatch test --- .../batch/useAutoRunHandlers.worktree.test.ts | 22 ++++++-- .../renderer/utils/worktreeSession.test.ts | 44 +++++++++++++++- .../hooks/batch/useAutoRunHandlers.ts | 52 ++++++++++++++++--- src/renderer/hooks/batch/useBatchProcessor.ts | 4 +- .../hooks/session/useSessionRestoration.ts | 17 ++++++ src/renderer/utils/worktreeSession.ts | 42 +++++++++------ 6 files changed, 151 insertions(+), 30 deletions(-) diff --git a/src/__tests__/renderer/hooks/batch/useAutoRunHandlers.worktree.test.ts b/src/__tests__/renderer/hooks/batch/useAutoRunHandlers.worktree.test.ts index 08051cab04..dc26e3dfa3 100644 --- a/src/__tests__/renderer/hooks/batch/useAutoRunHandlers.worktree.test.ts +++ b/src/__tests__/renderer/hooks/batch/useAutoRunHandlers.worktree.test.ts @@ -224,17 +224,18 @@ describe('handleStartBatchRun — worktree dispatch integration', () => { ); }); - it('still uses activeSession.autoRunFolderPath for document source', async () => { + it('copies queued docs to worktree and uses worktree folder path', async () => { const session = createMockSession({ autoRunFolderPath: '/my/autorun/folder', }); const deps = createMockDeps(); - // Populate store with target session + // Populate store with target session (has its own autoRunFolderPath) const worktreeChild = createMockSession({ id: 'worktree-child-99', state: 'idle', parentSessionId: session.id, + autoRunFolderPath: '/worktrees/feature/Auto Run Docs', }); useSessionStore.setState({ sessions: [session, worktreeChild], @@ -258,9 +259,22 @@ describe('handleStartBatchRun — worktree dispatch integration', () => { await result.current.handleStartBatchRun(config); }); - // folderPath (third arg) comes from the parent session, not the worktree + // Docs should be read from parent's folder + expect(window.maestro.autorun.readDoc).toHaveBeenCalledWith( + '/my/autorun/folder', + 'Phase 1.md', + undefined + ); + // Docs should be written to worktree's folder + expect(window.maestro.autorun.writeDoc).toHaveBeenCalledWith( + '/worktrees/feature/Auto Run Docs', + 'Phase 1.md', + '', + undefined + ); + // Batch run uses the worktree's folder path (where docs were copied) const [, , folderPath] = deps.startBatchRun.mock.calls[0]; - expect(folderPath).toBe('/my/autorun/folder'); + expect(folderPath).toBe('/worktrees/feature/Auto Run Docs'); }); it('does not call worktreeSetup for existing-open sessions', async () => { diff --git a/src/__tests__/renderer/utils/worktreeSession.test.ts b/src/__tests__/renderer/utils/worktreeSession.test.ts index 49a0da61f6..fb105130ee 100644 --- a/src/__tests__/renderer/utils/worktreeSession.test.ts +++ b/src/__tests__/renderer/utils/worktreeSession.test.ts @@ -4,7 +4,7 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { buildWorktreeSession } from '../../../renderer/utils/worktreeSession'; +import { buildWorktreeSession, isPathUnderRoot } from '../../../renderer/utils/worktreeSession'; import type { Session } from '../../../renderer/types'; // Mock generateId for deterministic IDs @@ -260,6 +260,48 @@ describe('buildWorktreeSession', () => { 'C:\\Users\\Admin\\Software\\Maestro-worktree\\Auto Run Docs' ); }); + + it('should use case-sensitive comparison for Unix paths', () => { + const parent = createMockParentSession({ + cwd: '/Projects/Main', + autoRunFolderPath: '/projects/main/Auto Run Docs', + }); + const session = buildWorktreeSession({ + parentSession: parent, + path: '/worktrees/feature-x', + name: 'feature-x', + defaultSaveToHistory: true, + defaultShowThinking: 'off', + }); + // Different case on Unix means different directory - treat as external + expect(session.autoRunFolderPath).toBe('/projects/main/Auto Run Docs'); + }); + }); + + describe('isPathUnderRoot', () => { + it('returns true for path under root (Unix)', () => { + expect(isPathUnderRoot('/projects/main/Auto Run Docs', '/projects/main')).toBe(true); + }); + + it('returns true when path equals root', () => { + expect(isPathUnderRoot('/projects/main', '/projects/main')).toBe(true); + }); + + it('returns false for external path', () => { + expect(isPathUnderRoot('/other/path', '/projects/main')).toBe(false); + }); + + it('returns false for partial prefix match', () => { + expect(isPathUnderRoot('/projects/main-fork/docs', '/projects/main')).toBe(false); + }); + + it('handles Windows case-insensitive', () => { + expect(isPathUnderRoot('C:\\Users\\Admin\\docs', 'c:\\users\\admin')).toBe(true); + }); + + it('handles Unix case-sensitive', () => { + expect(isPathUnderRoot('/Projects/Main/docs', '/projects/main')).toBe(false); + }); }); it('should create initial AI tab with correct settings', () => { diff --git a/src/renderer/hooks/batch/useAutoRunHandlers.ts b/src/renderer/hooks/batch/useAutoRunHandlers.ts index f33c0b06d0..7a5bc80eab 100644 --- a/src/renderer/hooks/batch/useAutoRunHandlers.ts +++ b/src/renderer/hooks/batch/useAutoRunHandlers.ts @@ -399,17 +399,57 @@ export function useAutoRunHandlers( } } - // Resolve the Auto Run folder path for the target session. - // When dispatching to a worktree, use the target session's own - // autoRunFolderPath (resolved relative to its cwd by buildWorktreeSession). - // Fall back to activeSession's path if the target doesn't have one set. + // Resolve the folder path for the batch run. + // When the user IS on the worktree, activeSession.autoRunFolderPath already + // points to the worktree's own directory (set by buildWorktreeSession). + // When dispatching from parent to worktree, copy queued docs to the + // worktree's Auto Run folder so subsequent runs find them locally. let folderPath = activeSession.autoRunFolderPath; if (targetSessionId !== activeSession.id) { const targetSession = useSessionStore .getState() .sessions.find((s) => s.id === targetSessionId); - if (targetSession?.autoRunFolderPath) { - folderPath = targetSession.autoRunFolderPath; + if (targetSession?.autoRunFolderPath && targetSession.autoRunFolderPath !== folderPath) { + const sshRemoteId = getSshRemoteId(activeSession); + const destFolder = targetSession.autoRunFolderPath; + // Copy only the queued documents from the parent's folder to the worktree + let copyFailed = false; + await Promise.all( + config.documents.map(async (doc) => { + try { + const readResult = await window.maestro.autorun.readDoc( + folderPath, + doc.filename + '.md', + sshRemoteId + ); + if (readResult.success && readResult.content != null) { + await window.maestro.autorun.writeDoc( + destFolder, + doc.filename + '.md', + readResult.content, + sshRemoteId + ); + } else { + copyFailed = true; + } + } catch (err) { + copyFailed = true; + window.maestro.logger.log( + 'warn', + `Failed to copy doc "${doc.filename}" to worktree: ${err instanceof Error ? err.message : String(err)}`, + 'AutoRunHandlers' + ); + } + }) + ); + if (copyFailed) { + notifyToast({ + type: 'warning', + title: 'Some Documents Failed to Copy', + message: 'Not all documents could be copied to the worktree. Check logs for details.', + }); + } + folderPath = destFolder; } } diff --git a/src/renderer/hooks/batch/useBatchProcessor.ts b/src/renderer/hooks/batch/useBatchProcessor.ts index afa9e216b2..0f5ea40859 100644 --- a/src/renderer/hooks/batch/useBatchProcessor.ts +++ b/src/renderer/hooks/batch/useBatchProcessor.ts @@ -604,8 +604,8 @@ export function useBatchProcessor({ /** * Start a batch processing run for a specific session with multi-document support. * Note: sessionId and folderPath can belong to different sessions when running - * in a worktree. The folderPath is resolved from the target session's own - * autoRunFolderPath (rebased onto its cwd) or falls back to the parent's path. + * in a worktree. Queued docs are copied to the worktree's own Auto Run folder + * before dispatch so subsequent runs find them locally. */ const startBatchRun = useCallback( async (sessionId: string, config: BatchRunConfig, folderPath: string) => { diff --git a/src/renderer/hooks/session/useSessionRestoration.ts b/src/renderer/hooks/session/useSessionRestoration.ts index e6fb4647ac..7e07e4a0bf 100644 --- a/src/renderer/hooks/session/useSessionRestoration.ts +++ b/src/renderer/hooks/session/useSessionRestoration.ts @@ -20,6 +20,7 @@ import { useGroupChatStore } from '../../stores/groupChatStore'; import { gitService } from '../../services/git'; import { generateId } from '../../utils/ids'; import { AUTO_RUN_FOLDER_NAME } from '../../components/Wizard'; +import { isPathUnderRoot } from '../../utils/worktreeSession'; // ============================================================================ // Return type @@ -169,6 +170,22 @@ export function useSessionRestoration(): SessionRestorationReturn { }; } + // Migration: worktree sessions should use their own Auto Run folder path, + // not the parent's. Old sessions inherited the parent's path directly. + // Rebase to projectRoot (which is the worktree's cwd) if the current path + // doesn't already point there. + if (session.parentSessionId && session.autoRunFolderPath && session.projectRoot) { + if (!isPathUnderRoot(session.autoRunFolderPath, session.projectRoot)) { + console.warn( + `[restoreSession] Worktree session autoRunFolderPath was under parent, rebasing to ${session.projectRoot}` + ); + session = { + ...session, + autoRunFolderPath: `${session.projectRoot}/${AUTO_RUN_FOLDER_NAME}`, + }; + } + } + // Migration: ensure fileTreeAutoRefreshInterval is set (default 180s for legacy sessions) if (session.fileTreeAutoRefreshInterval == null) { console.warn( diff --git a/src/renderer/utils/worktreeSession.ts b/src/renderer/utils/worktreeSession.ts index 24470122bc..f8c8f5db50 100644 --- a/src/renderer/utils/worktreeSession.ts +++ b/src/renderer/utils/worktreeSession.ts @@ -33,6 +33,27 @@ export interface BuildWorktreeSessionParams { worktreeParentPath?: string; } +/** Normalize path separators to forward slashes for comparison. */ +const normSep = (p: string) => p.replace(/\\/g, '/'); + +/** + * Check whether `filePath` is located under (or equal to) `root` (platform-aware). + * Windows paths use case-insensitive comparison; Unix paths are case-sensitive. + */ +export function isPathUnderRoot(filePath: string, root: string): boolean { + const normalizedFile = normSep(filePath); + const normalizedRoot = normSep(root); + const prefix = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/'; + const isWin = /^[A-Za-z]:/.test(filePath); + if (isWin) { + return ( + normalizedFile.toLowerCase() === normalizedRoot.toLowerCase() || + normalizedFile.toLowerCase().startsWith(prefix.toLowerCase()) + ); + } + return normalizedFile === normalizedRoot || normalizedFile.startsWith(prefix); +} + /** * Resolves the Auto Run folder path for a worktree session. * @@ -47,28 +68,15 @@ function resolveWorktreeAutoRunPath( ): string | undefined { if (!parentAutoRunPath) return undefined; - // Normalize to forward slashes for comparison - const norm = (p: string) => p.replace(/\\/g, '/'); - const normalized = norm(parentAutoRunPath); - // Check if the path is absolute (Unix / or Windows drive letter) const isAbsolute = /^\/|^[A-Za-z]:[/\\]/.test(parentAutoRunPath); if (!isAbsolute) return parentAutoRunPath; - // Check if the absolute path is under the parent's cwd - // Use case-insensitive comparison for Windows (drive letters, directory names) - const normalizedParentCwd = norm(parentCwd); - const prefix = normalizedParentCwd.endsWith('/') - ? normalizedParentCwd - : normalizedParentCwd + '/'; - - const normalizedLower = normalized.toLowerCase(); - const parentCwdLower = normalizedParentCwd.toLowerCase(); - const prefixLower = prefix.toLowerCase(); - - if (normalizedLower === parentCwdLower || normalizedLower.startsWith(prefixLower)) { + if (isPathUnderRoot(parentAutoRunPath, parentCwd)) { + const normalizedParentCwd = normSep(parentCwd); + const normalized = normSep(parentAutoRunPath); const relativePart = normalized.slice(normalizedParentCwd.length); - const result = norm(worktreeCwd) + relativePart; + const result = normSep(worktreeCwd) + relativePart; // Preserve original separator style return parentAutoRunPath.includes('\\') ? result.replace(/\//g, '\\') : result; } From eb350467019f0822865ca55049a2d25ff606488e Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Wed, 18 Mar 2026 18:40:46 -0500 Subject: [PATCH 6/7] fix: address PR review feedback - log readDoc failures, check both args for Windows detection - Log warning when readDoc returns success:false so "Check logs" toast is useful - Check both filePath and root for drive letter in isPathUnderRoot --- src/renderer/hooks/batch/useAutoRunHandlers.ts | 5 +++++ src/renderer/utils/worktreeSession.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/renderer/hooks/batch/useAutoRunHandlers.ts b/src/renderer/hooks/batch/useAutoRunHandlers.ts index 7a5bc80eab..2c56a291de 100644 --- a/src/renderer/hooks/batch/useAutoRunHandlers.ts +++ b/src/renderer/hooks/batch/useAutoRunHandlers.ts @@ -431,6 +431,11 @@ export function useAutoRunHandlers( ); } else { copyFailed = true; + window.maestro.logger.log( + 'warn', + `Failed to read doc "${doc.filename}" from source folder`, + 'AutoRunHandlers' + ); } } catch (err) { copyFailed = true; diff --git a/src/renderer/utils/worktreeSession.ts b/src/renderer/utils/worktreeSession.ts index f8c8f5db50..c410e89051 100644 --- a/src/renderer/utils/worktreeSession.ts +++ b/src/renderer/utils/worktreeSession.ts @@ -44,7 +44,8 @@ export function isPathUnderRoot(filePath: string, root: string): boolean { const normalizedFile = normSep(filePath); const normalizedRoot = normSep(root); const prefix = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/'; - const isWin = /^[A-Za-z]:/.test(filePath); + // Either argument having a drive letter means we're on Windows + const isWin = /^[A-Za-z]:/.test(filePath) || /^[A-Za-z]:/.test(root); if (isWin) { return ( normalizedFile.toLowerCase() === normalizedRoot.toLowerCase() || From 55d867b256729206691a4de303e1c8dbd7568cb6 Mon Sep 17 00:00:00 2001 From: Jonathan Sydorowicz Date: Wed, 18 Mar 2026 18:48:20 -0500 Subject: [PATCH 7/7] fix: strip trailing slash from parentCwd before slicing in resolveWorktreeAutoRunPath --- src/renderer/utils/worktreeSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/utils/worktreeSession.ts b/src/renderer/utils/worktreeSession.ts index c410e89051..a4107610ee 100644 --- a/src/renderer/utils/worktreeSession.ts +++ b/src/renderer/utils/worktreeSession.ts @@ -74,7 +74,8 @@ function resolveWorktreeAutoRunPath( if (!isAbsolute) return parentAutoRunPath; if (isPathUnderRoot(parentAutoRunPath, parentCwd)) { - const normalizedParentCwd = normSep(parentCwd); + // Strip trailing slash for consistent slicing + const normalizedParentCwd = normSep(parentCwd).replace(/\/$/, ''); const normalized = normSep(parentAutoRunPath); const relativePart = normalized.slice(normalizedParentCwd.length); const result = normSep(worktreeCwd) + relativePart;