From be2aeb371e79b21c361d05fcf8a5f0043002f2bc Mon Sep 17 00:00:00 2001 From: michele Date: Sun, 26 Jul 2026 17:38:30 +0200 Subject: [PATCH] feat(ops): implement Control Tower poller command netgescon:control-tower-poll and agy wrapper/parser scripts --- .../Commands/ControlTowerPollCommand.php | 225 ++++++++++++++++++ .../parse_205_runner_output.py | 40 ++++ .../run_205_followup_via_agy.sh | 12 + 3 files changed, 277 insertions(+) create mode 100644 app/Console/Commands/ControlTowerPollCommand.php create mode 100755 scripts/ops/antigravity-cli/parse_205_runner_output.py create mode 100755 scripts/ops/antigravity-cli/run_205_followup_via_agy.sh diff --git a/app/Console/Commands/ControlTowerPollCommand.php b/app/Console/Commands/ControlTowerPollCommand.php new file mode 100644 index 0000000..559ce73 --- /dev/null +++ b/app/Console/Commands/ControlTowerPollCommand.php @@ -0,0 +1,225 @@ +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 + } + } +} diff --git a/scripts/ops/antigravity-cli/parse_205_runner_output.py b/scripts/ops/antigravity-cli/parse_205_runner_output.py new file mode 100755 index 0000000..591c39a --- /dev/null +++ b/scripts/ops/antigravity-cli/parse_205_runner_output.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import sys +import json +import re + +def parse_output(text: str) -> dict: + fields = { + "task_id": None, + "esito_205": None, + "repository": "ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git", + "branch": "stabilization/205-zero", + "commit": None, + "test_eseguiti": None, + "blocco_dati": "no", + "note": None, + "raw_text": text + } + + patterns = { + "esito_205": r"ESITO_205:\s*(.+)", + "task_id": r"TASK_ID:\s*(.+)", + "repository": r"REPOSITORY:\s*(.+)", + "branch": r"BRANCH:\s*(.+)", + "commit": r"COMMIT:\s*(.+)", + "test_eseguiti": r"TEST_ESEGUITI:\s*(.+)", + "blocco_dati": r"BLOCCO_DATI:\s*(.+)", + "note": r"NOTE:\s*(.+)" + } + + for key, pat in patterns.items(): + m = re.search(pat, text, re.IGNORECASE) + if m: + fields[key] = m.group(1).strip() + + return fields + +if __name__ == "__main__": + input_text = sys.stdin.read() + parsed = parse_output(input_text) + print(json.dumps(parsed, ensure_ascii=False, indent=2)) diff --git a/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh b/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh new file mode 100755 index 0000000..c6b03fb --- /dev/null +++ b/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="$HOME/.local/bin:$PATH" + +if ! command -v agy &>/dev/null; then + echo "ERROR: agy CLI binary not found in PATH" >&2 + exit 1 +fi + +# Pass stdin directly into agy -p - +cat - | agy -p - --dangerously-skip-permissions