deSchouwVloot · het overkoepelende project
Losse projecten, één engineeringstandaard
Elk project dat ik bouw heeft z'n eigen repo, eigen issues, eigen architectuur en eigen workflow, en is los van de rest te ontwikkelen. deSchouwVloot is de laag daarboven: het legt vast hoe er gebouwd, gecontroleerd en vrijgegeven wordt — dezelfde poorten, dezelfde checks, per project een keuze in hoeveel er zonder mij doorgaat, en AI-agents die daarbinnen het grootste deel van het werk doen. Er draaien inmiddels vier repo's op, zonder dat er één regel gedeelde logica is geforkt; deze site is er zelf één van. Elke bewering hieronder is na te trekken in de publieke repo.
Kies een pad om verder te lezen
Of sla het verhaal over en lees de ontwerpkeuzes — de controles, de incidenten en de nulmeting.
De kernzinnen staan er sowieso; deze keuze voegt per stap alleen meer laag toe.
Hoe het op elkaar staat
- BiohackOS
Flutter-app, privérepo, eigen constitution.md en een eigen testset waar de checks op leunen.
eigen repoeigen issueseigen workflow - koenholman.nl
Deze site. Andere taal, andere stack, geen eigen server — en toch dezelfde poort en dezelfde checks.
eigen repoeigen issueseigen workflow - Een privéproject
Privé, en van een heel andere vorm dan waarvoor de vloot ooit gebouwd is. Aangehaakt zonder uitzondering in het contract.
eigen repoeigen issueseigen workflow
01 · In het project
Een issue begint waar het thuishoort
Werk ontstaat in de repo van het project dat het raakt: daar staat de code, daar staan de grenzen van dat project, en daar is te beoordelen of een idee er überhaupt in past. Eén zin is genoeg om te beginnen — uitgebreider mag ook — en wat er daarna mee gebeurt, gebeurt in die repo, langs de stations die het project zelf aanroept.
achtergrondHet issue leeft in de repo van het project zelf en wordt daar behandeld met de .fleet.yml van dát project: eigen lanes, eigen poorten, eigen labelnamen, eigen budgetten. De gedeelde workflows kennen die waarden niet — ze krijgen ze binnen als input van de aanroeper.
als het misgaatElk project draagt in diezelfde .fleet.yml een spine-lijst: de stations waarvan uitval stil zou zijn. Valt er daar één om, dan wordt dat in dat project gemeld in plaats van afgewacht.
02 · De gedeelde standaard
Zelfstandig, maar niet apart geregeld
Een project houdt alles wat het eigen maakt in eigen hand: eigen doelen, eigen grenzen in constitution.md, eigen tests, eigen releasecadans. Wat het deelt is de vorm eromheen — per station een caller van een paar regels naar de gedeelde workflows, en verder niets. Zo draait BiohackOS op dezelfde standaard als deze site, zonder dat het ene project iets van het andere hoeft te weten. Een nieuw project haakt op dezelfde manier aan, en deSchouwVloot verandert daar niet voor.
achtergrondHet dragende mechanisme is dat een reusable workflow draait in de context van de aanroeper: runs-on resolvet tegen de runnerpool van de aanroepende repo, github.repository is de aanroepende repo, en secrets: inherit geeft diens secrets door. De vloot levert de logica, het project levert de hardware en de secrets. Het contract tussen beide is één bestand: .fleet.yml — lanes, poorten, budgetten, labelnamen en de spine.
als het misgaatMist een project een script dat een station wél eist, dan draait dat station fail-closed en merget het per constructie nooit meer — dat was incident I28. De eis wordt nu uit de stationsdefinitie zelf afgeleid in plaats van uit een handlijst die achterloopt.
vier repo's · nul forks
Er draaien inmiddels vier repo's op de vloot. Twee daarvan hebben een compleet andere vorm dan het project waarvoor ze ooit gebouwd is — andere taal, andere stack, geen eigen server, alleen door GitHub geleverde machines. Voor geen van beide is één regel gedeelde logica gekopieerd. Dat is de echte toets: niet of een contract denkbaar generiek is, maar of een project dat er niet op ontworpen is er zonder uitzonderingen in past.
zijingang · niet de hoofdweg
Soms heb ik alleen een zin en nog geen plek. Daar is één poort voor, die er een bestemming bij zoekt en het issue daarnaartoe verplaatst — en weet hij het niet zeker, dan kiest hij niet maar vraagt hij het. Het meeste werk gaat daar niet langs: dat begint gewoon in de repo waar het thuishoort.
Lukt het niet om bij elke bestemming het genoemde pad op te halen, dan wegen paden niet mee en gedraagt de poort zich exact als voorheen. Een scherpere afstelling die stilletjes half werkt, is erger dan de botte versie.
achtergrondintake.yml is workflow_call-only, net als alle zestien workflows; een guard-script bewijst dat bij elke PR, zodat de poort nooit vanzelf kan starten. intake-decide.sh telt hele woorden uit routing.yml per consument, en weegt een genoemd bestandspad vijf keer zo zwaar als een trefwoord — een pad zegt iets over de plááts, een woord alleen over het onderwerp. Strikt de meeste punten wint; de winnaar krijgt het issue via een transfer, niet via een kopie.
als het misgaatGelijkspel of nul punten → label needs-routing en het issue blijft staan met een vraag. Die grensgevallen liggen vast in de golden-set: 08-grensgeval-domein-wint, 09-grensgeval-infra-wint, 10-gelijkspel-blijft-routing.
03 · Triage, plan, bouwen
De agent werkt het zelf uit
Triage bepaalt wat voor werk het is; plannen toetst het aan mijn vastgelegde grenzen en bakent de scope af voordat er iets gebouwd wordt. Bouwen voert dat plan daarna uit op een eigen machine, met een limiet — loopt de agent vast, dan stopt hij vanzelf.
achtergrondDe build draait op de self-hosted lane biohack-agent in een ephemeral container zonder docker-daemon, met een turn-budget van 80 en claude-sonnet-5 als model; escalatie gaat naar claude-opus-5. Concurrency-groepen staan op het issue.
als het misgaatRunner offline? De lane valt terug op ubuntu-latest, en runner-fleet-assert.yml meldt het. Turn-budget op? De agent stopt en escaleert naar needs-human in plaats van door te blijven draaien.
plan-critic · scope en criteria
epic-orchestrator.yml houdt de reeks vast: fase-issues met een vaste volgorde, en alleen de eerste staat open. plan-critic.sh heeft het plan al mechanisch getoetst op concrete stappen en op een genoemde verificatie.
als het misgaatVerwerpt de criticus het plan, dan wordt er niets gebouwd — een fout plan is het duurste faalpad, want je betaalt build, review en herstel op het verkeerde fundament. Twijfelgevallen komen er als bevinding uit, niet als afwijzing.
wel de Action, niet de standaardinstellingen
De bouwstap draait op de kant-en-klare Claude-Action — daar valt weinig aan te verbeteren. Wat het verschil maakt is alles eromheen, en dat is hier allemaal met de hand gezet: een turn-budget van 80 in plaats van ruim, een modelkeuze die uit het issuetype volgt, een concurrency-groep op het issue in plaats van op de run, en een allowlist die per commando is opgeschreven.
Die allowlist is de grootste knop. Ontbreekt er één commando, dan probeert de agent een omweg, krijgt weer een weigering, probeert iets anders — tot het budget op is. Eerder liep er zo een issue twee keer vast op 151 turns met 22 weigeringen en niets bruikbaars. Daarom telt de pijplijn die weigeringen nu achteraf: boven de drempel komt er vanzelf een melding dat er een regel ontbreekt, in plaats van dat het pas bij de derde dure run opvalt.
ontwerp · nog niet gebouwd
Bouwen gaat nu uitsluitend autonoom. Zelf meebouwen in een interactieve sessie werkt merkbaar prettiger dan diezelfde stap op een runner, dus er ligt een ontwerp om die bouwstap ook vanuit een chatsessie te kunnen oppakken. Additief, niet vervangend: de autonome route blijft de standaard voor achtergrondwerk, de tweede ingang is er alleen voor de gevallen waarin ik zelf meedoe. Allebei door dezelfde poort, dezelfde claim en dezelfde bot-identiteit — geen van beide krijgt een kortere weg.
Die claim bestaat trouwens al wél: vlak voor de agent begint leest de workflow de labels opnieuw, en draagt het issue er één dat zegt dat een sessie het al opgepakt heeft, dan stopt hij. Het label uit de gebeurtenis zelf is een momentopname; die her-check krimpt het venster waarin ze elkaar in de weg kunnen zitten tot vrijwel nul.
eigendom van kennis, niet van de pijplijn
Naast constitution.md en doelen.md brengt een project vaak ook zijn eigen retrieval mee: RAG die antwoorden ophaalt uit de eigen documentatie of logs, en een eigen MCP-server die de agent gestructureerde toegang geeft tot de tools en data van dat project. deSchouwVloot zelf blijft daarbuiten — het bevat geen domeinlogica, dus die kennis hoort bij het project, niet bij de machinerie eromheen.
Bij één van die projecten staat dat inmiddels ook echt te draaien: vóórdat de agent aan een plan of build begint, doet een los leesscript één zoekopdracht per kennistabel — bekende faalpatronen plus de relevante documentatie — en injecteert alleen de beste treffers, in plaats van hele documenten opnieuw in te lezen bij elke sessie zonder geheugen. Faalt die zoekopdracht, dan bouwt de agent gewoon door zonder die context: de kennisbron mag nooit een harde afhankelijkheid worden.
04 · Checks
Zeven bewakingen moeten groen zijn
Zeven bewakingen moeten groen zijn voordat er iets verdergaat. De tests draaien over de volledige rekenkern van het project, niet alleen over het ene stukje dat net veranderde. Menselijke review blijft het inhoudelijke oordeel; alleen de tests en de deterministische checks kunnen zelf tegenhouden.
achtergrondpr-check.yml draait zes jobs met concurrency op het PR-nummer: diff-classificatie, conventional-commit-titel, flutter analyze en flutter test (beide blokkerend), coverage (rapporteert), preview-APK, en eslint op de webkant. doctor.yml en gitflow-doctor.yml bewaken de invarianten; de doctor rapporteert hard en muteert nooit.
als het misgaatRood? De PR blokkeert, de pijplijn repareert zichzelf en draait opnieuw; blijft het rood, dan escaleert het. Loopt cockpit/project-digest.md uit de pas met de registry, dan blokkeert de digest-guard — die plattegrond werd handmatig bijgehouden tot hij drift ging vertonen, en is nu gegenereerd en CI-bewaakt.
impactanalyse · van vinkje naar bewijs
Elke feature-PR draagt een klein codeblok: db=geen, net=bestaand, risico=laag — twaalf velden, elk met een gesloten antwoordlijst. Dat verving een checklist van tien vinkjes die alleen toetste of ze waren aangevinkt, niet of ze ook klopten. Nu haalt de check de daadwerkelijke bestandenlijst van de PR op en houdt elk antwoord ertegen: claim je db=geen terwijl er een migratie in de diff zit, dan is dat aantoonbaar onwaar — en gaat de PR rood, zonder dat ik ernaar hoef te kijken.
De zeven controles zelf, de nulmeting eronder en de vier keren dat alles groen stond terwijl het stuk was: de ontwerpkeuzes, met een bron per bewering.
05 · Mens beslist
Hoeveel mag het zelf?
Per project zet ik één knop aan of uit. Staat hij uit, dan wacht nieuw werk op mijn goedkeuring. Staat hij aan, dan gaat werk dat door alle controles komt vanzelf door. Vier dingen blijven hoe dan ook bij mij liggen — die knop komt daar niet aan.
mergeauto-merge.yml kijkt naar gates.feature_approval in .fleet.yml: op true wacht een feature op approval, de rest mergt door op groen. branch-protection-assert.yml controleert dat de beveiliging op de branch ook echt aanstaat.
als het misgaatOntstaat er een merge-conflict, dan lost de pijplijn dat zelf op en draait opnieuw. Lukt dat niet, dan is needs-human de derde en laatste poort.
releaseissue-release.yml en promote-release.yml leveren pas als alle fase-issues gesloten zijn. release_environment: production betekent een GitHub-omgeving met required reviewer, dus de knop zit in het platform en niet in een script.
als het misgaatFaalt een release halverwege, dan blijft de epic open en gaat er niets half naar buiten. Eén release voor het geheel, of geen.
Ten eerste alles wat gevoelig ligt: wachtwoorden, wijzigingen aan de database, en de bouwstraat zelf. Ten tweede een noodknop die in één handeling het hele project terugzet naar handmatig. Ten derde een markering die ik op één voorstel plak om precies dat ene stil te zetten. En ten vierde elke wijziging die bestaand gedrag breekt.
Het verschil tussen aan en uit is kleiner dan het klinkt. In allebei de standen kan ik overal ingrijpen. Wat omdraait is alleen wat er gebeurt als ik niets doe: uit betekent dat er gewacht wordt, aan betekent dat er doorgegaan wordt.
En als iets niet duidelijk is, wacht het. Een configuratiebestand dat niet te lezen is, een ontbrekend onderdeel, een typefout in de stand — dat gaat allemaal terug naar mij, met de reden erbij. Twijfel telt hier niet als groen licht.
de knopeen repo-variabele (FLEET_AUTONOMY), geen commit — een schakelaar die een merge nodig heeft is geen schakelaar. De volgorde ligt vast in één resolver die per poort getest is: gevoelig pad, noodrem (FLEET_HALT), stoplabel, breaking change, en pas daarna de stand zelf.
wat er misgingIk bouwde er een extra route naast: werk dat een onafhankelijke controle heeft doorstaan, mag ook zonder mij door. Die route keek alleen niet naar de noodknop en niet naar de markering. Hij deed het juiste, maar de rem die erboven hing werkte er niet op — en dat merk je niet, want er gaat niets rood. Er is nu een test die controleert dat béide routes op dezelfde situatie hetzelfde antwoorden.
Omdat het meeste zichzelf oplost voordat het bij mij komt. Loopt er iets vast, dan wordt dat opgemerkt. Valt een controle om, dan probeert de pijplijn de fout eerst zelf te herstellen. Botst een voorstel met werk dat er intussen bij is gekomen, dan wordt ook dat eerst zelf opgelost. En wat af is, wordt wekelijks opgeruimd. Zelf herstellen is de regel; bij mij aankloppen de uitzondering.
Ook de documentatie bewaakt zichzelf. Een overzicht van de code hield ik vroeger met de hand bij, en dat liep ongemerkt achter. Nu wordt het gegenereerd, en loopt het uit de pas met de code, dan houdt het de bouw tegen. Achterstallige documentatie is daarmee een fout geworden in plaats van een gewoonte.
Waar die controles op kunnen leunen verschilt nog per project. Eén ervan heeft al een stevige eigen testset; nieuwe projecten groeien daar naartoe zodra ze aanhaken.
Eén project mag nog een stap verder, als ik dat aanzet — standaard staat het uit. Het meet elke nacht aan zichzelf hoe het ervoor staat, en mag daar zelf een voorstel over indienen. Zo'n voorstel legt daarna precies dezelfde weg af als elk ander idee: uitzoeken, plannen, bouwen, controleren, en mijn goedkeuring aan het eind.
zelfhersteleen watchdog voor vastgelopen runs, pr-autofix en pr-conflict-solver die een rode PR eerst zelf proberen te herstellen, de epic-orchestrator die bijhoudt welke fase van een epic open mag staan, en een branch-janitor die wekelijks gemergede branches opruimt — via de PR-historie, niet via een git-ancestor-check, want deze repo's squash-mergen.
documentatieeen digest-guard: de gegenereerde plattegrond wordt tegen de code gehouden, en een verschil laat de build vallen.
de nachtmetinghealth-checks, nieuwe momentopnames en loggaten-hypotheses, gebundeld tot één issue op mijn eigen repo. Opt-in, standaard uit.
06 · Live
Elk project gaat zelf naar buiten
Wat er buiten staat is nooit half: pas als een epic in zijn geheel binnen is, gaat het naar buiten, met mijn goedkeuring op de release-knop van dát project. Elk project dat aanhaakt houdt zijn eigen doelen, zijn eigen grondwet in constitution.md, zijn eigen aanvullingen op de tests en zijn eigen releasemoment — en deelt alleen de vorm: dezelfde poorten, dezelfde checks, en dezelfde vier dingen die altijd bij mij blijven — hoe ver ik een project verder ook laat doorlopen. Dat is wat deSchouwVloot laat zien: zelfstandige projecten die tóch als één geheel te bouwen en te beheren zijn.