Skip to content

Enable OpenCode and Droid in Maestro CLI#520

Closed
chr1syy wants to merge 14 commits intoRunMaestro:mainfrom
chr1syy:feature/cli-opencode-droid
Closed

Enable OpenCode and Droid in Maestro CLI#520
chr1syy wants to merge 14 commits intoRunMaestro:mainfrom
chr1syy:feature/cli-opencode-droid

Conversation

@chr1syy
Copy link
Contributor

@chr1syy chr1syy commented Mar 5, 2026

This pull request extends CLI agent support by adding compatibility for two new agent types, opencode and factory-droid, alongside existing ones. It updates error handling, agent detection logic, and configuration management to accommodate these new agents. The changes also ensure that custom agent configuration values are passed through all relevant code paths.

Agent support and error handling:

  • Added support for opencode and factory-droid agent types in both the run-playbook and send CLI commands, including detection logic and error handling for missing CLIs. [1] [2] [3] [4]
  • Updated the documented list of supported agent types and error codes in docs/cli.md to include OPENCODE_NOT_FOUND and DROID_NOT_FOUND.

Agent configuration management:

  • Ensured that custom agent configuration values (customPath, customArgs, customEnvVars, customModel) are passed to agent detection and spawning functions throughout the CLI codebase. [1] [2] [3] [4] [5]
  • Added a new helper function getAgentConfigValues in src/cli/services/storage.ts to retrieve stored config values for agents.
  • Updated the SessionInfo interface to include new fields for custom agent configuration.

Summary by CodeRabbit

  • New Features
    • Support for OpenCode and Factory Droid agents; per-session overrides (custom path, args, env, model) and SSH remote execution.
    • CLI commands to list SSH remote targets and retrieve stored agent configuration values.
  • Improvements
    • Detection and spawn flows now honor per-invocation overrides across send, playbook, batch, and synopsis runs.
  • Documentation
    • CLI docs updated; new error codes for missing OpenCode/Factory Droid.
  • Tests
    • Added detection, SSH, and config-retrieval tests and mocks.

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds OpenCode and Factory Droid agent support across the CLI: detection, command resolution, streaming spawn/parsing, per-session spawn overrides (customPath/customArgs/customEnvVars/customModel/sshRemoteConfig), storage accessors for agent configs/SSH remotes, tests, and related type/signature updates.

Changes

Cohort / File(s) Summary
Docs
docs/cli.md
Added OPENCODE_NOT_FOUND and DROID_NOT_FOUND; documented opencode and factory-droid as supported agents and updated requirements text.
CLI command handlers
src/cli/commands/send.ts, src/cli/commands/run-playbook.ts
Recognize opencode and factory-droid; call new detectOpenCode/detectDroid (accept customPath + ssh config); compute and forward per-session AgentSpawnOverrides to spawnAgent; updated error messages and detection calls.
Agent spawner core
src/cli/services/agent-spawner.ts
Introduced AgentSpawnOverrides; extended spawnAgent(..., overrides?); added detect/get command functions for OpenCode/Droid; added streaming spawners/parsers, SSH wrap support, per-session overrides, path caching, and new utilities for argument/env/stream handling.
Batch processing
src/cli/services/batch-processor.ts
Construct per-task spawnOverrides from session values (customPath/customArgs/customEnvVars/customModel/sshRemoteConfig) and pass as extra argument into spawnAgent for task and synopsis spawns.
Storage services
src/cli/services/storage.ts
Exported readSshRemotes(): SshRemoteConfig[] and getAgentConfigValues(agentId: string): Record<string, unknown> to expose SSH remotes and stored agent config values.
Types & agent-args utilities
src/shared/types.ts, src/main/utils/agent-args.ts
Extended SessionInfo with optional customPath, customArgs, customEnvVars, customModel, sshRemoteConfig; added AgentArgsConfig alias and widened agent-args function signatures.
Tests
src/__tests__/cli/services/agent-spawner.test.ts, src/__tests__/cli/commands/send.test.ts
Added tests for OpenCode and Droid detection/spawn scenarios; extended mocks (storage, child_process, SSH wrapper); updated spawnAgent arity expectations in send tests.
Misc / Manifest
tsconfig.json, other small files`
Large internal refactor within agent-spawner (utilities, caching, streaming) and scattered small adjustments across the diff.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as CLI Command
  participant Batch as Batch Processor
  participant Spawner as Agent Spawner
  participant AgentCLI as External Agent CLI
  participant Parser as Output Parser

  User->>CLI: run/send (agent id, session, optional overrides)
  CLI->>Batch: enqueue/execute task (includes spawnOverrides)
  Batch->>Spawner: spawnAgent(toolType, cwd, prompt, sessionId, spawnOverrides)
  Spawner->>Spawner: detectXxx(customPath?, sshRemoteConfig?) -> resolve command/env
  Spawner->>AgentCLI: invoke agent CLI (possibly via SSH) with args/env
  AgentCLI-->>Spawner: streaming output
  Spawner->>Parser: parse streaming output -> AgentResult
  Spawner-->>Batch: return AgentResult
  Batch-->>CLI: present/summarize result
  CLI-->>User: output (JSON or human)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.74% 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 'Enable OpenCode and Droid in Maestro CLI' is specific and directly describes the main objective of the changeset—adding support for two new agent types to the CLI.

✏️ 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.

@greptile-apps
Copy link

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR extends the Maestro CLI to support two new agent types — OpenCode and Factory Droid — alongside the existing claude-code and codex agents. It adds CLI detection (detectOpenCode/detectDroid), spawn functions (spawnOpenCodeAgent/spawnFactoryDroidAgent), and session-level override plumbing (customPath, customArgs, customEnvVars, customModel) to SessionInfo, with corresponding documentation updates.

Critical finding:

  • Overrides silently dropped for existing agents — The spawnAgent dispatcher forwards the new overrides parameter to spawnOpenCodeAgent and spawnFactoryDroidAgent, but not to spawnClaudeAgent or spawnCodexAgent. Both of those functions are called without overrides (lines 887, 891), meaning any customArgs, customEnvVars, or customModel configured for a Claude Code or Codex session will be silently discarded in CLI mode, even though send.ts and batch-processor.ts now construct and pass those overrides. This is a functional regression for existing agent types.

The new OpenCode and Factory Droid detection and spawn logic is well-structured, but the dispatcher leaves the two pre-existing agent types without override support.

Confidence Score: 2/5

  • Not safe to merge — overrides for existing agent types are silently dropped, causing a functional regression for claude-code/codex sessions with custom configuration.
  • The PR adds critical infrastructure (custom model, custom CLI args, custom env vars) to SessionInfo and plumbs it through send.ts and batch-processor.ts, but the spawnAgent dispatcher fails to forward the overrides parameter to the two pre-existing agent types (spawnClaudeAgent and spawnCodexAgent). This means any custom configuration for Claude Code or Codex sessions will be silently ignored in CLI mode, which is a clear functional regression. The new OpenCode and Factory Droid logic is well-structured, but the dispatcher is incomplete. This must be fixed before merge.
  • src/cli/services/agent-spawner.ts (spawnAgent dispatcher and claude-code/codex spawn functions)

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (send / run-playbook)
    participant BP as batch-processor
    participant SA as spawnAgent
    participant Detect as detect*(customPath?)
    participant Spawn as spawnXxxAgent
    participant Resolve as resolveAgentInvocation
    participant Storage as getAgentConfigValues

    CLI->>Detect: detectClaude / detectCodex / detectOpenCode / detectDroid
    Detect-->>CLI: { available, path }

    CLI->>SA: spawnAgent(toolType, cwd, prompt, sessionId, overrides)
    BP->>SA: spawnAgent(toolType, cwd, prompt, undefined, spawnOverrides)

    alt toolType = claude-code or codex
        SA->>Spawn: spawnClaudeAgent / spawnCodexAgent(cwd, prompt, sessionId)
        Note over SA,Spawn: ⚠️ overrides NOT forwarded — custom model/args/env silently dropped
    else toolType = opencode or factory-droid
        SA->>Spawn: spawnOpenCodeAgent / spawnFactoryDroidAgent(cwd, prompt, sessionId, overrides)
        Spawn->>Resolve: resolveAgentInvocation(toolType, cwd, prompt, sessionId, overrides)
        Resolve->>Storage: getAgentConfigValues(toolType)
        Storage-->>Resolve: agentConfigValues
        Resolve-->>Spawn: { args, env, contextWindow }
    end

    Spawn-->>SA: AgentResult
    SA-->>CLI: AgentResult
Loading

Last reviewed commit: 76ad2d3

Comment on lines 886 to +900
@@ -490,6 +891,14 @@ export async function spawnAgent(
return spawnClaudeAgent(cwd, prompt, agentSessionId);
}

if (toolType === 'opencode') {
return spawnOpenCodeAgent(cwd, prompt, agentSessionId, overrides);
}

if (toolType === 'factory-droid') {
return spawnFactoryDroidAgent(cwd, prompt, agentSessionId, overrides);
}
Copy link

Choose a reason for hiding this comment

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

overrides parameter not forwarded to claude-code and codex agents

The overrides parameter (carrying customPath, customArgs, customEnvVars, and customModel) is passed into spawnAgent from both send.ts and batch-processor.ts, but it is never forwarded to spawnCodexAgent (line 887) or spawnClaudeAgent (line 891).

Currently, both of those functions have hardcoded argument lists and don't accept an overrides parameter:

  • spawnClaudeAgent(cwd: string, prompt: string, agentSessionId?: string) — no overrides
  • spawnCodexAgent(cwd: string, prompt: string, agentSessionId?: string) — no overrides

In contrast, the new agent functions do correctly accept and use overrides:

  • spawnOpenCodeAgent(..., overrides?: AgentSpawnOverrides)
  • spawnFactoryDroidAgent(..., overrides?: AgentSpawnOverrides)

As a result, any custom model, custom CLI args, or custom environment variables configured for a Claude Code or Codex session are silently discarded in CLI mode, even though the PR adds those fields to SessionInfo and plumbs them through to spawnAgent.

To fix this consistently, spawnClaudeAgent and spawnCodexAgent should either:

  1. Accept and apply an overrides parameter (matching the new agents), or
  2. Be refactored to use resolveAgentInvocation the same way the new agents do

Example fix:

if (toolType === 'codex') {
    return spawnCodexAgent(cwd, prompt, agentSessionId, overrides);
}

if (toolType === 'claude-code') {
    return spawnClaudeAgent(cwd, prompt, agentSessionId, overrides);
}

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.

Caution

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

⚠️ Outside diff range comments (1)
docs/cli.md (1)

244-244: ⚠️ Potential issue | 🟡 Minor

Requirements section is incomplete - missing Factory Droid.

The requirements mention "Claude Code, Codex, or OpenCode" but omit Factory Droid, which is now a supported agent type according to line 80.

📝 Suggested fix
-- At least one AI agent CLI must be installed and in PATH (Claude Code, Codex, or OpenCode)
+- At least one AI agent CLI must be installed and in PATH (Claude Code, Codex, OpenCode, or Factory Droid)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/cli.md` at line 244, The requirements line that currently reads "At
least one AI agent CLI must be installed and in PATH (Claude Code, Codex, or
OpenCode)" needs to include the newly supported agent "Factory Droid"; update
that sentence in docs/cli.md to list "Factory Droid" alongside Claude Code,
Codex, and OpenCode so the Requirements section matches the supported agents
referenced elsewhere (see the agent list around line 80).
🧹 Nitpick comments (1)
src/cli/services/agent-spawner.ts (1)

