Skip to content

Add CLI IPC commands for remote Maestro control#530

Open
chr1syy wants to merge 28 commits intoRunMaestro:mainfrom
chr1syy:symphony/issue-511-mmgpua7l
Open

Add CLI IPC commands for remote Maestro control#530
chr1syy wants to merge 28 commits intoRunMaestro:mainfrom
chr1syy:symphony/issue-511-mmgpua7l

Conversation

@chr1syy
Copy link
Contributor

@chr1syy chr1syy commented Mar 7, 2026

Summary

  • Add CLI commands (send, status, open-file, refresh-files, refresh-auto-run, auto-run) for controlling Maestro sessions remotely via WebSocket IPC
  • Implement WebSocket client (maestro-client.ts) with server discovery (cli-server-discovery.ts) for CLI-to-Maestro communication
  • Wire up renderer handlers and main process message routing for all new CLI IPC message types
  • Add comprehensive test coverage for CLI commands, WebSocket client, message handlers, and server discovery

Test plan

  • Verify maestro status reports connectivity diagnostics correctly
  • Verify maestro send --tab focuses the correct session tab
  • Verify maestro open-file opens files in Maestro
  • Verify maestro refresh-files triggers file tree refresh
  • Verify maestro refresh-auto-run triggers Auto Run document refresh
  • Verify maestro auto-run configures auto-run sessions
  • Run npm run test to confirm all new tests pass

🤖 Generated with Claude Code

Created from #511

Summary by CodeRabbit

Release Notes

  • New Features

    • Added CLI commands for seamless desktop app integration: open files directly from command line, refresh file trees, manage auto-run document lists, configure and launch automated workflows with looping controls, and verify desktop app connectivity.
    • Enhanced send command with automatic session tab focusing.
  • Tests

    • Comprehensive test suite for all new CLI commands and remote integrations.

chr1syy and others added 24 commits March 7, 2026 20:28
Create src/shared/cli-server-discovery.ts with read/write/delete functions
for the CLI server discovery file (cli-server.json). Follows the same
pattern as cli-activity.ts with getConfigDir(), PID validation, and
atomic writes (write to .tmp then rename).

Exports: CliServerInfo interface, writeCliServerInfo, readCliServerInfo,
deleteCliServerInfo, isCliServerRunning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ensureCliServer() to web handlers that creates/starts the web server
during app initialization and writes the discovery file (cli-server.json)
so the CLI can locate and connect via WebSocket. The existing live:startServer
handler continues to work — if the server is already running it returns the
existing URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…stop

Add deleteCliServerInfo() calls in three places:
- quit-handler.ts performCleanup(): after web server stop, before stats DB close
- web.ts live:stopServer handler: when server is manually stopped
- web.ts live:disableAll handler: when all live sessions are disabled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests cover writeCliServerInfo, readCliServerInfo, deleteCliServerInfo,
and isCliServerRunning with mocked fs/os modules. Includes platform-specific
config dir tests and validation edge cases (21 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add new open_file_tab message handler across the web-server module:
- messageHandlers.ts: switch case, handler method, callback interface
- types.ts: OpenFileTabCallback type
- CallbackRegistry.ts: storage, getter, setter
- WebServer.ts: callback setter, message handler wiring
- web-server-factory.ts: forward to renderer via remote:openFileTab IPC

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the refresh_auto_run_docs message across the 5-file callback
pattern: messageHandlers → types → CallbackRegistry → WebServer → factory.
Also fixes pre-existing test mock missing setOpenFileTab/setRefreshFileTree
callback methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vents

Add onRemoteOpenFileTab, onRemoteRefreshFileTree, and onRemoteRefreshAutoRunDocs
listeners in preload/process.ts and corresponding type declarations in global.d.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…file_tree, refresh_auto_run_docs

Add three useEffect blocks in useRemoteIntegration.ts that subscribe to
preload IPC events and dispatch CustomEvents. Add corresponding event
listeners in App.tsx that handle file reading/opening, file tree refresh,
and auto-run document refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ow foregrounding

When focus: true is sent with a select_session message, the desktop
window is brought to the foreground via mainWindow.show() and
mainWindow.focus() before forwarding the session selection to the
renderer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… refresh_file_tree, refresh_auto_run_docs, select_session focus)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements MaestroClient class and withMaestroClient helper that connects
to the running Maestro desktop app via the discovery file from Phase 01.
Supports connect/disconnect lifecycle, typed command/response matching,
and configurable timeouts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `maestro-cli open-file <file-path> [--session <id>]` command that opens
a file as a preview tab in the running Maestro desktop app via WebSocket IPC.

Also extracts resolveSessionId() into maestro-client.ts as a shared helper
for commands that need session resolution with fallback to first available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds -t/--tab option to the CLI send command that, after a successful
send, connects to the Maestro desktop app via WebSocket and focuses
the agent's session tab. Gracefully warns if the desktop app is not
running without failing the send itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `maestro-cli status` command that checks if the Maestro desktop
app is running and reachable via WebSocket. Reports port and session
count on success, or clear error messages for common failure modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the configure_auto_run message handler across the full
WebSocket IPC stack: message handler, callback type, registry,
WebServer, factory wiring, preload listener, and renderer type
declarations. Uses response channel pattern for async results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add onRemoteConfigureAutoRun listener in useRemoteIntegration.ts that
dispatches maestro:configureAutoRun CustomEvent, and handler in App.tsx
that supports three modes: save as playbook, launch batch run, or
configure-only. Uses response channel pattern for WebSocket confirmation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `maestro-cli auto-run <docs...>` command that sends
configure_auto_run messages over WebSocket IPC to the desktop app.
Supports --prompt, --loop, --max-loops, --save-as, --launch, and
--reset-on-completion options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents maestro-cli commands (open-file, refresh-files, refresh-auto-run,
auto-run, status) in the agent system prompt so AI agents know how to
interact with the Maestro desktop app via CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…bSocket handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Adds a CLI↔Electron bridge: CLI server discovery, a WebSocket MaestroClient, five CLI commands (open-file, refresh-files, refresh-auto-run, auto-run, status), new WebSocket message types and callbacks, IPC/renderer handlers for remote operations, and extensive tests across CLI, WebSocket, web-server, renderer, and discovery code.

Changes

