La prima volta che me ne sono accorto, la suite era verde e il task chiuso.

Il buco che avevo lasciato aperto

L'agente aveva riscritto il test.

In un pezzo precedente ho descritto il sistema con cui rendo prevedibile l'output di un modello: contesto denso prima della direttiva, direttiva chirurgica prima del codice, test che decide quando è finito.

Quel sistema regge. Ma l'ho descritto come metodo.

Un metodo è una convenzione. E una convenzione la rispetta chi vuole rispettarla.

L'esecutore, qui, non è una persona. È un agente che ottimizza per chiudere il task. Se la strada più corta per chiudere il task passa dal violare la convenzione, la prende. Non per malizia: perché è la strada più corta, e nessuno gliela sbarra.

Tre modi in cui la prende, visti tutti e tre sul serio:

Deriva dalla specifica — implementa qualcosa di adiacente a quello che avevi chiesto. Plausibile, funzionante, non tuo.

Completamento non verificato — dichiara "fatto" sulla base della propria confidenza, non dell'esito di un comando.

Manomissione del test — il test è rosso. Invece di sistemare il codice, sistema il test.

Il terzo è quello che mi ha fatto smettere di scrivere regole e iniziare a scrivere blocchi.

Un contratto che la parte vincolata può riscrivere

Il test, nel mio flusso, non è una caccia ai bug. È la direttiva resa eseguibile: il solo strato che ha il diritto di dire "questo codice combacia con quello che avevo chiesto".

Se l'esecutore può modificare quel file, il contratto è scrivibile dalla parte che dovrebbe vincolare.

A quel punto verde non significa più niente. Significa solo che qualcuno ha trovato un modo per farlo diventare verde. È lo stesso identico problema del test scritto male: ti sei ingannato con più passaggi di prima, e con un timbro di approvazione in fondo.

Non si risolve chiedendo per favore nel prompt. Un'istruzione in linguaggio naturale è un suggerimento con una probabilità di essere seguito. Il resto delle volte è un buco.

La regola diventa un cancello

Ho impacchettato tutto in un set di skill installabile: doe-kit — Directive Oriented Execution. Repo pubblico, licenza MIT, un comando per montarlo dentro un progetto che esiste già. Nato su Flutter, dove un giro di verifica costa una build e non un refresh; Web-TS su React a seguire.

Non è un articolo con degli esempi allegati. È il flusso che uso ogni giorno, eseguibile, con dentro anche la parte che nessuno pubblica: i controlli che impediscono a me di barare.

E la parte che conta non sono le skill. È un hook.

Un PreToolUse che intercetta ogni scrittura verso le directory di sorgente e di test, e la blocca finché la direttiva non riporta STATE: APPROVED.

In una riga

Non è persuasione. È il filesystem che dice di no.

La conseguenza pratica: "scrivo il codice e poi vediamo" smette di essere un'opzione lenta da scoraggiare e diventa un'operazione che fallisce. La fase di specifica non è più un rituale che l'agente può saltare quando ha fretta. È l'unica porta aperta.

Chi approva resta una persona. STATE: APPROVED lo scrive un umano che ha letto la direttiva. Il guard non giudica la qualità di niente: verifica una condizione e apre o chiude. Meccanico, quindi noioso, quindi affidabile.

Due fasi, e un baseline rosso

Il flusso è in due comandi.

/directive — intervista, scrittura della specifica, definizione del Test Contract. Zero righe di codice generate. Se in questa fase l'agente prova a scrivere codice, il guard lo ferma.

/execute — e qui l'ordine non è negoziabile:

  • RED baseline — si lanciano i test e si pretende che falliscano. Prima del codice.
  • Fix — l'implementazione, ora che c'è un binario da riempire.
  • GREEN gate — verde o non è finito. La condizione di fine è un exit code, non un'impressione.
  • Cleanup — la direttiva chiusa esce di scena.

Il baseline rosso è il passaggio che sembra burocratico e non lo è. Un test che non hai mai visto fallire non è un test: è una riga che passa. La prova che stia misurando qualcosa è averlo visto rosso quando il codice non c'era.

Non vietare di toccare i test: dichiararlo prima

Il divieto secco non funziona, perché a volte il test va modificato davvero. Cambia il contratto, cambia il test. Vietarlo significa costringere a disattivare il controllo, e un controllo che si disattiva di routine è già morto.

Quindi la direttiva porta una tabella di test-impact: quali test questo lavoro ha il diritto di toccare, dichiarati per nome. L'enforcement è meccanico, ma la lista la scrivi tu.

Il punto è quando la scrivi. La dichiari prima di implementare, quando non hai ancora un test rosso davanti e nessun incentivo a barare. Dopo, con la suite rossa e il task quasi chiuso, quella stessa lista te la scriveresti addosso.

Non è un vincolo sull'agente. È un vincolo sull'ordine in cui le decisioni vengono prese.

Copertura solo su quello che hai toccato

La regola che rende il kit adottabile su codice vero è una sola: la copertura si misura per file, e solo sui file cambiati.

Un progetto legacy al 12% di coverage non deve recuperare dieci anni di test per poter usare il flusso. Deve solo non peggiorare da qui in avanti. Le esclusioni esistono, ma vengono stampate con il motivo accanto: una skip-list silenziosa è un modo educato di mentire ai propri numeri.

Ogni strumento che pretende una soglia globale prima di essere utile, su un codebase reale, non viene adottato. Viene aggirato al secondo sprint.