886-900: Add clarifying comment explaining intentional override parameter handling.

The overrides parameter is intentionally omitted from spawnCodexAgent and spawnClaudeAgent calls. The implementations already document this design: the CLI agent spawner lacks access to the settings store with global shell env vars and relies solely on the environment Maestro is running in (global shell vars are primarily used by the desktop app). However, a comment at the call site (lines 886–892) explaining why overrides are selectively passed would improve code clarity for future maintainers.

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

In `@src/cli/services/agent-spawner.ts` around lines 886 - 900, Add a brief
clarifying comment above the conditional block in agent-spawner.ts (around the
spawnCodexAgent, spawnClaudeAgent, spawnOpenCodeAgent, spawnFactoryDroidAgent
calls) explaining that the omissions of the overrides parameter for
spawnCodexAgent and spawnClaudeAgent are intentional because those agents rely
only on the environment Maestro is running in and do not have access to the CLI
settings store/global shell env vars, whereas spawnOpenCodeAgent and
spawnFactoryDroidAgent intentionally accept overrides from the CLI; keep the
comment concise and reference the difference in env/source of settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@docs/cli.md`:
- Line 244: The requirements line that currently reads "At least one AI agent
CLI must be installed and in PATH (Claude Code, Codex, or OpenCode)" needs to
include the newly supported agent "Factory Droid"; update that sentence in
docs/cli.md to list "Factory Droid" alongside Claude Code, Codex, and OpenCode
so the Requirements section matches the supported agents referenced elsewhere
(see the agent list around line 80).

---

Nitpick comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 886-900: Add a brief clarifying comment above the conditional
block in agent-spawner.ts (around the spawnCodexAgent, spawnClaudeAgent,
spawnOpenCodeAgent, spawnFactoryDroidAgent calls) explaining that the omissions
of the overrides parameter for spawnCodexAgent and spawnClaudeAgent are
intentional because those agents rely only on the environment Maestro is running
in and do not have access to the CLI settings store/global shell env vars,
whereas spawnOpenCodeAgent and spawnFactoryDroidAgent intentionally accept
overrides from the CLI; keep the comment concise and reference the difference in
env/source of settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bc08f30-fd9e-42a3-beb6-b3ee498be836

📥 Commits

Reviewing files that changed from the base of the PR and between 3b9a142 and 76ad2d3.

📒 Files selected for processing (7)
  • docs/cli.md
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/shared/types.ts

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: 1

🧹 Nitpick comments (1)
src/cli/services/agent-spawner.ts (1)

808-916: Consider extracting common spawn logic to reduce duplication.

spawnFactoryDroidAgent and spawnOpenCodeAgent are nearly identical (~100 lines each), differing only in:

  • Tool type ('factory-droid' vs 'opencode')
  • Command getter (getDroidCommand vs getOpenCodeCommand)
  • Parser instantiation (FactoryDroidOutputParser vs OpenCodeOutputParser)
  • Error message text

This is a candidate for extraction into a generic helper, though the existing Claude/Codex spawn functions follow a similar separated pattern. Deferring is acceptable if consistency with the existing approach is preferred.

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

In `@src/cli/services/agent-spawner.ts` around lines 808 - 916, The two functions
spawnFactoryDroidAgent and spawnOpenCodeAgent duplicate nearly identical
spawn/parse/resolve logic; refactor by extracting a generic spawnAgent helper
that accepts parameters for toolType (passed to resolveAgentInvocation), command
getter (getDroidCommand/getOpenCodeCommand), parser constructor
(FactoryDroidOutputParser/OpenCodeOutputParser), and an error message template;
have spawnFactoryDroidAgent and spawnOpenCodeAgent call this helper (keeping
resolveAgentInvocation, child spawn, stdout/stderr handling, merging usage via
mergeUsageStats, and the close/error handlers centralized) so only the differing
symbols (tool type string, command getter, parser class, and error text) remain
in the small wrapper functions.
🤖 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/cli/services/agent-spawner.ts`:
- Around line 258-260: Cached path returns a hardcoded source 'settings' even
when it was discovered via PATH; update the detection functions (detectOpenCode,
detectClaude, detectCodex, detectDroid) to preserve or correct the source when
returning a cached value by either (A) changing the cache to store an object
{path, source} and reading that here, or (B) if you prefer a minimal change,
return source: 'path' whenever the cached value (e.g., cachedOpenCodePath) is
used; update the cache write sites in the corresponding detection functions to
save the correct source if you choose option A.