Cohort / File(s) Summary
CLI Commands & Wiring
src/cli/commands/open-file.ts, src/cli/commands/auto-run.ts, src/cli/commands/refresh-files.ts, src/cli/commands/refresh-auto-run.ts, src/cli/commands/status.ts, src/cli/commands/send.ts, src/cli/index.ts
New CLI commands implemented and registered; send gains --tab option to focus session tab; commands resolve session, validate files/options, and call Maestro client.
Maestro Client (CLI)
src/cli/services/maestro-client.ts
New WebSocket-based MaestroClient with connect/sendCommand/disconnect, pending-request tracking/timeouts, resolveSessionId and withMaestroClient helpers.
CLI Discovery / Shared
src/shared/cli-server-discovery.ts, src/main/app-lifecycle/quit-handler.ts
New CLI discovery utilities (read/write/delete/isRunning) with platform path logic; quit handler deletes discovery file on shutdown.
Main IPC / Server Ensure
src/main/ipc/handlers/web.ts, src/main/ipc/handlers/index.ts, src/main/index.ts
Added ensureCliServer to create/start web server and maintain discovery info; exported and invoked during startup.
Web Server: Types, Handlers, Callbacks, Factory
src/main/web-server/types.ts, src/main/web-server/handlers/messageHandlers.ts, src/main/web-server/managers/CallbackRegistry.ts, src/main/web-server/WebServer.ts, src/main/web-server/web-server-factory.ts
Added message types (open_file_tab, refresh_file_tree, refresh_auto_run_docs, configure_auto_run); extended selectSession with optional focus; added callback types/setters and forwarding logic; configureAutoRun implements IPC request/response with timeout.
Preload / IPC Process API
src/main/preload/process.ts
Exposes new IPC subscribers and response sender: onRemoteOpenFileTab, onRemoteRefreshFileTree, onRemoteRefreshAutoRunDocs, onRemoteConfigureAutoRun, and sendRemoteConfigureAutoRunResponse.
Renderer: Integration & API types
src/renderer/App.tsx, src/renderer/global.d.ts, src/renderer/hooks/remote/useRemoteIntegration.ts
Adds remote listeners/handlers for openFileTab, refreshFileTree, refreshAutoRunDocs, configureAutoRun and corresponding global.d.ts entries; dispatches custom events to App. (Note: some duplicated registrations appear in diffs.)
Tests
src/__tests__/cli/..., src/__tests__/main/..., src/__tests__/shared/..., src/__tests__/renderer/...
Extensive new/updated tests for CLI commands, MaestroClient lifecycle and messaging, CLI discovery, web-server handlers, callback registry, web-server factory, and renderer remote integration.
Prompts / Docs
src/prompts/maestro-system-prompt.md
Inserted CLI Commands / Auto Run integration section (duplicated and later expanded in file).

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Command
    participant MC as MaestroClient
    participant WS as WebSocket
    participant Handler as WebSocket Handler
    participant CB as CallbackRegistry
    participant Main as Main (IPC)
    participant Renderer as Renderer

    CLI->>MC: withMaestroClient(action)
    MC->>MC: connect()
    MC->>WS: open socket
    WS-->>MC: open event

    CLI->>MC: sendCommand(configure_auto_run)
    MC->>WS: send JSON message
    WS->>Handler: receive message
    Handler->>CB: call configureAutoRun(sessionId, config)
    CB->>Main: emit remote:configureAutoRun (responseChannel)
    Main->>Renderer: IPC -> handle configureAutoRun
    Renderer-->>Main: sendRemoteConfigureAutoRunResponse(responseChannel, result)
    Main-->>CB: return result
    CB->>Handler: return success/playbookId
    Handler->>WS: send configure_auto_run_result
    WS-->>MC: message received
    MC-->>CLI: resolve promise with result

    CLI->>MC: disconnect()
    MC->>WS: close socket
Loading
sequenceDiagram
    participant CLI as CLI (open-file)
    participant MC as MaestroClient
    participant WS as WebSocket
    participant Handler as WebSocket Handler
    participant CB as CallbackRegistry
    participant Main as Main (IPC)
    participant Renderer as Renderer

    CLI->>MC: sendCommand(open_file_tab)
    MC->>WS: send JSON message
    WS->>Handler: handleOpenFileTab
    Handler->>CB: openFileTab(sessionId, filePath)
    CB->>Main: emit remote:openFileTab(sessionId, filePath)
    Main->>Renderer: IPC -> open file tab (reads file, opens tab)
    Renderer-->>Main: success boolean
    Main-->>CB: return success
    CB->>Handler: return success
    Handler->>WS: send open_file_tab_result
    WS-->>MC: receive result
    MC-->>CLI: resolve result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add CLI IPC commands for remote Maestro control' accurately summarizes the main objective of the pull request—introducing new CLI commands that communicate with the Maestro desktop app via IPC/WebSocket.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chr1syy chr1syy added the runmaestro.ai These issues are part of the Maestro Symphony program. label Mar 7, 2026
@greptile-apps
Copy link

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR introduces a full CLI IPC layer for remotely controlling Maestro sessions via WebSocket — adding six new CLI commands (send --tab, open-file, refresh-files, refresh-auto-run, auto-run, status), a new MaestroClient WebSocket wrapper, a cross-platform server discovery file mechanism, and all required renderer hooks and main-process callback wiring.

The architecture is sound: the discovery file approach cleanly decouples the CLI from the running app, the atomic write+rename in cli-server-discovery.ts prevents torn reads, and the withMaestroClient helper correctly handles connect/disconnect lifecycle. Test coverage is comprehensive across all new modules.

Key issues to address before merging:

  • auto-run --launch will silently fail: auto-run.ts sends full absolute paths as the filename field (e.g. /Users/alice/project/docs/my-doc.md), but the renderer's launch handler only strips the .md extension and passes the remaining path to startBatchRun. This produces a path like /auto-run-folder//Users/alice/project/docs/my-doc.md which will never be found. The fix is to send path.basename(d, '.md') as filename instead.

  • auto-run without --launch/--save-as always errors: The renderer's Case 3 explicitly returns success: false, making the CLI's "configure only" success branch dead code. Either the CLI should require one of those flags, or the renderer should implement persistent configure-only support.