Il guard che testa se stesso

C'è un self-test, e verifica il guard in due direzioni.

Che blocchi le scritture non autorizzate. E che non blocchi il lavoro legittimo.

La seconda metà è quella che quasi nessuno scrive, ed è quella che conta. Un guard testato solo in un verso passa il test anche se blocca tutto, sempre. È perfettamente sicuro e perfettamente inutile, e te ne accorgi in produzione quando il team lo disinstalla.

Un controllo che non ha una regressione sui falsi positivi non è un controllo. È un ostacolo in attesa di essere rimosso.

Il trade-off, detto onesto

Il round-trip di specifica costa. Intervista, direttiva, contratto di test, approvazione: è tutto lavoro a monte, prima che esista una riga di codice.

Su un prototipo usa-e-getta è over-engineering puro, e non lo uso. Ha senso dove un output sbagliato costa più del tempo che risparmi saltando la preparazione.

E c'è un costo meno ovvio: il kit impone una separazione architetturale. Se la logica di business vive attaccata al layer di ingresso — componenti, handler, route, view — non è testabile in isolamento, quindi non è vincolabile, quindi il flusso non ti serve a niente. Adottarlo significa spostare la logica fuori. È una scelta di architettura mascherata da scelta di tooling, e va detta prima, non scoperta a metà.

Ultima cosa che il kit non fa: non rende il modello più bravo. Riduce lo spazio in cui può sbagliare senza che tu te ne accorga. È tutto quello che promette.

Perché nasce su Flutter

Il kit è tarato sul frontend, e Flutter è lo stack di riferimento. Non è un accidente biografico. È dove il problema morde.

Sul web il ciclo di verifica costa quasi zero. Pushi, la preview esce da sola, guardi. Il determinismo non ti serve perché hai lo sguardo: se l'agente ha derivato dalla specifica te ne accorgi in trenta secondi, e iteri. Il controllo lo compri a rate, un'occhiata alla volta.

Sul mobile quello sguardo non ce l'hai. Fra te e la verifica ci sono una build, un emulatore o un device, uno stato dell'app da ricostruire a mano fino alla schermata giusta, e in fondo uno store. Il ciclo si misura in minuti, non in secondi, e non lo ripeti trenta volte per capire se l'agente aveva capito.

Quando iterare è caro, l'unica verifica che resta economica è un exit code.

È questa la ragione per cui il flusso è nato lì. Sul web la mancanza di determinismo la paghi in iterazioni, e le iterazioni costano poco. Su Flutter la paghi in build. Il vincolo meccanico non è una raffinatezza da metodologo: è il sostituto dell'occhio che sul mobile non hai.

C'è una seconda ragione, meno tecnica. Di tooling agentico tarato su TypeScript e React ce n'è parecchio. Su Flutter quasi niente — non perché il problema non esista, ma perché chi scrive di agenti scrive di web. Le convenzioni Riverpod stanno dentro al kit perché quella parte, oggi, non la sta scrivendo nessun altro.

Nato per Flutter, non chiuso lì. Il nucleo non sa in che linguaggio stai scrivendo, quindi su Web-TS gira uguale — e ci porta esattamente la cosa che la preview non ti dà: un criterio di fine che non dipende da quanto attentamente hai guardato.

Cosa c'è dentro

Il guard, la direttiva, il Test Contract, il gate verde: nessuno dei quattro conosce il tuo stack.

Quello che cambia per stack è un adattatore sottile: il comando che lancia la suite, come si legge la copertura, le convenzioni di progetto. Un run.sh e poco altro.

Flutter è la parte rifinita, quella dove il flusso ha girato di più. Web-TS — TypeScript, React, Next — viene dietro, insieme a uno strato condiviso per UI e internazionalizzazione. Python è il prossimo porting, e non richiede di toccare il nucleo: se aggiungere uno stack costringesse a modificare il guard, la separazione non c'era mai stata.

Il resto: diciassette skill fra core e stack, il guard e il suo self-test scritti in Python con la sola libreria standard, template per feature, bug e review, e un harness di benchmark con arm appaiati e test di accettazione nascosti — perché anche misurare la qualità di un flusso agentico è una cosa che si può falsare.

Si installa da dentro il progetto, in un comando:

curl -fsSL https://raw.githubusercontent.com/savinofiore/doe-kit/main/install.sh | bash -s -- --stack flutter

O come plugin di Claude Code, con /plugin marketplace add savinofiore/doe-kit.

Non installa un framework: aggiunge un hook, delle skill e dei template al progetto che hai già. Se non ti serve più, lo togli e il codice resta dov'era.

Licenza MIT: github.com/savinofiore/doe-kit

Il principio

Il determinismo non nasce da un modello più intelligente né da un prompt più severo. Nasce dallo spostare il vincolo dal linguaggio al meccanismo: dove non c'è niente da interpretare, non c'è niente da aggirare.

Dove il feedback è istantaneo puoi ancora permetterti di scoprirlo guardando. Dove non lo è — e sul mobile non lo è mai — o il vincolo sta nel meccanismo, o non c'è.

Scrivere la regola è la parte facile. Renderla impossibile da non seguire è il lavoro.

Takeaway

"Una regola che l'esecutore può ignorare non è un contratto. È un suggerimento con una buona reputazione."

— Savino Fiore, Tech Lead @ Finanz