---

Nitpick comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 808-916: The two functions spawnFactoryDroidAgent and
spawnOpenCodeAgent duplicate nearly identical spawn/parse/resolve logic;
refactor by extracting a generic spawnAgent helper that accepts parameters for
toolType (passed to resolveAgentInvocation), command getter
(getDroidCommand/getOpenCodeCommand), parser constructor
(FactoryDroidOutputParser/OpenCodeOutputParser), and an error message template;
have spawnFactoryDroidAgent and spawnOpenCodeAgent call this helper (keeping
resolveAgentInvocation, child spawn, stdout/stderr handling, merging usage via
mergeUsageStats, and the close/error handlers centralized) so only the differing
symbols (tool type string, command getter, parser class, and error text) remain
in the small wrapper functions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 743a3e1f-a5b5-492e-bf62-7a0fe9c18d34

📥 Commits

Reviewing files that changed from the base of the PR and between 76ad2d3 and a4d1f0c.

📒 Files selected for processing (2)
  • docs/cli.md
  • src/cli/services/agent-spawner.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/cli.md

- Track cached CLI path source and unify OpenCode/Droid spawn flow.

- Only pass send overrides when present; adjust CLI tests and agent-args typing.

Session: 019cbff3-a9dd-71f1-b326-7c9fb965b50e
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.

🧹 Nitpick comments (1)
src/__tests__/cli/services/agent-spawner.test.ts (1)

72-76: Mock wiring looks good, but test coverage for new agents is missing.

The mockGetAgentConfigValues is correctly wired and defaults to an empty object. However, the test suite doesn't include tests for:

  • detectOpenCode / detectDroid detection functions
  • spawnAgent with 'opencode' or 'factory-droid' tool types

Consider adding test cases for the new agent paths to ensure parity with the existing Claude/Codex coverage.

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

In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 72 - 76, Add
tests exercising the new agent detection and spawn paths: extend the test file
to stub/mock getAgentConfigValues via mockGetAgentConfigValues to return values
that trigger detectOpenCode and detectDroid, then assert detectOpenCode and
detectDroid behavior directly; add spawnAgent test cases that call spawnAgent
with tool types 'opencode' and 'factory-droid' and verify the spawned agent
type/behavior (similar assertions to existing Claude/Codex tests). Locate usages
of mockGetAgentConfigValues, the detectOpenCode and detectDroid functions, and
the spawnAgent function in the test suite and mirror the existing Claude/Codex
test patterns to cover these new branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 72-76: Add tests exercising the new agent detection and spawn
paths: extend the test file to stub/mock getAgentConfigValues via
mockGetAgentConfigValues to return values that trigger detectOpenCode and
detectDroid, then assert detectOpenCode and detectDroid behavior directly; add
spawnAgent test cases that call spawnAgent with tool types 'opencode' and
'factory-droid' and verify the spawned agent type/behavior (similar assertions
to existing Claude/Codex tests). Locate usages of mockGetAgentConfigValues, the
detectOpenCode and detectDroid functions, and the spawnAgent function in the
test suite and mirror the existing Claude/Codex test patterns to cover these new
branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1a1d6ec-329b-43bb-838f-c893610e86c6

📥 Commits

Reviewing files that changed from the base of the PR and between a4d1f0c and f4daab6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/main/utils/agent-args.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/cli/commands/send.ts

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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/cli/services/agent-spawner.ts (1)

117-120: ⚠️ Potential issue | 🟠 Major

Normalize Windows where output before taking the first match.

When where returns multiple matches, stdout.trim().split('\n')[0] leaves a trailing \r on Windows. That cached path is then invalid for isExecutable()/spawn(). Split on \r?\n or trim each line before selecting the first entry.