Confidence Score: 2/5

  • Not safe to merge — the primary new command (auto-run --launch) will silently fail at runtime due to a path mismatch, and "configure-only" mode always errors despite being supported by the CLI.
  • Two logic bugs directly affect the flagship auto-run command: full absolute paths are sent where the renderer expects bare document names (breaking --launch), and the "configure only" mode always returns an error despite the CLI implying it should work. The remaining plumbing (discovery file, open-file, refresh-*, status) looks correct and well-tested.
  • src/cli/commands/auto-run.ts and src/renderer/App.tsx (the configureAutoRun handler) need to be fixed before the auto-run launch path will function correctly.

Sequence Diagram

sequenceDiagram
    participant CLI as maestro-cli
    participant Disc as cli-server.json
    participant WS as WebSocket (MaestroClient)
    participant Main as Electron Main
    participant IPC as ipcMain
    participant Renderer as Renderer (App.tsx)

    Note over Main,Disc: On app start (ensureCliServer)
    Main->>WS: webServer.start()
    Main->>Disc: writeCliServerInfo({port, token, pid})

    Note over CLI,Renderer: CLI command (e.g. auto-run --launch)
    CLI->>Disc: readCliServerInfo()
    Disc-->>CLI: {port, token, pid}
    CLI->>CLI: isCliServerRunning() (kill -0 pid)
    CLI->>WS: connect ws://localhost:{port}/{token}/ws
    WS-->>CLI: open event
    CLI->>WS: sendCommand({type, sessionId, ...docs, requestId})
    WS->>Main: WebSocket message
    Main->>Main: messageHandlers.handleMessage()
    Main->>IPC: webContents.send('remote:configureAutoRun', sessionId, config, responseChannel)
    IPC->>Renderer: IPC event
    Renderer->>Renderer: dispatchEvent('maestro:configureAutoRun')
    Renderer->>Renderer: startBatchRun / playbooks.create
    Renderer->>IPC: ipcRenderer.send(responseChannel, result)
    IPC->>Main: ipcMain.once(responseChannel)
    Main->>WS: send({type:'configure_auto_run_result', success, ...})
    WS-->>CLI: message event → resolve sendCommand
    CLI->>CLI: print result / process.exit

    Note over Main,Disc: On app quit
    Main->>Disc: deleteCliServerInfo()
Loading

Last reviewed commit: 6c1a30f

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/app-lifecycle/quit-handler.ts (1)

182-190: ⚠️ Potential issue | 🟡 Minor

Discovery file deleted before server fully stops - minor ordering inconsistency.

The webServer?.stop() call at lines 184-186 is fire-and-forget, but deleteCliServerInfo() is called immediately after at line 190. This creates a brief window where the discovery file is gone but the socket is still reachable.

In contrast, web.ts (line 300-310 per context snippet) deletes the file after awaiting webServer.stop().

Since this is the shutdown path and the window is very brief, this is unlikely to cause real issues. However, for consistency, you could move the deletion into the .then() callback of stop() or accept this as a known limitation of fire-and-forget shutdown.

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

In `@src/main/app-lifecycle/quit-handler.ts` around lines 182 - 190, The discovery
file is deleted before the web server stop completes because webServer?.stop()
is fire-and-forget; move the deleteCliServerInfo() call to execute after stop()
resolves (e.g., in the promise .then() or after an await) so the CLI file is
removed only once getWebServer().stop() has finished; update the shutdown block
around getWebServer(), webServer.stop(), logger.info/logger.error and
deleteCliServerInfo() to perform deletion after successful stop (or in a finally
that runs after awaiting stop()).
🧹 Nitpick comments (2)
src/main/index.ts (1)

405-413: Orphaned comment on line 413.

