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