Lintujeme promptovou vrstvu

Na blogu bylo od července ticho, a to ze dvou důvodů.
Ten první byly tři týdny nad hranicí lesa, v severní Itálii a v Rakousku. Dvě místa za všechna ostatní.
Arco na severním cípu Lago di Garda, s Brentskými Dolomity nad hlavou — vápenec, který vypadá spíš nakreslený než zvětralý, a cesty, které jsou svisle častěji než vodorovně.

A v Rakousku Krimmelské vodopády — dost velké na to, aby byly vidět už od parkoviště, s cestou, která stoupá podél nich.

Druhým důvodem byla práce, a víc než obvykle: sprint pro klienta proti pevnému termínu a pak další hackathon ve formátu, o kterém jsme psali v červnu — týden paralelně běžících agentů, po kterém večer nezbylo nic na psaní.
Do klientského projektu přinesl kolega agnix — linter na soubory, kterými se konfigurují AI kódovací agenti. Chytlo to. Nasadili jsme ho tam, pak na CrushLogu, a jeden malý případ použití skončil na tomhle blogu — dost malý na to, aby se dal projít od začátku do konce, což je přesně to, co dělá zbytek tohohle článku.
Zároveň tím zavíráme něco, co jsme nechali otevřené. Před třemi měsíci jsme do backendu CrushLogu zapojili ArchUnit a napsali o tom článek. Jedna věta z něj nám od té doby nedávala pokoj:
Naše
CLAUDE.mduž ta pravidla obsahovala v próze. Próza build neshodí.
Použili jsme ji jako odůvodnění, proč přepsat javovské invarianty do ArchUnit testů. Fér. Ale přečtěte si ji znovu a všimněte si, co přiznává: samotná CLAUDE.md je pořád próza. Soubor, který řídí agenta — ten, který mu říká, jak psát kód, jenž pak pečlivě ověřujeme — byl vyňat ze standardu, který jsme uplatňovali na všechno pod ním.
Tak jsme to spravili. Z blogových konvencí jsme udělali skill s kontrolami, které umí selhat, namířili na něj agnix a celé to dali na GitHub jako SaaSForge-s-r-o/claude-skills.
Linter něco našel. Část toho byla na tomhle webu.
Konfigurace agenta selhává tiše, a v tom je celý problém
Chyba při kompilaci Javy vás zastaví. Rozbitý SKILL.md ne. Frontmatter se nepodaří naparsovat, skill se nezaregistruje a session pokračuje přesně tak, jako byste ho nikdy nenapsali — žádná chyba, žádné varování, žádný degradovaný režim. Prostě tiše dostanete výchozí chování a předpokládáte, že vaše instrukce fungují.
Právě tenhle způsob selhání dává lintru pro tuhle vrstvu smysl. Obvyklé obrany tu neplatí:
- Testy nepomůžou, protože není co tvrdit. Skill nespadl; on tam nebyl.
- Review nepomůže, protože recenzent čte, co soubor říká, ne jestli ho nástroj naparsoval.
- Vyzkoušet to taky spolehlivě nepomůže — skill, který se nenačetl, je k nerozeznání od skillu, který se načetl a jen se náhodou nespustil.
agnix je CLI nástroj v Rustu, který tuhle vrstvu validuje — CLAUDE.md, SKILL.md, AGENTS.md, definice subagentů, hooky, MCP konfigurace, manifesty pluginů — napříč hlavními formáty asistentů. Pracovali jsme s verzí 0.52.2.
Krok první: z prózy se stanou brány
Konvence jsme měli sepsané, a to už rok. Pole ve front matteru, translationKey spojující českou a anglickou verzi, obrázky 1200 × 630, uvést fotografa, budoucí data znamenají naplánováno, ne rozbito.
Vynutitelnou část jsme převedli do čtyř kontrol:
| Brána | Co kontroluje |
|---|---|
| Front matter | Povinné klíče jsou přítomné a správného typu; date odpovídá YYYY-MM-DD; tags je seznam |
| Jazyková parita | Každý článek existuje v každém jazyce se shodným translationKey |
| Titulní obrázek | Odkazovaný soubor existuje a má nakonfigurované rozměry |
| Build | hugo --buildFuture skončí s kódem 0 |
Celé to běží jako PostToolUse hook, takže se rozbitý článek zachytí při zápisu, ne až při buildu.
Dvě návrhová rozhodnutí se ukázala důležitější než samotné kontroly.
Validátor, který svému vstupu nerozumí, musí selhat, ne hádat. Napsali jsme si malý parser front matteru místo závislosti na PyYAML, protože hooky běží v prostředí uživatele a nemůžou předpokládat, že je nainstalovaný. To je v pořádku — dokud parser nenarazí na něco mimo podmnožinu, kterou umí. Pokušení je řádek přeskočit a jet dál. Udělali jsme z toho chybu:
if ":" not in raw:
raise FrontMatterError(f"unparseable line: {raw.strip()!r}")Parser, který tiše přijme vstup, jemuž nerozumí, je horší než žádný parser, protože vyrobí zelenou kontrolu nad nepřečteným souborem.
„Přeskočeno“ není „prošlo“. Build brána spouští Hugo. Na stroji bez Huga první verze vrátila „žádné problémy“ — což je technicky pravda a naprosto zavádějící. CI runner bez Huga by ohlásil zelenou build bránu, aniž by cokoli sestavil. Teď přeskočená brána řekne, že je přeskočená:
except FileNotFoundError:
if notices is not None:
notices.append(f"build gate skipped: {shlex.split(command)[0]!r} not on PATH")
return []Tohle je jediný způsob selhání, který by celé cvičení znehodnotil. Kontrola, která hlásí „neproběhlo“ jako „prošlo“, je horší než žádná kontrola, protože vyrábí jistotu.
Krok druhý: co linter našel
Na tomhle webu: 19 varování, 16 z nich mimo
První běh nad tímhle Hugo repozitářem našel devatenáct varování. Šestnáct z nich bylo XP-003, „napevno zadaná cesta Claude Code může způsobit problémy s přenositelností“, a ukazovalo na řádky jako tenhle:
content/en/blog/claude-code-skills-guide.md:16:32
warning: Hard-coded Claude Code path '.claude/' may cause portability issuesTo není konfigurace. To je publikovaný článek o .claude/skills/, kde je ta cesta předmětem textu. agnix prochází Markdown a aplikuje pravidla Claude Code na cokoli, co vypadá jako konfigurace agenta — a Hugo strom content/ plný článků o konfiguraci agenta je podle téhle heuristiky k nerozeznání od té skutečné.
Oprava patří do konfigurace, ne do článků. Upravovat správnou dokumentaci kvůli lintru by bylo obrácené:
# .agnix.toml
exclude = [
"content/**", # publikované články — próza o konfiguraci, ne konfigurace
"public/**",
"resources/**",
]Řekněme to naplno: 84 % prvního běhu byl pro tenhle repozitář šum. To není ani tak vada nástroje, jako spíš fakt o tom, co se stane, když namíříte linter konfigurace na obsahový repozitář. Nastavení rozsahu je plnohodnotný krok, ne dodatek — a každý text, který přeskočí rovnou k „nainstalujte si to, je to skvělé“, vám dělá medvědí službu.
Tři, které měly pravdu
Zbylé tři byly v CLAUDE.md a jsou to nálezy úplně jiné kategorie:
CC-MEM-006 Negativní instrukce „Never“ bez pozitivní alternativy
PE-001 Klíčové slovo „always“ na 47 procentech dokumentu
(40–60 procent je zóna „lost in the middle“)
PE-003 Slabý jazyk „should“ v kritické sekci „Content Security“Stojí za to zastavit se u PE-001. Kóduje reálnou, měřenou vlastnost pozornosti nad dlouhým kontextem — že materiálu uprostřed dlouhého dokumentu se dostává méně spolehlivé pozornosti než tomu na obou koncích — a dělá z ní build-kontrolovatelné pravidlo o tom, kam v souboru umístíte svou důležitou instrukci. To není lintování syntaxe. To je lintování návrhu instrukcí.
Napsali jsme - Cover images sourced from Unsplash (free license) — always credit photographer at bottom of article a zahrabali to do 47% hloubky. To pravidlo má pravdu. Přepsali jsme to na přímý imperativ, který nespoléhá na pozici v dokumentu.
PE-003 zachytilo External links should use HTTPS uvnitř sekce nadepsané Content Security. „Should“ v bezpečnostním pravidle je pozvánka. Teď je tam must. CC-MEM-006 zachytilo **Never commit secrets** bez pozitivní alternativy — říct agentovi, co nemá dělat, aniž byste mu řekli, co má dělat místo toho, znamená nechat ho improvizovat.
Nic z toho dnes nic nerozbíjí. Všechno z toho dělá ze souboru horší sadu instrukcí, než jakou by mohl být.
Na našem vlastním pluginu
Lintování nového repozitáře pluginu vyprodukovalo CC-PL-004: plugin.json postrádá doporučené pole version.
Což je zajímavé, protože tvar toho manifestu jsme okopírovali z oficiálního pluginu hookify od Anthropiku — a hookify pole version taky nemá. To doporučení je rozumné; marketplace, který sbírá pluginy, potřebuje, aby uživatelé věděli, co si nainstalovali. Jen se všeobecně nedodržuje, ani v referenčních implementacích, ze kterých lidé kopírují.
Doplnili jsme ho a zamkli testem, aby nemohlo zmizet:
self.assertRegex(plugin.get("version", ""), r"^\d+\.\d+\.\d+$")Krok třetí: co linter zachytit nedokáže
Tady musí přijít upřímnost, protože tohle je část, kterou by oznámení nástroje přeskočilo.
Naše vlastní testy našly chybu, kterou agnix vidět nemohl. Validátor přeskakoval soubory začínající _ při procházení adresáře, ale jeden změněný soubor validoval bezpodmínečně. Hook tedy blokoval na _index.md — což je Hugo sekční index, který legitimně nemá žádný z povinných klíčů článku — zatímco procházení adresáře ho správně ignorovalo. Blokující hook, který odmítá _index.md, by z pluginu udělal aktivního nepřítele na jakémkoli reálném Hugo webu. agnix ověřuje, že vaše konfigurace má správný tvar. Na to, jestli je vaše logika správná, nemá názor.
Dvě omezení, která jsme zdokumentovali místo zametení pod koberec. Správně velký titulní obrázek s úplně jiným motivem branou 3 projde — rozměry se zkontrolovat dají, námět ne. A build brána věří návratovému kódu, takže build, který vypisuje chyby na stderr a skončí nulou, se ohlásí jako úspěšný. Obojí jsme sepsali do adversariálního review v repozitáři, místo abychom předstírali, že je pokrytí úplné.
A ten únik. Surový výstup prvního běhu agnixu jsme zacommitovali jako záznam o integritě — neupravený, aby stav „před“ nešel tiše vylepšit. Kontrola před publikací zjistila, že obsahuje absolutní cesty s uživatelským jménem autora, chystající se do veřejného firemního repozitáře. Vlastnost, která ten artefakt činila důvěryhodným — surový, needitovaný výstup nástroje — je přesně ta vlastnost, kvůli které unikal. Sanitizovali jsme po jedné zdokumentované ose a napsali to dovnitř souboru.
Odměna
Pak jsme validátor namířili na tenhle web. Dvacet osm článků, padesát šest souborů, čtyři brány:
Front matter: 0 porušení
Jazyková parita: 0 porušení
Build: prošel
Titulní obrázky: 3 porušení
- blog-axon5-cover.jpg: 1200x673
- blog-flux-operator-cover.jpg: 1200x943
- blog-mcp-stateless-cover.jpg: 1200x800CLAUDE.md říká 1200 × 630 od začátku blogu. Tři obrázky se stejně rozjely a nic to nezachytilo — protože ta škoda není vidět na stránce. Je vidět v Open Graph náhledech, v kartičce, kterou někdo uvidí, když se odkaz nasdílí do Slacku. Na místě, kam se na vlastní web nikdy nedíváte.
Tohle je ten argument, předvedený na našem vlastním repozitáři nástrojem, který jsme kvůli němu postavili. Konvence byla sepsaná. Sepsat ji nestačilo. Nikdy to stačit nemělo.
Kde to nechává celý stack
Tři vrstvy, každá dělá tu nad sebou kontrolovatelnou:
CLAUDE.mdpopisuje konvence- Skill s branami vynucuje vynutitelnou podmnožinu
- agnix ověřuje, že skill má správný tvar
A omezení, které nikam nezmizí: agnix ověřuje formu vaší konfigurace agenta. ArchUnit ověřuje podstatu vašeho kódu. Ani jeden vám neřekne, jestli vaše instrukce říkají správné věci. Linter potvrdí, že CLAUDE.md je dobře utvořená próza ve správném tvaru s klíčovými slovy na pozicích přívětivých pro pozornost. Nepotvrdí, že ty konvence za něco stojí.
Tenhle úsudek je pořád na vás. Změnilo se to, že všechno mechanicky kontrolovatelné pod ním teď selhává nahlas místo aby se tiše rozjíždělo — což je přesně to, co jsme před třemi měsíci prohlásili za cíl a co jsme ještě neaplikovali na soubor, který to celé řídí.
Plugin je pod licencí MIT a nainstalujete ho dvěma příkazy:
/plugin marketplace add SaaSForge-s-r-o/claude-skills
/plugin install hugo-blog@claude-skillsRepozitář obsahuje surový výstup prvního běhu agnixu, adversariální review s jeho dvěma přijatými omezeními a záznam o ověření se seznamem toho, co tvrdíme, ale co jsme zatím nepozorovali v živé session. Raději vydáme přiznané neznámé než tiché předpoklady.
Související: Architektonické testy jako pás proti agentnímu rozjezdu · Spring Modulith v praxi
Titulní fotografie od Mick Haupt na Unsplash.