The comment "// via live:startServer IPC call from the renderer" appears to be a leftover from previous code and no longer applies to the context (it's after the ensureCliServer block, not related to any IPC call).

♻️ Suggested fix
 	await ensureCliServer({
 		getWebServer: () => webServer,
 		setWebServer: (server) => {
 			webServer = server;
 		},
 		createWebServer,
 	});
-	// via live:startServer IPC call from the renderer
 
 	app.on('activate', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/index.ts` around lines 405 - 413, The trailing comment "// via
live:startServer IPC call from the renderer" is orphaned and misleading after
the ensureCliServer block; remove or relocate it to the correct IPC handler
instead. Open the code around ensureCliServer (references: ensureCliServer,
getWebServer, setWebServer, createWebServer) and delete that comment or move it
next to the actual live:startServer IPC handler so comments align with the
related code.
src/main/ipc/handlers/web.ts (1)

56-95: Consider adding a guard against concurrent invocations.

While ensureCliServer is currently called only once during app startup, the function lacks protection against concurrent calls. If called simultaneously (e.g., future code paths), both calls could pass the !webServer.isActive() check before either completes webServer.start(), potentially leading to duplicate server start attempts.

A simple mutex or "starting" flag would future-proof this:

♻️ Suggested guard pattern
+let cliServerStarting: Promise<void> | null = null;
+
 export async function ensureCliServer(deps: WebHandlerDependencies): Promise<void> {
+	// Prevent concurrent startup attempts
+	if (cliServerStarting) {
+		return cliServerStarting;
+	}
+
 	const { getWebServer, setWebServer, createWebServer } = deps;
 
+	cliServerStarting = (async () => {
 	try {
 		// ... existing logic ...
 	} catch (error: any) {
 		logger.error(`Failed to start CLI server: ${error.message}`, 'CliServer');
+	} finally {
+		cliServerStarting = null;
 	}
+	})();
+
+	return cliServerStarting;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/web.ts` around lines 56 - 95, ensureCliServer can race
on startup: add a guard (e.g., a module-level "starting" boolean or Promise) to
prevent concurrent invocation from attempting to start the same server. Inside
ensureCliServer, obtain the current webServer via getWebServer(), then if no
server exists set a "starting" flag (or store a Promise) before calling
createWebServer()/setWebServer(); if another caller sees the flag, await the
existing starting Promise or return early. Also wrap the start sequence (the
webServer.start() and writeCliServerInfo calls) with the same guard so only one
caller invokes webServer.start(), using webServer.isActive() as a secondary
check; clear the flag (or resolve the Promise) on completion or error. Reference
symbols: ensureCliServer, getWebServer, setWebServer, createWebServer,
webServer.isActive, webServer.start, writeCliServerInfo.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/shared/cli-server-discovery.test.ts`:
- Around line 96-106: Wrap each test's process.env modification and restoration
in a try...finally block so cleanup always runs, and when restoring use
conditional deletion: if the saved original value is undefined use delete
process.env.APPDATA (or the appropriate env name) otherwise reassign the saved
value; apply this pattern around the setup/teardown surrounding
readCliServerInfo() and the other tests that set APPDATA (the same applies to
the other env vars referenced in this file) to avoid converting undefined into
the string "undefined" and to ensure restoration runs even if assertions fail.

In `@src/cli/services/maestro-client.ts`:
- Around line 76-92: The requestId is generated and stored in pendingRequests
but never injected into the outgoing message, and setupMessageHandler currently
matches responses by expectedType only; update the send path (before
this.ws!.send(JSON.stringify(message)) in the Promise that creates requestId) to
attach requestId to the message object, and modify setupMessageHandler to first
try to resolve pendingRequests by response.requestId (matching the stored
requestId key) and only fall back to matching by expectedType when requestId is
absent; keep pendingRequests cleanup (delete and clear timeout) the same and
ensure resolve/reject use the stored handlers.

In `@src/main/preload/process.ts`:
- Around line 452-469: The onRemoteConfigureAutoRun listener currently invokes
the renderer callback directly and if that callback throws or returns a rejected
promise the ipc responseChannel never receives a failure message, causing the
caller to time out; update the handler created in onRemoteConfigureAutoRun so it
wraps the callback call in try/catch and handles promise rejections, and on any
error send a failure payload like { success: false, error: String(err) } via
sendRemoteConfigureAutoRunResponse (or ipcRenderer.send(responseChannel, ...))
before returning; ensure the removeListener logic still unregisters the same
handler.

In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 732-762: handleConfigureAutoRun currently trusts message.documents
and forwards possibly malformed entries to this.callbacks.configureAutoRun; add
strict per-item validation before building config: iterate over the documents
array and for each entry on the local variable documents ensure it is an object
with a non-empty string filename and, if resetOnCompletion is present, that it
is a boolean (or require resetOnCompletion to be boolean when present),
otherwise call this.sendError with a descriptive message and return; only pass a
sanitized array (e.g., objects with filename:string and
resetOnCompletion?:boolean) into the config used for
this.callbacks.configureAutoRun so downstream code never receives invalid types.

In `@src/main/web-server/web-server-factory.ts`:
- Around line 542-568: The responseChannel currently uses Date.now() which can
collide for concurrent configureAutoRun calls; replace its creation with a
collision-free identifier (e.g., use crypto.randomUUID() or a UUID from the uuid
package, or combine Date.now() + sessionId + Math.random()) so each call builds
a unique channel string (update the responseChannel assignment used by
ipcMain.once and mainWindow.webContents.send). Ensure the new unique ID is used
consistently for both listener registration (ipcMain.once(responseChannel, ...))
and the sent message channel arg so promises cannot resolve with the wrong
response.

In `@src/renderer/App.tsx`:
- Around line 1711-1714: The handler currently ignores the event's sessionId and
always calls handleAutoRunRefresh() for the active UI session; change the
handler to extract the sessionId from the incoming Event (cast to the
CustomEvent or read e.detail.sessionId) and call the refresh routine for that
specific session (either by passing the sessionId into
handleAutoRunRefresh(sessionId) or invoking an existing function like
refreshAutoRunForSession(sessionId)); update the handler implementation in the
same block where handler and handleAutoRunRefresh are defined so the refresh
targets the requested session rather than the locally active one.
- Around line 1782-1785: The configure-only branch currently returns success
without persisting the provided config (config.documents, prompt, and loop
settings); modify the branch that checks saveAsPlaybook/launch so that when both
are false it reuses the same persistence/update path used by the save-as branch:
persist the incoming config.documents, prompt, and loop into the app state or
call the existing save/playbook persistence helper (the same routine used when
saveAsPlaybook is true), then return success via
window.maestro.process.sendRemoteConfigureAutoRunResponse; ensure you update the
same store or file-write helper the saveAsPlaybook path uses so the CLI-visible
desktop state reflects the configure-only change.
- Around line 1670-1689: The handler currently reads files using
window.maestro.fs.readFile/stat without the session's remote context and derives
the tab name by splitting only on '/', which breaks SSH sessions and Windows
paths; inside the handler (the async function handling open-file events), look
up the session's sshRemoteId (from the same sessions store used by
setActiveSessionId or activeSessionId), pass that sshRemoteId into both
window.maestro.fs.readFile and window.maestro.fs.stat calls so the SSH-backed
session uses the remote FS, and compute filename using a cross-platform split
(e.g. split on both '/' and '\\' or use a path basename helper) before calling
handleOpenFileTab (refer to handler, setActiveSessionId,
window.maestro.fs.readFile/stat, handleOpenFileTab, and sshRemoteId).

---

Outside diff comments:
In `@src/main/app-lifecycle/quit-handler.ts`:
- Around line 182-190: The discovery file is deleted before the web server stop
completes because webServer?.stop() is fire-and-forget; move the
deleteCliServerInfo() call to execute after stop() resolves (e.g., in the
promise .then() or after an await) so the CLI file is removed only once
getWebServer().stop() has finished; update the shutdown block around
getWebServer(), webServer.stop(), logger.info/logger.error and
deleteCliServerInfo() to perform deletion after successful stop (or in a finally
that runs after awaiting stop()).

---

Nitpick comments:
In `@src/main/index.ts`:
- Around line 405-413: The trailing comment "// via live:startServer IPC call
from the renderer" is orphaned and misleading after the ensureCliServer block;
remove or relocate it to the correct IPC handler instead. Open the code around
ensureCliServer (references: ensureCliServer, getWebServer, setWebServer,
createWebServer) and delete that comment or move it next to the actual
live:startServer IPC handler so comments align with the related code.

In `@src/main/ipc/handlers/web.ts`:
- Around line 56-95: ensureCliServer can race on startup: add a guard (e.g., a
module-level "starting" boolean or Promise) to prevent concurrent invocation
from attempting to start the same server. Inside ensureCliServer, obtain the
current webServer via getWebServer(), then if no server exists set a "starting"
flag (or store a Promise) before calling createWebServer()/setWebServer(); if
another caller sees the flag, await the existing starting Promise or return
early. Also wrap the start sequence (the webServer.start() and
writeCliServerInfo calls) with the same guard so only one caller invokes
webServer.start(), using webServer.isActive() as a secondary check; clear the
flag (or resolve the Promise) on completion or error. Reference symbols:
ensureCliServer, getWebServer, setWebServer, createWebServer,
webServer.isActive, webServer.start, writeCliServerInfo.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec9ec798-88c8-4f32-8199-86ca39aacc59

📥 Commits

Reviewing files that changed from the base of the PR and between 7acc502 and 844d9f9.

📒 Files selected for processing (30)
  • src/__tests__/cli/commands/auto-run.test.ts
  • src/__tests__/cli/commands/open-file.test.ts
  • src/__tests__/cli/services/maestro-client.test.ts
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/__tests__/shared/cli-server-discovery.test.ts
  • src/cli/commands/auto-run.ts
  • src/cli/commands/open-file.ts
  • src/cli/commands/refresh-auto-run.ts
  • src/cli/commands/refresh-files.ts
  • src/cli/commands/send.ts
  • src/cli/commands/status.ts
  • src/cli/index.ts
  • src/cli/services/maestro-client.ts
  • src/main/app-lifecycle/quit-handler.ts
  • src/main/index.ts
  • src/main/ipc/handlers/index.ts
  • src/main/ipc/handlers/web.ts
  • src/main/preload/process.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/prompts/maestro-system-prompt.md
  • src/renderer/App.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/shared/cli-server-discovery.ts

Comment on lines +1782 to +1785
// Case 3: Just configure (no launch, no save)
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

The configure-only branch currently does nothing.

When neither launch nor saveAsPlaybook is set, Lines 1783-1785 return success without persisting config.documents, prompt, or loop settings anywhere. src/cli/index.ts advertises configure-only as the default auto-run path, so the CLI can report success while leaving the desktop state unchanged.

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

In `@src/renderer/App.tsx` around lines 1782 - 1785, The configure-only branch
currently returns success without persisting the provided config
(config.documents, prompt, and loop settings); modify the branch that checks
saveAsPlaybook/launch so that when both are false it reuses the same
persistence/update path used by the save-as branch: persist the incoming
config.documents, prompt, and loop into the app state or call the existing
save/playbook persistence helper (the same routine used when saveAsPlaybook is
true), then return success via
window.maestro.process.sendRemoteConfigureAutoRunResponse; ensure you update the
same store or file-write helper the saveAsPlaybook path uses so the CLI-visible
desktop state reflects the configure-only change.

@chr1syy chr1syy marked this pull request as draft March 7, 2026 21:24
chr1syy and others added 4 commits March 8, 2026 10:30
Fix issues identified by Greptile and CodeRabbit reviews:
- Send full absolute path instead of basename in auto-run documents
- Fix WebSocket timeout race condition with settled flag
- Include requestId in outgoing messages for correct response matching
- Wrap test env var cleanup in try/finally with proper delete for undefined
- Catch callback failures in onRemoteConfigureAutoRun preload handler
- Validate document entries in messageHandlers before forwarding
- Use randomUUID for collision-free response channels
- Pass SSH remote context into file reads for open-file-tab
- Fix refresh-auto-run to target the requested session
- Return error for configure-only branch instead of silent no-op

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing onRemoteOpenFileTab, onRemoteRefreshFileTree,
  onRemoteRefreshAutoRunDocs, onRemoteConfigureAutoRun, and
  sendRemoteConfigureAutoRunResponse mocks to useRemoteIntegration test
- Run prettier --write on all files flagged by CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Promise.resolve() wrapper instead of testing void return for
truthiness, which TypeScript disallows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chr1syy chr1syy marked this pull request as ready for review March 8, 2026 14:20
Comment on lines +43 to +46
const documents = resolvedPaths.map((d) => ({
filename: d,
resetOnCompletion: options.resetOnCompletion || false,
}));
Copy link

Choose a reason for hiding this comment

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

Full absolute path sent as filename, but renderer expects bare name

filename: d stores the full absolute path (e.g. /Users/alice/project/docs/my-doc.md). In App.tsx the renderer's launch branch processes it as:

filename: doc.filename.replace(/\.md$/, '')
// → "/Users/alice/project/docs/my-doc"  (still a full path)

startBatchRun then resolves this against session.autoRunFolderPath, producing a path like /auto-run-folder//Users/alice/project/docs/my-doc.md which will never be found. All other callers of startBatchRun (e.g. useWizardHandlers.ts:1210, useSymphonyContribution.ts:241) pass a bare name without extension.

The fix is to send only the basename without extension (matching what the renderer expects), rather than the full path:

Suggested change
const documents = resolvedPaths.map((d) => ({
filename: d,
resetOnCompletion: options.resetOnCompletion || false,
}));
const documents = resolvedPaths.map((d) => ({
filename: path.basename(d, '.md'),
resetOnCompletion: options.resetOnCompletion || false,
}));

If the CLI must also communicate the full path for the renderer to locate the file, a separate filePath field would need to be added to both the protocol and the renderer's handler.

Comment on lines +1762 to +1770
if (config.launch) {
const folderPath = session.autoRunFolderPath;
if (!folderPath) {
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: false,
error: 'No Auto Run folder configured for this session',
});
return;
}
Copy link

Choose a reason for hiding this comment

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

"Configure only" path is unreachable — auto-run without --launch or --save-as always errors

Case 3 deliberately returns success: false, but auto-run.ts has a success branch for the "just configure" case (the else block that prints "Auto-run configured with N documents"). That branch is dead code because the renderer will always reject a bare configure_auto_run without launch or saveAsPlaybook.

Running maestro auto-run doc.md (no --launch, no --save-as) will always exit with code 1 and print the error message "Use --launch to start auto-run immediately, or --save-as to save as a playbook", contradicting the command's description ("Configure and optionally launch").

If "configure only" is not a supported mode, the CLI command should validate up-front and require --launch or --save-as, and the else branch in auto-run.ts should be removed. If it is intended to be supported, the renderer needs to implement a way to persist the configuration.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
src/renderer/App.tsx (1)

1716-1723: ⚠️ Potential issue | 🟠 Major

refresh-auto-run still targets the wrong session.

setActiveSessionId(sessionId) is asynchronous from this handler’s point of view, so the immediate handleAutoRunRefresh() still runs against the previously bound active session. refresh-auto-run --session ... can therefore refresh the wrong docs first.

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

In `@src/renderer/App.tsx` around lines 1716 - 1723, The handler currently calls
setActiveSessionId(sessionId) then immediately calls handleAutoRunRefresh(),
which runs against the old active session; change the flow so the refresh
targets the intended session by passing sessionId directly to the refresher
(e.g., update handleAutoRunRefresh to accept an optional sessionId parameter and
call handleAutoRunRefresh(sessionId) from the handler) or otherwise ensure the
refresh runs after the state commit (for example, trigger the same refresh logic
from the effect that reacts to activeSessionId). Refer to handler,
setActiveSessionId, and handleAutoRunRefresh when making the change.
🧹 Nitpick comments (1)
src/__tests__/renderer/hooks/useRemoteIntegration.test.ts (1)

124-137: Exercise the new remote subscriptions instead of only stubbing them.

These mocks never capture or invoke the added callbacks, so the new openFileTab / refreshFileTree / refreshAutoRunDocs / configureAutoRun wiring in useRemoteIntegration is still untested. Please store the handlers like the existing remote mocks and assert the dispatched CustomEvent payloads.

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

In `@src/__tests__/renderer/hooks/useRemoteIntegration.test.ts` around lines 124 -
137, The test currently stubs the remote subscription helpers
(onRemoteOpenFileTab, onRemoteRefreshFileTree, onRemoteRefreshAutoRunDocs,
onRemoteConfigureAutoRun) with vi.fn().mockImplementation(() => () => {}) which
never captures or invokes handlers; change each mock to capture the provided
callback (e.g., save it to a local variable) and return an unsubscribe, then in
the test invoke the saved handlers to simulate the remote events and assert that
useRemoteIntegration dispatches the expected CustomEvent payloads and calls
sendRemoteNewTabResponse / sendRemoteConfigureAutoRunResponse appropriately;
reference the mock names onRemoteOpenFileTab, onRemoteRefreshFileTree,
onRemoteRefreshAutoRunDocs, onRemoteConfigureAutoRun and the response spies
sendRemoteNewTabResponse and sendRemoteConfigureAutoRunResponse when adding the
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/shared/cli-server-discovery.test.ts`:
- Around line 329-357: Tests for isCliServerRunning temporarily replace
process.kill but restore it only after assertions, risking leaving it mocked if
an assertion throws; wrap the mock setup and restore in a try/finally in both
test cases so process.kill is always restored: in the 'should return true for
current PID' and 'should return false for non-existent PID' tests, assign
originalKill, set process.kill to the mock, execute the test logic in the try
block, and restore process.kill = originalKill in the finally block to ensure
cleanup even on failures.

In `@src/cli/commands/auto-run.ts`:
- Around line 48-54: The current parsing of options.maxLoops uses parseInt which
accepts strings like "3foo"; update the validation around the
loopEnabled/maxLoops logic to ensure the entire options.maxLoops string is a
valid positive integer before converting: either test the string with a
full-match regex (e.g. /^\d+$/) or convert with Number(options.maxLoops) and
check Number.isInteger(...) and > 0, then assign maxLoops to that integer (or
undefined); apply the check where maxLoops is set and in the subsequent error
branch that currently logs 'Error: --max-loops must be a positive integer'.

In `@src/cli/commands/send.ts`:
- Around line 136-140: The select_session command only sends sessionId (agentId)
which focuses the session but not the specific tab; update the withMaestroClient
callback where client.sendCommand is called (the select_session request) to
include the resolved tab id (e.g., add a tabId property) so the desktop focuses
the correct tab; to get that tabId, resolve the matching tab for the target
agent session before calling client.sendCommand (either by querying the local
data structure that maps agentSessionId/result.agentSessionId to tabId or by
calling the server API to look up agent-session → tab mapping) and pass that
tabId in the select_session payload, or if server-side support is preferred
implement a lookup endpoint that accepts agentSessionId and returns tabId and
call it here before issuing the select_session request.

In `@src/cli/services/maestro-client.ts`:
- Around line 63-68: Add a 'close' handler inside setupMessageHandler() that
mirrors disconnect() behavior: when the WebSocket (ws) emits 'close', iterate
over the pending requests map (pendingRequests) and reject each promise with an
Error indicating the Maestro connection closed, then clear the map and perform
any necessary cleanup; ensure the handler uses the same rejection message/shape
as disconnect() to immediately fail pending commands rather than waiting for the
command timeout.

In `@src/renderer/App.tsx`:
- Around line 1786-1789: The code sends success before the async startBatchRun
completes; change the branch to await startBatchRun(sessionId, batchConfig,
folderPath) and only call
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true }) after it resolves, and add a try/catch around the await so that
on failure you send a failure response (e.g., sendRemoteConfigureAutoRunResponse
with success: false and include the error details) using the same
responseChannel.
- Around line 1682-1694: The file read branch calls window.maestro.fs.readFile
and window.maestro.fs.stat with sshRemoteId but when creating the tab via
handleOpenFileTab it omits sshRemoteId, losing remote context; include
sshRemoteId in the object passed to handleOpenFileTab (alongside path, name,
content, lastModified) so the created tab retains the remote session identifier
used by the read/save/reload flows.

