Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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 () => {
Expand Down
167 changes: 166 additions & 1 deletion src/__tests__/renderer/utils/worktreeSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -139,6 +139,171 @@ 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 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 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', () => {
const parent = createMockParentSession();
const session = buildWorktreeSession({
Expand Down
70 changes: 66 additions & 4 deletions src/renderer/hooks/batch/useAutoRunHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -395,14 +399,72 @@ export function useAutoRunHandlers(
}
}

// 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 && 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;
window.maestro.logger.log(
'warn',
`Failed to read doc "${doc.filename}" from source folder`,
'AutoRunHandlers'
);
}
Comment thread
jSydorowicz21 marked this conversation as resolved.
} 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;
}
}

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]
);
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/hooks/batch/useBatchProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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) => {
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/hooks/session/useSessionRestoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`,
};
}
Comment thread
jSydorowicz21 marked this conversation as resolved.
}

// Migration: ensure fileTreeAutoRefreshInterval is set (default 180s for legacy sessions)
if (session.fileTreeAutoRefreshInterval == null) {
console.warn(
Expand Down
Loading