A fine agosto ho pubblicato doe-kit, un plugin che toglie all'agente il diritto di dire «ho finito». Il meccanismo era semplice da spiegare: prima del codice si scrive una direttiva, un umano la approva a mano mettendo STATE: APPROVED dentro il file, e finché quella riga non c'è un hook PreToolUse rifiuta ogni scrittura verso i sorgenti e i test.
Funzionava. Il problema che risolveva era reale, e resta reale: un agente implementa qualcosa di adiacente a quello che gli hai chiesto, poi dichiara fatto sulla base della propria confidenza, e la sua confidenza non è correlata alla correttezza.
Poi ho fatto il conto di quanto costava tenerlo in piedi.
Il conto che non avevo fatto
Una direttiva è un documento. L'esempio che ho spedito nel repo, quello che uso per far vedere com'è fatta, sono 812 parole.
Quel documento attraversa il contesto tre volte.
La prima quando l'agente lo scrive: legge i file coinvolti, cerca tutti i test che li toccano, esegue il gate di partenza e produce la specifica. La seconda quando lo rileggo io per approvarlo, perché l'approvazione non è un click, è la parte del processo che ha valore. La terza quando /execute lo riapre per eseguirlo.
Poi il task finisce e il documento viene cancellato.
Non è un dettaglio di pulizia. È il punto: l'unico artefatto che il processo produce con cura, quello che l'umano legge riga per riga, è anche l'unico che non sopravvive al task.
I test li scrivevo due volte
C'è una cosa peggiore del costo, ed è la ragione per cui ho smesso.
Dentro la direttiva c'è una sezione che si chiama Test Contract. Non è una descrizione dei test: sono i test, scritti come codice, dentro un documento markdown. Cinque casi, con le loro assertion.
Poi /execute li riscrive nei file di test veri.
Quindi la stessa suite esisteva due volte. Una nel documento, dove serviva a farsi approvare. Una nel repository, dove serviva a girare. E alla fine del task cancellavo la prima.
Scrivevo i test, li facevo leggere a un umano, li facevo riscrivere all'agente, e buttavo l'originale.
Quando l'ho visto scritto così è diventato ovvio: il documento non era la specifica. La specifica erano i test, e i test stavano già andando nel repository per conto loro. Il documento era l'imballaggio, e lo pagavo tre volte per poi buttarlo.
Cosa ho tenuto
Non tutto era imballaggio. La parte buona di /directive era l'intervista.
Su un task con lo scopo aperto, l'agente che si mette a scrivere test senza chiedere niente scrive test contro un comportamento immaginato. Dove vivono i codici sconto, se si sommano, su cosa si applicano, come emerge un rifiuto: sono decisioni di prodotto, e indovinarle produce una suite verde che verifica la cosa sbagliata.
Quell'intervista è sopravvissuta, come skill a sé: si chiama grill. Fa una cosa che l'intervista dentro la direttiva non faceva, e che è la differenza fra due messaggi e otto: mette tutta la frontiera aperta in un colpo solo, ognuna con la sua raccomandazione, e tiene da parte le domande che dipendono da una risposta non ancora data.
Se non hai deciso da dove arrivano i codici, chiedere cosa succede quando la fonte è irraggiungibile raccoglie solo un'ipotesi. Quella domanda torna al giro dopo.
L'intervista era la parte buona della direttiva. Il documento no.
Cosa ho tolto
lexi è quello che resta quando togli l'artefatto intermedio.
Nessun file di specifica, nessun file di approvazione, niente da cancellare a fine task. I test sono la specifica, e restano nel repository, che è dove stavano andando comunque.
Il flusso è questo:
task -> [grill, solo se lo scopo è aperto] -> seam confermati in un messaggio
-> per fetta: RED -> implementazione minima -> GREEN -> fetta successiva
C'è un solo checkpoint, ed è un messaggio, non un file: l'agente dichiara le funzioni che metterà sotto test, il loro test speculare, l'elenco delle fette e quali assertion esistenti si rompono. Si risponde in chat. Non c'è niente da editare a mano, e non c'è nessuno STATE da cambiare.
Due regole, un hook, 178 righe
Le regole che restano applicate meccanicamente invece che per iscritto sono due.
Un file sorgente sotto un percorso dichiarato testable non si può scrivere prima che esista il suo test speculare. Questo è il test-first, imposto dal filesystem invece che dal prompt.
Un file di test tracciato da git non può vedersi riscrivere le assertion. Aggiungere un caso a un file esistente è permesso, riscriverne uno no: quella è una modifica breaking, si concorda prima e si registra in .lexi/allow, che è scratch di un task solo ed è gitignorato.
Tutto ciò che sta fuori da testable resta libero. Layer di UI, design token, codice generato, binding di piattaforma: nessun test, nessuna domanda. La whitelist è volutamente stretta, così il guard morde sulla logica e non tassa una correzione di stile da una riga.
Il guard di doe-kit è 657 righe di Python. Quello di lexi ne ha 178.
Il conto finale
| doe-kit | lexi | |
|---|---|---|
| skill | 16 | 4 |
| guard | 657 righe | 178 |
| artefatto intermedio | direttiva, 812 parole nell'esempio | nessuno |
| destino dell'artefatto | cancellato a fine task | i test restano |
| passaggi nel contesto | tre, per un documento che si butta | uno, per file che restano |
Delle quattro skill, due non sono territorio mio: tdd tiene cosa rende un test degno di essere tenuto, e grill le domande. Seam, indipendenza delle assertion e anti-pattern hanno decenni, e la raccolta di mattpocock/skills copre lo stesso terreno meglio e più in largo. Stanno dentro lexi per una ragione pratica: me ne servono esattamente due, e installarne venticinque per arrivarci è un cattivo scambio.
La terza dipendenza è esterna e resta esterna. ponytail governa ogni passo verde: questa cosa deve esistere, c'è già in questa codebase, lo fa la stdlib o la piattaforma, si può fare in una riga. È ambiente una volta installato e lexi non lo ripete. Scrivere meno codice è una disciplina intera e qualcuno la mantiene già.
Cosa non copre
Il guard intercetta Edit, Write, MultiEdit e NotebookEdit. Le scritture che passano da Bash, un sed -i o un heredoc, gli camminano accanto senza essere viste.
La distinzione fra riscrittura e aggiunta è contenimento di sottostringa, non un diff vero. Legge un'aggiunta come un'aggiunta e una assertion riscritta come una riscrittura, ma un edit costruito apposta lo inganna.
E l'agente può scrivere da solo dentro .lexi/allow. La skill gli dice di chiedere prima, e il file registra cosa è stato rilasciato e quando.
Non è un muro. È attrito più una traccia di controllo, che su un esecutore non deterministico è tutto quello che si può ottenere onestamente.
Il resto è la regola che ho imparato buttando via il primo kit: se un artefatto non sopravvive al task, non era la specifica. Era una cerimonia.