---

Duplicate comments:
In `@src/renderer/App.tsx`:
- Around line 1716-1723: The handler currently calls
setActiveSessionId(sessionId) then immediately calls handleAutoRunRefresh(),
which runs against the old active session; change the flow so the refresh
targets the intended session by passing sessionId directly to the refresher
(e.g., update handleAutoRunRefresh to accept an optional sessionId parameter and
call handleAutoRunRefresh(sessionId) from the handler) or otherwise ensure the
refresh runs after the state commit (for example, trigger the same refresh logic
from the effect that reacts to activeSessionId). Refer to handler,
setActiveSessionId, and handleAutoRunRefresh when making the change.

---

Nitpick comments:
In `@src/__tests__/renderer/hooks/useRemoteIntegration.test.ts`:
- Around line 124-137: The test currently stubs the remote subscription helpers
(onRemoteOpenFileTab, onRemoteRefreshFileTree, onRemoteRefreshAutoRunDocs,
onRemoteConfigureAutoRun) with vi.fn().mockImplementation(() => () => {}) which
never captures or invokes handlers; change each mock to capture the provided
callback (e.g., save it to a local variable) and return an unsubscribe, then in
the test invoke the saved handlers to simulate the remote events and assert that
useRemoteIntegration dispatches the expected CustomEvent payloads and calls
sendRemoteNewTabResponse / sendRemoteConfigureAutoRunResponse appropriately;
reference the mock names onRemoteOpenFileTab, onRemoteRefreshFileTree,
onRemoteRefreshAutoRunDocs, onRemoteConfigureAutoRun and the response spies
sendRemoteNewTabResponse and sendRemoteConfigureAutoRunResponse when adding the
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02f7a999-9e7c-4900-9128-e54f60bb41eb

