Skip to content

feat(cli): add --visible flag to goal-run for desktop-owned Auto Runs - #1287

Open
pedramamini wants to merge 1 commit into
rcfrom
feat/1286-cli-visible-goal-run
Open

feat(cli): add --visible flag to goal-run for desktop-owned Auto Runs#1287
pedramamini wants to merge 1 commit into
rcfrom
feat/1286-cli-visible-goal-run

Conversation

@pedramamini

@pedramamini pedramamini commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #1286

What

Adds --visible to maestro-cli goal-run:

maestro-cli goal-run --visible [--exit-criteria "..."] [--max-iterations N] [--json] <agent-id> "<goal>"

With --visible, the Goal-Driven Auto Run is handed to the running desktop app so it executes as a live, desktop-owned Auto Run in the same UI surface as the Go/Spec buttons, instead of running headless in the CLI process. This lets a root/orchestrator agent launch a monitored, UI-visible free-text Goal worker without handing control back to the user.

How

The visible path threads a new launch_goal_run message through the existing CLI → desktop WebSocket bridge (the same four-layer path dispatch --new-tab and auto-run --launch already use) and terminates in the renderer at the exact startBatchRun({ goalConfig }) entry point the UI Go button uses. There is no new run engine: the desktop owns spawning, busy arbitration, stop-auto-run, and session list visibility, so those requirements come for free.

Layers touched (all additive):

  • CLI: --visible flag + runVisibleGoalRun() in goal-run.ts, registration in index.ts
  • Web server: launch_goal_run message handler, callback registry/types, WebServer wiring
  • Main → renderer: remote:launchGoalRun IPC bridge in web-server-factory.ts, preload subscription
  • Renderer: maestro:launchGoalRun remote event listener that builds a goalConfig and calls startBatchRun

Behavior / contract

  • Backward compatible: headless goal-run stays the default; --visible is purely additive (no deletions).
  • Fails closed: --visible errors with MAESTRO_NOT_RUNNING when the desktop app is unreachable rather than silently falling back to headless.
  • Deterministic busy handling: a busy agent returns a clear AGENT_BUSY error.
  • --json returns stable identifiers plus a deep link:
    { "ok": true, "mode": "visible", "visible": true, "agent_id": "...", "session_id": "...", "tab_id": "...", "status": "launched", "uri": "maestro://session/<id>/tab/<id>" }

Scope notes / open questions for the author

The issue's "recommended" API also listed --new-tab / --tab / --focus/--no-focus / --wait and a run_id. This PR intentionally implements the core UI-parity MVP the issue calls the key requirement, and leaves those out for now because:

  • Goal-Driven Auto Runs are modeled per-agent (batch state keyed by session/agent id), not per-tab, so --new-tab/--tab don't map cleanly onto the existing engine. The run attaches to the agent and surfaces on its active AI tab, which is what we return as tab_id.
  • run_id / provider session_id aren't known synchronously at launch (the provider session is assigned after the first agent turn), so they're omitted rather than faked.
  • Busy is handled as a deterministic clear error, which the issue lists as acceptable in lieu of --wait.

Happy to extend any of these if you'd prefer the fuller surface.

Tests

  • New src/__tests__/cli/commands/goalRunVisible.test.ts covers the visible path: message shape, JSON identifiers + deep link, fail-closed (MAESTRO_NOT_RUNNING), AGENT_BUSY, generic rejection, and human-readable output.
  • Updated existing web-server-factory and useRemoteIntegration mocks for the new bridge method.
  • npm run lint (all tsconfigs) and the CLI + web-server + goalDriven + remote-integration suites pass locally.

Note: local validation is single-OS; CI (ubuntu + windows) is the source of truth before merge.

Summary by CodeRabbit

  • New Features

    • Added goal-run --visible to launch Goal-Driven Auto Runs in the desktop interface.
    • Displays a deep link to the launched run in JSON or human-readable format.
    • Added validation and clear errors for unavailable, busy, or rejected launches.
    • Added desktop-to-CLI communication for starting visible runs and reporting results.
  • Tests

    • Added coverage for successful launches, deep-link formatting, missing sessions, busy agents, desktop connectivity, and rejection errors.

`maestro-cli goal-run --visible <agent> "<goal>"` hands a Goal-Driven Auto
Run to the running desktop app so it executes as a live, desktop-owned Auto
Run in the same UI surface as the Go/Spec buttons, instead of running
headless in the CLI process. This lets a root/orchestrator agent launch a
monitored, UI-visible free-text Goal worker.

