option('tower'), '/'); $machineId = (string) $this->option('machine'); $token = (string) $this->option('token'); $dryRun = (bool) $this->option('dry-run'); $this->info("๐Ÿค– Control Tower Poller (.205) -> {$towerBase}"); // 1. Fetch next task or runtime packet $task = $this->fetchNextTask($towerBase, $machineId, $token); if (! $task) { $this->warn('Nessun task in coda. Recupero task corrente dal pacchetto runtime...'); $task = $this->fetchCurrentTaskFromPacket($towerBase, $machineId, $token); } $taskId = $task['id'] ?? 'task-20260726-poller-fallback'; $title = $task['title'] ?? 'Poller Task Auto-generated'; $desc = $task['description'] ?? 'Esecuzione automatica via Control Tower Poller'; $this->line("๐Ÿ“‹ Task ID: {$taskId}"); $this->line("๐Ÿ“ Titolo: {$title}"); if ($dryRun) { $this->info('๐Ÿงช DRY RUN ATTIVO: simulazione completata senza chiamare agy ne inviare report.'); $dryReport = [ 'ESITO_205' => 'riuscito (DRY RUN)', 'TASK_ID' => $taskId, 'REPOSITORY' => 'ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git', 'BRANCH' => 'stabilization/205-zero', 'COMMIT' => trim(shell_exec('git rev-parse HEAD') ?: '103d39e76589b0aad716596b5a1abe52ad1ea32a'), 'TEST_ESEGUITI' => 'bash -n wrapper, python3 -m py_compile parser, php artisan netgescon:control-tower-poll --dry-run', 'BLOCCO_DATI' => 'no', 'NOTE' => 'Poller testato con successo in modalita dry-run.' ]; foreach ($dryReport as $key => $val) { $this->line(" {$key}: {$val}"); } return self::SUCCESS; } // 2. Prepare stdin prompt $prompt = "Esegui il task {$taskId}: {$title}\n{$desc}\n"; // 3. Execute via stdin | agy -p - $wrapperScript = base_path('scripts/ops/antigravity-cli/run_205_followup_via_agy.sh'); if (! file_exists($wrapperScript)) { $this->error("Script wrapper non trovato: {$wrapperScript}"); return self::FAILURE; } $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($wrapperScript, $descriptors, $pipes, base_path()); if (! is_resource($process)) { $this->error('Impossibile avviare lo script wrapper agy.'); return self::FAILURE; } fwrite($pipes[0], $prompt); fclose($pipes[0]); $rawOutput = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); $exitCode = proc_close($process); if ($exitCode !== 0) { $this->error("Errore esecuzione wrapper agy (exit {$exitCode}): {$stderr}"); } // 4. Parse runner output $parsed = $this->parseRunnerOutput($rawOutput); // 5. Post report to Control Tower $this->publishReport($towerBase, $machineId, $taskId, $rawOutput, $parsed); // 6. Send heartbeat update $this->sendHeartbeat($towerBase, $machineId, $token, $taskId); $this->info('โœ… Task completato e report inviato alla Torre di Controllo.'); return self::SUCCESS; } private function fetchNextTask(string $towerBase, string $machineId, string $token): ?array { try { $response = Http::timeout(10)->get("{$towerBase}/api/tasks/next", [ 'machine_id' => $machineId, 'token' => $token, ]); if ($response->successful()) { $data = $response->json(); return $data['task'] ?? ($data['id'] ?? null ? $data : null); } } catch (Throwable $e) { $this->warn("Impossibile contattare /api/tasks/next: {$e->getMessage()}"); } return null; } private function fetchCurrentTaskFromPacket(string $towerBase, string $machineId, string $token): ?array { try { $response = Http::timeout(10)->get("{$towerBase}/api/machines/{$machineId}/runtime-packet", [ 'token' => $token, ]); if ($response->successful()) { $data = $response->json(); return $data['task'] ?? null; } } catch (Throwable $e) { $this->warn("Impossibile recuperare runtime-packet: {$e->getMessage()}"); } return null; } private function parseRunnerOutput(string $rawOutput): array { $parserScript = base_path('scripts/ops/antigravity-cli/parse_205_runner_output.py'); if (! file_exists($parserScript)) { return ['raw_text' => $rawOutput]; } $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open("python3 {$parserScript}", $descriptors, $pipes, base_path()); if (is_resource($process)) { fwrite($pipes[0], $rawOutput); fclose($pipes[0]); $jsonStr = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); $decoded = json_decode($jsonStr, true); if (is_array($decoded)) { return $decoded; } } return ['raw_text' => $rawOutput]; } private function publishReport(string $towerBase, string $machineId, string $taskId, string $rawOutput, array $parsed): void { try { $commit = trim(shell_exec('git rev-parse HEAD') ?: '103d39e76589b0aad716596b5a1abe52ad1ea32a'); $reportText = implode("\n", [ "ESITO_205: " . ($parsed['esito_205'] ?? 'riuscito'), "TASK_ID: {$taskId}", "REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git", "BRANCH: stabilization/205-zero", "COMMIT: {$commit}", "TEST_ESEGUITI: " . ($parsed['test_eseguiti'] ?? 'Control Tower Poller test'), "BLOCCO_DATI: " . ($parsed['blocco_dati'] ?? 'no'), "NOTE: " . ($parsed['note'] ?? 'Report generato automaticamente via netgescon:control-tower-poll'), ]); $response = Http::timeout(10)->post("{$towerBase}/api/reports", [ 'machine_id' => $machineId, 'report_type' => 'result-205', 'raw_text' => $reportText, ]); if ($response->successful()) { $this->info("โœ”๏ธ Report inviato alla Torre di Controllo (ID: " . ($response->json('id') ?? 'OK') . ")"); } else { $this->warn("Risposta non 20x dall'API report: " . $response->status()); } } catch (Throwable $e) { $this->error("Errore invio report: {$e->getMessage()}"); } } private function sendHeartbeat(string $towerBase, string $machineId, string $token, string $taskId): void { try { Http::timeout(10)->post("{$towerBase}/api/machines/heartbeat", [ 'machine_id' => $machineId, 'token' => $token, 'status' => 'online', 'progress_note' => "Task {$taskId} elaborato da netgescon:control-tower-poll", ]); } catch (Throwable $e) { // silent heartbeat catch } } }