📥 Commits

Reviewing files that changed from the base of the PR and between 844d9f9 and 6c1a30f.

📒 Files selected for processing (18)
  • src/__tests__/cli/commands/auto-run.test.ts
  • src/__tests__/cli/commands/open-file.test.ts
  • src/__tests__/cli/services/maestro-client.test.ts
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
  • src/__tests__/shared/cli-server-discovery.test.ts
  • src/cli/commands/auto-run.ts
  • src/cli/commands/send.ts
  • src/cli/commands/status.ts
  • src/cli/index.ts
  • src/cli/services/maestro-client.ts
  • src/main/preload/process.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/prompts/maestro-system-prompt.md
  • src/renderer/App.tsx
  • src/renderer/hooks/remote/useRemoteIntegration.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/tests/cli/services/maestro-client.test.ts
  • src/cli/index.ts
  • src/prompts/maestro-system-prompt.md

Comment on lines +329 to +357
describe('isCliServerRunning', () => {
it('should return true for current PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));

const originalKill = process.kill;
process.kill = vi.fn().mockReturnValue(true) as unknown as typeof process.kill;

const result = isCliServerRunning();

expect(result).toBe(true);
expect(process.kill).toHaveBeenCalledWith(12345, 0);

process.kill = originalKill;
});

it('should return false for non-existent PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));

const originalKill = process.kill;
process.kill = vi.fn().mockImplementation(() => {
throw new Error('ESRCH: No such process');
}) as unknown as typeof process.kill;

const result = isCliServerRunning();

expect(result).toBe(false);

process.kill = originalKill;
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Wrap process.kill restoration in finally block.

The same cleanup issue previously flagged for env vars applies here. If an assertion fails before line 341 or 356, process.kill is left mocked, potentially corrupting subsequent tests or the test runner.

🛡️ Proposed fix using try/finally
 it('should return true for current PID', () => {
   mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));

   const originalKill = process.kill;
   process.kill = vi.fn().mockReturnValue(true) as unknown as typeof process.kill;

-  const result = isCliServerRunning();
-
-  expect(result).toBe(true);
-  expect(process.kill).toHaveBeenCalledWith(12345, 0);
-
-  process.kill = originalKill;
+  try {
+    const result = isCliServerRunning();
+
+    expect(result).toBe(true);
+    expect(process.kill).toHaveBeenCalledWith(12345, 0);
+  } finally {
+    process.kill = originalKill;
+  }
 });

 it('should return false for non-existent PID', () => {
   mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));

   const originalKill = process.kill;
   process.kill = vi.fn().mockImplementation(() => {
     throw new Error('ESRCH: No such process');
   }) as unknown as typeof process.kill;