The visible path threads a new `launch_goal_run` message through the existing
CLI -> desktop WebSocket bridge (the same four-layer path used by
`dispatch --new-tab` and `auto-run --launch`) and terminates in the renderer
at the exact `startBatchRun({ goalConfig })` entry point the UI Go button
already uses. No new run engine: the desktop owns spawning, busy arbitration,
stop, and session-list visibility.

Behavior:
- Backward compatible: headless `goal-run` stays the default; `--visible` is
  purely additive.
- Fails closed: `--visible` errors with MAESTRO_NOT_RUNNING when the desktop
  app is unreachable instead of silently falling back to headless.
- Deterministic busy handling: a busy agent returns a clear AGENT_BUSY error.
- With `--json`, returns stable identifiers (ok, mode, visible, agent_id,
  session_id, tab_id, status) plus a `maestro://session/<id>/tab/<id>` URI.

Layers touched: CLI command + registration, web-server message handler,
callback registry/types, main->renderer IPC bridge, preload, and the renderer
remote event listener. Adds CLI unit tests for the visible path.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds goal-run --visible, routing CLI requests through the desktop and renderer to start a visible Goal-Driven Auto Run with structured responses, deep links, validation, timeout handling, and error mapping.

Changes

Visible Goal Run

Layer / File(s) Summary
CLI visible launch path
src/cli/commands/goal-run.ts, src/cli/index.ts, src/__tests__/cli/commands/goalRunVisible.test.ts
Adds the --visible option, desktop delegation, deep-link output, failure codes, and coverage for success and rejection cases.
WebSocket launch contract
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
Adds typed launch results and callbacks, validates launch_goal_run messages, and routes requests through the callback registry.
Desktop-to-renderer execution bridge
src/main/web-server/web-server-factory.ts, src/main/preload/process.ts, src/renderer/global.d.ts, src/renderer/hooks/remote/*, src/__tests__/main/web-server/web-server-factory.test.ts, src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
Bridges requests over IPC response channels, handles missing or timed-out responses, validates renderer state, acknowledges launches, and starts the batch run asynchronously.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant WebServer
  participant Renderer
  participant AutoRun
  CLI->>WebServer: launch_goal_run(sessionId, goal config)
  WebServer->>Renderer: remote:launchGoalRun(responseChannel)
  Renderer->>Renderer: Validate session, agent state, goal, and AI tab
  Renderer->>CLI: success, tabId
  Renderer->>AutoRun: startBatchRun(goalConfig)
Loading

Possibly related PRs

Suggested labels: approved

Suggested reviewers: copilot, chr1syy, jsydorowicz21

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Most visible-run plumbing is in place, but the summary does not confirm the full #1286 scope, including Spec parity, session-list visibility, and the complete JSON identifier contract. Verify the shipped behavior covers the full issue contract, or narrow the linked issue to the MVP scope now implemented.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on the visible goal-run feature and its supporting IPC/WebSocket wiring plus tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a --visible goal-run path for desktop-owned Auto Runs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1286-cli-visible-goal-run

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.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds desktop-visible Goal-Driven Auto Runs to the CLI. The main changes are:

  • A new goal-run --visible CLI path with JSON output and deep links.
  • A launch_goal_run message across the WebSocket and Electron IPC bridges.
  • Renderer integration with the existing startBatchRun goal flow.
  • Tests for CLI responses and updated bridge mocks.

Confidence Score: 4/5

Visible launches can report success without starting and can overlap on the same agent.

  • The bridge and request shapes are consistently wired.
  • Startup errors occur after the success response has already been sent.
  • Busy checking does not atomically reserve the agent before acknowledging the launch.

src/renderer/hooks/remote/useAppRemoteEventListeners.ts

Important Files Changed

Filename Overview
src/cli/commands/goal-run.ts Adds visible launch dispatch, structured errors, JSON output, and deep-link generation.
src/main/web-server/handlers/messageHandlers.ts Validates and routes the new launch_goal_run WebSocket message.
src/main/web-server/web-server-factory.ts Bridges visible launches into the renderer with a unique response channel and timeout.
src/main/preload/process.ts Exposes the new launch subscription and response methods to the renderer.
src/renderer/hooks/remote/useRemoteIntegration.ts Forwards launch IPC messages into the renderer event system.
src/renderer/hooks/remote/useAppRemoteEventListeners.ts Starts goal runs but can report success before startup and allow concurrent launches for one agent.
src/tests/cli/commands/goalRunVisible.test.ts Covers CLI request shape, output modes, deep links, and rejection mapping.

Sequence Diagram

sequenceDiagram
    participant CLI as maestro-cli
    participant WS as WebSocket server
    participant Main as Electron main
    participant Renderer as Renderer
    participant Runner as Goal runner

    CLI->>WS: launch_goal_run
    WS->>Main: launchGoalRun(sessionId, config)
    Main->>Renderer: remote:launchGoalRun
    Renderer->>Renderer: Check session.state
    Renderer-->>Main: success: true
    Main-->>WS: launch_goal_run_result
    WS-->>CLI: status: launched
    Renderer->>Runner: startBatchRun(goalConfig)
    Runner--xRenderer: Initialization may reject
Loading

Reviews (1): Last reviewed commit: "feat(cli): add --visible flag to goal-ru..." | Re-trigger Greptile

Comment on lines +645 to +653
// Ack before starting: startBatchRun is long-running and would exceed the
// IPC/CLI timeout if awaited (same pattern as maestro:configureAutoRun).
window.maestro.process.sendRemoteLaunchGoalRunResponse(responseChannel, {
success: true,
...(tabId ? { tabId } : {}),
});

// Goal mode is document-less; folderPath is only stored in batch state.
startBatchRun(sessionId, batchConfig, session.autoRunFolderPath || '').catch((err) => {

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 Launch Failure Reports Success

The response reports success: true before startBatchRun begins. If goal initialization then rejects, the catch only logs the error, so the CLI exits successfully with status: "launched" even though no run started.

Comment on lines +606 to +612
window.maestro.process.sendRemoteLaunchGoalRunResponse(responseChannel, {
success: false,
error: `Agent "${session.name || session.id}" is busy`,
});
return;
}

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 Busy Check Does Not Reserve Agent

Two rapid requests can both observe an idle session.state because this check does not reserve the agent and goal startup does not immediately set that state to busy. Both requests can therefore report success and start overlapping goal loops for the same agent.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a --visible mode to maestro-cli goal-run that routes the request over the existing CLI to desktop WebSocket bridge so the desktop app runs the Goal-Driven Auto Run in the same Auto Run UI surface as the Go/Spec buttons, and returns identifiers (including a maestro:// deep link) back to the CLI.

Changes:

  • Add --visible flag and a CLI implementation that sends a launch_goal_run command to the running desktop app and emits JSON/human output.
  • Thread a new launch_goal_run message and callback through the web server, main to renderer IPC bridge, and preload API.
  • Add renderer-side remote listeners that translate the IPC event into startBatchRun({ goalConfig }), plus unit tests for the visible CLI path.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/cli/index.ts Adds the --visible CLI flag to goal-run.
src/cli/commands/goal-run.ts Implements runVisibleGoalRun() and fail-closed behavior for desktop-owned visible runs.
src/main/web-server/handlers/messageHandlers.ts Adds launch_goal_run WebSocket message handler and result emission.
src/main/web-server/managers/CallbackRegistry.ts Adds launchGoalRun callback registration and invocation plumbing.
src/main/web-server/types.ts Introduces LaunchGoalRunCallback and result typing.
src/main/web-server/WebServer.ts Exposes setLaunchGoalRunCallback() and wires launchGoalRun into handler callbacks.
src/main/web-server/web-server-factory.ts Bridges launchGoalRun from web server callbacks to main to renderer IPC (remote:launchGoalRun).
src/main/preload/process.ts Adds onRemoteLaunchGoalRun subscription and sendRemoteLaunchGoalRunResponse.
src/renderer/global.d.ts Extends the renderer process API typing for the new remote goal-run bridge.
src/renderer/hooks/remote/useRemoteIntegration.ts Subscribes to onRemoteLaunchGoalRun and dispatches a maestro:launchGoalRun CustomEvent.
src/renderer/hooks/remote/useAppRemoteEventListeners.ts Handles maestro:launchGoalRun by building goalConfig and calling startBatchRun.
src/tests/cli/commands/goalRunVisible.test.ts Adds test coverage for the CLI visible goal-run path and output contract.
src/tests/renderer/hooks/useRemoteIntegration.test.ts Updates renderer hook mocks for the new preload bridge methods.
src/tests/main/web-server/web-server-factory.test.ts Updates web server factory mocks to include the new callback wiring.
Comments suppressed due to low confidence (1)

src/main/web-server/handlers/messageHandlers.ts:1712

  • maxIterations validation claims to require a positive integer, but the current logic accepts non-integers (for example 2.5) and silently floors them. Reject non-integer values to match the stated contract and avoid surprising behavior.
			const n = Number(message.maxIterations);
			if (!Number.isFinite(n) || n < 1) {
				sendErrorResult('maxIterations must be a positive integer or null');
				return;
			}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

private handleLaunchGoalRun(client: WebClient, message: WebClientMessage): void {
const sessionId = typeof message.sessionId === 'string' ? message.sessionId : '';
const goal = typeof message.goal === 'string' ? message.goal.trim() : '';
// Goals can contain user-authored content with secrets or PII — log length only.
Comment on lines +29 to +36
/**
* Substring signature of MaestroClient / socket-level failures that mean the
* desktop app is down or unreachable (as opposed to a command it rejected).
* Mirrors the mapping in `dispatch.ts` so `--visible` can fail closed with a
* dedicated code instead of a generic error.
*/
function isMaestroUnreachable(message: string): boolean {
const lower = message.toLowerCase();
Comment on lines +641 to +650
// Focus the agent so the run is immediately visible, matching the UI
// Go button (which runs on the focused agent).
setActiveSessionId(sessionId);

// Ack before starting: startBatchRun is long-running and would exceed the
// IPC/CLI timeout if awaited (same pattern as maestro:configureAutoRun).
window.maestro.process.sendRemoteLaunchGoalRunResponse(responseChannel, {
success: true,
...(tabId ? { tabId } : {}),
});

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/__tests__/cli/commands/goalRunVisible.test.ts (1)

1-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage of the JSON success/error paths; missing symmetric human-readable error-path tests.

All error-path tests (MAESTRO_NOT_RUNNING, AGENT_BUSY, VISIBLE_LAUNCH_REJECTED) only assert JSON output. There's no test asserting console.error/formatError output for these same failures when useJson is false, even though a human-readable success case is covered. Consider adding one such case to close the gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/cli/commands/goalRunVisible.test.ts` around lines 1 - 130, The
error-path coverage for runVisibleGoalRun only verifies JSON output, leaving the
non-JSON behavior untested. Add a human-readable failure test using
useJson=false, preferably for a representative rejection such as AGENT_BUSY, and
assert console.error receives the formatted error while console.log is not used
for the failure output; preserve the existing JSON-path tests.
src/cli/commands/goal-run.ts (1)

29-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated desktop-unreachable detection logic.

The comment itself admits this function is a substring signature of MaestroClient / socket-level failures that mean the desktop app is down or unreachable, and states it "Mirrors the mapping in dispatch.ts." Maintaining two copies of this string-matching heuristic risks silent drift (e.g. a new failure mode added to one file but not the other), which would break the fail-closed guarantee --visible relies on. Extract this into a shared helper (e.g. src/cli/services/maestro-errors.ts) and import it from both goal-run.ts and dispatch.ts.

As per path instructions: "Before creating a new utility, helper, hook, component, type, or constant, check the relevant guide in docs/agent-guides/ and reuse or extend the canonical implementation instead of duplicating it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/goal-run.ts` around lines 29 - 48, Consolidate the
duplicated Maestro desktop-unreachable detection into one shared helper, first
checking the applicable guide in docs/agent-guides/ for the canonical location
or implementation. Move or extend the logic represented by isMaestroUnreachable
and update both goal-run.ts and dispatch.ts to import and reuse it, preserving
the existing failure classifications and --visible fail-closed behavior.

Source: Path instructions

src/renderer/hooks/remote/useAppRemoteEventListeners.ts (1)

584-663: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No visible test coverage for this handler's own logic.

This handler is the actual execution owner for visible goal runs (busy/connecting short-circuit, empty-goal validation, tab-id resolution for the deep link, ack-before-start ordering), but the only tests provided for the visible-run flow are CLI-level (goalRunVisible.test.ts, which mocks the transport and never exercises this code) and useRemoteIntegration.test.ts (which only verifies the forwarding effect, not this handler). Consider adding unit tests here for: session-not-found, busy/connecting rejection, empty-goal rejection, tabId fallback to the first AI tab, and the success ack shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/remote/useAppRemoteEventListeners.ts` around lines 584 -
663, Add unit coverage for the maestro:launchGoalRun handler in
useAppRemoteEventListeners, exercising session-not-found, busy/connecting
rejection, and empty-goal validation paths. Also verify tabId resolution falls
back to the first AI tab and that the success response acknowledges before
starting with the expected response shape. Mock session state, event details,
and process methods so tests target this handler rather than CLI transport or
forwarding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/goal-run.ts`:
- Around line 200-215: Move the getAgentDefinition(agent.toolType) and
AGENT_UNSUPPORTED validation to after the options.visible early-return in the
goal-run command, or guard it so visible runs bypass it. Ensure
runVisibleGoalRun executes for any toolType supported by the desktop app, while
retaining the existing validation for headless execution.

In `@src/main/preload/process.ts`:
- Around line 900-913: Update the callback failure handling around
Promise.resolve and the surrounding try/catch to send the fallback response,
then rethrow the original error so Sentry can capture unexpected renderer launch
failures. Preserve the existing error-to-message conversion and response channel
behavior in both asynchronous and synchronous failure paths.

In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 1707-1714: Update the maxIterations validation in the message
handler to reject non-integer numeric values, not just non-finite or values
below 1. Preserve acceptance of positive integers and the existing
null/undefined behavior, and avoid silently coercing fractional inputs via
Math.floor.
- Around line 1735-1736: Update the rejection handler in the goal-run launch
flow to normalize the caught value into an Error before accessing its message,
including safe handling for null and other non-Error rejections. Use the
normalized error when reporting through sendErrorResult, and ensure unexpected
exceptions are still surfaced rather than silently swallowed.

---

Nitpick comments:
In `@src/__tests__/cli/commands/goalRunVisible.test.ts`:
- Around line 1-130: The error-path coverage for runVisibleGoalRun only verifies
JSON output, leaving the non-JSON behavior untested. Add a human-readable
failure test using useJson=false, preferably for a representative rejection such
as AGENT_BUSY, and assert console.error receives the formatted error while
console.log is not used for the failure output; preserve the existing JSON-path
tests.

In `@src/cli/commands/goal-run.ts`:
- Around line 29-48: Consolidate the duplicated Maestro desktop-unreachable
detection into one shared helper, first checking the applicable guide in
docs/agent-guides/ for the canonical location or implementation. Move or extend
the logic represented by isMaestroUnreachable and update both goal-run.ts and
dispatch.ts to import and reuse it, preserving the existing failure
classifications and --visible fail-closed behavior.

In `@src/renderer/hooks/remote/useAppRemoteEventListeners.ts`:
- Around line 584-663: Add unit coverage for the maestro:launchGoalRun handler
in useAppRemoteEventListeners, exercising session-not-found, busy/connecting
rejection, and empty-goal validation paths. Also verify tabId resolution falls
back to the first AI tab and that the success response acknowledges before
starting with the expected response shape. Mock session state, event details,
and process methods so tests target this handler rather than CLI transport or
forwarding behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81d660a0-ab92-4947-8462-e50257e16fdb

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5c8fd and 2b1c45c.

📒 Files selected for processing (14)
  • src/__tests__/cli/commands/goalRunVisible.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
  • src/cli/commands/goal-run.ts
  • src/cli/index.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/renderer/global.d.ts
  • src/renderer/hooks/remote/useAppRemoteEventListeners.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts

Comment on lines +200 to +215
// --visible hands the run to the desktop app, which owns spawning (using the
// agent's configured binary, SSH, etc.) and busy arbitration. Skip the
// headless-only preflight below (detectAgent / checkAgentBusy / runGoal).
if (options.visible) {
await runVisibleGoalRun(
agent.id,
{
goal: trimmedGoal,
exitCriteria: options.exitCriteria?.trim() ?? '',
maxIterations: parseMaxIterations(options.maxIterations, useJson),
},
useJson
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

--visible is still gated by the CLI's own agent-support registry, contradicting the "skip headless-only preflight" intent.

The comment says this branch exists to hand the run to the desktop app, which owns spawning (using the agent's configured binary, SSH, etc.) and busy arbitration, skipping the headless-only preflight below (detectAgent / checkAgentBusy / runGoal). However, the getAgentDefinition(agent.toolType) / AGENT_UNSUPPORTED check (lines 188-198) runs unconditionally before this branch, not "below" it. So a visible launch for any toolType the CLI's local registry doesn't recognize — even one the desktop app fully supports for spawning — will be rejected with AGENT_UNSUPPORTED before runVisibleGoalRun is ever called. This undercuts the PR's core premise that the desktop app owns agent support for visible runs.

Move the def/AGENT_UNSUPPORTED check after the options.visible early return (or skip it entirely when options.visible is set).

🐛 Proposed fix
-		// Agent CLI must be supported and installed.
-		const def = getAgentDefinition(agent.toolType);
-		if (!def) {
-			const message = `Agent type "${agent.toolType}" is not supported in CLI batch mode yet.`;
-			if (useJson) {
-				emitError(message, 'AGENT_UNSUPPORTED');
-			} else {
-				console.error(formatError(message));
-			}
-			process.exit(1);
-		}
-
 		// --visible hands the run to the desktop app, which owns spawning (using the
 		// agent's configured binary, SSH, etc.) and busy arbitration. Skip the
 		// headless-only preflight below (detectAgent / checkAgentBusy / runGoal).
 		if (options.visible) {
 			await runVisibleGoalRun(
 				agent.id,
 				{
 					goal: trimmedGoal,
 					exitCriteria: options.exitCriteria?.trim() ?? '',
 					maxIterations: parseMaxIterations(options.maxIterations, useJson),
 				},
 				useJson
 			);
 			return;
 		}
+
+		// Agent CLI must be supported and installed (headless path only — visible
+		// runs are owned by the desktop app, which has its own agent support).
+		const def = getAgentDefinition(agent.toolType);
+		if (!def) {
+			const message = `Agent type "${agent.toolType}" is not supported in CLI batch mode yet.`;
+			if (useJson) {
+				emitError(message, 'AGENT_UNSUPPORTED');
+			} else {
+				console.error(formatError(message));
+			}
+			process.exit(1);
+		}
📝 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
// --visible hands the run to the desktop app, which owns spawning (using the
// agent's configured binary, SSH, etc.) and busy arbitration. Skip the
// headless-only preflight below (detectAgent / checkAgentBusy / runGoal).
if (options.visible) {
await runVisibleGoalRun(
agent.id,
{
goal: trimmedGoal,
exitCriteria: options.exitCriteria?.trim() ?? '',
maxIterations: parseMaxIterations(options.maxIterations, useJson),
},
useJson
);
return;
}
// --visible hands the run to the desktop app, which owns spawning (using the
// agent's configured binary, SSH, etc.) and busy arbitration. Skip the
// headless-only preflight below (detectAgent / checkAgentBusy / runGoal).
if (options.visible) {
await runVisibleGoalRun(
agent.id,
{
goal: trimmedGoal,
exitCriteria: options.exitCriteria?.trim() ?? '',
maxIterations: parseMaxIterations(options.maxIterations, useJson),
},
useJson
);
return;
}
// Agent CLI must be supported and installed (headless path only - visible
// runs are owned by the desktop app, which has its own agent support).
const def = getAgentDefinition(agent.toolType);
if (!def) {
const message = `Agent type "${agent.toolType}" is not supported in CLI batch mode yet.`;
if (useJson) {
emitError(message, 'AGENT_UNSUPPORTED');
} else {
console.error(formatError(message));
}
process.exit(1);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/goal-run.ts` around lines 200 - 215, Move the
getAgentDefinition(agent.toolType) and AGENT_UNSUPPORTED validation to after the
options.visible early-return in the goal-run command, or guard it so visible
runs bypass it. Ensure runVisibleGoalRun executes for any toolType supported by
the desktop app, while retaining the existing validation for headless execution.

Comment on lines +900 to +913
try {
// callback may return a promise even though typed as void
Promise.resolve(callback(sessionId, config, responseChannel)).catch((error) => {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
});
} catch (error) {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Rethrow callback failures after acknowledging them.

These paths send the fallback response but consume unexpected exceptions, preventing Sentry from seeing renderer launch failures. Send the response, then rethrow.

Proposed fix
					Promise.resolve(callback(sessionId, config, responseChannel)).catch((error) => {
						ipcRenderer.send(responseChannel, {
							success: false,
							error: error instanceof Error ? error.message : String(error),
						});
+						throw error;
					});
				} catch (error) {
					ipcRenderer.send(responseChannel, {
						success: false,
						error: error instanceof Error ? error.message : String(error),
					});
+					throw error;
				}

As per coding guidelines, "Do not silently swallow unexpected exceptions. Handle known recoverable errors explicitly, rethrow unexpected errors so Sentry can 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 {
// callback may return a promise even though typed as void
Promise.resolve(callback(sessionId, config, responseChannel)).catch((error) => {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
});
} catch (error) {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
try {
// callback may return a promise even though typed as void
Promise.resolve(callback(sessionId, config, responseChannel)).catch((error) => {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
throw error;
});
} catch (error) {
ipcRenderer.send(responseChannel, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/preload/process.ts` around lines 900 - 913, Update the callback
failure handling around Promise.resolve and the surrounding try/catch to send
the fallback response, then rethrow the original error so Sentry can capture
unexpected renderer launch failures. Preserve the existing error-to-message
conversion and response channel behavior in both asynchronous and synchronous
failure paths.

Source: Coding guidelines

Comment on lines +1707 to +1714
if (message.maxIterations !== undefined && message.maxIterations !== null) {
const n = Number(message.maxIterations);
if (!Number.isFinite(n) || n < 1) {
sendErrorResult('maxIterations must be a positive integer or null');
return;
}
maxIterations = Math.floor(n);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject fractional iteration limits.

1.5 passes validation and is silently changed to 1, despite the public positive-integer contract.

Proposed fix
-			if (!Number.isFinite(n) || n < 1) {
+			if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
				sendErrorResult('maxIterations must be a positive integer or null');
				return;
			}
-			maxIterations = Math.floor(n);
+			maxIterations = n;
📝 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
if (message.maxIterations !== undefined && message.maxIterations !== null) {
const n = Number(message.maxIterations);
if (!Number.isFinite(n) || n < 1) {
sendErrorResult('maxIterations must be a positive integer or null');
return;
}
maxIterations = Math.floor(n);
}
if (message.maxIterations !== undefined && message.maxIterations !== null) {
const n = Number(message.maxIterations);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
sendErrorResult('maxIterations must be a positive integer or null');
return;
}
maxIterations = n;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/web-server/handlers/messageHandlers.ts` around lines 1707 - 1714,
Update the maxIterations validation in the message handler to reject non-integer
numeric values, not just non-finite or values below 1. Preserve acceptance of
positive integers and the existing null/undefined behavior, and avoid silently
coercing fractional inputs via Math.floor.

Comment on lines +1735 to +1736
.catch((error) => {
sendErrorResult(`Failed to launch goal run: ${error.message}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Report and normalize bridge failures.

A non-Error rejection such as null makes error.message throw while handling the failure, so the CLI receives no typed result. Normalize and capture the error before replying.

Proposed fix
-			.catch((error) => {
-				sendErrorResult(`Failed to launch goal run: ${error.message}`);
+			.catch((error: unknown) => {
+				const err = error instanceof Error ? error : new Error(String(error));
+				captureException(err, {
+					extra: {
+						area: 'web-server',
+						handler: 'launch_goal_run',
+						sessionId,
+						requestId: message.requestId,
+					},
+				});
+				sendErrorResult(`Failed to launch goal run: ${err.message}`);
			});

As per coding guidelines, "Do not silently swallow unexpected exceptions."

📝 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
.catch((error) => {
sendErrorResult(`Failed to launch goal run: ${error.message}`);
.catch((error: unknown) => {
const err = error instanceof Error ? error : new Error(String(error));
captureException(err, {
extra: {
area: 'web-server',
handler: 'launch_goal_run',
sessionId,
requestId: message.requestId,
},
});
sendErrorResult(`Failed to launch goal run: ${err.message}`);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/web-server/handlers/messageHandlers.ts` around lines 1735 - 1736,
Update the rejection handler in the goal-run launch flow to normalize the caught
value into an Error before accessing its message, including safe handling for
null and other non-Error rejections. Use the normalized error when reporting
through sendErrorResult, and ensure unexpected exceptions are still surfaced
rather than silently swallowed.

Source: Coding guidelines

@pedramamini
pedramamini changed the base branch from main to rc July 23, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: CLI: add --visible flag for desktop Goal/Spec Auto Runs

2 participants