# Pere Montpeó — tots els apunts
> Índex navegable: https://peremontpeo.dev/llms.txt
# craftr: el producte que funcionava i que ningú podia omplir
URL: https://peremontpeo.dev/blog/postmortem-craftr/
Data: 2026-10-20
Descripció: Vaig construir el Lightroom dels usuaris de Cricut en quatre setmanes amb agents de codi. Funcionava. La visió tenia un forat que cap refactor tapa: per a la majoria d'usuaris, la biblioteca real és la de Cricut Design Space, i d'allà no en surt res.
Fa sis mesos vaig tenir per primera vegada un producte propi a punt per llançar. Funcionava, la usabilitat era bona, Stripe connectat, domini reservat. I un bon dia em vaig aturar a pensar en el primer pas que faria qualsevol usuari nou: «puja la teva biblioteca de dissenys». Com? Des d'on? Per a la majoria d'usuaris de Cricut, la biblioteca real és la de Design Space, i aquesta no es mou.
Aquest és el postmortem de [craftr](https://usecraftr.com): un mes de feina, 162 commits, un producte que funcionava amb un defecte que cap refactor arregla. I una lliçó sobre què ha canviat la IA (i què no) a l'hora de construir les teves pròpies eines.
## D'on surt craftr
Faig manualitats amb una Cricut, una màquina de retallar que treballa a partir de fitxers vectorials (SVG, DXF). I feia servir [QuiverAI](https://quiver.ai/), un model que genera vectors a partir de text ([en vaig parlar aquí](/blog/quiverai-ai-sdk)). Els dos mons encaixaven massa bé: les màquines de retallar mengen SVG, i la IA ja sap produir SVG.
Per tallar amb una Cricut passes obligatòriament per Design Space, el programari de la marca: hi puges el fitxer, l'ajustes i l'envies a la màquina. Molts usuaris no guarden l'original, i el disseny viu només allà, igual que els que compren dins la mateixa plataforma.
Alhora, el moment més feixuc del flux de treball no era dissenyar: era organitzar. La biblioteca de dissenys de Design Space és lenta i farragosa, i els meus dissenys originals estaven escampats entre l'ordinador i Dropbox. Vaig pensar el que pensa qualsevol que té un mal i sap dirigir agents: això ho puc arreglar. Un gestor de biblioteca de dissenys, el Lightroom dels usuaris de Cricut. Una biblioteca individual, sense equips, pensada també per a qui en viu. Primer per a Cricut, després hi afegiria fonts i fitxers de brodat.
També sabia que, si funcionava, Cricut ho acabaria copiant a la seva plataforma. Això em donava una finestra de temps, i amb agents de codi n'hi havia prou amb aquesta finestra per construir-ho tot.
## Problemes reals: bundles i llicències
Abans de picar res, vaig passar una estona a Reddit i altres comunitats per veure si el problema era compartit. Ho era, i amb dues cares que jo no patia:
1. **Els bundles de marketplaces.** Compres un paquet a Etsy o Creative Fabrica i reps un ZIP amb dotze fitxers del mateix disseny en quatre formats. Quin fas servir? Aquí craftr detectava duplicats, triava un fitxer com a principal i la resta com a variants, i et suggeria el format adequat per a la teva màquina (SVG per a Cricut, DXF per a Silhouette, l'altra gran marca de màquines de tall).
2. **Les llicències.** Molts usuaris de Cricut són petits negocis que personalitzen productes i els venen a Etsy. Cada disseny comprat porta una llicència (ús personal, comercial, print-on-demand) que ningú registra, i un dia arriba una reclamació per drets d'autor. craftr guardava l'origen, el tipus de llicència, una matriu de drets i la captura de la llicència com a prova.
## Què vaig construir
Vaig dirigir agents de codi i en quatre setmanes (del 3 al 31 de març) el projecte va acumular 162 commits. El que en va sortir:
- Un gestor web ràpid: cache agressiva i feina pesant en cues asíncrones perquè la navegació voli.
- Visors especialitzats per a il·lustracions vectorials, amb separació per capes i colors.
- Etiquetatge automàtic del contingut i categories que s'organitzen soles.
- Importació de ZIP de bundles amb detecció de duplicats i rol per a cada fitxer.
- Registre de llicències amb origen, tipus i prova.
- Generació de dissenys amb QuiverAI, amb streaming del SVG en directe.
- I per sota: Next.js, Drizzle, Postgres amb seguretat a nivell de fila (RLS), autenticació pròpia, disseny propi i Stripe cobrant subscripcions Pro.
El resultat, tant tècnic com d'usabilitat, em sembla excel·lent. Fins i tot vaig demanar a un agent una anàlisi de llançament: analitzava canals i em deia que, amb tracció als fòrums i comunitats, em faria ric ràpidament.
## El detall que no vaig tenir en compte
La promesa de craftr era «tota la teva biblioteca en un lloc». Però perquè hi arribi, l'usuari ha de poder portar-hi els seus dissenys. Els que tens en fitxers, perfecte: els ZIP dels bundles, els SVG comprats, els originals que guardes a Dropbox, tot això s'importa. El problema és que, per a la majoria d'usuaris de Cricut, la biblioteca real és la de Design Space, i aquesta no es mou: no hi ha exportació, ni massiva ni individual, ni cap API, ni cap altra via que un usuari normal pugui fer servir. craftr funcionava, però l'usuari típic no tenia amb què omplir-lo.
No ho vaig veure perquè jo era l'excepció: tenia els meus originals a Dropbox. I els problemes dels altres, els bundles i les llicències, els vaig validar amb una biblioteca que no calia rescatar.
La metàfora de Lightroom es trencava al primer pas. Un fotògraf pot importar el seu catàleg de mil fotos a qualsevol programa nou. Un usuari de Cricut no pot moure la seva biblioteca enlloc. Intentar-ho per la porta del darrere tampoc és una opció: les condicions de Cricut prohibeixen l'enginyeria inversa, i no és una batalla per lliurar sol. La frase «ho descarregaré tot i ho posaré a la plataforma nova» és impossible de dir a un usuari de Cricut.
Aquell dia vaig aparcar el projecte.
## Construir ja no és el coll d'ampolla
La tesi que volia provar amb aquest projecte es manté intacta, fins i tot reforçada: la IA està democratitzant el desenvolupament d'eines. Qui té un problema sap millor que ningú què necessita, i per primer cop es pot construir l'eina a mida sense equip, sense finançament i sense mesos de feina. Jo sol, dirigint agents, vaig muntar en un mes infraestructura de producció amb pagaments, seguretat i un disseny propi. Fa cinc anys això era un equip i un trimestre. Ara és un mes i una subscripció a un agent.
El que la IA no canvia és l'altra meitat de l'equació: conèixer el domini. I el domini inclou les regles de l'ecosistema: els formats, les migracions i, sobretot, els jardins tancats. Un agent executa les teves suposicions amb una eficàcia impressionant. També les equivocades. El meu agent de llançament projectava tracció sense cap dubte. Els agents no validen per tu; amplifiquen el que ja creus.
Vaig validar que el problema era real, però no que la solució fos viable. Si ho refés, la primera setmana no hi hauria codi: buscaria «export library from Cricut Design Space» i llegiria què diuen les comunitats d'usuaris abans d'escriure la primera línia. Construir és tan barat ara que la validació és l'única cosa cara que et queda.
## On queda tot plegat
Aparcat, amb Stripe a punt i beta testers a mig reclutar. Però no enterrat: dues portes han quedat obertes.
La primera és la comunitat d'impressió 3D, que m'han suggerit diverses persones. El problema és el mateix (una biblioteca de fitxers STL ingovernable), però l'ecosistema és l'oposat: formats oberts, fitxers al teu disc, res a veure amb jardins tancats. La segona és el fil que vaig detectar investigant els petits negocis: el que de debò els cal s'acosta més a un CRM que a un gestor de fitxers. Projectes per client, cobrat o no cobrat, llicències per saber què pots revendre.
De moment, craftr em serveix de demostració: les eines ja les podem fer tots. El que continua sent difícil, i continua sent feina nostra, és saber què es pot fer abans de fer-ho.
---
# pdrop: el meu Artifacts personal
URL: https://peremontpeo.dev/blog/pdrop/
Data: 2026-10-11
Descripció: Per què m'he fet un servei per publicar l'HTML que em generen els agents a una URL privada, com és per dins, com el va construir un equip d'agents i com el faig servir.
Els agents em generen cada dia dashboards, infografies i petits informes en HTML. Amb els Artifacts de Claude Code és trivial: l'agent puja un fitxer i surt una URL privada, amb versions, sense aprovacions pel mig. El problema és que viu a la infraestructura d'un tercer i només serveix per a aquell agent.
Jo tinc més agents: al portàtil, a OpenClaw al servidor de casa, i els que vindran. Tots viuen a la meva tailnet, la xarxa privada de [Tailscale](https://tailscale.com) que uneix els meus dispositius. Volia el mateix model per a tots ells, a casa, amb una regla clara: **res surt a internet si no ho decideixo jo**. Així va néixer pdrop.
## Per què em va sorgir
Quan un agent em fa un informe, el vull veure al mòbil, el vull poder tornar a obrir la setmana vinent i, de tant en tant, el vull enviar a algú. Les dues primeres coses demanen que l'agent pugui deixar-lo en algun lloc sense preguntar-me res. La tercera demana just el contrari: que res surti a internet sense que jo ho hagi mirat.
Tot el disseny penja d'aquestes dues meitats: **l'agent puja sense demanar permís, i el que es publica a internet ho decideix un humà**. Per no embolicar-me, distingeixo dos verbs: *pujar* és deixar una versió privada dins la tailnet; *publicar* és fer-ne una còpia visible des de fora.
No em protegeixo d'un agent maliciós, sinó dels accidents: un agent que publica per un `--public` inventat, que canvia el que veu un tercer o que esborra una cosa per error. Un agent amb control del meu navegador pot fer el que faig jo, i ho accepto. Aquesta honestedat simplifica molt el disseny.
## Com és per dins
pdrop corre en un servidor amb tres peces:
- **L'ingest** és l'únic codi propi. Rep un `.html` o un `.zip`, el valida, el desa com a versió nova i retorna la URL.
- **Caddy** serveix els fitxers directament del disc, sense passar per codi meu. Si l'ingest cau, tot el que ja hi ha segueix viu. La zona privada només escolta a la IP de Tailscale; la pública surt per un túnel de Cloudflare.
- **Una UI** petita on veig els artefactes, en previsualitzo les versions i decideixo què es publica.
Cada pujada crea una versió nova i immutable (`v1`, `v2`, `v3`...), i un enllaç `current` apunta a la més recent. Si l'agent la fa malbé, torna a l'anterior. La clau és la publicació: la URL pública no apunta a `current`, sinó a una versió concreta. Una versió publicada no canvia sota els peus de qui la mira, encara que l'agent hi pugi deu versions més.
Hi ha dos tokens. El de l'agent pot pujar, llistar i tornar enrere. El meu, que només viu al navegador, és l'únic que pot publicar i esborrar. Quan un agent intenta una acció d'humà, rep un error que li diu que m'ho demani.
## Com ha anat
Abans d'escriure codi vaig fer revisar el document de disseny dues vegades: primer Claude i després un «oracle», un agent amb el model més gran que només llegeix. Entre les dues passades van sortir 61 punts. Dos eren greus: la zona privada només la protegia el DNS (qualsevol que conegués la IP del servidor hi podia arribar), i el meu token vivia en un fitxer que els agents podien llegir. Arreglar-ho va costar uns paràgrafs del document. Un cop implementat, hauria costat una migració.
La implementació la va fer un equip d'agents un dissabte al matí. Una sessió feia de manager: no escrivia codi, repartia la feina, verificava tests i lint, i feia els commits. Cada peça la construïa un agent i l'atacava un altre, un revisor que no havia vist el procés. En van sortir unes 8.400 línies i més de 300 tests.
El que més valor va aportar van ser els revisors. Van trobar errors que els tests de l'autor no veien: una URL pública que sobrevivia a l'esborrament de l'artefacte, pujades tallades que es quedaven penjades per sempre, o el mateix exemple de la documentació per als agents, que no funcionava. Els bons els van trobar executant coses, no llegint codi.
Encara no està acabat del tot. Comprovar des de fora que la zona privada no respon és feina meva: un agent que comprova la seva pròpia seguretat des de dins de la xarxa no demostra res. Ho faré amb el mòbil fora de casa.
## Com el faig servir
Als agents els dono el token d'agent i una línia de documentació. Quan acaben un informe, el pugen amb la CLI de pdrop i em retornen la URL. L'obro al mòbil o al portàtil, sempre dins la tailnet. Si l'agent el refà, la mateixa URL mostra la versió nova, i si la nova és pitjor, l'anterior encara hi és.
Quan vull compartir alguna cosa, obro la UI, la previsualitzo i la publico. Abans de publicar, pdrop hi busca coses que no haurien de sortir (emails, IBAN, claus d'API) i m'avisa. No és una garantia, és una segona mirada. La URL pública caduca sola, i la puc renovar o despublicar quan vulgui.
El resultat és el que buscava: els agents treballen sense demanar-me res, i jo només intervinc en l'únic moment que importa, quan alguna cosa surt a internet.
---
# Tres intents per fer un assistent d'IA que serveixi: de les eines a les capacitats
URL: https://peremontpeo.dev/blog/assistent-ia-goil-capacitats/
Data: 2026-10-09
Descripció: Com he passat d'un assistent amb una eina per cada funcionalitat, a una exploració amb UI generada pel model, a un assistent amb capacitats que el model proposa i les persones aproven. Què va fallar i què he après.
A [Goil](https://goil.app) fem una plataforma perquè negocis (clubs, acadèmies, gestories, empreses, ajuntaments) es comuniquin amb la seva gent i la gestionin: una app amb la seva marca per als usuaris i un backoffice per a l'equip. Al gener vaig explicar [com vaig muntar el RAG](/blog/rag-pipeline-goil) perquè la IA conegués el context de cada negoci.
Aquest post explica el que va venir després: posar un assistent dins del producte que no només respongui, sinó que faci coses. M'hi he equivocat dues vegades abans d'arribar a una cosa que m'agrada. Les errades són la part interessant.
## Primer intent: una eina per cada funcionalitat
La idea més òbvia: un assistent dins del producte amb un conjunt d'eines escrites a mà, cadascuna lligada a una funcionalitat. «Quins esdeveniments tinc aquesta setmana?» crida una eina. «Crea un avís» en crida una altra.
Funciona a la demo. A producció, els problemes:
- **Els models d'aleshores triaven malament l'eina.** Els econòmics sobretot. Dues eines que s'assemblen, i en triaven la que no tocava.
- **Com més eines, pitjor.** Cada eina nova entra al context a cada petició. Més confusió, més cost, més latència. Afegir capacitat feia l'assistent pitjor.
- **Cada funcionalitat nova demanava una eina nova.** El producte avançava a un ritme i l'assistent anava sempre per darrere, com una segona capa que calia mantenir.
El problema no era el model. Era que havia convertit el catàleg del producte en el catàleg d'eines del model, i tots dos creixen a ritmes molt diferents.
## Segon intent: especialistes i interfície generada
La premissa era descarregar l'assistent d'eines. En lloc d'un sol assistent amb tot, diversos assistents especialitzats, cadascun amb les seves eines i la seva expertesa: un per a calendari i reserves, un altre per a notificacions, un altre per a gestió d'usuaris. Cada agent veu poques eines, així que en teoria cadascun tria bé.
En una branca d'exploració ho vaig provar, i de pas vaig donar més llibertat al model en un altre eix. El model rebia un catàleg de components i emetia una interfície en un llenguatge de marcatge pensat per això. També vaig provar un subagent que generava HTML per visualitzar dades. I, per no dependre de les eines que ja tenia, vaig començar a crear endpoints pensats només per a l'agent.
Va ser una bona exploració perquè va fallar de maneres molt clares:
- **Repartir les eines no resolia el problema, el movia.** Algú ha d'encaminar cada petició a l'especialista que toca, i les peticions que creuen dominis no encaixen en cap. Compartir context entre ells i donar-los memòria comuna era una feina nova que abans no existia. L'encaminament, el context i la memòria no escalaven millor que les eines.
- **Cap validació al servidor.** El que el model emetia es renderitzava tal qual. Un error del model era un error a la pantalla de l'usuari.
- **El model copiava valors dins la UI.** Si la xifra surt del model i no de la dada, la xifra pot ser incorrecta i ningú ho sabria.
- **El client controlava part del prompt.** Una part del que dirigia el comportament del model viatjava des del client. Mala idea.
- **L'HTML generat ignorava el sistema de disseny.** Cada resposta tenia la seva pròpia estètica. Res no semblava del producte.
- **Els endpoints a mida no escalen.** Era una API paral·lela per a l'agent, amb els seus contractes, que calia mantenir al costat de la bona.
Aquesta branca no va arribar a producció, i va fer bé de no arribar-hi. Però em va deixar quatre idees clares: repartir les eines entre agents no elimina el problema, el converteix en un d'encaminament i de context compartit; el que el model produeix ha de passar per validació; les dades han d'anar per referència i no per còpia; i l'agent ha d'usar el que el producte ja fa, no una versió paral·lela.
## Tercer intent: un assistent, moltes capacitats
La resposta ha estat girar el plantejament. Hi ha un únic assistent. Les seves eines i les seccions del seu prompt es munten a cada torn segons les **capacitats** habilitades per a aquella persona. Tres coses decideixen què té: el seu rol, el que inclou el pla del negoci i un interruptor que el negoci pot girar.
Ara per ara en tinc tres.
### `docs`: ajuda sobre el producte
Respon preguntes sobre com funciona el producte, a partir de la documentació. És la peça que ja venia del RAG.
### `data`: preguntar a les teves dades
El model escriu consultes de lectura restringides sobre un catàleg curat de les dades del negoci. Només el que he decidit exposar, i les dades personals no hi arriben mai. El resultat torna com a taula o gràfic, i el pots fixar al tauler.
### `actions`: el model proposa, tu decideixes
El model **no executa**. Proposa canvis a través d'un catàleg d'accions que reutilitza els casos d'ús que el producte ja té. La persona veu què passarà i ho aprova. Si l'acció és irreversible, o arriba a molta gent, et demana que confirmis a quantes persones arribarà. I el servidor ho torna a comprovar tot abans d'aplicar res.
## Poques eines genèriques, un catàleg que creix
El canvi que més pesa és aquest: en lloc d'una eina per funcionalitat, **poques eines genèriques**. Una per consultar, una per proposar una acció, una per cercar documentació. A sobre n'hi ha unes poques d'extra (vistes, formularis, visites guiades, més avall, encara experimentals), però cap lligada a una funcionalitat concreta. El que creix és el catàleg que hi ha darrere, no la llista d'eines que el model veu.
Afegir una funcionalitat a l'assistent ja no vol dir tocar el prompt ni inflar el context. Vol dir descriure-la al catàleg.
I una cosa que no esperava: els models econòmics van tornar a ser prou bons. Hi ha dues raons. La primera és que aquest any els models han avançat moltíssim: un model petit d'ara és molt més capaç que un de gran d'abans, i fa molt més que generar text (només cal comparar Sonnet 3.5 amb Sonnet 5.5, o amb GPT-Luna). La segona és que ara tenen una superfície petita i ben descrita, i triar entre tres coses és molt més fàcil que triar entre trenta.
Per saber-ho amb certesa, no vaig confiar en la intuïció. Vaig muntar avaluacions amb el vocabulari de cada tipus de negoci (un club no parla com una gestoria) i vaig comparar models amb les mateixes preguntes. Quan canvio el prompt, el catàleg o el model, ho sé per una mesura i no per una sensació.
I per poder fiar-me del backend que hi ha sota, hi he aplicat el que explico a [Cobertura no és verificació](/blog/cobertura-no-es-verificacio): mutation testing sobre el codi que l'assistent acaba executant. Una acció que un model proposa i una persona aprova ha de fer exactament el que diu.
## El que he construït a sobre
Un cop la base era sòlida, van sortir altres coses gairebé de franc. Són experimentals i les tinc en test A/B, així que no les presento com a acabades.
**UI generada, ara sí.** El model ja no escriu HTML. Emet una especificació de vista amb tipus i amb una llista tancada de components, i les dades hi van lligades per referència. El servidor la valida abans que arribi a ningú. La interfície sempre és del producte, i les xifres sempre són de la dada.
**Guia dins del backoffice.** L'assistent sap en quina pantalla ets. Pot **omplir un formulari prèviament** perquè el revisis, i mai l'envia. I pot fer **visites guiades** pas a pas, els passos de les quals viuen a la documentació. Així la documentació i les visites s'escriuen una sola vegada, no dues.
**Copilot i documentació alhora.** La mateixa conversa pot respondre «com es fa això?» i ajudar-te a fer-ho, sense canviar d'eina.
## Extra: el teu agent, els mateixos permisos
Hi ha una derivada que m'agrada molt. Les persones que gestionen un negoci ja tenen el seu assistent de confiança: Claude, ChatGPT, Cursor. Per què haurien d'obrir un altre xat?
Amb MCP i OAuth, aquests assistents es connecten a Goil i arriben a les **mateixes capacitats**, amb els **mateixos permisos** i les **mateixes aprovacions**. No hi ha una porta del darrere ni un nivell de confiança diferent. Si no ho podries fer al backoffice, el teu agent tampoc ho pot fer.
## El que he après
- **Poques eines genèriques guanyen a moltes d'específiques.** El catàleg ha de créixer; la llista d'eines, no.
- **El model proposa, les persones decideixen.** I el servidor ho torna a comprovar, perquè no es pot confiar només en el que ha vist la persona.
- **L'estructura va als catàlegs i als contractes, no als prompts.** Un prompt es llegeix, un contracte es valida.
- **Avaluacions abans que sensacions.** Si no pots mesurar si un canvi va millor, no ho saps.
- **Reutilitza el producte, no el dupliquis.** Una API paral·lela per a l'agent és deute des del primer dia.
## I ara què
Encara queda feina. M'interessa que l'assistent sigui proactiu i no només reactiu: resums que t'arribin sense que els demanis. I més pantalles i més visites guiades, que és on es nota més que l'assistent sap on ets.
Però la base ja no em preocupa.
---
# Cobertura no és verificació: mutation testing i CRAP per al codi que escriuen els agents
URL: https://peremontpeo.dev/blog/cobertura-no-es-verificacio/
Data: 2026-10-07
Descripció: Quan els agents escriuen el codi i també els tests, la cobertura diu que tot va bé i la revisió no dona l'abast. El problema, dues idees velles que el resolen, el gir de l'Uncle Bob aquest setembre, i el que hem posat a l'API de Goil: dues comandes a demanda i cap barrera.
A l'API de Goil hi ha un cas d'ús amb un 89% de cobertura de tests. Desa un registre i actualitza unes metadades dins d'un `Promise.all`. He substituït tot el `Promise.all([...])` per `Promise.all([])`, buit, i he passat la suite de tests. Cap test ha fallat.
Els tests executaven aquelles línies. No en comprovaven res. I la cobertura ho comptava com una bona notícia.
Aquest post explica el problema que hi ha darrere, en quines idees m'he basat per atacar-lo, com ha canviat d'opinió pel camí qui més les ha defensat, i què hem acabat posant a l'API de Goil.
**En resum**
- **El problema:** els agents escriuen el codi i també els tests, la revisió ja no dona l'abast, i l'únic senyal automàtic que tenim, la cobertura, diu que tot va bé encara que els tests no comprovin res.
- **La idea:** el **mutation testing** (trencar el codi a propòsit i mirar si algun test ho detecta) diu on els tests són buits. El **CRAP** (complexitat contra cobertura) diu per on començar.
- **El context:** l'Uncle Bob havia construït aquest 2026 un sistema d'agents amb aquestes mètriques com a barreres dures, i al setembre l'ha abandonat. Les mètriques s'han quedat; les barreres, no.
- **La solució:** dues comandes que corren a demanda, només sobre el que ha canviat, sense bloquejar res. I una *skill* perquè l'agent que escriu els tests sigui el primer a passar-los per la mutació.
- **Funciona:** troba forats reals que la cobertura amagava a l'API de Goil, i ja s'ha fet servir per verificar una PR de tests: d'un 50% a més d'un 89% de mutants morts.
## El problema
A [Com programo ara](/blog/com-programo-ara) explicava que ja no obro l'editor: defineixo l'objectiu i l'agent treballa fins que la PR és en verd. I explicava què ha passat a l'equip de Goil amb aquest canvi: fem tres vegades més PR que fa un any, i cadascuna espera cent vegades més a ser revisada. Escriure codi ja no és el pas lent. Revisar-lo, sí.
Aquell post no tocava una conseqüència que ara em preocupa més. L'agent no només escriu el codi: també escriu els tests. I un test escrit per complir l'expedient s'assembla molt a un test de debò. Crida la funció, rep un resultat, i fa una asserció. Sovint sobre el *mock* que ell mateix ha preparat, que vol dir que el test comprova que el test funciona.
Aquests tests passen. Pugen la cobertura. I no protegeixen de res.
No és teòric. Aquestes setmanes he obert dues PR a l'API de Goil que només toquen tests. Una en treu que no podien fallar per cap motiu real: comprovacions que un mòdul exporta el que ha d'exportar, còpies que es comparen amb elles mateixes, un test que només provava el seu *mock*. L'altra reescriu els que afirmaven sobre allò que ells mateixos havien preparat. Ningú no els havia vist en revisió.
El problema té tres capes, i la tercera és la que fa mal:
1. **Hi ha més codi que mai**, i bona part dels tests també els escriu un agent.
2. **La revisió no dona l'abast.** Ni humana ni amb agents revisors es pot llegir cada test i decidir si comprova alguna cosa.
3. **L'únic senyal automàtic diu que tot va bé.** La cobertura respon "s'ha executat aquesta línia?", no "algun test se n'adonaria si fos incorrecta?". I les barreres de cobertura que ja teníem mesuren aquest mateix senyal feble.
Quan tot està en verd, ningú mira. Aquest és el risc: no un test vermell, sinó una suite verda que no vigila res. Un senyal que diu que tot va bé quan no és així.
El que necessito és un senyal barat, que corri quan jo vulgui, i que m'assenyali on els tests són buits. No per substituir la revisió, sinó per dir-li on ha de posar l'atenció, que és el recurs escàs.
## Dues idees velles
### Mutation testing
La idea és senzilla: canvies el codi a propòsit i mires si algun test falla. Un `>` passa a ser `>=`, un `&&` passa a ser `||`, un text passa a ser buit, un bloc sencer desapareix. Cada canvi és un *mutant*. Si algun test falla, el mutant ha mort: els tests vigilen aquell comportament. Si tots continuen en verd, el mutant ha *sobreviscut*: pots trencar aquell tros de codi i ningú se n'adonarà.
Respon exactament la pregunta que la cobertura no respon. El `Promise.all` de l'inici és un mutant que sobreviu.
El 2016, Robert C. Martin, l'Uncle Bob, en va escriure [un post](https://blog.cleancoder.com/uncle-bob/2016/06/10/MutationTesting.html) amb `pitest`, l'eina de mutation testing per a Java. A la pregunta de quin percentatge de mutants morts cal buscar, responia:
> The question is absurd. There is no justifiable goal other than 100%.
M'agrada perquè canvia la naturalesa del número: un mutant viu no és un percentatge a millorar, és un forat concret.
Hi afegeixo una cosa que ell no diu en aquell post, però que he après fent-ho servir. A vegades un mutant no es pot matar perquè el canvi no altera res observable. Un `typeof x === 'string'` just després d'un `Set` que ja només conté strings. Una comprovació de `null` dues línies després d'una funció que ja llença si rep `null`. Quan passa, la resposta correcta no és inventar-se un test: és esborrar la línia. Era codi que sobrava.
### CRAP
El mutation testing té un cost: per a cada mutant torna a passar els tests. En un projecte gran no el pots córrer sobre tot el codi. Necessites saber per on començar.
El 2007, Alberto Savoia i Bob Evans van proposar el **CRAP**, *Change Risk Anti-Patterns*, i la seva eina per a Java, crap4j. Respon a una altra pregunta: si toco aquesta funció, quin risc hi ha que trenqui alguna cosa sense que ningú se n'assabenti?
Barreja dos ingredients. La **complexitat ciclomàtica**, que compta quants camins diferents pot prendre una funció: cada `if`, cada `case`, cada `&&` o `||`, cada bucle en suma un. I la **cobertura** d'aquella funció. La fórmula:
```text
CRAP = complexitat² × (1 - cobertura)³ + complexitat
```
Els exponents són el missatge. La complexitat va al quadrat, però la manca de tests va al cub: no tenir tests penalitza molt més que ser complex. I el `+ complexitat` final és un terra: una funció enrevessada mai surt gratis, ni amb el 100% de cobertura.
| Complexitat | Cobertura | CRAP | Com ho llegeixo |
|---|---|---|---|
| 1 | 0% | 2 | Trivial i sense tests. La llegeixes d'un cop d'ull |
| 5 | 100% | 5 | Té camins, però estan fixats |
| 10 | 50% | 22,5 | Comença a fer por |
| 5 | 0% | 30 | Cinc camins i cap test |
| 10 | 0% | 110 | Aquí hi viuen els bugs |
Els dos casos avorrits no són perillosos. Codi simple sense tests, l'entens llegint-lo. Codi complex amb tests, si el trenques els tests et criden. El que fa mal és la combinació, i el CRAP aïlla exactament aquesta intersecció i l'ordena.
Però té un punt cec, i és el que fa que no el pugui fer servir sol. El seu ingredient de cobertura és la mateixa cobertura de sempre: "s'ha executat". Una funció amb tests que no afirmen res puntua igual de bé que una amb tests de debò. El cas d'ús de l'inici té un CRAP de 12, banda moderada, gens alarmant. I li sobreviuen onze mutants.
Per això van tots dos junts. El CRAP és el radar que et diu on mirar. El mutation testing és la prova.
## Què ha fet l'Uncle Bob aquest 2026
Abans de decidir com fer-ho servir quedava una pregunta: barrera o senyal? Han de bloquejar la feina, o han de dir on mirar? I qui més havia apostat per la barrera acabava de canviar d'opinió.
Al seu GitHub hi ha [SwarmForge](https://github.com/unclebob/swarm-forge), un sistema per coordinar diversos agents, i una configuració amb sis rols en cadena: qui especifica, qui programa, qui neteja, qui revisa l'arquitectura, qui "endureix" el codi, i QA. Les dues mètriques hi eren com a barreres dures. Al rol que endureix el codi, just abans d'entregar-lo a QA:
> As the final verification sequence, run the language mutation tool, then soft Gherkin acceptance mutation [...], then the language CRAP tool [...] CRAP gate is 10 or below on **changed** files.
Fins i tot hi havia previst la trampa òbvia, que és trossejar funcions perquè el número baixi:
> A single `cond` or `case` that answers one question may stay above 10; do not split it into helpers that take booleans the caller already knew.
Ho tenia molt ben pensat. I la segona setmana de setembre ho va deixar estar. L'11 de setembre escrivia que havia passat setmanes construint un harness que obligava els agents a treballar exactament com ell volia, ple de barreres, tests, eines i protocols, i que era hora de [repensar-ho](https://x.com/unclebobmartin/status/2098432570887217520). Tres dies després:
> Since I stopped using my harness, my token consumption has fallen by a huge factor. That harness was massively inefficient.
El mateix cost que jo volia evitar. I el 18 de setembre en donava [el perquè](https://x.com/unclebobmartin/status/2100938985543446954): els agents encara necessiten supervisió d'alt nivell, la revisió humana del codi és un coll d'ampolla, i els models han millorat prou perquè els harness deterministes i estrictes siguin, en paraules seves, *suboptimal*.
El que va fer després és el que més m'interessa. Va crear [uml-viewer](https://github.com/unclebob/uml-viewer): un visor de l'arquitectura del projecte com a diagrama, on pots clicar fins a arribar al codi i demanar canvis a l'agent des d'allà. I el color de cada peça surt d'aquestes dues mètriques:
> Color coding comes from CRAP and mutation metrics. You can adjust the thresholds in config if you like.
Les seves eines de CRAP i de mutació, a més, ara es distribueixen com a *skills* per a agents, no com a peces del seu harness.
Llegit tot junt: **ha abandonat les barreres, no les mètriques.** El CRAP i la mutació han passat de decidir si una feina pot avançar a pintar on ha de mirar una persona. Que és exactament el problema que jo tenia: no em calia una barrera, em calia saber on mirar.
## La solució que hem posat a l'API de Goil
D'aquí surt el disseny: un radar que l'agent i qui revisa poden consultar quan cal, i cap barrera.
### Dues comandes, a demanda
L'API de Goil és TypeScript, amb vitest per als tests i oxlint com a linter, i la solució n'aprofita tant com pot. Una comanda és per al mutation testing, amb [StrykerJS](https://stryker-mutator.io), l'equivalent de `pitest` per a JavaScript i TypeScript. L'altra, per al CRAP. Totes dues miren només el que ha canviat respecte a la branca principal, inclosos els fitxers que encara no s'han afegit a git, perquè un cas d'ús que acabes d'escriure és exactament això.
No corren en cap *hook* ni en cap pas de la CI. Es fan servir quan valen el que costen: un mòdul nou, un cas d'ús nou, o refer un cas d'ús antic i gros. Sobre un fitxer acotat, el CRAP tarda uns tres segons i la mutació uns deu. Sobre una peça que importa mig projecte, la mutació pot passar de minuts. En un canvi rutinari costen temps i tokens per una resposta que ja saps.
### La complexitat, del linter que ja teníem
La primera versió calculava la complexitat amb un recorregut de l'arbre sintàctic escrit a mà. oxlint ja té la regla de complexitat d'ESLint: si poses el màxim permès a zero, totes les funcions la incompleixen i totes informen del seu número. Menys codi propi, i el número és el que diria el linter de l'equip.
I aquí va sortir la sorpresa. oxlint compta cada `?.` com una bifurcació, que ho és, perquè si el valor és nul el camí salta. El meu recorregut no les comptava. El cas d'ús de l'inici en té cinc: el meu script li donava complexitat 7, i la real és 12. La meva pròpia eina feia el mateix que els tests que volia vigilar: deia que tot anava millor del que anava.
### Bandes, no un llindar
Faig servir les bandes que documenta l'eina de l'Uncle Bob: fins a 5, *net*; fins a 30, *moderat*; per sobre de 30, *crappy*. Pinten l'informe, no el jutgen. I quan no puc saber la cobertura d'una funció, la tracto com a no coberta. No saber-ho no és el mateix que estar bé.
### Cap gate
Cap barrera que bloquegi res. És la decisió que més val la pena discutir, així que aquí van les raons:
- **El CRAP hereta el punt cec de la cobertura.** Pot fer de radar, no de barrera.
- **Una barrera es burla.** Premia trossejar funcions perquè el número baixi, sense que baixi el risc.
- **Sobre un diff, dispara on no toca.** Toques una línia d'un cas d'ús antic de tres-centes, i la barrera et culpa de totes les funcions que hi ha.
- **Qui més les havia defensat les ha tret**, i pel mateix cost que jo vull evitar.
- **No afegeix cua.** La revisió ja és el coll d'ampolla de l'equip. Una barrera més que bloqueja PR el faria pitjor, no millor.
Al post anterior defensava convertir cada criteri en una regla i mirar les barreres en lloc del codi. Això segueix sent una regla, però en forma de *skill*, que és una de les formes que hi enumerava, i no de barrera. La diferència és el senyal: una barrera ha de mesurar el que diu, i el CRAP no ho fa.
### La skill: que l'agent es vigili
Aquesta és la peça que respon més directament el problema. Si qui escriu els tests és l'agent, el primer que els ha de passar per la mutació és el mateix agent, abans que arribin a ningú.
La *skill* és un document que l'agent carrega quan toca, i que li diu quan fer servir cada comanda: el CRAP primer si ha de triar per on atacar codi antic; directament la mutació si és codi que acaba d'escriure. També li diu com llegir un mutant que sobreviu (un forat de test que cal tapar, o una línia que sobra i cal esborrar) i quan no fer servir cap de les dues. La disciplina viu a les instruccions, no al codi de sortida d'un script. Exactament com ho fa ell ara.
## Funciona? Per a mi i per a Goil
És molt nou i la PR que l'afegeix tot just s'ha obert, així que no tinc mesos de dades. Però la pregunta que importa és si troba coses que d'una altra manera no trobaria, i si serveix per millorar-les. A totes dues ja puc respondre.
**Troba el que la cobertura amaga.** Dels onze supervivents del cas d'ús de l'inici, un és tot el bloc que desa les dades. Són tests per escriure que ningú hauria escrit mirant la cobertura.
**Ja s'ha fet servir en una PR de debò.** La segona de les PR de neteja de tests, la que reescriu els tests que afirmaven sobre els seus propis *mocks*, la vaig verificar amb aquesta eina abans que l'eina tingués PR pròpia. En la suite d'uns controladors on vaig mesurar l'abans, el 50% dels mutants morien; després de reescriure els tests, entre el 89% i el 94%. Altres controladors van quedar entre el 90% i el 98%, i dos *mappers*, al 100%. Abans de reescriure'ls, la meitat dels canvis al codi d'aquella suite passaven sense que cap test se n'adonés. Ara ho sé amb un número, i el mateix número diu quant ha millorat.
**Apunta on hi ha hagut problemes de debò.** La primera vegada que vaig passar el CRAP pel punt d'entrada de l'API, la funció que apaga el procés quan arriba un desplegament va sortir *crappy*: complexitat 7, cobertura 0%. En aquella funció hi havia hagut un bug real de producció, ja arreglat. L'arreglo no va venir amb cap test. No l'hauria buscat allà.
**Per a mi**, que és on més ho noto, canvia on poso l'atenció. En lloc de llegir tests un per un per endevinar si comproven alguna cosa, puc mirar quines funcions surten arriscades i quins mutants sobreviuen, i anar-hi directe.
**Per a Goil**, la PR que ho afegeix a l'API és en revisió ara mateix. El que pot aportar a l'equip és un criteri repetible per a una pregunta que fins ara depenia de qui revisava: aquests tests comproven alguna cosa? Ho pot respondre qualsevol persona i qualsevol agent, i no depèn de qui tingui temps per llegir-los.
I la prova que em va convèncer més: quan la vaig fer revisar, l'eina queia exactament en el problema que havia de detectar.
## Revisat per dos models, a cegues
Abans d'obrir la PR, vaig demanar dues revisions a agents que no havien vist res de la conversa: només el canvi i un document amb el context. I els vaig demanar que executessin les comandes, no que llegissin el diff.
La primera, amb Opus, va tornar amb tres errors. Tots tres anaven en la direcció que fa més mal: codi sense tests que sortia com a cobert. L'eina que havia de detectar funcions arriscades i mal provades en declarava algunes cobertes al 100% quan no les cobria res, entre elles dues de fluxos de pagament. Dues funcions que comencen a la mateixa línia es consideraven niuades l'una dins de l'altra, cadascuna excloïa les línies de l'altra, no quedava res per comptar, i "res per comptar" es convertia en "tot cobert". Justament el que m'havia proposat evitar. I un altre error desplaçava el final d'algunes funcions fins a 55 línies.
La segona, amb GLM-5.3-flash a OpenCode, va provar una altra cosa i va trobar el que un fitxer petit mai ensenya: StrykerJS tria els tests de la primera passada pels imports. Mutar una peça importada per mig projecte arrossega mig projecte, i llavors qualsevol test que ja estigués en vermell atura tota la passada abans de provar cap mutant.
Les dues revisions van trobar coses diferents. Cap de les dues va trobar el que jo hauria trobat revisant la meva pròpia feina, que és res. És el mateix problema del principi, un nivell més amunt: qui fa la feina no és qui millor la verifica.
## El que ve
El següent pas és la versió barata del visor de l'Uncle Bob, pensada per a la revisió asíncrona: un diagrama Mermaid al cos de la PR, amb els fitxers del canvi agrupats per capa, pintats segons la seva pitjor funció i amb els mutants supervivents anotats. GitHub el renderitza directament, i qui revisa sap per quin fitxer començar sense obrir-ne cap. Tinc un prototip que funciona, i més endavant podria marcar també les dependències que van en la direcció equivocada entre capes. El deixo fora fins que vegi si de debò algú se'l mira.
## Per acabar
Al post anterior deia que el que queda és criteri: saber què val la pena construir, què deixar entrar i què no, i detectar quan un resultat verd és en realitat un error. Amb agents que escriuen el codi i els tests, la suite verda és aquest resultat. La cobertura no ho pot dir. Un mutant que sobreviu, sí.
---
# No tenia un problema de genealogia, tenia un problema de software
URL: https://peremontpeo.dev/blog/gedlint/
Data: 2026-10-03
Descripció: Com vaig passar d'un script de Python a gedlint, un linter en Rust per a GEDCOM amb regles, reparacions i un ratchet per adoptar-lo sobre un arbre brut.
L'arbre que vaig exportar de MyHeritage tenia 509 individus i cap garantia de ser un fitxer correcte. Al [post anterior](/blog/myheritage-agents-autonoms) deia, de passada, que un linter validava cada commit. Aquest és el post d'aquest linter: [gedlint](https://github.com/pmontp19/gedlint).
Arreglar l'arbre era feina de genealogia: fonts, dates, identitats. Que un fitxer sigui correcte i continuï sent-ho és un problema de software, i el vaig tractar com a tal.
## El problema
El GEDCOM brut venia amb línies òrfenes sense nivell, UTF-8 trencat entre dues línies `CONC`, camps propietaris, comes als cognoms i un `_UPD` per registre. Tenir el fitxer no volia dir tenir un fitxer bo.
Vaig començar amb un script de Python (`gedcheck.py`). Funcionava, però era a mida d'un sol arbre. Vaig mirar el mercat: validadors n'hi ha (Java, .NET, Python, JS, Go), alguns amb `--fix`, i a Rust només un parser abandonat des del 2021 (la comparativa és al README). Ningú feia, alhora, el que jo necessitava:
1. Comportament de `clippy`: categories i severitats configurables.
2. Reparacions (`--fix`) amb nivell de seguretat.
3. Regles pensades per als defectes reals de l'export de MyHeritage.
4. Un sol binari, en streaming, sense runtime.
5. Que corri al navegador sense enviar res enlloc.
6. Que es pugui adoptar sobre un arbre brut, sense arreglar-ho tot el primer dia.
"Cap eina feia les sis" és el que puc defensar. Així que en vaig fer una.
## 48 hores per a un MVP
El 6 de setembre: un POC i un MVP amb motor en streaming i 21 tests (un per regla). A la nit, v0.2.0 amb regles de cardinalitat i CI, i poc després un `AGENTS.md`. El 7, binaris precompilats, una GitHub Action i anotacions inline al diff.
El CI no és un extra. Un linter que ningú executa és un script. Quan el vaig connectar a l'arbre es va posar vermell durant tot el dia, i un dels errors era del linter: la regla E005 només disparava a la primera línia del fitxer perquè un flag no es tornava a posar a `false`. Arreglada, va trobar 140 casos reals, `CONT` niuats sota un `CONC`. Eren les mateixes 140 línies que una reescriptura d'historial a l'arbre va reintroduir el 10 de setembre, i que el CI va caçar sol.
Una decisió del primer dia encara mana: **zero dependències**. Ni `clap`, ni `serde`. Arguments i JSON a mà. Sembla excessiu, però el resultat és un sol binari minúscul, sense runtime, i és el que va fer possible el visor web.
## Disseny: de script a linter
Quan va funcionar, vaig escriure l'[RFC 014](https://github.com/pmontp19/gedlint/blob/main/docs/rfc-014-contract.md), inspirada en Biome. El que més m'agrada és el que **no** n'agafo:
> No CST, no per-rule query engine. GEDCOM is a line-oriented format with an explicit level column. The single streaming pass is what makes gedlint viable on large files, WASM-portable and dependency-free.
Sí que en prenc les **metadades de les regles com a dades**, els grups, l'aplicabilitat de les reparacions (segura o potencialment incorrecta) i la configuració per nom de regla. GEDCOM no té sintaxi de comentaris, així que no hi ha `// gedlint-ignore`: la configuració i el baseline són l'única manera de silenciar res.
`RULES` és la font única de veritat de `--explain`, del validador de configuració, de la pàgina de referència i de la fitxa del visor. Un test falla el build si el motor emet un codi que el registre no coneix, o a l'inrevés. La documentació no pot divergir perquè no hi ha dues còpies.
## El ratchet
Això és el que separa un linter útil d'un que acabes silenciant. Quan l'arbre ja tenia 544 individus, gedlint donava 1.014 diagnòstics en la configuració per defecte, 516 dels quals eren etiquetes de vendor. Si el CI falla amb 1.014 troballes el primer dia, l'única sortida és apagar-ho tot, i llavors el gate no serveix.
Ho vaig veure aviat, perquè l'arbre ja venia brut. La solució és el baseline ([#22](https://github.com/pmontp19/gedlint/issues/22), obert el 7 de setembre i fusionat el 8):
```sh
gedlint --write-baseline gedlint.baseline.json arbre/*.ged
gedlint --baseline gedlint.baseline.json arbre/*.ged
```
- Cada entrada s'identifica per **(codi de regla, empremta del missatge)** més un recompte, **mai per número de línia**: inserir deu línies al principi no invalida res.
- Una troballa coincideix mentre el recompte no s'esgoti. Les sobrants són noves i fan fallar el build.
- Les que ja no hi són es reporten com a progrés, i `--write-baseline` les poda. El trinquet només va en una direcció.
La clau és (regla, empremta), no la línia: les dues troballes antigues segueixen casant tot i desplaçades. Només la nova trenca el build.
Així es veu a la terminal, amb un arbre sintètic: un `W301` nou apareix, les dues troballes antigues queden com a baselined.
```text
$ gedlint --baseline gedlint.baseline.json arbre.ged
WARN [W301:suspicious] line 7: @I1@: died (1890) before being born (1900)
arbre.ged: 1 new diagnostics (0 errors, 1 warnings, 0 infos), 2 baselined, 0 resolved, 21 lines, 2 INDI, 0 FAM [GEDCOM 5.5.1]
$ echo $?
1
```
El gate deixa de ser "cap error" i passa a ser "res de nou". Un `W301` (mort abans del naixement) introduït per una edició no queda enterrat entre centenars de troballes antigues. I un import rutinari de MyHeritage que torni a posar comes als cognoms fa fallar el build: la resposta correcta és netejar les dades, no regenerar el baseline.
La configuració de l'arbre és curta:
```toml
[lints]
presets = ["recommended", "hispanic-naming", "hygiene"]
[lints.rules]
"U502" = "off"
"W306" = "info"
```
Els rulesets `hispanic-naming` i `hygiene` són opt-in perquè codifiquen convencions, no l'especificació.
## Privacitat com a requisit
L'arbre són persones vives. Això va condicionar el disseny, i una vegada el va haver de corregir.
**El visor web.** Perquè el motor no depèn de res ni toca fs ni xarxa, el mateix codi compila a `wasm32`. El visor ([pmontp19.github.io/gedlint](https://pmontp19.github.io/gedlint)) el fa córrer dins el navegador, amb una CSP `connect-src 'none'`: l'arbre no surt de la pestanya. Va arribar el mateix 8 de setembre, al vespre.
**El baseline que filtrava.** El 10 de setembre ([#61](https://github.com/pmontp19/gedlint/issues/61)) em vaig adonar que l'empremta normalitzava dígits i referències, però deixava passar text lliure. Un baseline generat sobre un arbre real estava a punt de ser commitejat, i contenia cognoms i el cos d'una nota. El baseline és un fitxer que et diuen que commitegis, així que ha de tenir les propietats de privacitat d'un lockfile. L'endemà ho vaig arreglar: ara l'empremta és un hash i res no reconstrueix mai un missatge a partir d'ell.
## Les regles neixen d'errors reals
Cada error real genera una regla, cada regla genera falsos positius i cada correcció neteja deute. Alguns exemples:
- La regla de coherència genealògica (W308 a W312) va néixer de casos caçats a mà a l'arbre. Després vaig comparar les 36 comprovacions del Consistency Checker de MyHeritage amb les meves i vaig tancar els buits, tot offline.
- Una auditoria contra corpus externs (FamilySearch 7.0, Habsburg amb 34.020 individus, fitxers de tortura) va destapar falsos positius que ara són tests.
- Una regla de línia massa llarga (W713) va caçar un off-by-one **meu** a `ged.py`, l'script que normalitza l'arbre: partia les línies a 255 sense comptar el terminador, i el límit de la spec l'inclou. Un bug d'una eina descobert per la regla de l'altra.
- Un `--fix` que modificava un fitxer "net" ([#44](https://github.com/pmontp19/gedlint/issues/44)): les reparacions no consultaven la configuració. Ara tota reparació queda gated per config, i un test ho lliga.
- Un gate que es trenca en silenci ([#60](https://github.com/pmontp19/gedlint/issues/60)): un script de l'Action va petar i el pas va acabar amb `success`. El pitjor mode de fallada d'un gate és un build verd que no ha validat res.
## Què falta
- Més reparacions segures. Les d'identitat (dates, vincles, fusions) no ho han de ser mai: `--fix` no toca semàntica.
- Més corpus d'arbres no anglòfons, per no sobreajustar a l'exportador de MyHeritage ni a les convencions ibèriques.
- Un baseline és còmode, i la comoditat el pot convertir en un cementiri. Les sessions on es revisen les entrades són on es guanya o es perd el trinquet.
Tot el codi de gedlint l'han escrit agents, principalment GLM i Opus, sota les meves issues i les meves regles. El flux de feina el descric a [Com programo ara](/blog/com-programo-ara).
Si el teu arbre viu en un GEDCOM: `cargo install --git github.com/pmontp19/gedlint`, o arrossega el fitxer al visor web. Els falsos positius i les regles noves es poden obrir com a issue, amb recomptes i codis, sense dades.
---
# Tinc mem: un Frinkiac de Plats bruts fet per agents
URL: https://peremontpeo.dev/blog/tincmem/
Data: 2026-10-03
Descripció: Com he fet tincmem.cat, un cercador de fotogrames de Plats bruts per fer mems i GIFs: subtítols que ja deien qui parla, un pipeline amb ffmpeg i un agent que va escriure tot el codi.
Sempre que busco un mem o un GIF per enviar a algun grup, acabo a *The Office*. M'agrada molt, i a internet n'hi ha milers, de qualsevol moment de la sèrie. Amb la cultura catalana no passa. Si vols respondre amb el David dient «ah, vale» o amb la Sra. Carbonell, has de buscar el capítol, trobar el minut i fer una captura de pantalla.
Els Simpson tenen [Frinkiac](https://frinkiac.com): escrius una frase, et surt el fotograma, hi poses text i el comparteixes. Vaig pensar que *Plats bruts* també es mereixia el seu, i que ara mateix fer-lo hauria de ser fàcil. Així va néixer [tincmem.cat](https://tincmem.cat).
**En resum**
- Cerques qualsevol frase dels 73 capítols de *Plats bruts*, tries el fotograma i en fas un mem o un GIF.
- No he escrit cap línia: ho ha fet un agent. Jo vaig posar l'objectiu i el vaig dirigir en uns quants punts clau.
- No calia transcriure res: els subtítols de 3Cat ja hi eren, i el color de cada línia diu qui parla.
- El pipeline tria 69.520 fotogrames a partir de 31.295 frases: el més nítid de cada moment i sense repetir plans.
- També el poden fer servir els agents: `llms.txt`, respostes en Markdown i un servidor MCP. Un bot de SEO em va esgotar la quota gratuïta de Cloudflare.
- El codi era la part fàcil; la difícil, que les dades fossin bones.
## El prompt: un objectiu
Fa poc vaig explicar [com programo ara](/blog/com-programo-ara): defineixo un objectiu i l'agent treballa fins que la feina està feta. Tinc mem n'és un exemple extrem. Opus 5.5 acabava de sortir i el volia posar a prova, així que li vaig donar el mínim: un projecte inspirat en Frinkiac però per a *Plats bruts*, que primer estudiés com ho havien fet i després em digués quina infraestructura caldria. Res de tecnologies, d'on treure els capítols o com havia de ser la web.
Després sí que hi vaig posar criteri: provar-ho primer amb un sol capítol, la cerca amb BM25, el servidor MCP, el nom i el domini, i arreglar els GIFs quan vaig veure que a l'iPhone no s'acabaven mai.
Jo m'imaginava que caldria transcriure l'àudio amb algun model de veu i alinear el text amb el vídeo. L'agent va anar per un altre camí. Va trobar els capítols a l'API de 3Cat i, al mateix lloc que el vídeo, els subtítols en català en format VTT. La transcripció ja existia, amb els temps exactes de cada frase.
El primer commit és del 23 de setembre i funcionava amb un sol capítol. El 25 ja era a tincmem.cat amb domini propi. Una setmana i 39 commits després, la web és la que pots veure ara.
Cinc dies després del primer commit ja sortia a la premsa: [Nació Digital](https://naciodigital.cat/next/trend/una-font-inagotable-de-mems-de-plats-bruts-la-nova-creacio-viral-dun-usuari-algu-ho-havia-de-fer.html), [Racó Català](https://www.racocatala.cat/noticia/72170/aplicacio-permet-convertir-tots-fotogrames-dialegs-plats-bruts-mems) i [El Món a RAC1](https://x.com/pmontp19/status/2104852329954787654) se'n van fer ressò.
## Els subtítols ja diuen qui parla
La sorpresa més gran van ser els colors. Els subtítols de TV3 fan servir colors per distingir els personatges principals, i el VTT ho guarda amb una etiqueta per parlant. Al primer capítol, quan el David es retroba amb el Lopes:
```text
00:06:04.440 --> 00:06:05.920
Perdona, tu no ets el Lopes?
00:06:06.960 --> 00:06:10.000
Ens coneixem?
Sóc el David. El David Güell!
```
Groc és el David, cian el Lopes, verd l'Emma i fúcsia la Sra. Carbonell. Però el fitxer no ho diu enlloc: diu `yellow`, no `David`. L'agent ho va deduir del context, mirant a qui anomenen els altres just abans o després de cada frase. Aquí mateix ho tens: el groc diu «Sóc el David». Amb prou frases, la correspondència és clara.
Gràcies a això, a la web pots filtrar per personatge. Busques «vale» i pots quedar-te només amb les del David. Un subtítol amb dues persones parlant, com el de l'exemple, es parteix en dues frases, una per parlant, i les cançons es marquen a part.
## De capítol a fotograma
Tenir la frase i el moment exacte no és tenir un bon fotograma. Agafar el primer fotograma de cada subtítol dona molts mals resultats: cares a mig parpelleig, imatges mogudes o el pla anterior abans del tall. El pipeline fa quatre passos per a cada capítol.
**1. Canvis de pla.** ffmpeg detecta on canvia el pla amb el seu filtre de detecció d'escenes. Això serveix per dues coses: posar un moment candidat just després de cada tall (els plans sense diàleg també poden ser bons mems) i saber a quin pla pertany cada fotograma.
**2. Tres moments per frase.** De cada frase en surten tres candidats: just després de començar, al mig i just abans d'acabar. Una frase llarga sovint té més d'una cara aprofitable. Els moments massa junts (a menys de 0,4 segons) es fusionen.
**3. El més nítid de tres.** De cada moment se'n treuen tres fotogrames separats per una fracció de segon, i es queda el que té les vores més marcades. La mesura és la variància d'un filtre laplacià: una imatge moguda té les vores esborrades i dona un valor baix. És un truc senzill que elimina gairebé tots els fotogrames borrosos.
**4. Fora els repetits.** Dins d'un mateix pla, si el David parla tres frases seguides sense moure's, en surten tres fotogrames quasi idèntics. El pipeline redueix cada imatge a 16×12 píxels en gris, la compara amb les que ja ha desat del mateix pla i, si la diferència mitjana és molt petita, la descarta.
A més, per a cada capítol genera miniatures, un vídeo petit (400×300) per fer GIFs directament al navegador i una imatge de previsualització amb el subtítol cremat, perquè quan comparteixes un enllaç ja es vegi el mem.
Cada capítol original pesa uns 860 MB, i el pipeline l'esborra en acabar. Al final, els 73 capítols són 31.295 frases i 69.520 fotogrames.
## Una web estàtica (i R2)
Tota la web és un sol `index.html`. La cerca és al navegador, amb [MiniSearch](https://github.com/lucaong/minisearch); els GIFs es fan al navegador amb [gifenc](https://github.com/mattdesl/gifenc) a partir del vídeo petit, i els fotogrames de cada capítol només es baixen quan calen.
La part estàtica són uns 12 MB, sobretot les frases en JSON, i va en un Worker de Cloudflare. Els fotogrames no hi caben: els fitxers estàtics dels Workers tenen un límit de 20.000 fitxers i aquí n'hi ha més de 69.000 només de fotogrames. Van a R2, l'emmagatzematge d'objectes de Cloudflare, amb un domini propi.
Aquí hi va haver un cas de CORS que val la pena explicar. Per copiar imatges i fer GIFs, el navegador necessita capçaleres CORS a R2. Però la memòria cau de Cloudflare no fa cas de `Vary: Origin`: si una sola petició sense `Origin` (un crawler, la previsualització d'un enllaç) arriba primer, la memòria cau guarda la resposta sense CORS, i després la web veu imatges trencades. La solució va ser una Transform Rule que posa `Access-Control-Allow-Origin: *` a tot el domini dels mitjans, i demanar sempre les imatges amb `crossorigin`.
El nom, per cert, també té la seva història. L'agent el va batejar Platsiac, i després va proposar «Tinc meme»: gairebé tots els capítols de la primera temporada es diuen «Tinc alguna cosa», com *Tinc pis*, *Tinc por* o *Tinc enveja*. Jo hi vaig canviar *meme* per «mem», la forma que recomana el TERMCAT, i tincmem.cat estava lliure. I si vas a una adreça que no existeix, la pàgina d'error és un fotograma del T6C12 on l'Emma diu: «No existeix! Te l'has inventada!».
## També per a agents
Com que l'agent ho feia tot, li vaig demanar que la web també la poguessin fer servir altres agents. Ho va fer Claude, en dues tandes. La primera, a la mateixa sessió local que el pipeline: el servidor MCP, el `llms.txt`, un `robots.txt` que ho deixa rastrejar tot, el sitemap i les dades estructurades. La segona, tres dies després, en una sessió de Claude Code al núvol que va acabar en una PR: el Markdown, les imatges directes i la cerca sense índex.
La web es pinta amb JavaScript, i un agent que la llegeixi només hi troba els botons i l'ajuda del teclat, cap frase. Per això té:
- Un [`llms.txt`](https://tincmem.cat/llms.txt) que explica com fer-la servir.
- Cerca i fotogrames en Markdown: `tincmem.cat/index.md?q=…` o qualsevol enllaç amb `Accept: text/markdown`.
- La imatge directa d'un fotograma, amb el subtítol cremat, afegint `.jpg` a l'enllaç. Es pot posar en un `` o en Markdown.
- Un servidor MCP a `mcp.tincmem.cat/mcp` perquè un assistent pugui cercar frases i retornar el fotograma.
Així, pots demanar a Claude «busca'm un mem de Plats bruts per dir que no tinc ganes d'anar a la feina» i et torna la imatge.
## Un bot em va deixar sense quota
Cada fotograma té la seva adreça (`/T1C1/642400/213`) i una previsualització que genera el Worker. El sitemap en llista més de 31.000, una per frase. Vaig donar d'alta la web a Google Search Console i, poques hores després, va caure: s'havia acabat la quota gratuïta de Workers, 100.000 peticions al dia per compte. El primer sospitós era Google, però les dades deien una altra cosa. Entre el 25 i el 29 de setembre, Googlebot no va arribar a 2.000 peticions, i AhrefsBot, un rastrejador de SEO, en va fer unes 260.000. I com que la quota és per compte, també va caure el servidor MCP.
La solució immediata va ser el pla de pagament de Workers, 5 dòlars al mes, i en 30 segons tornava a funcionar. El dia abans, la sessió al núvol ja havia preparat un canvi per treure feina al Worker, i el vaig desplegar just després. D'una banda, la portada ja no hi passa, i això són moltes menys peticions. De l'altra, la cerca dels agents no construeix cap índex. Això no era per la quota sinó per la CPU: el pla gratuït només deixa 10 ms per petició, i muntar l'índex de MiniSearch en trigava entre 400 i 700. Ara és una cerca lineal sobre un fitxer de text pla, que hi cap de sobres.
## Analítiques sense cookies
Volia saber què passava sense posar cookies ni banners de consentiment. Faig servir dues coses:
- **Cloudflare Web Analytics** per a visites i pàgines. Com que la web navega amb `pushState`, vaig haver d'activar-hi els canvis d'historial perquè comptés cada fotograma com una vista.
- **Esdeveniments propis** (cerques, fotogrames vistos, accions) que el Worker envia a Cloudflare Analytics Engine, i un petit script que els consulta.
## Teresina S.A., al calaix
Com que el pipeline és genèric (una configuració per sèrie: l'id del programa a 3Cat, el color de cada personatge i l'estil de la web), vaig provar-ho amb *Teresina S.A.* El codi funcionava, però el resultat no em va convèncer: els fotogrames no sempre coincidien amb qui parlava i hi havia parts en castellà. Ara és al calaix.
És un bon recordatori que la part difícil d'aquest projecte no era el codi: era que les dades fossin bones. A *Plats bruts* ho són. Els subtítols són fidels, els colors són consistents i el pla acostuma a mostrar qui parla.

Si trobes una frase que no surt, o en fas un mem que valgui la pena, [escriu-me](mailto:hola@tincmem.cat). I si no, ja saps: [tincmem.cat](https://tincmem.cat).
---
# Com programo ara: sense editor, amb agents i amb la revisió com a coll d'ampolla
URL: https://peremontpeo.dev/blog/com-programo-ara/
Data: 2026-10-01
Descripció: Del Copilot i l'editor obert a sessions autònomes que no paren fins que la PR és en verd. El meu setup actual, com ha evolucionat i què trenca quan entregues molt més que mai.
Aquestes són totes les meves PR mergejades per mes, de feina i de projectes personals, perquè surten del mateix temps i dels mateixos recursos. A sota, el temps que triga a arribar al merge una PR que ha de revisar algú de l'equip: les personals no esperen cap company. No és una mètrica de productivitat: una PR no és valor. Però la forma és clara. El volum total ha passat de 32 a 214 al mes, i la mediana d'espera de les que revisa l'equip, de 12 minuts a 27,8 hores. Escriure codi ja no és el pas lent. Revisar-lo, sí.
Aquest post explica com programo ara, com hi he arribat i per què la revisió s'ha convertit en el coll d'ampolla.
**En resum**
- Des del febrer de 2026 no obro l'editor: defineixo un objectiu i l'agent treballa fins que la PR és en verd i revisada.
- Entrego molt més, però cada PR espera molt més: el pas que depèn d'una persona (la revisió) no escala. A escala personal, el mateix límit és la meva memòria de treball.
- Els agents revisors tenen ganes de trobar coses i allarguen el bucle. El que retallo ho converteixo en regla perquè no torni a passar.
- El que ho fa possible: bucles de feedback ràpids en local, barreres dures (linters, tests, CI) i context de producció només de lectura.
- Ja no planifico: executo directament.
El meu setup canvia de setmana en setmana. Això és una foto de l'1 d'octubre de 2026, no una recepta definitiva.
## Fins al novembre de 2025: autocompletar
VS Code, Copilot per autocompletar i el xat dins l'editor. Una finestra de terminal amb Claude Code, dues com a molt, i un sol projecte a la vegada. Escrivia i llegia tot el codi a mà: cada línia passava pels meus ulls abans de ser a la PR.
És còmode perquè l'esforç i el control van units. I té un sostre evident: el teu ritme d'escriptura i de lectura.
## El clic del novembre de 2025
El novembre de 2025 la indústria, sobretot a Silicon Valley, va fer un clic: els models podien fer molt més del que pensàvem. Opus 4.5, Gemini 3 Pro i companyia. Per a mi va ser el pas d'autocompletar a agent de programació autònom.
Cap al febrer de 2026 ja no obria l'editor. Terminal, agent i diff a la PR. Sense editor no vol dir sense criteri: vol dir que el criteri es mou d'*escriure* a *definir i verificar*.
## El setup d'avui
Avui el setup té quatre peces:
- **Agents**: Claude Code és el meu *daily driver* a la feina, ara sobretot amb Opus 5.5: ho capta a la primera i necessita molt poc *steering*. Sonnet 5.5 per a tasques de menys valor, i a vegades Fable fa d'oracle quan necessito una segona opinió difícil. Gemini (pla Google AI Pro) el faig servir poc, perquè Antigravity, fins i tot al terminal, és molt dolent; el reprenc amb Gemini 3.8 Flash i vull provar Gemini 4, sortit la nit del 30 de setembre. Per a sessions llargues i barates amb un pla fix, OpenCode amb Z.ai GLM: ara GLM-5.3-flash, per la visió, que aprofito sobretot a genealogia.
- **Orquestració**: el que canvia és l'entorn on corren els agents. He passat per [cmux](https://cmux.com), un terminal per a macOS pensat per a agents, i per [Herdr](https://herdr.dev/), on segueixen en marxa encara que tanquis el portàtil. Ara soc a [Orca](https://github.com/stablyai/orca). Em dona un worktree aïllat per sessió sense fricció: una còpia de treball de git per a cada agent, perquè no es trepitgin. I el puc instal·lar com a servidor remot i connectar-m'hi des del mòbil per Tailscale, una VPN privada. Una sessió avança sense que jo sigui davant l'ordinador.
- **Git**: també el fan els agents. Quan jo ho dic i tot és en verd i aprovat, fan el merge i netegen l'entorn.
- **Desplegament**: als projectes personals, el merge desplega directament a producció. A la feina és l'únic pas que no és automàtic.
### QA: l'agent prova la seva feina
Perquè un agent treballi sol, primer ha de poder comprovar la seva feina.
**Frontend.** [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) és el meu millor amic, i [agent-browser](https://agent-browser.dev) també va molt bé. Un MCP és un connector que dona eines i dades a l'agent. Amb un model amb visió, el QA del frontend no necessita cap test e2e escrit: obre l'app, reprodueix el bug o prova la funcionalitat nova i m'ho ensenya. El QA al navegador del dia a dia el fa Claude Code. Ho complemento amb el MCP de Mobbin per buscar patrons de disseny reals, i n'he parlat més al [post de QA amb agents](/blog/qa-ai-agents-browser-automation).
**API i backend.** També ho prova tot: crida l'API, comprova respostes i efectes. A vegades escriu tests e2e o d'integració; d'altres, es fa els seus propis scripts per provar parts de forma integrada sense haver d'aixecar tota l'API. El que importa és que el resultat sigui una prova que l'agent ha executat, no una opinió.
### Les al·lucinacions ja no són el que eren
Altres MCP han perdut pes. Abans feia servir sobretot Context7; ara, Firecrawl, i cada cop menys. Les al·lucinacions s'han reduït dràsticament. Molts models van directament al codi de la dependència, o munten un petit experiment per confirmar la causa de forma empírica, no per sensació. Algunes llibreries ja porten la documentació incrustada. Un MCP es queda només si el model no resol el problema sol. El [context engineering](https://x.com/trq212/status/2080710971228918066) també va d'això: treure el que ja no cal.
### Donar-li context per fer troubleshooting
Treure context que sobra no vol dir deixar l'agent a cegues. Amb un bug, el que li falta és context de producció, i això m'ha funcionat més que qualsevol prompt. Un agent que només veu el codi endevina. Un que veu què ha passat de debò, ho resol. Per això li dono accés, **sempre en mode lectura**, a:
- **PostHog (MCP)**: què feia l'usuari abans de l'error, quins esdeveniments hi ha, sessions i errors.
- **Logs de Grafana**: què ha dit el servidor en aquell moment.
- **La base de dades i les cues**, a vegades amb un MCP, només per mirar.
- **El context de la infraestructura**: com està muntada i què es connecta amb què.
- **Un entorn on reproduir el bug.** Aquest és el més important. Si l'agent pot reproduir-lo, pot verificar que l'ha arreglat; si no, només pot opinar.
Un exemple d'avui mateix: s'havia perdut la connexió entre Redis i la instància que processa les cues, i no s'havia recuperat. L'agent ha llegit els logs, ha mirat les cues i els *jobs* encallats i, com que coneixia la infraestructura, ha lligat caps i ha trobat la causa sense que jo li expliqués cap peça. Reprodueix, troba la causa, arregla i torna a reproduir.
### Sessions amb un objectiu
Amb verificació i context, n'hi ha prou amb un objectiu. Defineixo un goal, l'encàrrec amb la condició de final: «arregla l'issue #123, afegeix un test que falli abans i passi després, i deixa la PR en verd». L'agent no para fins que la PR és en verd i revisada. Jo hi soc a l'inici (el goal) i al final (la revisió).
Perquè funcioni, l'agent necessita feedback ràpid: tests, checks i linters en local, regles dures i un CI ràpid. Rarament es queda donant voltes; el risc és que es desviï de l'objectiu, i per això el goal ha de ser concret.
El CLI d'Orca deixa que una sessió n'orquestri unes altres. Li puc dir:
- «Agafa els issues que tinc assignats, obre un workspace per a cadascun i posa-hi un agent a treballar.» Jo verifico i integro al final, amb una revisió adversària: un segon agent que intenta trobar-hi errors.
- «Mira totes les meves PR, fes *rebase* si cal i resol els conflictes en paral·lel.»
### Una idea no espera
L'orquestració gestiona les sessions que ja tinc en marxa. Per a una idea nova, moltes vegades l'obro a l'app de Claude per a iPhone, que em dona un entorn al núvol amb el meu projecte. No cal esperar a seure davant l'ordinador. Puc tenir una idea durant el cap de setmana i dilluns ja tinc alguna cosa provada.
### Ja no planifico
Amb un goal concret, el pla sobra. Amb Opus 4.5 i 5 planificava molt, i amb Fable encara més, per implementar després amb Sonnet. Ara, pel dia a dia, no planifico: executo directament. Els plans tenen molt de *drift*: el codi canvia entre el pla i la implementació, i l'agent troba coses que el pla no preveia. Els models actuals ho gestionen sense gaires problemes.
Si a tu no et funciona així, la meva hipòtesi, de més a menys probable: un model poc recent; falta de context o de documentació; o un projecte complex com un ERP (SAP, Microsoft Dynamics), amb pocs exemples a les dades d'entrenament i un bucle de feedback de minuts o hores. Ajuda molt un monorepo amb bones pràctiques d'arquitectura (DDD, hexagonal). Des d'Opus 5 no calen regles explícites de com crear mòduls, ports o models nous.
### Revisar sessions anteriors
El que sí que faig molt és mirar enrere: demano a l'agent que revisi les sessions anteriors i busqui patrons. On he hagut d'intervenir, què he repetit, què faltava a `AGENTS.md`, el fitxer d'instruccions del repo per als agents. D'aquí surten regles noves i skills, paquets d'instruccions reutilitzables. L'objectiu és que la següent sessió autònoma no necessiti que hi intervingui.
### Skills: provar-les i quedar-me amb el que funciona
No totes les skills les escric jo. Provo les de fora un temps i n'integro als projectes les parts que funcionen. Un exemple és la skill de revisió del backoffice, adaptada al nostre producte.
L'última que m'ha anat molt bé és [Ponytail](https://github.com/DietrichGebert/ponytail): l'agent actua com un desenvolupador sènior mandrós. Abans d'escriure res, es pregunta si cal que existeixi, si ja és al codebase, si ho resol la biblioteca estàndard, la plataforma o una dependència instal·lada, i si pot ser una línia. Només llavors escriu el mínim codi que funcioni, sense estalviar-se mai la validació d'entrades, la seguretat ni l'accessibilitat. Els agents tendeixen a sobreconstruir, i menys codi vol dir menys superfície per revisar. Els autors afirmen ~54% menys de codi de mitjana en les seves proves; són xifres seves, no meves.
Tres col·leccions més que faig servir sovint, de tres autors que respecto:
- [Les de Matt Pocock](https://github.com/mattpocock/skills), «skills per a enginyers de veritat, no per fer *vibe coding*»: petites, composables i que funcionen amb qualsevol model. La que més faig servir és `grill-with-docs`: una entrevista implacable que afina un pla o un disseny i, de passada, deixa documents (ADR i glossari). Aquest post va néixer d'un *grill* així, abans del primer esborrany.
- [Les d'Addy Osmani](https://github.com/addyosmani/agent-skills), que codifiquen el cicle de vida (definir, planificar, construir, verificar, revisar, publicar) amb ordres com `/spec`, `/plan`, `/build`, `/review` i `/ship`. Les faig servir com a punt de partida i en retallo el que no encaixa amb el nostre repo.
- [Les d'Emil Kowalski](https://github.com/emilkowalski/skills), per a *design engineering*: criteri sobre els detalls d'interfície, els components i, sobretot, les animacions. En animacions han estat un abans i un després: l'agent ja no n'afegeix per afegir, decideix quan, quant i amb quina corba. El seu curs és a [animations.dev](https://animations.dev/).
### En números
Tot plegat es tradueix en volum. Alguns números meus recents, extrets de les sessions locals:
- **Claude Code**: 168 sessions en 14 dies, fins a 28 de superposades, i unes 10 crides a eines per cada missatge meu. Els meus missatges tenen una mediana de 68 caràcters. L'agent condueix el terminal: el 80% de les crides són `bash`.
- **OpenCode**: 112 sessions en 15 dies, amb unes 88 h de feina d'agent amb GLM-5.3-flash sota un pla fix.
- **Totes les PR**: 214 mergejades al setembre, davant de 32 un any abans, entre feina i projectes personals (com el de [genealogia](/blog/myheritage-agents-autonoms)).
- **Contribucions a GitHub**: 1.298 el setembre de 2026, davant d'unes 265 un any abans.
- **Cost**: el monitor d'ús (CodexBar) estima, a preus d'API, uns 3.100 $ i 6.100 milions de tokens a Claude en 30 dies, amb Opus com a model principal. Amb el pla Team no és el que pago, però dona la mida. Va coincidir amb el període en què més funcionalitats i contribucions he fet, a l'empresa i als projectes personals.
## Programari: de picar codi a dissenyar el sistema
Aquest volum no és només cosa meva: és el cicle de vida del programari (SDLC) que canvia. Els agents n'han anat menjant fases:
GitHub ho resumeix [com un canvi de rol](https://github.blog/developer-skills/career-growth/from-coder-to-orchestrator-how-agents-shift-the-role-of-a-developer/): de programador a orquestrador.
[Addy Osmani](https://x.com/addyosmani/status/2072885435312042327) proposa pensar-ho com a nivells d'autonomia: quanta llibertat dones a l'agent depèn del risc i de l'evidència que pot aportar, no de què sigui capaç de fer. Em va ajudar a posar nom a allò que ja feia: més autonomia on equivocar-se costa poc i verificar és barat (frontend, projectes personals), menys on no (el desplegament a l'empresa).
El desplegament n'és l'exemple clar. Als meus projectes personals, el merge va a producció sense passos manuals. A l'empresa, amb diversos tenants, he de controlar molt bé quina versió té cada client i quan, i aquest pas continua sent meu.
La revisió és la fase que s'ha mogut a mitges: «agent primer, humà després». Aquest «després» és on s'atura tot.
## La revisió és el coll d'ampolla
L'«agent primer» ja el tinc muntat: un pipeline dins d'Orca amb Claude Code. Un *watcher* busca les PR on em demanen revisió, crea un worktree aïllat i hi posa un agent a revisar-la. En 14 dies, 49 sessions de revisió. L'agent té prohibit fer commit, push o `gh pr review`, comentar o fer merge. Em lliura l'origen de l'issue, què fa el canvi, les troballes per severitat amb fitxer i línia, i un veredicte amb el comentari exacte. Jo hi dono un cop d'ull i el publico: l'agent llegeix, la persona aprova. I ara mateix aquest cop d'ull és el coll d'ampolla de l'equip.
Per què arriba a 27,8 hores si l'agent ja pre-revisa? Perquè aquest temps va des que s'obre la PR fins al merge, i hi caben els bucles. Un LLM revisant té **ganes de trobar coses**: és verbós i vol ajudar. Abans un company mirava la PR, la provava i deia què veia. Ara ho fa el seu agent, troba coses, l'autor les implementa i un altre agent del pipeline torna a demanar canvis. Això pot durar hores, i abans no passava.
Per això cal el filtre. A mi em diu «approve» amb *nitpicks* i *considers* que jo trec. Altres vegades aplica criteris de l'equip que mai van quedar escrits a `AGENTS.md`. Aquest és el pitjor cas: no era un error de l'agent, era coneixement que només vivia al cap de les persones.
El meu criteri per retallar: trec el que és massa petit per allargar la funcionalitat o el fix, i a vegades en surt un issue de seguiment. Si hi ha un bug clar, demano una nova revisió.
Torno al gràfic de l'inici. Les meves PR amb revisió d'equip han passat de 31 el setembre de 2025 a 49 el març de 2026 i a 79 el setembre de 2026 (89 l'agost). La mediana d'espera, de 12 minuts a 42 minuts i a 27,8 hores. Fins fa dos mesos es movia entre uns minuts i unes poques hores. A tot l'equip, de 62 a 184 PR mergejades al mes, i la mediana de 12 minuts a 20,4 hores. Fem tres vegades més PR i cadascuna espera cent vegades més.
Tampoc és que les PR siguin més grans. La mediana de les meves ha passat de 453 a 290 línies canviades, i de 9 a 5 fitxers. L'espera no ve de la mida, ve del volum. I són més petites per una bona raó. Abans, gestionar branques, PR i releases a mà tenia un cost, i en una PR de funcionalitat hi posaves dues o tres coses petites per estalviar-te'l. Ara git va en pilot automàtic i és fàcil fer més PR, més petites i concises, que és la idea de git des del principi.
El volum també creix per un altre motiu: han baixat els errors, però han pujat els bugs identificats. Els models actuals semblen mirar de reüll tot el que els envolta: fas un refactor petit o proves una funcionalitat nova i en detecten molts més. Jo acabo el que estava fent i li demano que obri un issue per cada cosa que trobi pel camí. Abans, enmig d'una funcionalitat nova, era inviable dedicar temps a bugs menors que cap client havia vist.
A l'equip, almenys un company revisava i validava cada PR, a més del CI en verd. Amb 3 PR al dia, la revisió d'un company hi cabia. Ara, en un bon dia, en fem 20. La revisió ja no és manual, però sovint encara hi ha una persona al mig, i les PR passen hores obertes esperant-la.
**Els workflows establerts depenen de les persones**: de qui revisa, de qui té temps, de qui coneix aquell mòdul. Escalen mentre el volum és constant; quan es multiplica per tres, es converteixen en cua. No és que l'equip sigui més lent: és que el pas que no escala s'ha quedat sol. I triplicar les PR té un preu per a qui les revisa. Per això el problema és de l'equip, no només meu.
I no és un problema només nostre:
- Anthropic [ha explicat](https://claude.com/blog/agentic-coding-is-straining-ci-heres-how-we-scaled-test-impact-analysis-at-anthropic) que el volum de codi i de tests generat per agents va ofegar el seu propi CI, i que van haver de redissenyar-lo.
- GitHub [reconeix](https://github.blog/ai-and-ml/github-copilot/better-tools-made-copilot-code-review-worse-heres-how-we-actually-improved-it/) que millorar les eines de la revisió de Copilot l'havia fet pitjor, fins que van repensar el flux de treball.
- GitHub també proposa [partir una PR gegant generada per IA en una pila revisable](https://github.blog/engineering/turn-one-giant-ai-generated-pull-request-to-a-reviewable-stack/). Si hi ha tantes PR gegants que cal un format nou, el problema és general.
## Cap on vaig: l'agent ha de menjar-se la revisió
Perquè la revisió deixi de ser el sostre, ha de deixar de dependre només de persones. La idea que més em serveix: **cada *nitpick* que retallo, i cada criteri que només viu al cap d'algú, ha de convertir-se en una regla**. Una línia a `AGENTS.md`, una skill, un linter, un test.
A l'equip, cadascú experimenta amb el seu setup i comparteix el que funciona. Els canvis a `AGENTS.md` i les skills concretes sí que entren al flux habitual: van al repositori per PR, com qualsevol codi, i els rep tothom.
El que ja hem convertit en barreres: hem passat d'ESLint a Oxlint, més ràpid i amb més regles; algunes regles han passat de *warn* a *error*; un *ratchet* detecta els errors nous sense exigir un codi perfecte el primer dia; i tenim tests unitaris i d'integració. El camí que veig:
1. **Més agents revisant.** Fins avui el Copilot de GitHub ens ha anat prou bé: entén l'abast de la PR i sempre troba alguna cosa que s'escapa. Cal sumar-hi més revisors automàtics, no substituir-los.
2. **Més comprovacions estàtiques**: seguretat, tests, tipus, linters. Allò que un check pot decidir no ho ha de decidir una persona.
3. **Criteris escrits.** El que avui decideixo amb un cop d'ull ha de quedar escrit a la font perquè no torni a sortir. És el que ja faig amb les sessions anteriors, aplicat a la revisió.
4. **Bucles.** L'agent que revisa i el que corregeix treballen fins que convergeixen, i només escalen a l'humà quan no hi ha consens. És el que Addy Osmani anomena [*loop engineering*](https://addyosmani.com/blog/loop-engineering/): deixar de ser qui li dona prompts a l'agent per dissenyar el sistema que ho fa. Hi cita Boris Cherny, el responsable de Claude Code a Anthropic: «Ja no li dono prompts a Claude. Tinc bucles que ho fan i decideixen què cal fer. La meva feina és escriure bucles.»
5. **L'humà es queda amb el que és irreductible**: intenció, risc i gust.
### El bucle sencer: PostHog *self-driving*
Un exemple de fins on pot arribar el bucle és el [*self-driving* de PostHog](https://posthog.com/docs/self-driving). El producte genera senyals (errors, sessions, salut dels serveis), els senyals s'agrupen en informes i un agent n'investiga cada un. Si és accionable, obre una PR perquè la revisis i la fusionis; si no, demana la teva opinió. Després mesura si el canvi ha funcionat i ho torna al bucle com a senyal nou. Hi ha memòria entre execucions perquè no reobri sempre la mateixa PR, i tot passa dins de les barreres que tu defineixes.
PostHog ho ven, és evident, però la idea val sense comprar res. Un agent de codi té el teu codi, però no el teu context: les dades reals d'ús li diuen què val la pena arreglar. No n'hi ha prou de fer una PR, cal mesurar si el canvi ha funcionat. I l'humà es queda al final, revisant i fusionant.
És el mateix patró que he anat explicant, portat a l'extrem: l'agent no només executa l'encàrrec, descobreix què cal fer. I torna al coll d'ampolla: amb una màquina que obre PR soles, el pas que no escala és, encara més, la revisió.
## El que no m'ha funcionat
Arribar fins aquí ha tingut ensopegades.
**Perdre feina a git.** Ho feia tot en un mateix arbre, i amb agents que fan *stash*, *checkout* i *reset*, perdre canvis és qüestió de temps. Ho he patit, sobretot amb models antics (Sonnet 4, GLM-4 o anteriors, crec). Tenia regles perquè no poguessin fer *push*: el feia jo, després de mirar els canvis, i mai amb *force*. Amb els models actuals això ha evolucionat: avui fan el merge quan jo ho dic. Però el remei de debò és el worktree per sessió: cap agent trepitja la feina d'un altre. L'altre remei és el mode auto de Claude Code, que avalua les ordres abans d'executar-les i atura les destructives.
**Perdre el control.** Els primers dies amb agents autònoms tenia la sensació de no controlar res; sovint ni sabia què havia fet l'agent. Llegir-ho tot no escala, així que he canviat el que miro: no el codi, sinó les barreres. Linters, CI, tests ben fets i verificacions al final de cada tasca. Unes instruccions en un context s'obliden o s'interpreten; un check que falla, no. Les dades personals i els secrets, a més, queden fora del context.
Una conseqüència incòmoda: en moltes tasques els agents escriuen codi igual de bo o millor que el meu. Llegir-lo línia a línia per «corregir-lo» deixa de tenir sentit. El que aporto jo és saber si el resultat és el correcte: quan reviso, no miro l'estil, miro si fa el que toca.
**Firstmate.** Hi vaig dedicar unes setmanes ([Firstmate](https://github.com/kunchenguid/firstmate): «Talk to one agent. Ship with a crew»). El concepte m'encanta, però per dins no em va convèncer: poca visibilitat, processos que trigaven molt sense saber per què i límits d'ús que s'esgotaven de seguida, potser només per la paral·lelització. Tot i això, el recomano: l'autor, [Kun Chen](https://github.com/kunchenguid), antic enginyer de Meta, Microsoft i Atlassian, té molt bones idees i ha adoptat de ple l'enginyeria de programari agèntica. Val la pena seguir el [seu canal de YouTube](https://www.youtube.com/channel/UCb69t9ZkE5z1KvCmfJoaifA). Me'n vaig quedar la idea de l'orquestrador, que ara faig servir en diversos projectes en paral·lel, de feina i personals, amb una skill per implementar funcionalitats grans i una altra per comprovar que s'aplica bé.
**La càrrega cognitiva.** Més projectes en paral·lel vol dir més coses al cap, i la meva memòria de treball té un límit. Amb 3 o 4 tasques obertes, sé on era cadascuna i puc desconnectar i tornar. Amb 10 o més ja no sé on era la primera, de què anava ni què havia demanat. Les últimes setmanes això s'ha traduït en un bloqueig. L'agent no es cansa; jo sí.
Què m'ha ajudat, per ordre d'importància:
1. **Especificar bé l'issue al principi.** Si l'encàrrec és clar, no cal que me'l recordi.
2. **Regles clares a `AGENTS.md`**, perquè l'orquestrador sàpiga què ha de fer, com i quan ha de parar.
3. **Un indicador clar de quan una cosa està feta** (*done*). Sense això, tot sembla obert.
4. **Demanar resums de l'estat de tot el que està en marxa.** En lloc de recordar-ho, ho pregunto.
5. **La campana d'Orca**: les sessions m'esperen amb una notificació en lloc de reclamar-me atenció.
És el mateix coll d'ampolla que a l'equip, a escala d'una persona: el que depèn de mi fa cua.
## Per començar
Si encara no treballes amb agents autònoms, aquests són els passos amb més retorn que faria:
1. Un worktree per sessió d'agent.
2. Mesura l'espera de les teves PR, de l'obertura al merge. Amb `gh pr list --state merged --json createdAt,mergedAt` n'hi ha prou.
3. Dona-li context de producció només de lectura i un entorn on reproduir bugs.
4. Posa un agent a revisar primer, sense permisos per publicar res.
5. Cada setmana, demana-li que revisi les sessions anteriors i converteix el que repeteixes en regles a `AGENTS.md`.
I una cosa que no faria encara: deixar que faci el merge sense que li ho hagis dit.
## Per acabar: el que queda és criteri
Fa un any el coll d'ampolla era escriure codi. Ara és revisar-lo, perquè és l'últim pas que depèn d'una persona. D'aquí a un any serà una altra cosa.
La meva aposta és que la IA anirà agafant més parts de l'SDLC. Els models van tapant les mancances que tenien: el que avui encara reviso a mà, demà ho farà un bucle. Anthropic mateix explica que [delega una part creixent del desenvolupament d'IA a la mateixa IA](https://www.anthropic.com/institute/recursive-self-improvement). Tot el que pugui ser programari, serà programari.
Com a persones, ens haurem d'anar separant de l'SDLC, perquè cada fase que depèn d'una persona acaba sent una cua. Però **de moment els enginyers de programari encara hi serem**. El que canvia és quin és el filtre. Qui comença ara, o està estudiant, té un camí difícil: picar codi, la part que abans era la porta d'entrada, és just la que els agents ja fan millor. Només se'n sortiran els que tinguin **criteri i bones idees**: saber què val la pena construir, què deixar entrar i què no, i detectar quan un resultat verd és en realitat un error.
Si ja has trobat com desfer aquest coll d'ampolla al teu equip, o simplement en vols parlar, [parlem-ne](/contact).
---
# De MyHeritage als agents autònoms: com he transformat la meva recerca genealògica
URL: https://peremontpeo.dev/blog/myheritage-agents-autonoms/
Data: 2026-10-01
Descripció: Del SaaS a un repositori git amb agents d'IA, linter de CI i nivells de prova: com he reprès el meu arbre genealògic com un projecte d'enginyeria de dades.
Fa un any vaig començar a digitalitzar un arbre genealògic que vaig trobar a casa dels avis. [MyHeritage](https://www.myheritage.com) em va ajudar a arrencar-lo: hi vaig posar quatre noms i de sobte tenia branques senceres via Smart Matches, alertes automàtiques, un arbre que creixia sol. Però el ritme era desesperantment lent: moltes hores picant dades a mà davant la pantalla per avançar poc.
Darrere d'aquesta comoditat aparent, la realitat era una altra: una quantitat ingent de feina mecànica, una taxa de frustració alta i la sensació constant de xocar contra murs de pedra.
Aquest setembre he reprès el projecte amb un enfocament radicalment diferent: agents autònoms d'intel·ligència artificial que treballen hores seguides, pràcticament desatesos, mentre jo decideixo quines hipòtesis perseguir i tracto la genealogia com un projecte d'enginyeria de dades i recerca històrica. El contrast amb aquell arbre trobat en un calaix és tan gran que val la pena documentar-lo.
---
## 1. El punt de partida: la il·lusió de l'automatització a MyHeritage
Quan comences amb MyHeritage, la sensació inicial és màgica: hi poses quatre noms i de sobte el sistema et proposa branques senceres a través dels Smart Matches i Record Matches. Però quan vols fer recerca seriosa i rigorosa a Catalunya o a la península, topes de cara amb la realitat.
La feina manual no desapareix: es disfressa. Els motors comercials només troben el que ja està preindexat o el que altres usuaris han pujat als seus arbres (sovint amb errors arrossegats de generació en generació). Qualsevol línia que s'allunyi dels camins més transitats requereix anar a arxius diocesans, descarregar pàgines una a una i transcriure a mà lletra processal o itàlica del segle XIX.
L'ecosistema, a sobre, és tancat i les dades són brutes. L'exportació [GEDCOM](https://gedcom.io) de MyHeritage és un camp de mines: camps propietaris (`_MARNM`), cadenes de caràcters UTF-8 trencades entre línies `CONC/CONT`, enllaços a fotos allotjades en servidors que caduquen al cap de pocs dies, i desenes de grafies diferents per al mateix municipi.
I encara hi ha els murs de pedra inevitables. Si el rector d'una parròquia no tenia un índex alfabètic al final del llibre, o si el registre civil d'un avantpassat és anterior a 1870, la plataforma es queda cega. M'hi vaig quedar bloquejat mesos sencers en línies directes tan bàsiques com els avis o el matrimoni d'uns besavis.
---
## 2. El canvi de paradigma: genealogia com a codi i agents d'IA
En reprendre la recerca, vaig decidir canviar completament les regles del joc. L'arbre ja no és un fitxer atrapat en un servei SaaS: és un repositori privat de codi amb control de versions git. Les dades no s'editen alegrement: s'aplica una metodologia científica de nivells de prova (T0 pista d'índex, T1 acta original transcrita, T2 dues fonts independents creuades).
Una integració contínua (CI) amb un *ratchet* vigila cada canvi: un *linter* en Rust ([`gedlint`](https://github.com/pmontp19/gedlint)) valida cada commit via GitHub Actions. La gràcia del sistema de trinquet (*ratchet*) és que no exigeix un arbre perfecte el primer dia, sinó que impedeix que entri cap error o advertència nova, forçant una millora contínua de la neteja de dades.
I tot això ho executa un equip multiagent autònom: agents d'IA (utilitzant entorns com [Claude Code](https://www.claude.com/product/claude-code), OpenCode, Codex i Antigravity) que actuen com a assistents d'investigació, analistes paleogràfics i enginyers de dades, compartint *skills* i protocols comuns.
---
## 3. Les tècniques que han marcat la diferència
### A. Enginyeria de context: el pipeline de tires de marges (Strips)
Els llibres sacramentals històrics solen tenir centenars de pàgines per volum. Fer que un model multimodal inspeccioni desenes de pàgines completes és inviable per cost, latència i saturació de context visual (*lost in the middle*).
La solució és pura **enginyeria de context**: un script retalla exclusivament la franja lateral del marge (el 20-30% on el rector anotava el nom del batejat o els contraents) i munta tires condensades de 4 pàgines per làmina. Això redueix dràsticament el consum de tokens i permet que un agent de visió escanegi desenes d'anys de registres en pocs minuts, localitzant la pàgina exacta abans de demanar la descàrrega sencera en alta resolució.
### B. El poder del Full-Text Search per IA (FamilySearch HTR)
Fins fa molt poc, si un llibre no estava indexat manualment per voluntaris, simplement no existia a les cerques. Això ha canviat radicalment aquest 2026. L'ús de la nova cerca full-text d'IA de [FamilySearch](https://www.familysearch.org) (reconeixement HTR sobre microfilms manuscrits no indexats) ens ha permès desbloquejar branques esquives en altres comunitats autònomes o arxius notarials llunyans, trobant notes marginals manuscrites que cap cercador convencional havia catalogat mai.
### C. Cirurgia atòmica sobre el graf i normalització del parentiu català (`ged.py`)
Deixar que un model de llenguatge editi un fitxer GEDCOM a mà és una recepta per a la corrupció de referències creuades (`FAMC`, `FAMS`, `CHIL`, `HUSB`, `WIFE`). Per garantir edicions quirúrgiques segures, vam crear una eina nativa en Python pur. L'agent només fa operacions atòmiques (`add-indi`, `link`, `validate`) que verifiquen la integritat byte a byte abans d'escriure a disc.
A més, el motor de càlcul de relacions s'ha adaptat a la **terminologia genuïna de parentiu en català**:
- Distingeix estrictament entre **germans** (de pare/consanguinis o de mare/uterins) i **germanastres** (parentiu pur d'afinitat sense sang, fills de cònjuges diferents d'unions prèvies).
- Resol les branques col·laterals asimètriques amb denominacions tradicionals catalanes com l'**oncle valencià** (cosí germà del pare o de la mare) i el **nebot valencià** (fill del cosí germà), bandejant calcs absurds de l'anglès.
- Anomena amb precisió l'**oncle avi** (i no pas "besoncle"), l'**oncle besavi**, el **nebot net** i les generacions ascendents fins a **rerebesavis** (quadravis) sense deformacions com "trastavi".
### D. La skill d'estratègies genealògiques: Ahnentafel, agnació i xarxa FAN
En lloc de deixar que cada agent improvisi, hem empaquetat el coneixement metodològic en una *skill* modular compartida entre entorns:
- **Ahnentafel directa (amplitud BFS)** per cobrir capes equilibrades d'avantpassats (2^n).
- **Línia agnatícia / patrilineal (profunditat DFS)** amb una atenció clau a la tradició successòria catalana: **hereus, pubilles i cabalers**. Si una pubilla heretava el mas, els capítols matrimonials notarials sovint obligaven el marit a canviar o anteposar el cognom de la casa, alterant la continuïtat biològica del cromosoma Y sota el mateix cognom registrat.
- **Xarxa FAN (recerca en clúster)**: Davant d'un bloqueig o homonímia, l'agent analitza els padrins de bateig de tots els fills d'una parella, els testimonis de casament i els marmessors testamentaris. Els patrons de padrins forasters revelen gairebé sempre la parròquia d'origen de la mare.
- **El factor fills**: Com canvia l'estratègia quan hi ha descendència (fent servir la fertilitat i l'onomàstica tradicional com a brúixola) versus parelles sense fills, on cal saltar immediatament a testaments, capbreus i dispenses de consanguinitat.
### E. El protocol "de menys a més" i el valor dels negatius
Un agent autònom no ha d'operar a cegues. El nostre protocol estableix cinc fases progressives:
1. Validació prèvia interna (arbre local i catàleg de negatius).
2. Consultes puntuals d'índex T0 a baix cost.
3. Inspecció visual compacta de tires de marges (T0+).
4. Descàrrega d'alta resolució i transcripció literal de l'acta sencera (T1).
5. Corroboració per una segona font independent (T2) i commit amb traçabilitat.
I al centre de tot plegat: **el registre de negatius**. Registrar exactament quins llibres, municipis i rangs d'anys ja s'han revisat sense èxit estalvia desenes d'hores i evita bucles infinits de cerques redundants en sessions posteriors.
---
## 4. La convergència amb el món físic: documents de família
Un dels aprenentatges més potents d'aquest procés ha estat adonar-me que la IA més avançada necessita connectar-se amb els papers vells del calaix.
Els documents originals que es conserven a casa són mines d'or de primer ordre:
- El **llibre de família original dels avis**, que confirma de cop dates de naixement civils, municipis d'origen i defuncions sense haver d'esperar certificats d'arxiu.
- Un **DNI històric d'un besavi** de mitjan segle XX, que aporta la professió, la filiació materna contrastada i el domicili exacte d'aquella dècada.
- **Títols de propietat de sepultures de cementiris municipals**, que permeten situar branques senceres de besavis i oncles avis inhumats conjuntament.
Aquesta documentació domèstica primària serveix per validar i tancar com a T2 hipòtesis que sovint teníem encallades només a través d'actes parroquials antigues.
---
El sistema es va posar a prova real en quatre episodis concrets, cadascun d'una mena diferent de problema: escala, quan calia fer córrer diversos agents en paral·lel; dades heretades, quan calia netejar fantasmes de l'arbre antic; disciplina, quan calia normalitzar centenars de valors sense editar-los a mà; i rigor documental, quan calia decidir com escriure una data incerta. Són el nucli dur de tot el que he après.
## 5. Orquestració multiagent: cinc PR en un dia (i què va fallar)
El 14 de setembre de 2026 el sistema es va posar a prova de veritat: tres sessions d'agents en paral·lel (worktrees independents), cinc pull requests obertes el mateix dia i una verificació d'arxiu que ho travessava tot. El balanç: 551 individus, 141 famílies, zero errors de validació, avisos reduïts de 52 a 41. Però el més valuós van ser les lliçons, perquè gairebé totes van venir d'incidents reals:
1. **Els números de sessió col·lideixen**: tres sessions van titular la seva entrada "Sessió 17" el mateix dia. La regla que n'ha sortit és mecànica: el número surt de llegir el DIARI a `origin/main` amb fetch previ i sumar-hi u, mai de la memòria de la sessió.
2. **Els fitxers d'estat compartits són el coll d'ampolla**: quatre de cinc PR tocaven el DIARI i dues l'ESTAT; una va donar conflicte real de fusió. Cada sessió escriu el seu informe propi i només proposa l'entrada de diari; l'orquestrador integra en fusionar.
3. **Les branques envelleixen en hores**: les sessions filles partien d'un main sense les PR germanes i arrossegaven supòsits obsolets (recomptes, fitxers encara no fusionats). Rebase obligatori a l'inici i dependències declarades al briefing.
4. **El revisor automàtic també s'ha de revisar**: [CodeRabbit](https://www.coderabbit.ai) va caçar errors reals (un nivell de prova incoherent, una cobertura exagerada, una identitat mal argumentada), però també va demanar canvis contra la convenció del repo. Cada troballa es verifica contra la metodologia abans d'aplicar-se, i els descartes queden motivats per escrit.
5. **Missatge abans que presa**: quan calia afegir una verificació a la branca d'una sessió activa, un missatge directe la va integrar en minuts sense conflictes. Prendre la branca d'un altre només si és inactiva.
La conclusió m'interessa més que la tècnica: l'orquestrador humà ja no pica dades, resol conflictes de coneixement entre agents. I el sistema, amb totes les seves regles apreses a cops, millora sol a cada sessió.
---
## 6. Caça de fantasmes: el Consistency Checker invertit
Els Smart Matches no només afegeixen persones: de vegades en fusionen dues en una. El patró que ens ha netejat l'arbre aquesta setmana és invertir el Consistency Checker: en comptes d'acceptar l'aparellament automàtic, buscar la incoherència biològica i resoldre-la amb acta T1.
El cas canònic: dos presumptes germans separats per 117 dies. Impossible biològicament. L'anàlisi del cognom matern ho confirmava (deu germans amb un cognom, un amb un altre) i descartava la hipòtesi d'una segona muller intercalada (la primera continuava tenint fills sense interrupció). Decisió: desvincular sense inventar cap muller fictícia, deixar l'individu orfe amb NOTE de raonament i anar a l'acta. El baptisme va confirmar els pares reals, una parella diferent del mateix poble.
El mateix patró, aplicat amb l'arbre familiar de 1974 com a testimoni T1, va eliminar duplicats i fantasmes arrossegats de generació en generació: una Maria Rosa duplicada, dues germanes que eren confluència de noms de germanes reals, una Tecla fusionada amb el seu propi duplicat. En cada cas, la regla és la mateixa: cap fusió o desvinculació sense acta llegida i notes NOTE de traçabilitat a banda i banda.
I aquí una cosa ha portat a l'altra: les regles de coherència nascudes per caçar aquests casos al validador local (espaiat de germans, homònims vius, longevitat, matrimonis impossibles) es van proposar al linter del CI, van tornar com a regles noves i els seus propis falsos positius, un cop corregits, van netejar el baseline. La història completa de com s'ha anat construint el linter dona per un post sencer; aquí en queda l'esquelet: cada error real genera regla, cada regla genera falsos positius, cada correcció neteja deute.
---
## 7. Normalització massiva amb mapa idempotent
Quan el linter va marcar una vintena de famílies amb grafies inconsistents als fills, la temptació era editar a mà. El patró que funciona és el contrari: mapa declaratiu versionat.
Primer, inventari extractiu que lliga cada valor amb el seu context. Després, distinció explícita entre el mecànic i el que exigeix acta: en el nostre cas, 11 canvis NORMALITZAR aprovats per l'usuari amb l'eina de preguntes, i 9 casos VERIFICAR T1 que queden pendents perquè toquen identitat (noms primers, cognoms materns amb variant documental, famílies amb incògnita). La decisió de format es pren amb l'humà, no s'assumeix.
Després, dry-run obligatori, còpia de seguretat, aplicació, validació doble i notes NOTE de traçabilitat per fitxa. La propietat que ho fa robust: el mapa és idempotent i re-aplicable. Després d'un merge amb conflicte, es torna a aplicar i els valors nous queden normalitzats sense reeditar a mà. És el mateix patró que ja havia servit per a la normalització de topònims: escriure la decisió una vegada, al repo, i deixar que el script la garanteixi per sempre.
---
## 8. Rigor BIRT vs. BAPM i l'àncora com a pista
Tres lliçons de paleografia pràctica que han canviat com escrivim dates.
Primera: un baptisme no és un naixement. Quan l'acta diu "batejat el 19 de maig" sense data de naixement, el BIRT queda sense data i el BAPM porta el dia. Conservar una data de naixement heretada d'un arbre imprès al costat d'un baptisme d'acta és barrejar nivells de prova.
Segona: els llibres no sempre van en ordre cronològic. Un tram sencer anava organitzat per cognom, no per data. Qui busca per any en aquell tram, no troba; qui sap com està ordenat, sí.
Tercera: l'àncora de l'índex és una pista, mai una coordenada. En una sola setmana hem vist àncores errònies en data i pàgina, un matrimoni datat un mes més tard a l'índex que a l'acta, i una entrada d'índex que situava un baptisme en una pàgina on hi havia tres actes d'altres famílies. La regla que n'ha sortit: calibrar sempre amb capçaleres d'any reals i dues mostres, i escriure el calibratge a l'informe perquè la següent sessió no repeteixi la cacera.
---
## 9. Què he après sobre el paper humà
Els quatre episodis anteriors comparteixen una lliçó de fons: la intel·ligència artificial no fa màgia per si sola. Sense regles clares i sense supervisió metodològica, els agents poden confondre persones homònimes o inventar connexions aparentment plausibles.
La clau de l'èxit ha estat una divisió de funcions molt neta:
- **L'agent** assumeix la feina intensiva: descàrrega estructurada, retall i muntatge de tires de marges, lectura paleogràfica preliminar, manteniment del graf GEDCOM i compliment automàtic de les regles de CI.
- **L'humà (manager)** aporta el context insubstituïble: la memòria oral familiar (les anècdotes dels avis, els oficis i negocis tradicionals, les rutes migratòries que cap document eclesiàstic recull d'entrada), la decisió de quines hipòtesis prioritzar i el judici crític per discernir quan una prova documental és realment concloent.
Passar de la dependència passiva d'eines comercials a la direcció activa d'un equip d'agents autònoms sobre dades pròpies ha estat el salt més gran que he fet mai en genealogia. Ja no estic omplint buits en una web aliena: estic construint un corpus històric familiar rigorós, auditable, lliure i perenne.
Del calaix de casa a un corpus auditable en un repositori git: el mateix arbre, un any de diferència, un ofici nou pel mig.
---
# Incendis Catalunya: una integració de Home Assistant
URL: https://peremontpeo.dev/blog/incendiscat/
Data: 2026-07-06
Descripció: He construït i publicat ha-incendiscat, una integració de Home Assistant per seguir incendis forestals a Catalunya en temps real, i he desglossat quant m'ha costat en tokens fer-ho amb Claude Code.
Fa tres dies un incendi ha cremat [més de 2.300 hectàrees del massís de les Gavarres](https://ca.wikipedia.org/wiki/Incendi_de_les_Gavarres_de_2026), a l'Empordà, i ha obligat a confinar vuit municipis. No és un fet aïllat: cada estiu el país viu sota l'amenaça del foc, i la resposta institucional (Bombers, Agents Rurals amb el Pla Alfa) genera dades públiques en temps real que gairebé ningú fa servir fora dels seus propis visors.
Jo tinc [Home Assistant](https://www.home-assistant.io/) a casa. Vaig pensar: per què no portar aquesta informació allà on ja miro i automatitzo l'estat de casa?
## Home Assistant, en dues línies
Home Assistant és una plataforma d'automatització de la llar, de codi obert i local-first: centralitza sensors, dispositius i integracions de tercers, i deixa definir automacions ("si passa X, fes Y"). Cada font de dades s'hi connecta com una *integració*; jo n'he construït una de nova, distribuïble via [HACS](https://hacs.xyz/).
## Què fa Incendis Catalunya
`ha-incendiscat` connecta Home Assistant amb dues fonts públiques:
- El [visor d'actuacions](https://experience.arcgis.com/experience/f6172fd2d6974bc0a8c51e3a6bc2a735) dels Bombers de la Generalitat: incendis actius en temps real (ubicació, fase, vehicles desplegats).
- El [Pla Alfa](https://experience.arcgis.com/experience/2cf7ebbe492f401db826cb21eae9bfae) dels Agents Rurals: nivell de risc d'incendi diari (0-4) per municipi.
Amb això genera un mapa amb els focs actius, sensors agregats (nombre d'incendis, distància al més proper, vehicles desplegats, risc Pla Alfa), un `binary_sensor` que s'activa quan hi ha un incendi dins del teu radi d'alerta, events per enganxar-hi automatitzacions, i un blueprint llest per rebre una notificació push al mòbil.



Cap de les dues fonts és una API oficial. Són FeatureServers d'ArcGIS pensats per alimentar visors web, no per integrar-se en altres sistemes. Poden canviar d'esquema sense avís, així que bona part del disseny és tolerància a fallades: llegir camps amb valors per defecte, marcar el servei com a "degradat" després de tres errors seguits del mateix tipus, i no esborrar mai dades ja carregades.
## Com s'ha construït
Vaig començar pel que sol quedar-se pel camí: documents de disseny abans de codi. Vaig estudiar dues integracions HACS que ataquen el mateix problema en altres territoris: [`ha-wildfire-monitor`](https://github.com/johnbr/ha-wildfire-monitor) (Califòrnia, API REST de CAL FIRE) i [`ha-pyrovigil`](https://github.com/Duarte-Mercedes-Santos/ha-pyrovigil) (Portugal, el mateix patró ArcGIS FeatureServer que Catalunya). D'aquí en vaig treure un pla de 16 tasques.
Cada tasca és un commit: model de dades i client ArcGIS, coordinador amb polling i events, sensors i binary_sensors, entitats de geolocalització per cada foc, flux de configuració en dos passos, client del Pla Alfa, entitats de diagnòstic, traduccions (ca/es/en), blueprint de notificació, README. Després, una ronda només d'enduriment: revisió de codi, hardening del client ArcGIS, neteja general.


El bug més interessant no va sortir de cap revisió, va sortir d'usar-ho de debò: vaig muntar una instància de Home Assistant aïllada per provar la integració abans d'instal·lar-la a casa, i allà vaig descobrir que la vista d'ArcGIS dels Bombers no permet detectar baixes de forma incremental: un incendi que es resol simplement deixa d'aparèixer, no queda marcat com a tancat. Vaig haver de canviar l'estratègia de sincronització a "descarregar-ho tot cada cicle i comparar amb l'anterior", perquè amb sincronització incremental un incendi resolt es quedava viu per sempre a Home Assistant.
## Claude Code i OpenCode: quant costa fer això amb IA
Tot el codi d'aquest projecte l'he escrit conversant amb agents. Val la pena desglossar-ho perquè els números sorprenen.
[Claude Code](https://claude.com/claude-code) ha fet el gruix de la feina: 13 sessions repartides en 4 dies, gairebé totes amb Sonnet 5, amb Opus 4.8 puntualment per a un checklist de prellançament, un dubte semàntic sobre nivells de risc i una revisió de codi. En total, **~138 milions de tokens** processats, però un **96% (~132M) són lectures de cache**. Fora del cache, la feina real és ~570.000 tokens d'entrada nova, ~835.000 de sortida generada, i ~4,1M escrits a cache. A sobre, dos subagents de recerca puntuals (per estudiar CI/CD de referència i mapes tipus flightradar per mostrar els focs com una targeta).
[OpenCode](https://opencode.ai/) hi ha entrat només dues vegades, i amb un model gratuït (`nemotron-3-ultra-free`): un cop per netejar referències internes a "Task N" que havien quedat als docstrings (l'`AGENTS.md` del projecte demana explícitament no referenciar-les mai als comentaris, perquè el pla evoluciona i la referència queda òrfena), i un altre per mirar analítiques de la integració a HACS. En total, ~1,3 milions de tokens. Pràcticament testimonial comparat amb Claude Code. Hagués fet servir GLM-5.2 per això o més petit, però l'API de ZAI no estava disponible.
La feina de fons (les 16 tasques, el bug de sincronització trobat usant-ho de debò, el canvi de nom a mig camí) s'ha fet gairebé tota dins d'una conversa contínua amb Claude Code, on jo enviava captures de pantalla ("per què surt -1km?", "per què veig 0 incendis?") i l'agent iterava fins que la interfície feia el que tocava. OpenCode ha quedat per feina puntual i de baix risc. La lliçó no és quin agent és millor: és que el gruix del cost, en temps i en tokens, és la iteració amb l'ús real, no escriure la primera versió del codi.
---
Codi obert, llicència MIT, instal·lable via HACS: [github.com/pmontp19/ha-incendiscat](https://github.com/pmontp19/ha-incendiscat)
Si vius a prop d'una zona forestal o nucli aïllat m'agradaria saber si la feu servir.
---
# QuiverAI i l'AI SDK: quan el text és una imatge (i es pot fer streaming)
URL: https://peremontpeo.dev/blog/quiverai-ai-sdk/
Data: 2026-03-10
Descripció: Fa uns dies vaig afegir suport per a QuiverAI a l'AI SDK de Vercel, i el procés va revelar un cas d'ús que trobo especialment curiós: un model que genera text... però el text és una imatge.
**Languages:** [Català](#catalan) • [English](#english)
---
## Català {#catalan}
Fa uns dies vaig afegir suport per a [QuiverAI](https://quiver.ai/) a l'AI SDK de Vercel, i el procés va revelar un cas d'ús que trobo especialment curiós: **un model que genera text... però el text és una imatge**.
## SVG: el format que viu en dos mons
Un SVG és, tècnicament, XML. Text pla. Pots obrir-lo amb el bloc de notes i llegir-lo:
```xml
```
Però si el poses en un `` o el carregues al navegador, veus una imatge vectorial. Escalable fins a l'infinit, sense píxels.
Això fa que QuiverAI sigui un cas especial: és un model de generació d'imatges, però la seva sortida és text. I quan algo és text, es pot fer **streaming**.
## `LanguageModelV3` i `ImageModelV3`: la mateixa API, dues experiències
L'AI SDK té dos tipus de models per a casos com aquest:
- **`LanguageModelV3`**: retorna text. Suporta `generateText` i `streamText`.
- **`ImageModelV3`**: retorna bytes (`Uint8Array`). Suporta `generateImage`.
A `quiverai-ai-provider` he implementat els dos. Criden exactament la mateixa API de QuiverAI per sota, però exposen interfícies molt diferents cap a l'aplicació.
### `generateImage`: l'experiència clàssica
```ts
import { quiverai } from 'quiverai-ai-provider';
import { generateImage } from 'ai';
const { images } = await generateImage({
model: quiverai.image('arrow-preview'),
prompt: 'Un cercle vermell amb vora blava',
});
const svg = new TextDecoder().decode(images[0].uint8Array);
```
Esperes, arriba la imatge sencera, la renderitzes. Simple, net. L'esperable per a la majoria de casos d'ús.
### `streamText`: el dibuix en directe
Ara ve la part divertida. Com que el SVG és text, pots fer streaming token a token. Cada fragment que arriba és un tros petit de markup XML — a vegades un sol caràcter, a vegades uns pocs bytes:
```xml
<
svg
xmlns
="
http://www.w3.org/2000/svg
"
```
Si vas acumulant els fragments i re-renderitzes el SVG al navegador a cada chunk, obtens un efecte de **dibuix en directe**: veus com l'SVG es construeix sol, capa a capa, línia a línia.
```ts
import { quiverai } from 'quiverai-ai-provider';
import { streamText } from 'ai';
const result = streamText({
model: quiverai('arrow-preview'),
prompt: 'Un cercle vermell amb vora blava',
});
let svg = '';
for await (const chunk of result.textStream) {
svg += chunk;
renderSVG(svg); // el navegador tolera SVG incomplet!
}
```
El truc és que els navegadors moderns toleren SVG parcialment format. No cal esperar el tag de tancament `` per renderitzar algo; el motor del navegador fa el possible amb el que té.
El problema és fer-ho consistent per tots els navegadors. Si arriba un chunk a mitges — per exemple, `
```
But if you put it in an `` tag or load it in the browser, you see a vector image. Infinitely scalable, no pixels.
This makes QuiverAI special: it's an image generation model, but its output is text. And when something is text, you can **stream it**.
### `LanguageModelV3` and `ImageModelV3`: Same API, Two Experiences
The AI SDK has two types of models for cases like this:
- **`LanguageModelV3`**: returns text. Supports `generateText` and `streamText`.
- **`ImageModelV3`**: returns bytes (`Uint8Array`). Supports `generateImage`.
In `quiverai-ai-provider` I implemented both. They call exactly the same QuiverAI API underneath, but expose very different interfaces to the application.
#### `generateImage`: The Classic Experience
```ts
import { quiverai } from 'quiverai-ai-provider';
import { generateImage } from 'ai';
const { images } = await generateImage({
model: quiverai.image('arrow-preview'),
prompt: 'A red circle with a blue border',
});
const svg = new TextDecoder().decode(images[0].uint8Array);
```
You wait, the entire image arrives, you render it. Simple, clean. What you'd expect for most use cases.
#### `streamText`: Live Drawing
Now comes the fun part. Since the SVG is text, you can stream it token by token. Each chunk that arrives is a small piece of XML markup — sometimes a single character, sometimes a few bytes:
```xml
<
svg
xmlns
="
http://www.w3.org/2000/svg
"
```
If you accumulate the fragments and re-render the SVG in the browser with each chunk, you get a **live drawing effect**: you see how the SVG builds itself, layer by layer, line by line.
```ts
import { quiverai } from 'quiverai-ai-provider';
import { streamText } from 'ai';
const result = streamText({
model: quiverai('arrow-preview'),
prompt: 'A red circle with a blue border',
});
let svg = '';
for await (const chunk of result.textStream) {
svg += chunk;
renderSVG(svg); // the browser tolerates incomplete SVG!
}
```
The trick is that modern browsers tolerate partially formatted SVG. You don't need to wait for the closing `` tag to render something; the browser engine does its best with what it has.
The problem is making it consistent across all browsers. If a chunk arrives mid-way — for example, `