diff --git a/.claude/agents/code-developer.md b/.claude/agents/code-developer.md index 68f36dba..bea3968c 100644 --- a/.claude/agents/code-developer.md +++ b/.claude/agents/code-developer.md @@ -385,8 +385,12 @@ Before completing any task, verify: - Make assumptions - verify with existing code - Create unnecessary complexity -**Bash Tool**: -- Use `run_in_background=false` for all Bash/CLI calls to ensure foreground execution +**Bash Tool (CLI Execution in Agent)**: +- Use `run_in_background=false` for all Bash/CLI calls - agent cannot receive task hook callbacks +- Set timeout ≥60 minutes for CLI commands (hooks don't propagate to subagents): + ```javascript + Bash(command="ccw cli -p '...' --tool codex --mode write", timeout=3600000) // 60 min + ``` **ALWAYS:** - **Search Tool Priority**: ACE (`mcp__ace-tool__search_context`) → CCW (`mcp__ccw-tools__smart_search`) / Built-in (`Grep`, `Glob`, `Read`) diff --git a/.claude/commands/workflow/lite-execute.md b/.claude/commands/workflow/lite-execute.md index 1ca4ffe6..6402aa1c 100644 --- a/.claude/commands/workflow/lite-execute.md +++ b/.claude/commands/workflow/lite-execute.md @@ -172,10 +172,23 @@ Output: **Operations**: - Initialize result tracking for multi-execution scenarios - Set up `previousExecutionResults` array for context continuity +- **In-Memory Mode**: Echo execution strategy from lite-plan for transparency ```javascript // Initialize result tracking previousExecutionResults = [] + +// In-Memory Mode: Echo execution strategy (transparency before execution) +if (executionContext) { + console.log(` +📋 Execution Strategy (from lite-plan): + Method: ${executionContext.executionMethod} + Review: ${executionContext.codeReviewTool} + Tasks: ${executionContext.planObject.tasks.length} + Complexity: ${executionContext.planObject.complexity} +${executionContext.executorAssignments ? ` Assignments: ${JSON.stringify(executionContext.executorAssignments)}` : ''} + `) +} ``` ### Step 2: Task Grouping & Batch Creation @@ -393,16 +406,8 @@ ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write **Execution with fixed IDs** (predictable ID pattern): ```javascript -// Launch CLI in foreground (NOT background) -// Timeout based on complexity: Low=40min, Medium=60min, High=100min -const timeoutByComplexity = { - "Low": 2400000, // 40 minutes - "Medium": 3600000, // 60 minutes - "High": 6000000 // 100 minutes -} - +// Launch CLI in background, wait for task hook callback // Generate fixed execution ID: ${sessionId}-${groupId} -// This enables predictable ID lookup without relying on resume context chains const sessionId = executionContext?.session?.id || 'standalone' const fixedExecutionId = `${sessionId}-${batch.groupId}` // e.g., "implement-auth-2025-12-13-P1" @@ -414,16 +419,12 @@ const cli_command = previousCliId ? `ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write --id ${fixedExecutionId} --resume ${previousCliId}` : `ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write --id ${fixedExecutionId}` -bash_result = Bash( +// Execute in background, stop output and wait for task hook callback +Bash( command=cli_command, - timeout=timeoutByComplexity[planObject.complexity] || 3600000 + run_in_background=true ) - -// Execution ID is now predictable: ${fixedExecutionId} -// Can also extract from output: "ID: implement-auth-2025-12-13-P1" -const cliExecutionId = fixedExecutionId - -// Update TodoWrite when execution completes +// STOP HERE - CLI executes in background, task hook will notify on completion ``` **Resume on Failure** (with fixed ID): diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index c849fd66..6bfe48e2 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -386,8 +386,10 @@ async function outputAction(conversationId: string | undefined, options: OutputV if (options.final) { // Final result only with usage hint - if (result.stdout) { - console.log(result.stdout.content); + // Prefer parsedOutput (filtered, intermediate content removed) over raw stdout + const outputContent = result.parsedOutput?.content || result.stdout?.content; + if (outputContent) { + console.log(outputContent); } console.log(); console.log(chalk.gray('─'.repeat(60))); diff --git a/ccw/src/tools/cli-executor-core.ts b/ccw/src/tools/cli-executor-core.ts index f8e3ff20..9305b51c 100644 --- a/ccw/src/tools/cli-executor-core.ts +++ b/ccw/src/tools/cli-executor-core.ts @@ -981,6 +981,13 @@ async function executeCliTool( // Create new turn - cache full output when not streaming (default) const shouldCache = !parsed.data.stream; + + // Compute parsed output (filtered, intermediate content removed) for final display + const computedParsedOutput = flattenOutputUnits(allOutputUnits, { + excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'], + stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message + }); + const newTurnOutput = { stdout: stdout.substring(0, 10240), // Truncate preview to 10KB stderr: stderr.substring(0, 2048), // Truncate preview to 2KB @@ -988,6 +995,7 @@ async function executeCliTool( cached: shouldCache, stdout_full: shouldCache ? stdout : undefined, stderr_full: shouldCache ? stderr : undefined, + parsed_output: computedParsedOutput || undefined, // Filtered output for final display structured: allOutputUnits // Save structured IR units }; @@ -1148,9 +1156,7 @@ async function executeCliTool( exit_code: code, duration_ms: duration, output: newTurnOutput, - parsedOutput: flattenOutputUnits(allOutputUnits, { - excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought'] - }) + parsedOutput: computedParsedOutput // Use already-computed filtered output }; resolve({ diff --git a/ccw/src/tools/cli-history-store.ts b/ccw/src/tools/cli-history-store.ts index 649f2870..762b4994 100644 --- a/ccw/src/tools/cli-history-store.ts +++ b/ccw/src/tools/cli-history-store.ts @@ -27,6 +27,7 @@ export interface ConversationTurn { cached?: boolean; stdout_full?: string; stderr_full?: string; + parsed_output?: string; // Filtered output (intermediate content removed) structured?: CliOutputUnit[]; // Structured IR sequence for advanced parsing }; } @@ -326,6 +327,7 @@ export class CliHistoryStore { const hasCached = turnsInfo.some(col => col.name === 'cached'); const hasStdoutFull = turnsInfo.some(col => col.name === 'stdout_full'); const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full'); + const hasParsedOutput = turnsInfo.some(col => col.name === 'parsed_output'); if (!hasCached) { console.log('[CLI History] Migrating database: adding cached column to turns table...'); @@ -342,6 +344,11 @@ export class CliHistoryStore { this.db.exec('ALTER TABLE turns ADD COLUMN stderr_full TEXT;'); console.log('[CLI History] Migration complete: stderr_full column added'); } + if (!hasParsedOutput) { + console.log('[CLI History] Migrating database: adding parsed_output column to turns table...'); + this.db.exec('ALTER TABLE turns ADD COLUMN parsed_output TEXT;'); + console.log('[CLI History] Migration complete: parsed_output column added'); + } } catch (err) { console.error('[CLI History] Migration error:', (err as Error).message); // Don't throw - allow the store to continue working with existing schema @@ -448,8 +455,8 @@ export class CliHistoryStore { `); const upsertTurn = this.db.prepare(` - INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full) - VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full) + INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full, parsed_output) + VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full, @parsed_output) ON CONFLICT(conversation_id, turn_number) DO UPDATE SET timestamp = @timestamp, prompt = @prompt, @@ -461,7 +468,8 @@ export class CliHistoryStore { truncated = @truncated, cached = @cached, stdout_full = @stdout_full, - stderr_full = @stderr_full + stderr_full = @stderr_full, + parsed_output = @parsed_output `); const transaction = this.db.transaction(() => { @@ -496,7 +504,8 @@ export class CliHistoryStore { truncated: turn.output.truncated ? 1 : 0, cached: turn.output.cached ? 1 : 0, stdout_full: turn.output.stdout_full || null, - stderr_full: turn.output.stderr_full || null + stderr_full: turn.output.stderr_full || null, + parsed_output: turn.output.parsed_output || null }); } }); @@ -543,7 +552,8 @@ export class CliHistoryStore { truncated: !!t.truncated, cached: !!t.cached, stdout_full: t.stdout_full || undefined, - stderr_full: t.stderr_full || undefined + stderr_full: t.stderr_full || undefined, + parsed_output: t.parsed_output || undefined } })) }; @@ -588,6 +598,7 @@ export class CliHistoryStore { turnNumber: number; stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; + parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; cached: boolean; prompt: string; status: string; @@ -614,6 +625,7 @@ export class CliHistoryStore { turnNumber: number; stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; + parsedOutput?: { content: string; totalBytes: number; offset: number; hasMore: boolean }; cached: boolean; prompt: string; status: string; @@ -652,6 +664,19 @@ export class CliHistoryStore { }; } + // Add parsed output if available (filtered output for final display) + if (turn.parsed_output) { + const parsedContent = turn.parsed_output; + const totalBytes = parsedContent.length; + const content = parsedContent.substring(offset, offset + limit); + result.parsedOutput = { + content, + totalBytes, + offset, + hasMore: offset + limit < totalBytes + }; + } + return result; } diff --git a/ccw/src/tools/cli-output-converter.ts b/ccw/src/tools/cli-output-converter.ts index 0b1edf67..626a008c 100644 --- a/ccw/src/tools/cli-output-converter.ts +++ b/ccw/src/tools/cli-output-converter.ts @@ -1117,13 +1117,15 @@ export function flattenOutputUnits( excludeTypes?: CliOutputUnitType[]; includeTimestamps?: boolean; separator?: string; + stripCommandJsonBlocks?: boolean; // Strip embedded command execution JSON code blocks from stdout } ): string { const { includeTypes, excludeTypes, includeTimestamps = false, - separator = '\n' + separator = '\n', + stripCommandJsonBlocks = false } = options || {}; // Special handling for streaming_content: concatenate all into a single stdout unit @@ -1163,7 +1165,20 @@ export function flattenOutputUnits( // Extract text content based on type if (typeof unit.content === 'string') { - text += unit.content; + let content = unit.content; + + // Strip command execution JSON code blocks if requested (codex agent_message often includes these) + if (stripCommandJsonBlocks && unit.type === 'stdout') { + // Pattern 1: Backtick-wrapped JSON blocks + // Format: ```...{"command":"...","output":"...","exitCode":N,"status":"..."...}...``` + // Uses [\s\S]*? to match any characters (including newlines) non-greedily + content = content.replace(/```[\s\S]*?\{"command":[\s\S]*?,"status":"[^"]*"\}[\s\S]*?```/g, '').trim(); + // Pattern 2: Raw JSON command execution (no backticks) + // Matches complete JSON object with command/output/exitCode/status fields + content = content.replace(/\{"command":[\s\S]*?,"status":"[^"]*"\}/g, '').trim(); + } + + text += content; } else if (typeof unit.content === 'object' && unit.content !== null) { // Handle structured content with type-specific formatting switch (unit.type) {