diff --git a/src/__tests__/renderer/components/BatchRunnerModal.test.tsx b/src/__tests__/renderer/components/BatchRunnerModal.test.tsx index 8c094c3d..411ad4e9 100644 --- a/src/__tests__/renderer/components/BatchRunnerModal.test.tsx +++ b/src/__tests__/renderer/components/BatchRunnerModal.test.tsx @@ -139,7 +139,12 @@ describe('BatchRunnerModal', () => { }; (window.maestro.git as Record).branches = vi.fn().mockResolvedValue({ branches: ['main', 'develop'] }); - (window.maestro.git as Record).checkGhCli = vi.fn().mockResolvedValue({ installed: true, authenticated: true }); + (window.maestro.git as Record).checkGhCli = vi.fn(() => ({ + then: (cb: (value: { installed: boolean; authenticated: boolean }) => void) => { + cb({ installed: true, authenticated: true }); + return Promise.resolve({ installed: true, authenticated: true }); + }, + })); (window.maestro.git as Record).worktreeInfo = vi.fn().mockResolvedValue({ success: true, exists: false, diff --git a/src/__tests__/renderer/utils/fileExplorer.test.ts b/src/__tests__/renderer/utils/fileExplorer.test.ts index 87e0cd80..845da367 100644 --- a/src/__tests__/renderer/utils/fileExplorer.test.ts +++ b/src/__tests__/renderer/utils/fileExplorer.test.ts @@ -300,9 +300,11 @@ describe('fileExplorer utils', () => { const error = new Error('Permission denied'); vi.mocked(window.maestro.fs.readDir).mockRejectedValue(error); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); await expect(loadFileTree('/restricted')).rejects.toThrow( 'Permission denied' ); + consoleSpy.mockRestore(); }); it('respects default maxDepth of 10', async () => { diff --git a/src/__tests__/renderer/utils/gitDiffParser.test.ts b/src/__tests__/renderer/utils/gitDiffParser.test.ts index e50bdcd3..1132dfd5 100644 --- a/src/__tests__/renderer/utils/gitDiffParser.test.ts +++ b/src/__tests__/renderer/utils/gitDiffParser.test.ts @@ -459,6 +459,7 @@ Binary files /dev/null and b/image.png differ`; describe('diffText preservation', () => { it('preserves the original diff text in the result', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const diffText = `diff --git a/file.ts b/file.ts --- a/file.ts +++ b/file.ts @@ -473,6 +474,7 @@ Binary files /dev/null and b/image.png differ`; const result = parseGitDiff(diffText); expect(result[0].diffText).toBe(diffText); + consoleSpy.mockRestore(); }); it('preserves individual diff sections for multiple files', () => { diff --git a/src/__tests__/web/components/ThemeProvider.test.tsx b/src/__tests__/web/components/ThemeProvider.test.tsx index 1ad56745..cb277cd5 100644 --- a/src/__tests__/web/components/ThemeProvider.test.tsx +++ b/src/__tests__/web/components/ThemeProvider.test.tsx @@ -36,6 +36,16 @@ vi.mock('../../../web/hooks/useDeviceColorScheme', () => ({ const mockedCssCustomProperties = vi.mocked(cssCustomProperties); const mockedUseDeviceColorScheme = vi.mocked(useDeviceColorSchemeModule.useDeviceColorScheme); +let consoleErrorSpy: ReturnType | undefined; + +beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + consoleErrorSpy?.mockRestore(); + consoleErrorSpy = undefined; +}); // Test themes const customDarkTheme: Theme = { diff --git a/src/main/agent-detector.ts b/src/main/agent-detector.ts index d72bf7e0..6473bc1c 100644 --- a/src/main/agent-detector.ts +++ b/src/main/agent-detector.ts @@ -43,6 +43,7 @@ export interface AgentConfig { yoloModeArgs?: string[]; // Args for YOLO/full-access mode (e.g., ['--dangerously-bypass-approvals-and-sandbox']) workingDirArgs?: (dir: string) => string[]; // Function to build working directory args (e.g., ['-C', dir]) imageArgs?: (imagePath: string) => string[]; // Function to build image attachment args (e.g., ['-i', imagePath] for Codex) + noPromptSeparator?: boolean; // If true, don't add '--' before the prompt in batch mode (OpenCode doesn't support it) } const AGENT_DEFINITIONS: Omit[] = [ @@ -126,6 +127,7 @@ const AGENT_DEFINITIONS: Omit ['--model', modelId], // Model selection (e.g., 'ollama/qwen3:8b') yoloModeArgs: ['run'], // 'run' subcommand auto-approves all permissions (YOLO mode is implicit) imageArgs: (imagePath: string) => ['-f', imagePath], // Image/file attachment: opencode run -f /path/to/image.png + noPromptSeparator: true, // OpenCode doesn't support '--' before prompt (breaks yargs parsing) // Agent-specific configuration options shown in UI configOptions: [ { diff --git a/src/main/group-chat/group-chat-agent.ts b/src/main/group-chat/group-chat-agent.ts index 5f1122d4..2a1e677c 100644 --- a/src/main/group-chat/group-chat-agent.ts +++ b/src/main/group-chat/group-chat-agent.ts @@ -200,9 +200,11 @@ export async function addParticipant( prompt, contextWindow: getContextWindowValue(agentConfig, agentConfigValues || {}), customEnvVars: configResolution.effectiveCustomEnvVars ?? effectiveEnvVars, + noPromptSeparator: agentConfig?.noPromptSeparator, }); console.log(`[GroupChat:Debug] Spawn result: ${JSON.stringify(result)}`); + console.log(`[GroupChat:Debug] noPromptSeparator: ${agentConfig?.noPromptSeparator ?? false}`); if (!result.success) { console.log(`[GroupChat:Debug] ERROR: Spawn failed!`); diff --git a/src/main/group-chat/group-chat-moderator.ts b/src/main/group-chat/group-chat-moderator.ts index aa010780..82855746 100644 --- a/src/main/group-chat/group-chat-moderator.ts +++ b/src/main/group-chat/group-chat-moderator.ts @@ -30,6 +30,7 @@ export interface IProcessManager { prompt?: string; customEnvVars?: Record; contextWindow?: number; + noPromptSeparator?: boolean; }): { pid: number; success: boolean }; write(sessionId: string, data: string): boolean; diff --git a/src/main/group-chat/group-chat-router.ts b/src/main/group-chat/group-chat-router.ts index 38263621..f959522e 100644 --- a/src/main/group-chat/group-chat-router.ts +++ b/src/main/group-chat/group-chat-router.ts @@ -384,6 +384,7 @@ ${message}`; // Get the base args from the agent configuration const args = [...agent.args]; const agentConfigValues = getAgentConfigCallback?.(chat.moderatorAgentId) || {}; + console.log(`[GroupChat:Debug] agentConfigValues for ${chat.moderatorAgentId}: ${JSON.stringify(agentConfigValues)}`); const baseArgs = buildAgentArgs(agent, { baseArgs: args, prompt: fullPrompt, @@ -422,10 +423,12 @@ ${message}`; prompt: fullPrompt, contextWindow: getContextWindowValue(agent, agentConfigValues), customEnvVars: configResolution.effectiveCustomEnvVars ?? getCustomEnvVarsCallback?.(chat.moderatorAgentId), + noPromptSeparator: agent.noPromptSeparator, }); console.log(`[GroupChat:Debug] Spawn result: ${JSON.stringify(spawnResult)}`); console.log(`[GroupChat:Debug] Moderator process spawned successfully`); + console.log(`[GroupChat:Debug] noPromptSeparator: ${agent.noPromptSeparator ?? false}`); console.log(`[GroupChat:Debug] =================================================`); } catch (error) { console.error(`[GroupChat:Debug] SPAWN ERROR:`, error); @@ -679,9 +682,11 @@ Please respond to this request.${readOnly ? ' Remember: READ-ONLY mode is active prompt: participantPrompt, contextWindow: getContextWindowValue(agent, agentConfigValues), customEnvVars: configResolution.effectiveCustomEnvVars ?? getCustomEnvVarsCallback?.(participant.agentId), + noPromptSeparator: agent.noPromptSeparator, }); console.log(`[GroupChat:Debug] Spawn result for ${participantName}: ${JSON.stringify(spawnResult)}`); + console.log(`[GroupChat:Debug] noPromptSeparator: ${agent.noPromptSeparator ?? false}`); // Track this participant as pending response participantsToRespond.add(participantName); @@ -933,10 +938,12 @@ Review the agent responses above. Either: prompt: synthesisPrompt, contextWindow: getContextWindowValue(agent, agentConfigValues), customEnvVars: configResolution.effectiveCustomEnvVars ?? getCustomEnvVarsCallback?.(chat.moderatorAgentId), + noPromptSeparator: agent.noPromptSeparator, }); console.log(`[GroupChat:Debug] Synthesis spawn result: ${JSON.stringify(spawnResult)}`); console.log(`[GroupChat:Debug] Synthesis moderator process spawned successfully`); + console.log(`[GroupChat:Debug] noPromptSeparator: ${agent.noPromptSeparator ?? false}`); console.log(`[GroupChat:Debug] ================================================`); } catch (error) { console.error(`[GroupChat:Debug] SYNTHESIS SPAWN ERROR:`, error); diff --git a/src/main/ipc/handlers/process.ts b/src/main/ipc/handlers/process.ts index 7310071b..6dd77def 100644 --- a/src/main/ipc/handlers/process.ts +++ b/src/main/ipc/handlers/process.ts @@ -203,6 +203,7 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void contextWindow, // Pass configured context window to process manager customEnvVars: effectiveCustomEnvVars, // Pass custom env vars (session-level or agent-level) imageArgs: agent?.imageArgs, // Function to build image CLI args (for Codex, OpenCode) + noPromptSeparator: agent?.noPromptSeparator, // OpenCode doesn't support '--' before prompt }); logger.info(`Process spawned successfully`, LOG_CONTEXT, { diff --git a/src/main/process-manager.ts b/src/main/process-manager.ts index f6a69e7d..97e23770 100644 --- a/src/main/process-manager.ts +++ b/src/main/process-manager.ts @@ -57,6 +57,7 @@ interface ProcessConfig { imageArgs?: (imagePath: string) => string[]; // Function to build image CLI args (e.g., ['-i', path] for Codex) contextWindow?: number; // Configured context window size (0 or undefined = not configured, hide UI) customEnvVars?: Record; // Custom environment variables from user configuration + noPromptSeparator?: boolean; // If true, don't add '--' before the prompt (e.g., OpenCode doesn't support it) } interface ManagedProcess { @@ -194,10 +195,10 @@ export class ProcessManager extends EventEmitter { * Spawn a new process for a session */ spawn(config: ProcessConfig): { pid: number; success: boolean } { - const { sessionId, toolType, cwd, command, args, requiresPty, prompt, shell, shellArgs, shellEnvVars, images, imageArgs, contextWindow, customEnvVars } = config; + const { sessionId, toolType, cwd, command, args, requiresPty, prompt, shell, shellArgs, shellEnvVars, images, imageArgs, contextWindow, customEnvVars, noPromptSeparator } = config; // For batch mode with images, use stream-json mode and send message via stdin - // For batch mode without images, append prompt to args with -- separator + // For batch mode without images, append prompt to args with -- separator (unless noPromptSeparator is true) const hasImages = images && images.length > 0; const capabilities = getAgentCapabilities(toolType); let finalArgs: string[]; @@ -219,8 +220,12 @@ export class ProcessManager extends EventEmitter { finalArgs = [...finalArgs, ...imageArgs(tempPath)]; } } - // Add the prompt at the end - finalArgs = [...finalArgs, '--', prompt]; + // Add the prompt at the end (with or without -- separator) + if (noPromptSeparator) { + finalArgs = [...finalArgs, prompt]; + } else { + finalArgs = [...finalArgs, '--', prompt]; + } logger.debug('[ProcessManager] Using file-based image args', 'ProcessManager', { sessionId, imageCount: images.length, @@ -229,7 +234,12 @@ export class ProcessManager extends EventEmitter { } else if (prompt) { // Regular batch mode - prompt as CLI arg // The -- ensures prompt is treated as positional arg, not a flag (even if it starts with --) - finalArgs = [...args, '--', prompt]; + // Some agents (e.g., OpenCode) don't support the -- separator + if (noPromptSeparator) { + finalArgs = [...args, prompt]; + } else { + finalArgs = [...args, '--', prompt]; + } } else { finalArgs = args; } @@ -494,6 +504,13 @@ export class ProcessManager extends EventEmitter { childProcess.stdout.on('data', (data: Buffer | string) => { const output = data.toString(); + // Debug: Log all stdout data for group chat sessions + if (sessionId.includes('group-chat-')) { + console.log(`[GroupChat:Debug:ProcessManager] STDOUT received for session ${sessionId}`); + console.log(`[GroupChat:Debug:ProcessManager] Raw output length: ${output.length}`); + console.log(`[GroupChat:Debug:ProcessManager] Raw output preview: "${output.substring(0, 500)}${output.length > 500 ? '...' : ''}"`); + } + if (isStreamJsonMode) { // In stream-json mode, each line is a JSONL message // Accumulate and process complete lines @@ -668,6 +685,13 @@ export class ProcessManager extends EventEmitter { const stderrData = data.toString(); logger.debug('[ProcessManager] stderr event fired', 'ProcessManager', { sessionId, dataPreview: stderrData.substring(0, 100) }); + // Debug: Log all stderr data for group chat sessions + if (sessionId.includes('group-chat-')) { + console.log(`[GroupChat:Debug:ProcessManager] STDERR received for session ${sessionId}`); + console.log(`[GroupChat:Debug:ProcessManager] Stderr length: ${stderrData.length}`); + console.log(`[GroupChat:Debug:ProcessManager] Stderr preview: "${stderrData.substring(0, 500)}${stderrData.length > 500 ? '...' : ''}"`); + } + // Accumulate stderr for error detection at exit (with size limit to prevent memory exhaustion) managedProcess.stderrBuffer = appendToBuffer(managedProcess.stderrBuffer || '', stderrData); @@ -705,6 +729,19 @@ export class ProcessManager extends EventEmitter { jsonBufferLength: managedProcess.jsonBuffer?.length || 0, jsonBufferPreview: managedProcess.jsonBuffer?.substring(0, 200) }); + + // Debug: Log exit details for group chat sessions + if (sessionId.includes('group-chat-')) { + console.log(`[GroupChat:Debug:ProcessManager] EXIT for session ${sessionId}`); + console.log(`[GroupChat:Debug:ProcessManager] Exit code: ${code}`); + console.log(`[GroupChat:Debug:ProcessManager] isStreamJsonMode: ${isStreamJsonMode}`); + console.log(`[GroupChat:Debug:ProcessManager] isBatchMode: ${isBatchMode}`); + console.log(`[GroupChat:Debug:ProcessManager] resultEmitted: ${managedProcess.resultEmitted}`); + console.log(`[GroupChat:Debug:ProcessManager] streamedText length: ${managedProcess.streamedText?.length || 0}`); + console.log(`[GroupChat:Debug:ProcessManager] jsonBuffer length: ${managedProcess.jsonBuffer?.length || 0}`); + console.log(`[GroupChat:Debug:ProcessManager] stderrBuffer length: ${managedProcess.stderrBuffer?.length || 0}`); + console.log(`[GroupChat:Debug:ProcessManager] stderrBuffer preview: "${(managedProcess.stderrBuffer || '').substring(0, 500)}"`); + } if (isBatchMode && !isStreamJsonMode && managedProcess.jsonBuffer) { // Parse JSON response from regular batch mode (not stream-json) try { diff --git a/src/prompts/maestro-system-prompt.md b/src/prompts/maestro-system-prompt.md index d18d9106..c5788870 100644 --- a/src/prompts/maestro-system-prompt.md +++ b/src/prompts/maestro-system-prompt.md @@ -51,3 +51,7 @@ This restriction ensures: If a user requests an operation that would write outside your assigned directory, explain the restriction and ask them to either: 1. Change to the appropriate session/agent for that directory 2. Explicitly confirm they want to override this safety measure + +### Recommended Operations + +Format you responses in Markdown. When referencing file paths, use backticks (ex: `path/to/file`). \ No newline at end of file