Suggested fix
-			if (code === 0 && stdout.trim()) {
-				resolve(stdout.trim().split('\n')[0]); // First match
+			if (code === 0 && stdout.trim()) {
+				const firstMatch = stdout
+					.split(/\r?\n/)
+					.map((line) => line.trim())
+					.find(Boolean);
+				resolve(firstMatch);
 			} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/services/agent-spawner.ts` around lines 117 - 120, The code in the
proc.on('close', (code) => { ... }) handler resolves the executable path using
stdout.trim().split('\n')[0], which can leave a trailing '\r' on Windows; update
the resolution to normalize line endings by splitting on /\r?\n/ (or splitting
on '\n' and trimming each line) and then take the first non-empty trimmed entry
before calling resolve so the cached path is valid for isExecutable() and
spawn(); adjust the logic in the function that spawns the child process (the
proc.on('close' callback) to use the normalized/trimmed firstMatch.
🤖 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/cli/services/agent-spawner.ts`:
- Around line 156-164: The code is caching or masking one-off customPathOverride
into shared cachedClaudePath and falling back to PATH detection; instead, in the
branches that handle customPathOverride (the block using
isExecutable(customPathOverride), writing cachedClaudePath, and returning
available true), stop mutating cachedClaudePath and return the result for that
call only (return { available: true, path: customPathOverride, source:
'settings' } directly). If the override is not executable, do not fall back to
PATH detection—return { available: false, path: customPathOverride, source:
'settings' } (or otherwise indicate unavailable) rather than logging and
continuing; apply the same change to the similar blocks around the other ranges
(the blocks referenced at 209-217, 258-266, 307-315) so overrides remain
per-call and do not leak into cachedClaudePath.
- Around line 578-605: The spawnStreamingAgent implementation always calls
spawn(...) locally; update it to support SSH by (1) getting sshRemoteConfig from
resolveAgentInvocation (or from overrides) alongside args/env/contextWindow, (2)
if sshRemoteConfig is present, call wrapSpawnWithSsh(...) from
ssh-spawn-wrapper.ts instead of directly calling spawn, passing the command
(from commandGetter(overrides?.customPath)), args, options (SpawnOptions), and
the SSH store adapter, and (3) ensure the same returned child/process interface
is used downstream so streaming logic remains unchanged (update references to
spawnStreamingAgent/commandGetter/resolveAgentInvocation/sshRemoteConfig/wrapSpawnWithSsh
accordingly).

---

Outside diff comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 117-120: The code in the proc.on('close', (code) => { ... })
handler resolves the executable path using stdout.trim().split('\n')[0], which
can leave a trailing '\r' on Windows; update the resolution to normalize line
endings by splitting on /\r?\n/ (or splitting on '\n' and trimming each line)
and then take the first non-empty trimmed entry before calling resolve so the
cached path is valid for isExecutable() and spawn(); adjust the logic in the
function that spawns the child process (the proc.on('close' callback) to use the
normalized/trimmed firstMatch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55eaf705-0358-4464-9dc3-a46a2cf8f0f3

📥 Commits

Reviewing files that changed from the base of the PR and between f4daab6 and ac67a5e.

📒 Files selected for processing (1)
  • src/cli/services/agent-spawner.ts

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

@coderabbitai re-review please

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@chr1syy Sure! I'll kick off a fresh review of PR #520 now.

✅ Actions performed

Full review triggered.

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: 2

🧹 Nitpick comments (2)
src/cli/services/agent-spawner.ts (2)

256-271: Cache-match check is a good optimization, but override still leaks.

The check at lines 257-263 avoids redundant isExecutable calls when the override matches the cached path. However, valid overrides are still written to the global cache (line 265), and invalid overrides fall through to PATH detection rather than returning unavailable. This is the same issue pattern as detectClaude/detectCodex, though mitigated by the match-check.

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

In `@src/cli/services/agent-spawner.ts` around lines 256 - 271, The override
handling leaks invalid overrides into PATH detection and writes to the global
cache regardless; update the logic in the override branch around
cachedOpenCodePath and customPathOverride so you only set cachedOpenCodePath
(and return available=true) when await isExecutable(customPathOverride)
succeeds, and if isExecutable returns false immediately return { available:
false, path: customPathOverride, source: 'settings' } instead of falling through
— mirror the safe behavior used in detectClaude/detectCodex and ensure
cachedOpenCodePath is never assigned for non-executable overrides.

569-585: Consider stricter typing for parser interface.

The StreamJsonParser type uses any for parseJsonLine return and method parameters. While the concrete parsers (OpenCodeOutputParser, FactoryDroidOutputParser) likely have proper internal typing, a shared event type or discriminated union would improve type safety at the boundary.

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

In `@src/cli/services/agent-spawner.ts` around lines 569 - 585, The
StreamJsonParser interface currently uses any for parseJsonLine and its event
parameter; define a shared Event type (or discriminated union) and replace all
any occurrences in StreamJsonParser with that concrete type so callers get
proper typing. Update the signatures of parseJsonLine, extractSessionId,
isResultMessage, and extractUsage to use the new Event type (or union) and
adjust OpenCodeOutputParser and FactoryDroidOutputParser implementations to
return/accept that Event type (or narrow variants) so the parser boundary is
strongly typed and compatible with downstream consumers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/cli.md`:
- Around line 244-245: Update the requirements text in the CLI docs where the
bullet currently reads that an AI agent CLI "must be installed and in PATH" to
reflect that the CLI supports customPath overrides; change the wording to allow
binaries that are either in PATH or specified via the customPath configuration
(reference the phrase "installed and in PATH" in the current bullets) and keep
the second bullet about Maestro config files intact. Ensure the new sentence
mentions the supported agent names (Claude Code, Codex, OpenCode, Factory Droid)
and explicitly references the customPath option so readers know non-PATH
installs are supported.

In `@src/shared/types.ts`:
- Around line 31-34: SessionInfo currently omits SSH configuration causing
CLI-only agent paths (send.ts, batch-processor.ts, run-playbook.ts) to pass
AgentSpawnOverrides (customPath/customArgs/customEnvVars/customModel) to
spawnAgent() without any SSH wrapping, so add ssh support to the CLI surface:
extend SessionInfo (or AgentSpawnOverrides) to include AgentSshRemoteConfig
(sshRemoteConfig?: AgentSshRemoteConfig), update the detect* functions to check
sshRemoteConfig in addition to customPath, and change the CLI spawn callers that
pass AgentSpawnOverrides to call the same wrapSpawnWithSsh() logic (or wrap
their spawnAgent() invocation when sshRemoteConfig is present) so
SessionOverrides/AgentSshRemoteConfig are honored during preflight and spawn
just like group-chat-agent.ts does.

---

Nitpick comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 256-271: The override handling leaks invalid overrides into PATH
detection and writes to the global cache regardless; update the logic in the
override branch around cachedOpenCodePath and customPathOverride so you only set
cachedOpenCodePath (and return available=true) when await
isExecutable(customPathOverride) succeeds, and if isExecutable returns false
immediately return { available: false, path: customPathOverride, source:
'settings' } instead of falling through — mirror the safe behavior used in
detectClaude/detectCodex and ensure cachedOpenCodePath is never assigned for
non-executable overrides.
- Around line 569-585: The StreamJsonParser interface currently uses any for
parseJsonLine and its event parameter; define a shared Event type (or
discriminated union) and replace all any occurrences in StreamJsonParser with
that concrete type so callers get proper typing. Update the signatures of
parseJsonLine, extractSessionId, isResultMessage, and extractUsage to use the
new Event type (or union) and adjust OpenCodeOutputParser and
FactoryDroidOutputParser implementations to return/accept that Event type (or
narrow variants) so the parser boundary is strongly typed and compatible with
downstream consumers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c0e94b5-b2e6-4e92-9a02-b5f919d474f7

📥 Commits

Reviewing files that changed from the base of the PR and between 4b596b7 and a4213cb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • docs/cli.md
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/main/utils/agent-args.ts
  • src/shared/types.ts

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

@coderabbitai re-review

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@chr1syy Sure! I'll kick off another fresh review of PR #520 right away.

✅ Actions performed

Full review triggered.

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: 1

♻️ Duplicate comments (1)
src/cli/services/agent-spawner.ts (1)

172-189: ⚠️ Potential issue | 🟠 Major

Keep customPathOverride per-call.

These branches still cache a one-off override and still mask an invalid override by falling back to PATH detection. That leaks one session's override into later detections, and it can make preflight return available: true even though the later spawn will still use the invalid override and fail with ENOENT.

Suggested direction
 if (customPathOverride) {
 	if (await isExecutable(customPathOverride)) {
-		cachedOpenCodePath = { path: customPathOverride, source: 'settings' };
 		return { available: true, path: customPathOverride, source: 'settings' };
 	}
-	console.error(
-		`Warning: Custom OpenCode path "${customPathOverride}" is not executable, falling back to PATH detection`
-	);
+	console.error(`Warning: Custom OpenCode path "${customPathOverride}" is not executable`);
+	return { available: false, path: customPathOverride, source: 'settings' };
 }

Apply the same pattern to Claude, Codex, and Droid: do not write override values into the shared cache, and do not fall through to PATH detection for an invalid override.

Also applies to: 229-245, 282-305, 342-365

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

In `@src/cli/services/agent-spawner.ts` around lines 172 - 189, The code is
caching customPathOverride into cachedClaudePath and falling back to PATH
detection when the override is invalid, which leaks a one-off override across
calls and masks invalid overrides; change the logic in the Claude detection
block (and analogously in the Codex and Droid blocks) so that when
customPathOverride is provided you validate it with isExecutable and return its
result per-call without writing it into cachedClaudePath (or
cachedCodexPath/cachedDroidPath), and if the override is invalid return
available: false (do not fall through to PATH detection or mutate the shared
cache); keep caching only results derived from PATH detection.
🧹 Nitpick comments (1)
src/cli/commands/send.ts (1)

146-165: Consider simplifying the overrides handling.

The current pattern works correctly, but could be simplified since spawnAgent handles undefined override fields gracefully:

♻️ Optional simplification
-	const hasOverrides =
-		agent.customPath !== undefined ||
-		agent.customArgs !== undefined ||
-		agent.customEnvVars !== undefined ||
-		agent.customModel !== undefined ||
-		agent.sshRemoteConfig !== undefined;
-	const overrides = hasOverrides
-		? {
-				customPath: agent.customPath,
-				customArgs: agent.customArgs,
-				customEnvVars: agent.customEnvVars,
-				customModel: agent.customModel,
-				sshRemoteConfig: agent.sshRemoteConfig,
-			}
-		: undefined;
-
-	// Spawn agent — spawnAgent handles --resume vs --session-id internally
-	const result = overrides
-		? await spawnAgent(agent.toolType, agent.cwd, message, options.session, overrides)
-		: await spawnAgent(agent.toolType, agent.cwd, message, options.session);
+	// Spawn agent — spawnAgent handles --resume vs --session-id internally
+	const result = await spawnAgent(agent.toolType, agent.cwd, message, options.session, {
+		customPath: agent.customPath,
+		customArgs: agent.customArgs,
+		customEnvVars: agent.customEnvVars,
+		customModel: agent.customModel,
+		sshRemoteConfig: agent.sshRemoteConfig,
+	});
🤖 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 146 - 165, Replace the
hasOverrides/ternary pattern with a single overrides object and always pass it
to spawnAgent: construct overrides = { customPath: agent.customPath, customArgs:
agent.customArgs, customEnvVars: agent.customEnvVars, customModel:
agent.customModel, sshRemoteConfig: agent.sshRemoteConfig } (no conditional) and
call spawnAgent(agent.toolType, agent.cwd, message, options.session, overrides);
this relies on spawnAgent to handle undefined fields and removes the unnecessary
hasOverrides/ternary logic.
🤖 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/cli/services/agent-spawner.ts`:
- Around line 476-497: When overrides.sshRemoteConfig is present, avoid passing
a locally-cached absolute binary path from getClaudeCommand (or other
get*Command calls) into wrapSpawnWithSsh; instead, if overrides.customPath is
not provided, set the pre-wrap spawn command to the agent's remote-resolvable
binary name (use agentDef.binaryName or the agent's canonical binary name) so
wrapSpawnWithSsh receives a path that will be resolved on the remote host.
Concretely: in spawnClaudeAgent / spawnStreamingAgent (and here where
spawnCommand is derived via getClaudeCommand) check overrides?.sshRemoteConfig
and overrides?.customPath before using get*Command; if sshRemoteConfig is true
and no customPath, replace spawnCommand with agentDef.binaryName (leave
spawnArgs, cwd, env handling and buildExpandedEnv usage intact) so the SSH
wrapper gets a remote-resolvable command.

---

Duplicate comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 172-189: The code is caching customPathOverride into
cachedClaudePath and falling back to PATH detection when the override is
invalid, which leaks a one-off override across calls and masks invalid
overrides; change the logic in the Claude detection block (and analogously in
the Codex and Droid blocks) so that when customPathOverride is provided you
validate it with isExecutable and return its result per-call without writing it
into cachedClaudePath (or cachedCodexPath/cachedDroidPath), and if the override
is invalid return available: false (do not fall through to PATH detection or
mutate the shared cache); keep caching only results derived from PATH detection.

---

Nitpick comments:
In `@src/cli/commands/send.ts`:
- Around line 146-165: Replace the hasOverrides/ternary pattern with a single
overrides object and always pass it to spawnAgent: construct overrides = {
customPath: agent.customPath, customArgs: agent.customArgs, customEnvVars:
agent.customEnvVars, customModel: agent.customModel, sshRemoteConfig:
agent.sshRemoteConfig } (no conditional) and call spawnAgent(agent.toolType,
agent.cwd, message, options.session, overrides); this relies on spawnAgent to
handle undefined fields and removes the unnecessary hasOverrides/ternary logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e404069-5b4d-4f70-8dfa-e20a020ffb4a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b596b7 and 333c2f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • docs/cli.md
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/main/utils/agent-args.ts
  • src/shared/types.ts

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

@coderabbitai re-review

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@chr1syy Sure! I'll kick off another fresh review of PR #520 right away.

✅ Actions performed

Full review triggered.

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: 2

♻️ Duplicate comments (2)
src/cli/services/agent-spawner.ts (2)

172-180: ⚠️ Potential issue | 🟠 Major

Keep one-off customPathOverride values out of the shared cache.

These branches still cache a per-session override and fall through to cached/PATH detection when the override is invalid. That lets availability checks return true even though the later spawn will still use the bad override, and it also leaks one session’s override into later detections.

Suggested fix pattern
 if (customPathOverride) {
 	if (await isExecutable(customPathOverride)) {
-		cachedClaudePath = { path: customPathOverride, source: 'settings' };
 		return { available: true, path: customPathOverride, source: 'settings' };
 	}
-	console.error(
-		`Warning: Custom Claude path "${customPathOverride}" is not executable, falling back to PATH detection`
-	);
+	return { available: false, path: customPathOverride, source: 'settings' };
 }

Apply the same pattern to detectCodex, detectOpenCode, and detectDroid.

Also applies to: 229-237, 282-297, 342-357

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

In `@src/cli/services/agent-spawner.ts` around lines 172 - 180, The
customPathOverride branch is storing per-session one-off overrides into the
shared cache (e.g., setting cachedClaudePath = { path: customPathOverride,
source: 'settings' }) which leaks into later detections and can wrongly report
availability; change the logic so that when customPathOverride is provided and
isExecutable(customPathOverride) succeeds you return the result ({ available:
true, path: customPathOverride, source: 'settings' }) without mutating the
shared cachedClaudePath, and when it fails do not write to the cache — just warn
and fall back to normal detection; apply the same non-caching pattern to the
equivalent branches in detectCodex, detectOpenCode, and detectDroid and only set
the shared cached* variables when the value is discovered via PATH detection or
other persistent discovery logic.

865-880: ⚠️ Potential issue | 🟠 Major

Use the remote-resolvable Codex binary name for SSH runs.

This branch still seeds wrapSpawnWithSsh() from getCodexCommand(). If Codex was previously detected locally, the cached absolute path can be forwarded to the remote host, which then tries to execute a path from the caller machine.

Minimal fix
-	let spawnCommand = getCodexCommand(overrides?.customPath);
+	let spawnCommand =
+		overrides?.sshRemoteConfig && !overrides?.customPath && agentDef?.binaryName
+			? agentDef.binaryName
+			: getCodexCommand(overrides?.customPath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/services/agent-spawner.ts` around lines 865 - 880, The SSH branch is
passing the local absolute path from getCodexCommand() to wrapSpawnWithSsh(),
causing remote hosts to try executing a local path; when
overrides.sshRemoteConfig is set, switch the command to the remote-resolvable
binary name (use agentDef?.binaryName or fallback to getCodexCommand()) before
calling wrapSpawnWithSsh so the remote executes the binary name, not a local
path; update the logic around spawnCommand/spawnArgs (the block containing
overrides.sshRemoteConfig, wrapSpawnWithSsh, getCodexCommand, and
agentDef?.binaryName) to set spawnCommand appropriately and then call
wrapSpawnWithSsh with that value.
🧹 Nitpick comments (2)
src/cli/commands/send.ts (2)

146-160: Consider filtering out undefined fields from overrides.

When hasOverrides is true, the object includes all fields even if only one is defined. This passes unnecessary undefined properties to spawnAgent.

♻️ Optional: Build overrides with only defined fields
-	const hasOverrides =
-		agent.customPath !== undefined ||
-		agent.customArgs !== undefined ||
-		agent.customEnvVars !== undefined ||
-		agent.customModel !== undefined ||
-		agent.sshRemoteConfig !== undefined;
-	const overrides = hasOverrides
-		? {
-				customPath: agent.customPath,
-				customArgs: agent.customArgs,
-				customEnvVars: agent.customEnvVars,
-				customModel: agent.customModel,
-				sshRemoteConfig: agent.sshRemoteConfig,
-			}
-		: undefined;
+	const overrides = Object.fromEntries(
+		Object.entries({
+			customPath: agent.customPath,
+			customArgs: agent.customArgs,
+			customEnvVars: agent.customEnvVars,
+			customModel: agent.customModel,
+			sshRemoteConfig: agent.sshRemoteConfig,
+		}).filter(([, v]) => v !== undefined)
+	);
+	const hasOverrides = Object.keys(overrides).length > 0;
🤖 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 146 - 160, The overrides object
currently includes all keys even when their values are undefined (built under
hasOverrides), which passes unnecessary undefined props to spawnAgent; change
the construction of overrides (referencing hasOverrides, overrides, and
agent.customPath / customArgs / customEnvVars / customModel / sshRemoteConfig)
so it only contains keys whose values are !== undefined—e.g. create an empty
object and conditionally assign each property if defined or build then filter
entries (Object.entries/...filter) and return undefined when no keys remain—so
spawnAgent receives only present overrides.

163-165: Conditional call can be simplified.

Since spawnAgent accepts undefined for the optional overrides parameter, the ternary is redundant.

♻️ Optional: Simplify to single call
-	const result = overrides
-		? await spawnAgent(agent.toolType, agent.cwd, message, options.session, overrides)
-		: await spawnAgent(agent.toolType, agent.cwd, message, options.session);
+	const result = await spawnAgent(agent.toolType, agent.cwd, message, options.session, overrides);
🤖 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 163 - 165, The ternary that
conditionally calls spawnAgent is redundant because spawnAgent accepts an
optional overrides parameter; replace the conditional expression in the
assignment to result with a single call to spawnAgent(agent.toolType, agent.cwd,
message, options.session, overrides) so overrides can be passed directly (it may
be undefined) and remove the duplicate call paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/cli.md`:
- Line 244: Update the sentence that currently requires an agent CLI in PATH or
via customPath to also mention SSH-only setups by referencing the
sshRemoteConfig option; specifically, change the line that says "At least one AI
agent CLI must be installed in PATH or set via customPath (Claude Code, Codex,
OpenCode, or Factory Droid)" to include "or configured via sshRemoteConfig" (and
optionally a brief note that the CLI can run through an SSH remote host), so
users know SSH-only agent configurations are supported.

In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 87-93: The mock factory for '../../../cli/services/storage' is
missing an export for readSshRemotes which causes module evaluation to fail;
update the vi.mock call that defines mockGetAgentCustomPath and
mockGetAgentConfigValues to also export readSshRemotes (e.g., add a
mockReadSshRemotes function or vi.fn that returns a sensible default like an
empty array/object) so imports of readSshRemotes in agent-spawner.ts resolve
during tests; keep the existing mockGetAgentCustomPath and
mockGetAgentConfigValues names and behavior and add the new mock export inside
the same mock factory.

---

Duplicate comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 172-180: The customPathOverride branch is storing per-session
one-off overrides into the shared cache (e.g., setting cachedClaudePath = {
path: customPathOverride, source: 'settings' }) which leaks into later
detections and can wrongly report availability; change the logic so that when
customPathOverride is provided and isExecutable(customPathOverride) succeeds you
return the result ({ available: true, path: customPathOverride, source:
'settings' }) without mutating the shared cachedClaudePath, and when it fails do
not write to the cache — just warn and fall back to normal detection; apply the
same non-caching pattern to the equivalent branches in detectCodex,
detectOpenCode, and detectDroid and only set the shared cached* variables when
the value is discovered via PATH detection or other persistent discovery logic.
- Around line 865-880: The SSH branch is passing the local absolute path from
getCodexCommand() to wrapSpawnWithSsh(), causing remote hosts to try executing a
local path; when overrides.sshRemoteConfig is set, switch the command to the
remote-resolvable binary name (use agentDef?.binaryName or fallback to
getCodexCommand()) before calling wrapSpawnWithSsh so the remote executes the
binary name, not a local path; update the logic around spawnCommand/spawnArgs
(the block containing overrides.sshRemoteConfig, wrapSpawnWithSsh,
getCodexCommand, and agentDef?.binaryName) to set spawnCommand appropriately and
then call wrapSpawnWithSsh with that value.

---

Nitpick comments:
In `@src/cli/commands/send.ts`:
- Around line 146-160: The overrides object currently includes all keys even
when their values are undefined (built under hasOverrides), which passes
unnecessary undefined props to spawnAgent; change the construction of overrides
(referencing hasOverrides, overrides, and agent.customPath / customArgs /
customEnvVars / customModel / sshRemoteConfig) so it only contains keys whose
values are !== undefined—e.g. create an empty object and conditionally assign
each property if defined or build then filter entries (Object.entries/...filter)
and return undefined when no keys remain—so spawnAgent receives only present
overrides.
- Around line 163-165: The ternary that conditionally calls spawnAgent is
redundant because spawnAgent accepts an optional overrides parameter; replace
the conditional expression in the assignment to result with a single call to
spawnAgent(agent.toolType, agent.cwd, message, options.session, overrides) so
overrides can be passed directly (it may be undefined) and remove the duplicate
call paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cc96888-8054-48ce-815a-a324aef7f12d

📥 Commits

Reviewing files that changed from the base of the PR and between 4b596b7 and 2705525.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • docs/cli.md
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/main/utils/agent-args.ts
  • src/shared/types.ts

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

@coderabbitai re-review

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@chr1syy Sure! I'll kick off another fresh review of PR #520 right away.

✅ Actions performed

Full review triggered.

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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/cli/commands/send.ts (1)

109-170: ⚠️ Potential issue | 🟠 Major

Gate SSH overrides on enabled.

AgentSshRemoteConfig is a config object, not a truthy switch. This path forwards any defined sshRemoteConfig into both detection and spawnAgent(), but spawnAgent() currently flips to SSH mode on presence alone. A stored { enabled: false, remoteId: null } will therefore break a local send call by trying to wrap it for SSH anyway.

Normalize the SSH config once and reuse it
+	const sshRemoteConfig = agent.sshRemoteConfig?.enabled ? agent.sshRemoteConfig : undefined;
+
 	// Verify agent CLI is available
 	if (agent.toolType === 'claude-code') {
-		const claude = await detectClaude(agent.customPath, agent.sshRemoteConfig);
+		const claude = await detectClaude(agent.customPath, sshRemoteConfig);
 		if (!claude.available) {
 			emitErrorJson(
 				'Claude Code CLI not found. Install with: npm install -g `@anthropic-ai/claude-code`',
@@
 	} else if (agent.toolType === 'codex') {
-		const codex = await detectCodex(agent.customPath, agent.sshRemoteConfig);
+		const codex = await detectCodex(agent.customPath, sshRemoteConfig);
@@
 	} else if (agent.toolType === 'opencode') {
-		const opencode = await detectOpenCode(agent.customPath, agent.sshRemoteConfig);
+		const opencode = await detectOpenCode(agent.customPath, sshRemoteConfig);
@@
 	} else if (agent.toolType === 'factory-droid') {
-		const droid = await detectDroid(agent.customPath, agent.sshRemoteConfig);
+		const droid = await detectDroid(agent.customPath, sshRemoteConfig);
@@
-		if (agent.sshRemoteConfig !== undefined) {
-			next.sshRemoteConfig = agent.sshRemoteConfig;
+		if (sshRemoteConfig) {
+			next.sshRemoteConfig = sshRemoteConfig;
 		}
🤖 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 109 - 170, The SSH config object
(agent.sshRemoteConfig) is treated as a truthy switch but may have enabled:
false; normalize it once (e.g., const ssh = agent.sshRemoteConfig &&
agent.sshRemoteConfig.enabled ? agent.sshRemoteConfig : undefined) and use that
normalized ssh value for all calls: pass ssh into
detectClaude/detectCodex/detectOpenCode/detectDroid and set next.sshRemoteConfig
= ssh when building overrides, and finally pass the same ssh-aware overrides
into spawnAgent; this ensures SSH mode is only used when
AgentSshRemoteConfig.enabled is true.
🧹 Nitpick comments (1)
src/__tests__/cli/services/agent-spawner.test.ts (1)

702-794: Add SSH coverage for the new detector paths.

These cases only exercise local PATH lookup and cache reuse. The production change also threads sshRemoteConfig through detectOpenCode() and detectDroid(), so a remote-only regression would still pass this suite.

Based on learnings: When implementing features that spawn agent processes, support SSH remote execution by checking for sshRemoteConfig, using wrapSpawnWithSsh() from ssh-spawn-wrapper.ts, and passing the SSH store adapter.

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

In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 702 - 794,
Tests for detectOpenCode and detectDroid only cover local PATH detection and
cache behavior and miss exercising SSH execution paths; update the tests to also
supply an sshRemoteConfig to the detector calls and assert that the code uses
wrapSpawnWithSsh by ensuring the spawn path is wrapped and the SSH store adapter
is passed into the spawn wrapper. Specifically, in the detectOpenCode and
detectDroid test blocks, add cases that call freshDetectOpenCode(overridePath,
sshRemoteConfig) and freshDetectDroid(overridePath, sshRemoteConfig) (or
otherwise import/construct sshRemoteConfig), stub/wrap wrapSpawnWithSsh from
ssh-spawn-wrapper.ts to return a spawn function, and verify the wrapped spawn
was invoked so remote-only behavior is covered.
🤖 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/cli/services/agent-spawner.ts`:
- Around line 847-869: The SSH branch currently overwrites spawnCommand with
agentDef?.binaryName ignoring overrides?.customPath; change it to preserve a
provided custom path the same way spawnClaudeAgent and spawnStreamingAgent do by
only falling back to agentDef?.binaryName when !overrides?.customPath (e.g., set
spawnCommand = !overrides?.customPath ? agentDef?.binaryName ??
getCodexCommand() : getCodexCommand(overrides?.customPath)), then pass that
spawnCommand into wrapSpawnWithSsh and continue to set spawnArgs, spawnCwd and
spawnEnv from the sshWrapped result.

In `@src/cli/services/batch-processor.ts`:
- Around line 442-455: The code forwards session.sshRemoteConfig into spawnAgent
via spawnOverrides which enables SSH-backed Auto Run while the processor still
does local document I/O (see the remainingTasks / while (remainingTasks > 0)
loop), causing stuck replay on remote sessions; fix by preventing SSH-backed
runs here: if session.sshRemoteConfig is set, do not include sshRemoteConfig in
spawnOverrides (or skip/throw before calling spawnAgent), or alternatively move
the playbook document read/write/recount logic (the code that computes
remainingTasks and updates playbook docs) into the same remote execution context
before passing sshRemoteConfig to spawnAgent so local files aren’t used for
remote sessions. Ensure the change references spawnOverrides, spawnAgent,
remainingTasks and the while loop so SSH runs are blocked until the document
flow is remote-aware.

---

Outside diff comments:
In `@src/cli/commands/send.ts`:
- Around line 109-170: The SSH config object (agent.sshRemoteConfig) is treated
as a truthy switch but may have enabled: false; normalize it once (e.g., const
ssh = agent.sshRemoteConfig && agent.sshRemoteConfig.enabled ?
agent.sshRemoteConfig : undefined) and use that normalized ssh value for all
calls: pass ssh into detectClaude/detectCodex/detectOpenCode/detectDroid and set
next.sshRemoteConfig = ssh when building overrides, and finally pass the same
ssh-aware overrides into spawnAgent; this ensures SSH mode is only used when
AgentSshRemoteConfig.enabled is true.

---

Nitpick comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 702-794: Tests for detectOpenCode and detectDroid only cover local
PATH detection and cache behavior and miss exercising SSH execution paths;
update the tests to also supply an sshRemoteConfig to the detector calls and
assert that the code uses wrapSpawnWithSsh by ensuring the spawn path is wrapped
and the SSH store adapter is passed into the spawn wrapper. Specifically, in the
detectOpenCode and detectDroid test blocks, add cases that call
freshDetectOpenCode(overridePath, sshRemoteConfig) and
freshDetectDroid(overridePath, sshRemoteConfig) (or otherwise import/construct
sshRemoteConfig), stub/wrap wrapSpawnWithSsh from ssh-spawn-wrapper.ts to return
a spawn function, and verify the wrapped spawn was invoked so remote-only
behavior is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 619f4509-dd5b-4779-a5a3-8b55b142e994

📥 Commits

Reviewing files that changed from the base of the PR and between 4b596b7 and 7253114.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • docs/cli.md
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/main/utils/agent-args.ts
  • src/shared/types.ts

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

@coderabbitai re-review

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@chr1syy Sure! I'll kick off another fresh review of PR #520 right away.

✅ Actions performed

Full review triggered.

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: 2

♻️ Duplicate comments (2)
src/cli/services/agent-spawner.ts (1)

161-179: ⚠️ Potential issue | 🟠 Major

Don’t fall back after a bad per-session customPath.

These detector branches are the preflight for the same override later passed into spawnAgent(). If customPathOverride is invalid but the binary exists on PATH, detection reports available: true, and the later spawn still uses the bad override and fails with ENOENT. Return unavailable for that call instead of continuing to PATH lookup.

Also applies to: 217-235, 269-287, 321-339

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

In `@src/cli/services/agent-spawner.ts` around lines 161 - 179, The detector
functions (e.g., detectClaude) currently ignore a provided customPathOverride
when it is non-executable and continue to report available=true if a PATH binary
exists, which causes spawnAgent to later fail with the invalid override; change
the logic so that if customPathOverride is provided but is not executable the
detector returns available: false (and path/source not set) immediately instead
of falling back to PATH detection. Locate similar branches in the other
detectors noted (the blocks around lines 217-235, 269-287, 321-339) and apply
the same behavior so any invalid per-session customPathOverride causes the
detector to return unavailable rather than continuing to probe PATH or report
available:true. Ensure spawnAgent still receives the same override behavior
(i.e., detectors must not claim availability when a provided override is
invalid).
src/cli/services/batch-processor.ts (1)

448-469: ⚠️ Potential issue | 🟠 Major

Enabled SSH Auto Run is being silently dropped here.

if (!session.sshRemoteConfig?.enabled) omits the override for the only case that should use it, so SSH-configured playbooks just run locally with no warning. Since this loop still reads and writes documents on the local filesystem, it’s safer to fail fast when SSH is enabled than to ignore the session setting.

Safer guard
 				const spawnOverrides: {
 					customPath?: string;
 					customArgs?: string;
 					customEnvVars?: Record<string, string>;
 					customModel?: string;
 					sshRemoteConfig?: AgentSshRemoteConfig;
 				} = {
 					customPath: session.customPath,
 					customArgs: session.customArgs,
 					customEnvVars: session.customEnvVars,
 					customModel: session.customModel,
 				};
-				if (!session.sshRemoteConfig?.enabled) {
-					spawnOverrides.sshRemoteConfig = session.sshRemoteConfig;
-				}
+				if (session.sshRemoteConfig?.enabled) {
+					throw new Error(
+						'CLI Auto Run does not support sshRemoteConfig yet because playbook documents are managed on the local filesystem.'
+					);
+				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/services/batch-processor.ts` around lines 448 - 469, The current
guard incorrectly negates the SSH check and omits passing
session.sshRemoteConfig into spawnOverrides for SSH-enabled sessions; update the
condition around spawnOverrides.sshRemoteConfig so that when
session.sshRemoteConfig?.enabled is true you assign
spawnOverrides.sshRemoteConfig = session.sshRemoteConfig (instead of only
assigning when not enabled), and if the loop is not designed to run remotely,
add a fail-fast check before calling spawnAgent (e.g., throw or log an error and
exit) when session.sshRemoteConfig?.enabled to avoid silently running playbooks
locally; reference spawnOverrides, session.sshRemoteConfig, and spawnAgent to
locate and fix the logic.
🤖 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/cli/services/agent-spawner.ts`:
- Around line 466-482: The SSH wrapping currently triggers for any
sshRemoteConfig object; change the guard to check enabled by using
overrides?.sshRemoteConfig?.enabled before calling wrapSpawnWithSsh so
disabled-but-present configs do not force remote execution. Update the three
similar blocks (the one using wrapSpawnWithSsh that assigns
spawnCommand/spawnArgs/spawnCwd/spawnEnv and any other occurrences around the
referenced ranges) to gate on sshRemoteConfig.enabled, and keep using
wrapSpawnWithSsh(...) and buildExpandedEnv(...) unchanged inside the new
conditional; also mirror this check in the related detection code paths (e.g.,
detectClaude, detectCodex, detectOpenCode, detectDroid) so all spawn paths
consistently require sshRemoteConfig.enabled.
- Around line 847-849: The spawnCommand selection currently favors
agentDef.binaryName and never consults getCodexCommand(), which ignores the
cached path from detectCodex(); change the logic so you first call
getCodexCommand(overrides?.customPath) and use its result if truthy, otherwise
fall back to agentDef?.binaryName (or a default); update the expression that
defines spawnCommand to prefer getCodexCommand(...) before using
agentDef.binaryName so cached/custom paths are respected.

---

Duplicate comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 161-179: The detector functions (e.g., detectClaude) currently
ignore a provided customPathOverride when it is non-executable and continue to
report available=true if a PATH binary exists, which causes spawnAgent to later
fail with the invalid override; change the logic so that if customPathOverride
is provided but is not executable the detector returns available: false (and
path/source not set) immediately instead of falling back to PATH detection.
Locate similar branches in the other detectors noted (the blocks around lines
217-235, 269-287, 321-339) and apply the same behavior so any invalid
per-session customPathOverride causes the detector to return unavailable rather
than continuing to probe PATH or report available:true. Ensure spawnAgent still
receives the same override behavior (i.e., detectors must not claim availability
when a provided override is invalid).

In `@src/cli/services/batch-processor.ts`:
- Around line 448-469: The current guard incorrectly negates the SSH check and
omits passing session.sshRemoteConfig into spawnOverrides for SSH-enabled
sessions; update the condition around spawnOverrides.sshRemoteConfig so that
when session.sshRemoteConfig?.enabled is true you assign
spawnOverrides.sshRemoteConfig = session.sshRemoteConfig (instead of only
assigning when not enabled), and if the loop is not designed to run remotely,
add a fail-fast check before calling spawnAgent (e.g., throw or log an error and
exit) when session.sshRemoteConfig?.enabled to avoid silently running playbooks
locally; reference spawnOverrides, session.sshRemoteConfig, and spawnAgent to
locate and fix the logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b92e2dc3-dc7c-48d0-ba27-15b7643ea742

📥 Commits

Reviewing files that changed from the base of the PR and between 4b596b7 and 79f831a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • docs/cli.md
  • src/__tests__/cli/commands/send.test.ts
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/cli/commands/run-playbook.ts
  • src/cli/commands/send.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/cli/services/storage.ts
  • src/main/utils/agent-args.ts
  • src/shared/types.ts

Comment on lines +466 to +482
if (overrides?.sshRemoteConfig) {
const sshWrapped = await wrapSpawnWithSsh(
{
command: spawnCommand,
args: spawnArgs,
cwd,
customEnvVars: effectiveCustomEnvVars,
agentBinaryName: agentDef?.binaryName,
},
overrides.sshRemoteConfig,
getCliSshRemoteStore()
);
spawnCommand = sshWrapped.command;
spawnArgs = sshWrapped.args;
spawnCwd = sshWrapped.cwd;
spawnEnv = buildExpandedEnv(sshWrapped.customEnvVars);
}
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

Only SSH-wrap when the override is actually enabled.

detectClaude(), detectCodex(), detectOpenCode(), and detectDroid() all treat SSH as active only when sshRemoteConfig.enabled is true, but these spawn paths wrap on any sshRemoteConfig object. A persisted-but-disabled config will therefore pass local preflight and still be launched remotely. Gate these branches on overrides?.sshRemoteConfig?.enabled.

Based on learnings, When implementing features that spawn agent processes, support SSH remote execution by checking for sshRemoteConfig, using wrapSpawnWithSsh() from ssh-spawn-wrapper.ts, and passing the SSH store adapter.

Also applies to: 652-668, 854-870

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

In `@src/cli/services/agent-spawner.ts` around lines 466 - 482, The SSH wrapping
currently triggers for any sshRemoteConfig object; change the guard to check
enabled by using overrides?.sshRemoteConfig?.enabled before calling
wrapSpawnWithSsh so disabled-but-present configs do not force remote execution.
Update the three similar blocks (the one using wrapSpawnWithSsh that assigns
spawnCommand/spawnArgs/spawnCwd/spawnEnv and any other occurrences around the
referenced ranges) to gate on sshRemoteConfig.enabled, and keep using
wrapSpawnWithSsh(...) and buildExpandedEnv(...) unchanged inside the new
conditional; also mirror this check in the related detection code paths (e.g.,
detectClaude, detectCodex, detectOpenCode, detectDroid) so all spawn paths
consistently require sshRemoteConfig.enabled.

Comment on lines +847 to +849
let spawnCommand = !overrides?.customPath
? agentDef?.binaryName || getCodexCommand()
: getCodexCommand(overrides?.customPath);
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

Local Codex runs ignore the detected/custom binary path.

When overrides?.customPath is absent, this always selects agentDef.binaryName (codex) and never consults getCodexCommand(). That bypasses the cached path populated by detectCodex(), so installations that rely on the stored custom path pass preflight and then fail at spawn.

Suggested fix
-	let spawnCommand = !overrides?.customPath
-		? agentDef?.binaryName || getCodexCommand()
-		: getCodexCommand(overrides?.customPath);
+	let spawnCommand =
+		overrides?.sshRemoteConfig && !overrides?.customPath && agentDef?.binaryName
+			? agentDef.binaryName
+			: getCodexCommand(overrides?.customPath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/services/agent-spawner.ts` around lines 847 - 849, The spawnCommand
selection currently favors agentDef.binaryName and never consults
getCodexCommand(), which ignores the cached path from detectCodex(); change the
logic so you first call getCodexCommand(overrides?.customPath) and use its
result if truthy, otherwise fall back to agentDef?.binaryName (or a default);
update the expression that defines spawnCommand to prefer getCodexCommand(...)
before using agentDef.binaryName so cached/custom paths are respected.

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 6, 2026

Escalated through reviews, ne PR for minimal changes: #524

@chr1syy chr1syy closed this Mar 6, 2026
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.

1 participant