-  const result = isCliServerRunning();
-
-  expect(result).toBe(false);
-
-  process.kill = originalKill;
+  try {
+    const result = isCliServerRunning();
+
+    expect(result).toBe(false);
+  } finally {
+    process.kill = originalKill;
+  }
 });
📝 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
describe('isCliServerRunning', () => {
it('should return true for current PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));
const originalKill = process.kill;
process.kill = vi.fn().mockReturnValue(true) as unknown as typeof process.kill;
const result = isCliServerRunning();
expect(result).toBe(true);
expect(process.kill).toHaveBeenCalledWith(12345, 0);
process.kill = originalKill;
});
it('should return false for non-existent PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));
const originalKill = process.kill;
process.kill = vi.fn().mockImplementation(() => {
throw new Error('ESRCH: No such process');
}) as unknown as typeof process.kill;
const result = isCliServerRunning();
expect(result).toBe(false);
process.kill = originalKill;
});
describe('isCliServerRunning', () => {
it('should return true for current PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));
const originalKill = process.kill;
process.kill = vi.fn().mockReturnValue(true) as unknown as typeof process.kill;
try {
const result = isCliServerRunning();
expect(result).toBe(true);
expect(process.kill).toHaveBeenCalledWith(12345, 0);
} finally {
process.kill = originalKill;
}
});
it('should return false for non-existent PID', () => {
mockFs.readFileSync.mockReturnValue(JSON.stringify(sampleInfo));
const originalKill = process.kill;
process.kill = vi.fn().mockImplementation(() => {
throw new Error('ESRCH: No such process');
}) as unknown as typeof process.kill;
try {
const result = isCliServerRunning();
expect(result).toBe(false);
} finally {
process.kill = originalKill;
}
});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/shared/cli-server-discovery.test.ts` around lines 329 - 357,
Tests for isCliServerRunning temporarily replace process.kill but restore it
only after assertions, risking leaving it mocked if an assertion throws; wrap
the mock setup and restore in a try/finally in both test cases so process.kill
is always restored: in the 'should return true for current PID' and 'should
return false for non-existent PID' tests, assign originalKill, set process.kill
to the mock, execute the test logic in the try block, and restore process.kill =
originalKill in the finally block to ensure cleanup even on failures.

Comment on lines +48 to +54
const loopEnabled = options.loop || options.maxLoops !== undefined;
const maxLoops = options.maxLoops !== undefined ? parseInt(options.maxLoops, 10) : undefined;

if (maxLoops !== undefined && (isNaN(maxLoops) || maxLoops < 1)) {
console.error('Error: --max-loops must be a positive integer');
process.exit(1);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reject partially numeric --max-loops values.

parseInt('3foo', 10) returns 3, so malformed input currently passes validation. Validate the entire string before converting, or switch to Number() plus Number.isInteger().

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

In `@src/cli/commands/auto-run.ts` around lines 48 - 54, The current parsing of
options.maxLoops uses parseInt which accepts strings like "3foo"; update the
validation around the loopEnabled/maxLoops logic to ensure the entire
options.maxLoops string is a valid positive integer before converting: either
test the string with a full-match regex (e.g. /^\d+$/) or convert with
Number(options.maxLoops) and check Number.isInteger(...) and > 0, then assign
maxLoops to that integer (or undefined); apply the check where maxLoops is set
and in the subsequent error branch that currently logs 'Error: --max-loops must
be a positive integer'.

Comment on lines +136 to +140
await withMaestroClient(async (client) => {
await client.sendCommand(
{ type: 'select_session', sessionId: agentId, focus: true },
'select_session_result'
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--tab still focuses the session, not the target tab.

This only sends sessionId, so multi-tab sessions reopen whichever tab is already active in the desktop UI. If send resumed a different result.agentSessionId, the wrong tab stays focused. Please resolve the matching tab ID and include it in the select request, or add a server-side lookup from agent-session ID to tab ID.

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

In `@src/cli/commands/send.ts` around lines 136 - 140, The select_session command
only sends sessionId (agentId) which focuses the session but not the specific
tab; update the withMaestroClient callback where client.sendCommand is called
(the select_session request) to include the resolved tab id (e.g., add a tabId
property) so the desktop focuses the correct tab; to get that tabId, resolve the
matching tab for the target agent session before calling client.sendCommand
(either by querying the local data structure that maps
agentSessionId/result.agentSessionId to tabId or by calling the server API to
look up agent-session → tab mapping) and pass that tabId in the select_session
payload, or if server-side support is preferred implement a lookup endpoint that
accepts agentSessionId and returns tabId and call it here before issuing the
select_session request.

Comment on lines +63 to +68
ws.on('error', (err) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
reject(new Error(`Failed to connect to Maestro: ${err.message}`));
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that MaestroClient handles open/error but not post-connect close, and that
# pending requests are only rejected on explicit disconnect().
rg -n "ws\\.on\\('open'|ws\\.on\\('error'|ws\\.on\\('close'|pendingRequests|Client disconnected" src/cli/services/maestro-client.ts

Repository: RunMaestro/Maestro

Length of output: 736


🏁 Script executed:

cat -n src/cli/services/maestro-client.ts | head -160

Repository: RunMaestro/Maestro

Length of output: 5353


Add a close handler in setupMessageHandler() to fail pending requests immediately when the socket closes.

After a successful connection, setupMessageHandler() only registers a message handler. If Maestro closes the connection mid-command, pending requests hang until the 10-second command timeout instead of failing immediately. Add a close handler that rejects all pending requests (similar to how disconnect() does at lines 107–111).

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

In `@src/cli/services/maestro-client.ts` around lines 63 - 68, Add a 'close'
handler inside setupMessageHandler() that mirrors disconnect() behavior: when
the WebSocket (ws) emits 'close', iterate over the pending requests map
(pendingRequests) and reject each promise with an Error indicating the Maestro
connection closed, then clear the map and perform any necessary cleanup; ensure
the handler uses the same rejection message/shape as disconnect() to immediately
fail pending commands rather than waiting for the command timeout.

Comment on lines +1682 to +1694
const [content, stat] = await Promise.all([
window.maestro.fs.readFile(filePath, sshRemoteId),
window.maestro.fs.stat(filePath, sshRemoteId).catch(() => null),
]);
if (content !== null) {
const filename = filePath.split(/[\\/]/).pop() || filePath;
const lastModified = stat?.modifiedAt ? new Date(stat.modifiedAt).getTime() : undefined;
handleOpenFileTab({
path: filePath,
name: filename,
content,
lastModified,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Carry sshRemoteId into the created file tab.

This read path uses the remote FS, but the tab payload drops sshRemoteId. On SSH-backed sessions, later reload/save flows will lose the remote context even though the existing DocumentGraph open path in this file preserves it.

🔧 Proposed fix
 					handleOpenFileTab({
 						path: filePath,
 						name: filename,
 						content,
+						sshRemoteId,
 						lastModified,
 					});
📝 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
const [content, stat] = await Promise.all([
window.maestro.fs.readFile(filePath, sshRemoteId),
window.maestro.fs.stat(filePath, sshRemoteId).catch(() => null),
]);
if (content !== null) {
const filename = filePath.split(/[\\/]/).pop() || filePath;
const lastModified = stat?.modifiedAt ? new Date(stat.modifiedAt).getTime() : undefined;
handleOpenFileTab({
path: filePath,
name: filename,
content,
lastModified,
});
const [content, stat] = await Promise.all([
window.maestro.fs.readFile(filePath, sshRemoteId),
window.maestro.fs.stat(filePath, sshRemoteId).catch(() => null),
]);
if (content !== null) {
const filename = filePath.split(/[\\/]/).pop() || filePath;
const lastModified = stat?.modifiedAt ? new Date(stat.modifiedAt).getTime() : undefined;
handleOpenFileTab({
path: filePath,
name: filename,
content,
sshRemoteId,
lastModified,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 1682 - 1694, The file read branch calls
window.maestro.fs.readFile and window.maestro.fs.stat with sshRemoteId but when
creating the tab via handleOpenFileTab it omits sshRemoteId, losing remote
context; include sshRemoteId in the object passed to handleOpenFileTab
(alongside path, name, content, lastModified) so the created tab retains the
remote session identifier used by the read/save/reload flows.

Comment on lines +1786 to +1789
startBatchRun(sessionId, batchConfig, folderPath);
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wait for batch startup before returning success.

startBatchRun is async, but this branch sends { success: true } before startup finishes. If launch fails, the CLI gets a false positive and the catch path never reports the real error.

🔧 Proposed fix
-					startBatchRun(sessionId, batchConfig, folderPath);
+					await startBatchRun(sessionId, batchConfig, folderPath);
 					window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
 						success: true,
 					});
📝 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
startBatchRun(sessionId, batchConfig, folderPath);
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
startBatchRun(sessionId, batchConfig, folderPath);
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
Suggested change
startBatchRun(sessionId, batchConfig, folderPath);
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
await startBatchRun(sessionId, batchConfig, folderPath);
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 1786 - 1789, The code sends success before
the async startBatchRun completes; change the branch to await
startBatchRun(sessionId, batchConfig, folderPath) and only call
window.maestro.process.sendRemoteConfigureAutoRunResponse(responseChannel, {
success: true }) after it resolves, and add a try/catch around the await so that
on failure you send a failure response (e.g., sendRemoteConfigureAutoRunResponse
with success: false and include the error details) using the same
responseChannel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

runmaestro.ai These issues are part of the Maestro Symphony program.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant