[start_line_number]\n"); } $targetDir = $argv[1]; if (!is_dir($targetDir)) { exit("Error: Directory '$targetDir' does not exist\n"); } // Change to the target directory chdir($targetDir); echo "Working in directory: " . getcwd() . "\n"; $prompt = trim(file_get_contents('prompt')); $inputs = file('input', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!$inputs) exit("No inputs\n"); // NEW: Support starting from a specific line number (now second argument) $startFrom = 1; // Default: start from line 1 if ($argc > 2 && is_numeric($argv[2])) { $startFrom = max(1, (int)$argv[2]); if ($startFrom > 1) { echo "Starting from line $startFrom (skipping first " . ($startFrom - 1) . " lines)\n"; $inputs = array_slice($inputs, $startFrom - 1); } } // NEW: Use runfiles subdirectory $baseOutputDir = 'runfiles'; is_dir($baseOutputDir) || mkdir($baseOutputDir, 0777, true); $runIdentifier = date('Ymd_His'); $runDir = sprintf('%s/run_%s', $baseOutputDir, $runIdentifier); $suffix = 1; while (is_dir($runDir)) { $runDir = sprintf('%s/run_%s_%02d', $baseOutputDir, $runIdentifier, $suffix++); } mkdir($runDir, 0777, true); $rawLogsDir = $runDir . '/raw_logs'; is_dir($rawLogsDir) || mkdir($rawLogsDir, 0777, true); $logFiles = []; $logCli = function ($message, $channel = 'global') use (&$logFiles, $rawLogsDir) { if (!isset($logFiles[$channel])) { $logFiles[$channel] = sprintf('%s/%s.log', $rawLogsDir, $channel); } file_put_contents($logFiles[$channel], $message, FILE_APPEND | LOCK_EX); echo $message; }; $logCli(sprintf("[%s] Run initialized in %s%s", date('Y-m-d H:i:s'), $runDir, PHP_EOL), 'startup'); if ($startFrom > 1) { $logCli(sprintf("[%s] Starting from input line %d%s", date('Y-m-d H:i:s'), $startFrom, PHP_EOL), 'startup'); } $runCommand = function ($command, $channel = 'global') use ($logCli) { $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($command, $descriptorSpec, $pipes); if (!is_resource($process)) { $logCli(sprintf("Failed to start command: %s%s", $command, PHP_EOL), $channel); return ['', '', 1]; } fclose($pipes[0]); $stdoutStream = $pipes[1]; $stderrStream = $pipes[2]; stream_set_blocking($stdoutStream, false); stream_set_blocking($stderrStream, false); $stdout = ''; $stderr = ''; while ($stdoutStream || $stderrStream) { $read = []; if ($stdoutStream) { $read[] = $stdoutStream; } if ($stderrStream) { $read[] = $stderrStream; } if (!$read) { break; } $write = null; $except = null; $changed = stream_select($read, $write, $except, null); if ($changed === false) { break; } foreach ($read as $stream) { $chunk = fread($stream, 8192); if ($chunk === false) { $chunk = ''; } $isStdout = ($stdoutStream && $stream === $stdoutStream); $isStderr = ($stderrStream && $stream === $stderrStream); if ($chunk === '') { if (feof($stream)) { fclose($stream); if ($isStdout) { $stdoutStream = null; } if ($isStderr) { $stderrStream = null; } } continue; } if ($isStdout) { $stdout .= $chunk; } elseif ($isStderr) { $stderr .= $chunk; $logCli($chunk, $channel); } } } if ($stdoutStream) { fclose($stdoutStream); } if ($stderrStream) { fclose($stderrStream); } $exitCode = proc_close($process); return [$stdout, $stderr, $exitCode]; }; $notesFile = 'notes'; if (!file_exists($notesFile)) { file_put_contents($notesFile, ''); } $instruction = 'CRITICAL RESPONSE FORMAT: Your response MUST be ONLY valid JSON: {"completed": true} or {"completed": false} - NO markdown formatting - NO explanations before or after the JSON - NO additional text outside the JSON object - You may add extra JSON fields for logging/context, but the "completed" boolean field is required - Return true only if the task is successfully completed, and add an extra json field describing the current status - Return false if the task failed or cannot be completed, and add an extra json field describing the error'; $maxAttempts = 2; $summary = []; $thinkingRecords = []; $tokenUsage = []; $run = $startFrom; // Start run numbering from the starting line foreach ($inputs as $line) { $file = sprintf('%s/run_%02d.txt', $runDir, $run); $runLogChannel = sprintf('run_%02d', $run); $completed = false; $finalStatus = 'unknown'; $lastAttemptStatus = 'unknown'; $attemptsUsed = 0; $context = $prompt . "\n" . $line; file_put_contents($file, ''); $logCli(sprintf("[%s] Starting run %02d with input: %s%s", date('Y-m-d H:i:s'), $run, $line, PHP_EOL), $runLogChannel); for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { $attemptsUsed = $attempt; $notesSection = ''; $notesContent = trim(file_get_contents($notesFile)); if ($notesContent !== '') { $notesSection = "Historical notes:\n{$notesContent}\n\n"; } $fullPrompt = $notesSection . $context . "\n" . $instruction; // NEW: Added --model gpt-5-codex to codex exec command $cmd = 'codex exec --model gpt-5-codex --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox ' . escapeshellarg($fullPrompt); [$output, $stderr] = $runCommand($cmd, $runLogChannel); // Parse token usage from stderr $tokens = null; if (preg_match('/tokens used\s+([0-9,]+)/i', $stderr, $matches)) { $tokens = (int)str_replace(',', '', $matches[1]); $tokenUsage[] = [ 'run' => $run, 'attempt' => $attempt, 'tokens' => $tokens, 'input' => $line, 'timestamp' => date('Y-m-d H:i:s'), ]; $logCli(sprintf("Parsed tokens: %s%s", number_format($tokens), PHP_EOL), $runLogChannel); } file_put_contents( $file, sprintf("Attempt %d prompt:\n%s\n\nAttempt %d output:\n%s\n\n", $attempt, $fullPrompt, $attempt, $output) . ($tokens !== null ? sprintf("Attempt %d tokens: %s\n\n", $attempt, number_format($tokens)) : ''), FILE_APPEND ); $attemptStatus = 'unknown'; $json = json_decode(trim($output), true); if (is_array($json) && array_key_exists('completed', $json)) { $attemptStatus = $json['completed'] ? 'completed' : 'not completed'; } else { $attemptStatus = 'invalid response'; } $lastAttemptStatus = $attemptStatus; $attemptRecord = [ 'run' => $run, 'attempt' => $attempt, 'completed' => ($attemptStatus === 'completed'), 'status' => $attemptStatus, 'output_file' => $file, ]; if ($tokens !== null) { $attemptRecord['tokens'] = $tokens; } $thinkingRecords[] = [ 'run' => $run, 'input' => $line, 'attempt' => $attempt, 'status' => $attemptStatus, 'prompt' => $fullPrompt, 'output' => $output, 'output_file' => $file, 'tokens' => $tokens, ]; $logCli(json_encode($attemptRecord, JSON_UNESCAPED_SLASHES) . PHP_EOL, $runLogChannel); if ($attemptStatus === 'completed') { $completed = true; $finalStatus = sprintf('completed (attempt %d)', $attempt); break; } $context .= "\nPrevious attempt {$attempt} output:\n{$output}\nPlease continue from this state."; } if (!$completed) { $finalStatus = sprintf('%s after %d attempts', $lastAttemptStatus, $attemptsUsed); } $logCli( sprintf( "[%s] Run %02d finished: %s%s", date('Y-m-d H:i:s'), $run, $finalStatus, PHP_EOL ), $runLogChannel ); // DISABLED: if ($completed) { // DISABLED: // Update notes with efficiency learnings // DISABLED: $currentNotes = file_get_contents($notesFile); // DISABLED: $runOutput = file_get_contents($file); // DISABLED: // DISABLED: $notesPrompt = sprintf( // DISABLED: "Review the current notes and this completed run, then update the notes to include any relevant efficiency improvements.\n\n" . // DISABLED: "CURRENT NOTES:\n%s\n\n" . // DISABLED: "COMPLETED RUN:\n" . // DISABLED: "Input: %s\n" . // DISABLED: "Attempts: %d\n" . // DISABLED: "Output file content:\n%s\n\n" . // DISABLED: "TASK:\n" . // DISABLED: "1. Analyze what happened in this run (especially if it took multiple attempts)\n" . // DISABLED: "2. Extract any patterns, solutions, or techniques that would make future runs faster/more efficient. You can build scripts under tools folder and add them to the notes if helps decrease the number of codex calls and improve efficiency so that the next runs can use them. Make sure to test them thoroughly. Make sure the learnings apply for all future work of this type not just this specific instance.\n" . // DISABLED: "3. Aggregate these learnings with the existing notes\n" . // DISABLED: "4. Output the COMPLETE updated notes content (do not use placeholders or truncate)\n" . // DISABLED: "5. Focus on: build patterns, common fixes, what to check first, what to avoid, time-saving techniques\n\n" . // DISABLED: "Respond with ONLY the full updated notes text, no explanations before or after.", // DISABLED: $currentNotes, // DISABLED: $line, // DISABLED: $attemptsUsed, // DISABLED: $runOutput // DISABLED: ); // DISABLED: // DISABLED: $notesCmd = 'codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox ' . escapeshellarg($notesPrompt); // DISABLED: [$updatedNotes, $notesStderr] = $runCommand($notesCmd, 'notes_update'); // DISABLED: // DISABLED: // Parse token usage from notes update // DISABLED: if (preg_match('/tokens used\s+([0-9,]+)/i', $notesStderr, $matches)) { // DISABLED: $notesTokens = (int)str_replace(',', '', $matches[1]); // DISABLED: $tokenUsage[] = [ // DISABLED: 'run' => $run, // DISABLED: 'attempt' => 'notes_update', // DISABLED: 'tokens' => $notesTokens, // DISABLED: 'input' => sprintf('Updating notes for run %d', $run), // DISABLED: 'timestamp' => date('Y-m-d H:i:s'), // DISABLED: ]; // DISABLED: } // DISABLED: // DISABLED: if (trim($updatedNotes) !== '') { // DISABLED: file_put_contents($notesFile, $updatedNotes, LOCK_EX); // DISABLED: } // DISABLED: } $summary[] = [ 'run' => $run, 'input' => $line, 'completed' => $completed, 'status' => $finalStatus, 'output' => $file, ]; $run++; } $logCli(PHP_EOL . "Summary" . PHP_EOL, 'wrapup'); $logCli("Run | Input | Completed | Status | Output" . PHP_EOL, 'wrapup'); $summaryLines = [ "Summary", "Run | Input | Completed | Status | Output", ]; foreach ($summary as $row) { $line = sprintf( "%d | %s | %s | %s | %s", $row['run'], $row['input'], $row['completed'] ? 'true' : 'false', $row['status'], $row['output'] ); $logCli($line . PHP_EOL, 'wrapup'); $summaryLines[] = $line; } $summaryTextFile = $runDir . '/summary.txt'; file_put_contents($summaryTextFile, implode(PHP_EOL, $summaryLines) . PHP_EOL); $summaryJsonFile = $runDir . '/summary.json'; file_put_contents($summaryJsonFile, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); if ($thinkingRecords) { $thinkingJsonLines = array_map( function ($record) { return json_encode($record, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); }, $thinkingRecords ); $thinkingText = ''; foreach ($thinkingRecords as $record) { $thinkingText .= sprintf( "Run %d | Input: %s | Attempt %d | Status: %s", $record['run'], $record['input'], $record['attempt'], $record['status'] ); if ($record['tokens'] !== null) { $thinkingText .= sprintf(" | Tokens: %s", number_format($record['tokens'])); } $thinkingText .= sprintf( "\nPrompt:\n%s\n\nOutput:\n%s\n\n----\n", $record['prompt'], $record['output'] ); } file_put_contents($runDir . '/thinking.jsonl', implode(PHP_EOL, $thinkingJsonLines) . PHP_EOL); file_put_contents($runDir . '/thinking.txt', $thinkingText); } // Generate token usage summary if ($tokenUsage) { $tokenSummaryFile = $runDir . '/token_usage.json'; $tokenSummaryTextFile = $runDir . '/token_usage.txt'; $totalTokens = array_sum(array_column($tokenUsage, 'tokens')); $tokensByRun = []; foreach ($tokenUsage as $usage) { $runKey = $usage['run']; if (!isset($tokensByRun[$runKey])) { $tokensByRun[$runKey] = 0; } $tokensByRun[$runKey] += $usage['tokens']; } $tokenSummary = [ 'total_tokens' => $totalTokens, 'total_entries' => count($tokenUsage), 'average_tokens_per_entry' => $totalTokens / count($tokenUsage), 'max_tokens' => max(array_column($tokenUsage, 'tokens')), 'min_tokens' => min(array_column($tokenUsage, 'tokens')), 'tokens_by_run' => $tokensByRun, 'detailed_usage' => $tokenUsage, 'note' => 'Token counts represent total tokens. Breakdown by type (input/cached/output) not available from codex CLI.', ]; file_put_contents($tokenSummaryFile, json_encode($tokenSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); // Create human-readable text summary $tokenText = "Token Usage Summary\n"; $tokenText .= "===================\n\n"; $tokenText .= sprintf("Total Tokens: %s\n", number_format($totalTokens)); $tokenText .= sprintf("Total Entries: %d\n", count($tokenUsage)); $tokenText .= sprintf("Average per Entry: %s\n", number_format((int)($totalTokens / count($tokenUsage)))); $tokenText .= sprintf("Max: %s\n", number_format(max(array_column($tokenUsage, 'tokens')))); $tokenText .= sprintf("Min: %s\n\n", number_format(min(array_column($tokenUsage, 'tokens')))); $tokenText .= "Tokens by Run:\n"; foreach ($tokensByRun as $runNum => $tokens) { $tokenText .= sprintf(" Run %02d: %s\n", $runNum, number_format($tokens)); } $tokenText .= "\nDetailed Usage:\n"; foreach ($tokenUsage as $usage) { $tokenText .= sprintf( " Run %02d | Attempt %s | Tokens: %s | %s\n", $usage['run'], $usage['attempt'], number_format($usage['tokens']), $usage['timestamp'] ); } $tokenText .= "\nNote: Token counts represent total tokens. Breakdown by type (input/cached/output) is not available from the codex CLI.\n"; file_put_contents($tokenSummaryTextFile, $tokenText); $logCli(sprintf("%sToken usage summary saved to:%s - %s%s - %s%s", PHP_EOL, PHP_EOL, $tokenSummaryFile, PHP_EOL, $tokenSummaryTextFile, PHP_EOL), 'wrapup'); } $logCli("Detailed outputs stored in {$runDir}" . PHP_EOL, 'wrapup');