Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
494 changes: 254 additions & 240 deletions docs/releases.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/__tests__/main/ipc/handlers/autorun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ describe('autorun IPC handlers', () => {
'autorun:restoreBackup',
'autorun:deleteBackups',
'autorun:createWorkingCopy',
'autorun:watchStatus',
'autorun:unwatchStatus',
];

for (const channel of expectedChannels) {
Expand Down
117 changes: 116 additions & 1 deletion src/main/ipc/handlers/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import chokidar, { FSWatcher } from 'chokidar';
import Store from 'electron-store';
import { logger } from '../../utils/logger';
import { createIpcHandler, CreateHandlerOptions } from '../../utils/ipcHandler';
import { SshRemoteConfig } from '../../../shared/types';
import { SshRemoteConfig, PlaybookStatus } from '../../../shared/types';
import { MaestroSettings } from './persistence';
import { isWebContentsAvailable } from '../../utils/safe-send';
import {
Expand Down Expand Up @@ -55,6 +55,10 @@ function getSshRemoteById(
const autoRunWatchers = new Map<string, FSWatcher>();
let autoRunWatchDebounceTimer: NodeJS.Timeout | null = null;

// Playbook STATUS.json watchers (keyed by project path)
const statusWatchers = new Map<string, FSWatcher>();
let statusWatchDebounceTimer: NodeJS.Timeout | null = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use per-project debounce timers to avoid cross-project status loss.

A single module-level debounce timer means one project’s status update can cancel another project’s pending update. This can drop or misorder events when multiple runs/watchers are active.

💡 Suggested fix (isolate debounce state by project)
- let statusWatchDebounceTimer: NodeJS.Timeout | null = null;
+ const statusWatchDebounceTimers = new Map<string, NodeJS.Timeout>();

...
- if (statusWatchDebounceTimer) {
- 	clearTimeout(statusWatchDebounceTimer);
- }
- statusWatchDebounceTimer = setTimeout(async () => {
- 	statusWatchDebounceTimer = null;
+ const existingTimer = statusWatchDebounceTimers.get(projectPath);
+ if (existingTimer) {
+ 	clearTimeout(existingTimer);
+ }
+ const timer = setTimeout(async () => {
+ 	statusWatchDebounceTimers.delete(projectPath);
  	try {
  		const content = await fs.readFile(statusFilePath, 'utf-8');
  		const status = JSON.parse(content) as PlaybookStatus;
  		...
  	} catch (err) {
  		logger.warn(`${LOG_CONTEXT} Failed to read STATUS.json: ${err}`, LOG_CONTEXT);
  	}
  }, 500);
+ statusWatchDebounceTimers.set(projectPath, timer);

...
 if (statusWatchers.has(projectPath)) {
 	statusWatchers.get(projectPath)?.close();
 	statusWatchers.delete(projectPath);
+	const timer = statusWatchDebounceTimers.get(projectPath);
+	if (timer) {
+		clearTimeout(timer);
+		statusWatchDebounceTimers.delete(projectPath);
+	}
 	logger.info(`Stopped watching STATUS.json in: ${projectPath}`, LOG_CONTEXT);
 }

...
 for (const [projectPath, watcher] of statusWatchers) {
 	watcher.close();
 	logger.info(`Cleaned up STATUS.json watcher for: ${projectPath}`, LOG_CONTEXT);
 }
 statusWatchers.clear();
+for (const timer of statusWatchDebounceTimers.values()) {
+	clearTimeout(timer);
+}
+statusWatchDebounceTimers.clear();

Also applies to: 1313-1317, 1366-1369, 1383-1387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/autorun.ts` at line 60, The module-level debounce timer
statusWatchDebounceTimer causes cross-project cancellations; replace it with a
per-project timer map (e.g., statusWatchDebounceTimers: Map<string,
NodeJS.Timeout | null>) and update all usages (where statusWatchDebounceTimer is
cleared, set, or checked—including the handlers around the status-watch logic
and the blocks referenced at the other occurrences) to use a project-specific
key (projectId or runId) to get/clear/set the timer, and ensure timers are
cleaned up when a project/watch stops to avoid leaks.

Comment on lines +59 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shared debounce timer broken for concurrent watchers

statusWatchDebounceTimer is a module-level singleton, but the statusWatchers map can hold multiple concurrent watchers (one per projectPath). Each watcher's handleStatusChange closure at line 1312 reads and writes this same timer. When two projects have active watchers and both STATUS.json files change within 500ms of each other, watcher B's callback clears watcher A's pending timeout — A's status update is silently dropped and never sent to the renderer, until the next STATUS.json write for that project.

Replace the singleton with a per-path map:

// Replace line 60 with:
const statusWatchDebounceTimers = new Map<string, NodeJS.Timeout>();

Then in handleStatusChange (lines 1312–1331), scope the debounce to projectPath:

const handleStatusChange = async () => {
    const existing = statusWatchDebounceTimers.get(projectPath);
    if (existing) clearTimeout(existing);
    const timer = setTimeout(async () => {
        statusWatchDebounceTimers.delete(projectPath);
        try {
            const content = await fs.readFile(statusFilePath, 'utf-8');
            const status = JSON.parse(content) as PlaybookStatus;
            const mainWindow = getMainWindow();
            if (isWebContentsAvailable(mainWindow)) {
                mainWindow.webContents.send('autorun:statusChanged', { projectPath, status });
            }
        } catch (err) {
            logger.warn(`${LOG_CONTEXT} Failed to read STATUS.json: ${err}`, LOG_CONTEXT);
        }
    }, 500);
    statusWatchDebounceTimers.set(projectPath, timer);
};

Also clear pending timers in unwatchStatus (line 1366) and before-quit (line 1383) to prevent stale callbacks from firing after a watcher is closed.


/**
* Tree node interface for autorun directory scanning.
*
Expand Down Expand Up @@ -1270,13 +1274,117 @@ export function registerAutorunHandlers(
)
);

// Watch for .maestro/STATUS.json changes in a project directory
// When the file changes, read and parse it, then emit the parsed status to the renderer
ipcMain.handle(
'autorun:watchStatus',
createIpcHandler(handlerOpts('watchStatus'), async (projectPath: string) => {
// Stop any existing status watcher for this path
if (statusWatchers.has(projectPath)) {
statusWatchers.get(projectPath)?.close();
statusWatchers.delete(projectPath);
}

const statusFilePath = path.join(projectPath, '.maestro', 'STATUS.json');

// Read initial status if file exists
let initialStatus: PlaybookStatus | null = null;
try {
const content = await fs.readFile(statusFilePath, 'utf-8');
initialStatus = JSON.parse(content) as PlaybookStatus;
} catch {
// File doesn't exist yet — that's fine
}
Comment on lines +1292 to +1297
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t swallow non-ENOENT errors while loading initial STATUS.json.

This catch currently hides JSON parse and filesystem permission errors as if the file simply does not exist, which makes failures hard to diagnose and can keep stale/empty UI state.

💡 Suggested fix (handle expected errors, rethrow unexpected ones)
 			let initialStatus: PlaybookStatus | null = null;
 			try {
 				const content = await fs.readFile(statusFilePath, 'utf-8');
 				initialStatus = JSON.parse(content) as PlaybookStatus;
-			} catch {
-				// File doesn't exist yet — that's fine
+			} catch (error) {
+				const err = error as NodeJS.ErrnoException;
+				if (err.code === 'ENOENT') {
+					// File doesn't exist yet — expected
+				} else if (error instanceof SyntaxError) {
+					logger.warn(`${LOG_CONTEXT} Invalid STATUS.json format: ${statusFilePath}`, LOG_CONTEXT);
+				} else {
+					throw error;
+				}
 			}
As per coding guidelines: "`src/**/*.{ts,tsx}`: Do not silently swallow errors. Handle expected/recoverable errors explicitly ... For unexpected errors, re-throw them to allow Sentry to capture them."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const content = await fs.readFile(statusFilePath, 'utf-8');
initialStatus = JSON.parse(content) as PlaybookStatus;
} catch {
// File doesn't exist yet — that's fine
}
try {
const content = await fs.readFile(statusFilePath, 'utf-8');
initialStatus = JSON.parse(content) as PlaybookStatus;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === 'ENOENT') {
// File doesn't exist yet — expected
} else if (error instanceof SyntaxError) {
logger.warn(`${LOG_CONTEXT} Invalid STATUS.json format: ${statusFilePath}`, LOG_CONTEXT);
} else {
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/autorun.ts` around lines 1292 - 1297, The current
try/catch around reading and parsing statusFilePath swallows all errors
(including JSON parse and permission errors) and sets initialStatus silently;
change it to only ignore ENOENT (file-not-found) errors and rethrow all others:
when awaiting fs.readFile(statusFilePath, 'utf-8') catch errors and if
(err?.code === 'ENOENT') return/continue, otherwise rethrow; then perform
JSON.parse(content) in its own try/catch and if parsing fails rethrow (or wrap
and rethrow with context) so JSON syntax errors are not swallowed; preserve the
use of initialStatus and PlaybookStatus for types and ensure unexpected errors
propagate for Sentry.


// Watch the .maestro directory for STATUS.json changes
const maestroDir = path.join(projectPath, '.maestro');
try {
await fs.stat(maestroDir);
} catch {
await fs.mkdir(maestroDir, { recursive: true });
}
Comment on lines +1299 to +1305
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 watchStatus creates .maestro/ directory as an unexpected side effect

This block unconditionally creates the .maestro directory the first time any Auto Run starts, even if the user has never written a STATUS.json file. Users who don't use playbooks will find a .maestro directory appearing in their project root.

chokidar supports watching a non-existent file path — it will detect the file when it is eventually created. The mkdir guard is unnecessary here and can be removed.

Suggested change
// Watch the .maestro directory for STATUS.json changes
const maestroDir = path.join(projectPath, '.maestro');
try {
await fs.stat(maestroDir);
} catch {
await fs.mkdir(maestroDir, { recursive: true });
}
const watcher = chokidar.watch(statusFilePath, {


const watcher = chokidar.watch(statusFilePath, {
persistent: true,
ignoreInitial: true,
});

const handleStatusChange = async () => {
if (statusWatchDebounceTimer) {
clearTimeout(statusWatchDebounceTimer);
}
statusWatchDebounceTimer = setTimeout(async () => {
statusWatchDebounceTimer = null;
try {
const content = await fs.readFile(statusFilePath, 'utf-8');
const status = JSON.parse(content) as PlaybookStatus;
const mainWindow = getMainWindow();
if (isWebContentsAvailable(mainWindow)) {
mainWindow.webContents.send('autorun:statusChanged', {
projectPath,
status,
});
}
} catch (err) {
logger.warn(`${LOG_CONTEXT} Failed to read STATUS.json: ${err}`, LOG_CONTEXT);
}
}, 500);
};

watcher.on('add', handleStatusChange);
watcher.on('change', handleStatusChange);
watcher.on('unlink', () => {
// File was deleted — clear status in renderer
const mainWindow = getMainWindow();
if (isWebContentsAvailable(mainWindow)) {
mainWindow.webContents.send('autorun:statusChanged', {
projectPath,
status: null,
});
}
});

watcher.on('error', (error) => {
logger.error(
`${LOG_CONTEXT} STATUS.json watcher error for ${projectPath}`,
LOG_CONTEXT,
error
);
});

statusWatchers.set(projectPath, watcher);
logger.info(`Started watching STATUS.json in: ${projectPath}`, LOG_CONTEXT);

return { status: initialStatus };
})
);

// Stop watching STATUS.json for a project
ipcMain.handle(
'autorun:unwatchStatus',
createIpcHandler(handlerOpts('unwatchStatus', false), async (projectPath: string) => {
if (statusWatchers.has(projectPath)) {
statusWatchers.get(projectPath)?.close();
statusWatchers.delete(projectPath);
logger.info(`Stopped watching STATUS.json in: ${projectPath}`, LOG_CONTEXT);
}
return {};
})
);

// Clean up all watchers on app quit
app.on('before-quit', () => {
for (const [folderPath, watcher] of autoRunWatchers) {
watcher.close();
logger.info(`Cleaned up Auto Run watcher for: ${folderPath}`, LOG_CONTEXT);
}
autoRunWatchers.clear();

for (const [projectPath, watcher] of statusWatchers) {
watcher.close();
logger.info(`Cleaned up STATUS.json watcher for: ${projectPath}`, LOG_CONTEXT);
}
statusWatchers.clear();
});

logger.debug(`${LOG_CONTEXT} Auto Run IPC handlers registered`);
Expand All @@ -1288,3 +1396,10 @@ export function registerAutorunHandlers(
export function getAutoRunWatcherCount(): number {
return autoRunWatchers.size;
}

/**
* Get the current number of active STATUS.json watchers (for testing/debugging)
*/
export function getStatusWatcherCount(): number {
return statusWatchers.size;
}
31 changes: 31 additions & 0 deletions src/main/preload/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export interface WorktreeSettings {
prTargetBranch?: string;
}

/**
* Playbook status from .maestro/STATUS.json
*/
export interface PlaybookStatusInfo {
feature?: string;
phase?: string;
summary?: string;
tests?: { pass: number; fail: number };
artifact?: string;
}

/**
* Playbook definition
*/
Expand Down Expand Up @@ -120,6 +131,26 @@ export function createAutorunApi() {
loopNumber,
sshRemoteId
),

watchStatus: (projectPath: string): Promise<{ status: PlaybookStatusInfo | null }> =>
ipcRenderer.invoke('autorun:watchStatus', projectPath),

unwatchStatus: (projectPath: string) =>
ipcRenderer.invoke('autorun:unwatchStatus', projectPath),

onStatusChanged: (
handler: (data: { projectPath: string; status: PlaybookStatusInfo | null }) => void
) => {
const wrappedHandler = (
_event: Electron.IpcRendererEvent,
data: {
projectPath: string;
status: PlaybookStatusInfo | null;
}
) => handler(data);
ipcRenderer.on('autorun:statusChanged', wrappedHandler);
return () => ipcRenderer.removeListener('autorun:statusChanged', wrappedHandler);
},
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/prompts/maestro-system-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ To recall recent work, read the file and scan the most recent entries by timesta

When a user wants an auto-run document (or playbook), create a detailed multi-document, multi-point Markdown implementation plan in the `{{AUTORUN_FOLDER}}` folder. Use the format `$PREFIX-XX.md`, where `XX` is the two-digit phase number (01, 02, etc.) and `$PREFIX` is the effort name. Always zero-pad phase numbers to ensure correct lexicographic sorting. Break phases by relevant context; do not mix unrelated task results in the same document. If working within a file, group and fix all type issues in that file together. If working with an MCP, keep all related tasks in the same document. Each task must be written as `- [ ] ...` so auto-run can execute and check them off with comments on completion.

**Multi-phase efforts:** When creating 3 or more phase documents for a single effort, place them in a dedicated subdirectory prefixed with today's date (e.g., `{{AUTORUN_FOLDER}}/YYYY-MM-DD-Feature-Name/FEATURE-NAME-01.md`). This allows users to add the entire folder at once and keeps related documents organized with a clear creation date.
**Multi-phase efforts:** When creating 3 or more phase documents for a single effort, place them in a single flat subdirectory directly under `{{AUTORUN_FOLDER}}`, prefixed with today's date (e.g., `{{AUTORUN_FOLDER}}/YYYY-MM-DD-Feature-Name/FEATURE-NAME-01.md`). Do NOT create nested subdirectories — all phase documents for a given effort go into one folder, never `project/feature/` nesting. This allows users to add the entire folder at once and keeps related documents organized with a clear creation date.

**Context efficiency:** Each checkbox task runs in a fresh agent context. Group logically related work under a single checkbox when: (1) tasks modify the same file(s), (2) tasks follow the same pattern/approach, or (3) understanding one task is prerequisite to the next. Keep tasks separate when they're independent or when a single task would exceed reasonable scope (~500 lines of change). A good task is self-contained and can be verified in isolation.

Expand Down
54 changes: 54 additions & 0 deletions src/renderer/components/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,60 @@ export const RightPanel = memo(
)}
</div>

{/* Playbook status from .maestro/STATUS.json */}
{currentSessionBatchState.playbookStatus && (
<div
className="mb-2 px-2 py-1.5 rounded text-[11px] leading-relaxed"
style={{
backgroundColor: theme.colors.accent + '10',
borderLeft: `2px solid ${theme.colors.accent}`,
}}
>
<div className="flex items-center gap-2 flex-wrap">
{currentSessionBatchState.playbookStatus.feature && (
<span className="font-mono font-bold" style={{ color: theme.colors.accent }}>
{currentSessionBatchState.playbookStatus.feature}
</span>
)}
{currentSessionBatchState.playbookStatus.phase && (
<span
className="px-1 py-0.5 rounded text-[10px] font-medium uppercase"
style={{
backgroundColor: theme.colors.accent + '20',
color: theme.colors.accent,
}}
>
{currentSessionBatchState.playbookStatus.phase}
</span>
)}
{currentSessionBatchState.playbookStatus.tests && (
<span
className="text-[10px] font-mono"
style={{
color:
currentSessionBatchState.playbookStatus.tests.fail > 0
? theme.colors.error
: theme.colors.success,
}}
>
{currentSessionBatchState.playbookStatus.tests.pass}✓
{currentSessionBatchState.playbookStatus.tests.fail > 0 &&
` ${currentSessionBatchState.playbookStatus.tests.fail}✗`}
</span>
)}
</div>
{currentSessionBatchState.playbookStatus.summary && (
<div
className="mt-1 truncate"
style={{ color: theme.colors.textDim }}
title={currentSessionBatchState.playbookStatus.summary}
>
{currentSessionBatchState.playbookStatus.summary}
</div>
)}
</div>
)}

{/* Current document name - for single document runs */}
{currentSessionBatchState.documents &&
currentSessionBatchState.documents.length === 1 && (
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ type AutoRunTreeNode = {
children?: AutoRunTreeNode[];
};

/** Playbook status data from .maestro/STATUS.json (mirrors shared/types PlaybookStatus) */
type PlaybookStatusData = {
feature?: string;
phase?: string;
summary?: string;
tests?: { pass: number; fail: number };
artifact?: string;
};

interface ProcessConfig {
sessionId: string;
toolType: string;
Expand Down Expand Up @@ -1432,6 +1441,14 @@ interface MaestroAPI {
loopNumber: number,
sshRemoteId?: string
) => Promise<{ workingCopyPath: string; originalPath: string }>;
// Playbook STATUS.json watching
watchStatus: (projectPath: string) => Promise<{
status: PlaybookStatusData | null;
}>;
unwatchStatus: (projectPath: string) => Promise<Record<string, never>>;
onStatusChanged: (
handler: (data: { projectPath: string; status: PlaybookStatusData | null }) => void
) => () => void;
};
// Playbooks API (saved batch run configurations)
playbooks: {
Expand Down
19 changes: 17 additions & 2 deletions src/renderer/hooks/batch/batchReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* - Debug logging for state transition auditing
*/

import type { BatchRunState, AgentError } from '../../types';
import type { BatchRunState, AgentError, PlaybookStatus } from '../../types';
import {
transition,
canTransition,
Expand Down Expand Up @@ -225,7 +225,8 @@ export type BatchAction =
| { type: 'CLEAR_ERROR'; sessionId: string }
| { type: 'SET_COMPLETING'; sessionId: string } // RUNNING -> COMPLETING
| { type: 'COMPLETE_BATCH'; sessionId: string; finalSessionIds?: string[] }
| { type: 'INCREMENT_LOOP'; sessionId: string; newTotalTasks: number };
| { type: 'INCREMENT_LOOP'; sessionId: string; newTotalTasks: number }
| { type: 'UPDATE_PLAYBOOK_STATUS'; sessionId: string; status: PlaybookStatus | undefined };

/**
* Batch state reducer
Expand Down Expand Up @@ -563,6 +564,20 @@ export function batchReducer(state: BatchState, action: BatchAction): BatchState
};
}

case 'UPDATE_PLAYBOOK_STATUS': {
const { sessionId, status } = action;
const currentState = state[sessionId];
if (!currentState) return state;

return {
...state,
[sessionId]: {
...currentState,
playbookStatus: status,
},
};
}

default:
return state;
}
Expand Down
36 changes: 36 additions & 0 deletions src/renderer/hooks/batch/useBatchProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,32 @@ export function useBatchProcessor({
// State machine: INITIALIZING -> RUNNING (initialization complete)
dispatch({ type: 'SET_RUNNING', sessionId });

// Start watching .maestro/STATUS.json for playbook status updates
const statusProjectPath = effectiveCwd;
let statusCleanup: (() => void) | null = null;
try {
const { status: initialStatus } =
await window.maestro.autorun.watchStatus(statusProjectPath);
if (initialStatus) {
dispatch({
type: 'UPDATE_PLAYBOOK_STATUS',
sessionId,
status: initialStatus,
});
}
statusCleanup = window.maestro.autorun.onStatusChanged((data) => {
if (data.projectPath === statusProjectPath) {
dispatch({
type: 'UPDATE_PLAYBOOK_STATUS',
sessionId,
status: data.status ?? undefined,
});
}
});
} catch {
// STATUS.json watching is optional — don't fail the batch
}

Comment on lines +842 to +867
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make STATUS watcher lifecycle non-leaky and non-silent.

At Line 864 and Line 1762, errors are swallowed silently, and cleanup is only on the normal completion path. If any awaited operation throws before cleanup, the status listener/watch can remain active for that session. Please move watcher teardown into a finally block and explicitly report failures (Sentry/context logging) instead of empty catches.

Suggested direction
+// import { captureException } from '../../../utils/sentry';

 let statusCleanup: (() => void) | null = null;
 const statusProjectPath = effectiveCwd;

-try {
+try {
   const { status: initialStatus } = await window.maestro.autorun.watchStatus(statusProjectPath);
   ...
   statusCleanup = window.maestro.autorun.onStatusChanged(...);
-} catch {
-  // STATUS.json watching is optional — don't fail the batch
+} catch (error) {
+  // expected-recoverable path: watcher unavailable
+  // captureException(error, { context: { sessionId, statusProjectPath, phase: 'watchStatus:init' } });
 }

+try {
+  // existing batch execution body
+} finally {
+  try {
+    statusCleanup?.();
+  } catch (error) {
+    // captureException(error, { context: { sessionId, statusProjectPath, phase: 'watchStatus:unsubscribe' } });
+  }
+
+  try {
+    await window.maestro.autorun.unwatchStatus(statusProjectPath);
+  } catch (error) {
+    // captureException(error, { context: { sessionId, statusProjectPath, phase: 'watchStatus:unwatch' } });
+  }
+}

As per coding guidelines, src/**/*.{ts,tsx}: “Do not silently swallow errors… Use Sentry utilities (captureException, captureMessage) from src/utils/sentry.ts for explicit error reporting with context.”

Also applies to: 1756-1765

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/batch/useBatchProcessor.ts` around lines 842 - 867, The
STATUS.json watcher currently swallows errors and only cleans up on the success
path; refactor the watcher setup using a try/catch/finally so that statusCleanup
is always called in the finally block (ensure statusCleanup is invoked if
non-null) and replace the empty catch with explicit error reporting using the
Sentry helpers (captureException or captureMessage) imported from
src/utils/sentry.ts, providing context such as statusProjectPath and sessionId;
keep the existing behavior of dispatching 'UPDATE_PLAYBOOK_STATUS' for
initialStatus and onStatusChanged, but ensure any failure from
window.maestro.autorun.watchStatus or onStatusChanged is logged to Sentry rather
than being silently ignored.

// Prevent system sleep while Auto Run is active
window.maestro.power.addReason(`autorun:${sessionId}`);

Expand Down Expand Up @@ -1727,6 +1753,16 @@ export function useBatchProcessor({
}
}

// Clean up STATUS.json watcher
if (statusCleanup) {
statusCleanup();
}
try {
await window.maestro.autorun.unwatchStatus(statusProjectPath);
} catch {
// Ignore cleanup errors
}

// Critical: Always flush debounced updates and dispatch COMPLETE_BATCH to clean up state.
// These operations are safe regardless of mount state - React handles reducer dispatches gracefully,
// and broadcasts are external calls that don't affect React state.
Expand Down
Loading
Loading