Intro: il gate che rifiuta il «fatto»
Un gate, nel senso in cui lo usa Shopify, è un controllo che una modifica scritta dall'AI deve superare prima di andare avanti. Ricostruiti in Claude Code, solo il primo ha ripagato: i test di accettazione. Shopify ha pubblicato quattro gate e nessun numero, quindi questo esperimento li costruisce su un repository vero, esegue cinquanta richieste spegnendo a turno ogni gate e conta cosa ha intercettato ciascuno e quanto è costato.
Helix è diventato pubblico il 21 settembre 2026. Fa passare ogni checkpoint attraverso quattro gate, in ordine: test, design review, due reviewer e un ingegnere. Il post non dà alcun tasso di successo. I reviewer sono la parte che la maggior parte delle persone copia, e su questi cinquanta run il tasso di successo si è mosso solo quando c'erano i test. I reviewer non hanno aggiunto nulla, a un costo circa triplo.
Dallo stesso banco di prova escono altri due risultati. Un hook che in teoria non si può scavalcare ha tre uscite nella sua stessa documentazione. E quando i test e i reviewer erano in disaccordo, l'agente ha fatto qualcosa che nessuno aveva previsto. Il conteggio parte dal caso senza alcun gate.
Quattro gate, zero numeri, un banco di prova
Il repository di prova è tinydb, un vero database di documenti, con 226 test verdi sul commit usato.
Helix è lo strumento interno di Shopify per riportare le sue app mobile al codice nativo. Il suo post enuncia la regola senza giri di parole: ogni checkpoint passa attraverso quattro gate, in ordine.
| Gate | Cosa fa nel post di Shopify |
|---|---|
| Comportamento | Esegue test in cui ogni caso rilevante deve passare |
| Design | Chiede a un secondo modello di fare il revisore di design perfezionista |
| Review | Due agent reviewer indipendenti e isolati nel contesto; il ciclo si ripete finché entrambi approvano |
| Ingegnere | Una persona chiude il ciclo |
La frase che vende il progetto è "An attempt is allowed to be wrong. It is not allowed to ship until it isn't." Il gate di design cade per una libreria, e il post dice che una modifica di sola logica lo salta semplicemente.
Quello che il post non dà mai è un numero. L'app Shop è stata ricostruita in 12 settimane e l'app Shopify ha più di 300 schermate, e questo è tutto il registro. Non ci sono tasso di successo, costo né conteggio dei difetti sfuggiti.
Il banco di prova è fatto di cinque richieste scritte come le scrive uno sviluppatore: operazioni push e pull, una query between, la rinomina di una tabella, una ricerca ordinata e una dimensione della write cache. Dietro ogni richiesta ci sono cinque requisiti nascosti che l'agente non vede mai, le cose che si aspetterebbe un senior ingegnere attento. In totale sono 25 requisiti.
Il worker è Sonnet 5.5, eseguito in modalità headless. Cinque configurazioni per cinque task per due ripetizioni fanno 50 run. Una precisazione: le versioni esplicite di queste richieste passano al primo colpo, quindi i prompt sono stati accorciati a ciò che le persone scrivono davvero.
Senza alcun gate passano 35 requisiti su 50. Cioè il 70%, con tre run su dieci completamente puliti.
Il task di ordinamento mostra che aspetto hanno gli errori. L'agente ha indicizzato ogni documento con il campo di ordinamento, e appena un documento ne è privo l'intera ricerca va in crash. Cinque requisiti, zero superati, una chiave mancante.
L'hook che rifiuta il «fatto» e le sue tre uscite
Uno Stop hook è uno script che Claude Code esegue ogni volta che l'agente prova a chiudere il suo turno. La guida di Anthropic nomina il problema che affronta: Claude si ferma quando il lavoro sembra finito. Senza un controllo eseguibile, "sembra finito" è l'unico segnale disponibile, e il ciclo di verifica sei tu.
L'hook sta nel file di impostazioni del progetto, sotto hooks, Stop, come un unico comando con un timeout. Qui è un wrapper di una riga che chiama lo script del gate. Il contratto è il codice di uscita: 0 lascia chiudere il turno, 2 lo blocca, e qualunque cosa lo script abbia stampato sullo standard error torna all'agente come motivo.
In pratica funziona così. L'agente riassume la sua modifica e si ferma. L'hook esegue i test di accettazione, uno fallisce, e il turno non si chiude. L'agente legge il fallimento come feedback dell'hook, in questo caso un test che si aspettava un errore di tipo e non l'ha ottenuto, corregge il codice e si ferma di nuovo. Stavolta il gate tace.
Una regola nel prompt è un consiglio. Un hook no. Eppure la stessa pagina di documentazione elenca tre vie d'uscita.
| Uscita | Cosa dice la documentazione |
|---|---|
| Il tetto | Dopo che gli stop hook hanno fatto proseguire il turno 8 volte di fila, Claude Code scavalca il blocco successivo e chiude il turno. Il conteggio si azzera ogni volta che l'agente chiama un tool, e una variabile d'ambiente lo alza. |
| Il timeout | 600 secondi di default per un hook di tipo comando, e un hook che va in timeout non produce alcuna decisione. Una suite di test lenta apre il gate da sola. |
| Il flag | L'input dell'hook contiene stop_hook_active, vero quando la sessione sta già proseguendo a causa di uno stop hook. La documentazione dice di controllarlo, così non blocchi mai su una condizione che non può risolversi. |
Il gate qui usato registra quel flag e lo ignora. Si arrende da solo dopo sei blocchi, così i cicli restano contabili.
C'è un'altra uscita che nessuno documenta. Dopo essere stato bloccato una volta, l'agente è andato a cercare i test: ha eseguito cat sullo script dell'hook e ha cercato il file dei test su tutto il disco. Solo la allow list lo ha fermato. Per questo i test di accettazione stanno fuori dall'albero del repository, le impostazioni vietano la lettura dell'hook e dei test nascosti, e l'agente vede il fallimento ma mai il file.
Gate 1: i test di accettazione
Il gate dei test di accettazione è un insieme di test, scritti indipendentemente dall'implementatore, che definiscono cosa significa "fatto". Il primo gate di Shopify non sono i test dell'agente stesso. Un subagent genera casi di test in stile integrazione per ogni checkpoint, spingendo sui casi limite oltre il percorso felice, e quei test definiscono cosa significa provato.
Lo scrittore qui è quindi un Sonnet headless separato. Vede la richiesta e il repository prima della modifica, in sola lettura, non può eseguire codice, e gli viene detto di scrivere un test per ogni comportamento che un senior attento si aspetterebbe: campi mancanti, tipi sbagliati, limiti, persistenza, la cache e il comportamento esistente invariato. Scrive tra 23 e 53 test, 39 in media, in circa 40 secondi. Il file finisce fuori dal repository, quindi quando un test fallisce l'agente legge l'asserzione e mai il test.
I soli test portano il tasso di successo dal 70% all'84%: 42 requisiti su 50, e sei run completamente puliti invece di tre.
| Task | Senza test | Con i test |
|---|---|---|
| Ricerca ordinata | 5 su 10 | 10 su 10 |
| Dimensione della write cache | 8 su 10 | 10 su 10 |
| Push e pull | 4 su 10 | 4 su 10 |
Il crash per campo mancante nel task di ordinamento è esattamente il tipo di caso su cui lo scrittore fa domande. Per il task della cache, lo scrittore ha preteso un errore di valore per una dimensione della cache inferiore a uno.
Il task push e pull non si muove. Manca tre requisiti, due dei quali riguardano un documento senza il campo, e la richiesta non diceva nulla in proposito. Lo scrittore si copre proprio dove il prompt tace: il suo test accetta un'eccezione o un no-op silenzioso, quindi il gate non può recuperare un requisito che nessuno ha enunciato.
Sui 30 run con gate che includono i test, il gate ha rimandato indietro l'agente 28 volte con un requisito reale che falliva, e cinque volte con tutti i requisiti superati. Uno di questi cinque era il task di rinomina: lo scrittore voleva che i nomi non stringa fossero rifiutati, nessuno lo aveva chiesto, e l'agente ha perso un giro per questo.
La prima versione di questo gate rieseguiva i test dell'implementatore stesso. Ha lasciato passare ogni run al primo stop mentre un requisito nascosto falliva, perché i test di un agente testano ciò che ha costruito.
Il prezzo del gate è $0.20 a run invece di $0.085, e 60 secondi invece di 33.
Gate 3: due reviewer, come l'ha scritto Shopify
Il gate di review è una coppia di agent reviewer, ciascuno in un contesto nuovo. Le parole di Shopify: due agent reviewer indipendenti e isolati nel contesto controllano il nuovo codice, ogni rilievo va corretto, e il ciclo si ripete finché entrambi i reviewer approvano. La guida di Anthropic spiega perché l'isolamento conta: un reviewer in un contesto nuovo vede solo il diff e i criteri che gli dai, non il ragionamento che ha prodotto la modifica.
Ogni reviewer qui è un Sonnet headless con tool in sola lettura e al massimo 15 turni. Restituisce un verdetto come JSON strutturato: un flag approve e dei rilievi, ciascuno con gravità blocking o minor. Due reviewer girano in parallelo sulla richiesta e sul diff completo, i loro rilievi vengono combinati, e il gate blocca quando uno dei due trova qualcosa di bloccante. Una coppia costa tra 3 e 6 centesimi per stop.
| Configurazione | Requisiti superati | Tasso di successo | Blocchi |
|---|---|---|---|
| Nessun gate | 35 su 50 | 70% | nessuno |
| Solo reviewer | 34 su 50 | 68% | 1 run su 10 |
| Solo test | 42 su 50 | 84% | 6 run su 10 (3 giusti, 3 falsi) |
| Reviewer dopo i test | 41 su 50 | 82% | 2 run su 10, entrambi giusti |
I soli reviewer finiscono due punti sotto l'assenza di gate, e i reviewer dopo i test finiscono due punti sotto i soli test.
Il task di ordinamento spiega il numero. In un run entrambi i reviewer hanno bloccato: l'ordinamento indicizza il documento, un documento senza quel campo solleva un errore di chiave, e l'intera ricerca fallisce. L'agente ha corretto e ha superato cinque su cinque.
In altri due run entrambi i reviewer hanno visto lo stesso identico crash e l'hanno valutato minor. Un documento senza il campo di ordinamento solleva un semplice errore di chiave, e la docstring lo documenta. Entrambi i reviewer hanno approvato, ed entrambi i run sono finiti a zero su cinque. Stesso modello, stesso diff, stesso bug, due gravità diverse. I reviewer non hanno mai sbagliato quando bloccavano. Il problema è ciò che lasciano passare.
Un paper di agosto ha misurato la stessa forma su un benchmark pubblico. Zero-shot ha ottenuto il 77%, un reviewer il 77%, e due reviewer indipendenti il 75%, tutto nel rumore rispetto a nessuna review.
| Setup su LiveCodeBench | Punteggio |
|---|---|
| Zero-shot | 77% |
| Un reviewer | 77% |
| Due reviewer indipendenti | 75% |
| Reviewer più critico (adversarial review) | 87% |
Reviewer più critico, e il ciclo che non finisce mai
L'adversarial review è un reviewer abbinato a un critico che verifica la review attraverso un disaccordo strutturato prima che l'agente principale modifichi il codice. Lo stesso paper ha trovato che è la forma che funziona. Ha ottenuto l'87% dove due reviewer hanno ottenuto il 75%, e ha battuto la baseline più forte a cinque agenti, ferma all'82%.
| Benchmark | Zero-shot | Con adversarial review | Costo |
|---|---|---|---|
| SWE-bench | 71.6 | 75.2 | circa 4.5x i token |
Così il critico è entrato nel gate. Il reviewer A fa la review. Il critico B riceve i rilievi più il diff e marchia ciascuno come confermato o respinto. Solo un rilievo bloccante confermato chiude il gate.
Su questo repository il risultato è stato 39 su 50, cioè il 78%. La coppia reviewer e critico non ha bloccato nessuna volta in dieci run. Durante la validazione ha respinto un falso rilievo su cui la forma a due reviewer avrebbe bloccato.
Quello che il critico non ha potuto risolvere è il ciclo. Tre run hanno raggiunto sei blocchi sullo stesso test di accettazione, e il gate si è arreso con il requisito reale ancora fallito a ogni stop. In due di questi il test bloccante era un errore dello scrittore, e l'agente lo ha detto: non avrebbe modificato il codice per un test che non poteva leggere.
Il terzo è quello da ricordare. Il test di accettazione voleva che un update fallito lasciasse intatti tutti i documenti, quindi l'agente ha riscritto il metodo update della tabella. I reviewer hanno bloccato, perché la modifica al metodo update non era stata richiesta e il task erano due nuove funzioni. L'agente ha annullato. Il suo ultimo messaggio diceva che il test di accettazione e la code review chiedevano cose opposte, quindi si era fermato senza cambiare nulla. Sei blocchi dopo, il contatore lo ha lasciato uscire.
Il punto di resa qui sta a sei. Il tetto di Claude Code è otto blocchi consecutivi e si azzera a ogni chiamata di tool, quindi in cinquanta run non è scattato nemmeno una volta.
Il gate 4 sei tu: cosa è arrivato all'umano, il conto, il kit
Il quarto gate è l'ingegnere, e il post di Shopify promette che i gate non diventano più permissivi quando nessuno guarda. Ciò che arriva all'ingegnere è quello che gli altri gate lasciano passare. Il registro qui sotto risponde a quale gate si è ripagato.
| Configurazione | Superati su 50 | Tasso di successo | Costo per run | Rimasti all'umano |
|---|---|---|---|---|
| Nessun gate | 35 | 70% | $0.085 | 15 |
| Test di accettazione | 42 | 84% | $0.20 | 8 |
| Solo reviewer | 34 | 68% | $0.15 | 16 |
| Test con reviewer | 41 | 82% | $0.28 | 9 |
| Test con reviewer più critico | 39 | 78% | $0.28 | 11 |
Ogni riga con gate richiede da 54 a 86 secondi per run, contro i 33 senza gate.
I blocchi su tutti i cinquanta run si ripartiscono così. I test hanno rimandato indietro l'agente 28 volte a ragione e cinque volte a torto. I reviewer hanno bloccato tre volte, mai a torto, e tre cicli si sono arresi. L'intera matrice è costata $9.95 di spesa API a prezzo di listino, come la riporta la CLI, e circa 3136 secondi, cioè 52 minuti, di tempo di worker sommato.
Il verdetto per un repository come questo: tieni il gate dei test, con uno scrittore che non è l'implementatore e test che l'agente non può modificare. Aspettati circa un falso blocco su sei, e dai al ciclo un contatore. Il gate dei due reviewer come l'ha scritto Shopify non ha mosso il tasso di successo qui, in nessuna delle due forme. Se lo tieni, tienilo come consiglio nel feedback, non come soglia.
Una trappola lungo la strada. La documentazione descrive un flag nudo che salta hook, skill, plugin e memoria per le chiamate da script. Con un login da abbonamento legge solo una API key e restituisce un errore, quindi ogni reviewer viene isolato con una fonte di impostazioni vuota e gli hook disattivati.
I limiti sono reali: un repository, un modello worker, cinque task, due ripetizioni per cella. Una differenza di due punti tra due righe è rumore. I quattordici punti tra nessun gate e il gate dei test non lo sono. Il gate di design non è stato costruito, e l'ingegnere non è stato cronometrato.
Dei quattro gate di Shopify, su questo repository, solo i test hanno ripagato. L'intero kit è nel pack gratuito: il file di impostazioni con l'hook e la deny list, lo stop script, il gate, i comandi writer e reviewer con i loro schemi, e il template del registro.
AIDive