Files
nanobot-runtime/skills/reflect/README.md
2026-09-02 15:22:39 +02:00

188 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# reflect — jak to funguje
Skill hledá v mých vlastních session logách **opakující se chyby**, pojmenuje je a navrhne
opravu. Nálezy pak procházíš ty, jeden po druhém, a rozhoduješ, co se použije.
**Sám od sebe nikdy nic nezmění.**
## Dva režimy
```text
ANALÝZA — denně 03:30, bez tebe REVIEW — jen když napíšeš /reflect
cron → reflect_auto.py ├─ vezme 1 otevřený nález
├─ destiluje session z okna ├─ ukáže diagnózu, důkazy, diff + šanci
├─ LLM tah → nálezy ├─ čeká na tvoje rozhodnutí
├─ zapíše do findings.jsonl ├─ aplikuje → git commit → audit
└─ Telegram (jen když je co) └─ další nález
NEEDITUJE NIC edituje jen to, co schválíš
```
## Nálezy jsou o nedávném chování, ne o celé historii
Běh se dívá jen na **posledních 21 dní** (`--window-days`). Není to úspora — je to
podmínka, aby nález něco znamenal: korpus má 3,5 měsíce a jedna dávka 200 kB, takže
dohánění celé historie po jedné dávce za noc znamenalo, že cursor tři noci stál na
konci května a nálezy z něj se předkládaly jako aktuální.
Dvě hranice, každá jinak:
| | Co dělá |
|---|---|
| **cursor** | podlaha — co se jednou analyzovalo, se neanalyzuje znovu (jinak by se zdvojily počty) |
| **okno** | strop — co je starší než 21 dní, se přeskočí a cursor to mine natrvalo |
Kdybys někdy potřeboval archeologii, vrať `cursor` ve `state.json` a spusť
`--window-days 0`.
## Proč tě to neotravuje každý den
Denně přibude jen pár session — v nich se vzor nepozná, jedna chyba je náhoda. Proto:
| Kdy | Status | Telegram |
|---|---|---|
| vzor poprvé, 1 výskyt | `watch` | ne, jen se počítá |
| vzor podruhé (≥2× a ve ≥2 session) | `open` | ano |
| vzor už jednou opravený se vrátí **po** opravě | `open` + **regrese** | ano |
| vzor už jednou opravený, ale důkazy jsou starší než oprava | `watch` | ne |
| vzor jsi zamítl | `watch` napořád | ne, už nikdy |
Zamítnutí je rozhodnutí, ne odklad — zamítnutý vzor se znovu neotevře.
## Co ti /reflect nabídne
| Napíšeš | Stane se |
|---|---|
| `ok` / `aplikuj` | patch se použije a commitne |
| `uprav: <text>` | přepíšeš návrh vlastními slovy, ukáže se nový diff |
| `přeskoč` | nález zůstane otevřený na příště |
| `zamítni` | nález se zavře natrvalo |
| `konec` | konec review |
Rozhoduješ vždycky nad **hotovým diffem, ne nad větou o něm**. Nález, který od analýzy patch
nedostal (většina), si ho složí ještě předtím, než ti ho předloží: agent napíše návrh do JSON
a nechá ho ověřit (`--set-patch`). Skript ho **nejdřív ověří proti souboru a teprve pak
uloží**, takže nepoužitelný pokus ve `findings.jsonl` nezůstane — a agent do store nesahá
vůbec. Nález, který se editací souboru opravit nedá („míň se překotně přizvukovat"), diff
nedostane — to ti agent řekne rovnou a nabídne jen přeskočení nebo zamítnutí.
Vždy jen **jeden** nález najednou. Nálezy se číslují `1..N` podle pořadí, ne podle
interního id. **Nahoře jsou regrese**, teprve pak se řadí podle závažnosti: závažnost
odhaduje model znovu v každém běhu a u téhož vzoru kolísá, kdežto „tohle už jednou
opravené bylo a vrátilo se" je fakt z auditu.
## Šance, že oprava zabere
Pod diffem je odhad typu `Šance, že zabere: ~40 %`. Nejde o změřenou úspěšnost, ale
o zařazení do jednoho ze **čtyř pásem** (~80 / ~60 / ~40 / ~20 %) podle toho, co patch dělá:
| Co patch mění | Pásmo |
|---|---|
| mechaniku — skript nebo hradlo, které nejde ukecat | ~80 % |
| tvrdý zákaz do souboru, který je v kontextu ve chvíli, kdy chyba vzniká (`SOUL.md`, `AGENTS.md`, `SKILL.md` dotčeného skillu) | ~60 % |
| přeformulování existujícího pokynu v takovém souboru | ~40 % |
| soubor, který v tu chvíli v kontextu není, nebo rozhodnutí nechává na úvaze agenta | ~20 % |
Hlavní osa je **jestli je opravovaný text vůbec v kontextu, když chyba nastává**
sebelíp formulovaná věta v souboru, který se v tu chvíli nenačítá, chování změnit nemůže.
O pásmo dolů jde nález, který už jednou opravený byl a vrátil se (regrese), a nález, jehož
důkazy pocházejí z nesouvisejících situací. **Zastaralý nález odhad nedostane vůbec** — vzor
už možná dávno zmizel, takže není co předpovídat. Ke každému číslu patří věta, co ho tam
zařadilo.
## Kde co leží
| Cesta | Co |
|---|---|
| `reflect/findings.jsonl` | všechny nálezy, jeden JSON na řádek |
| `reflect/state.json` | kam se došlo (cursor), použité okno + statistiky běhů |
| `results/<datum>_reflect.md` | plný report jednoho běhu + okno, které pokryl, a kolik výskytů na 100 session mají už známé vzory |
| `log/reflect.log` | audit — každé rozhodnutí, i zamítnutí a přeskočení |
**„Naposledy" u nálezu je nejnovější datum v důkazech**, ne datum, kdy se záznam
naposledy přepsal. Když je starší než okno posledního běhu, je nález **zastaralý**:
analýza tam už nedohlédne, takže se sám nikdy neobnoví — spíš než patch si zaslouží
zamítnutí. `/reflect` ti to u něj řekne.
**Počty v nálezu (`7× ve 4 session`) jsou kumulativní součet napříč běhy** — sečtené
z toho, co model napočítal v jednotlivých oknech, ne měření nad celým korpusem. Skript
u nich hlídá jen to, co hlídat umí: vzor nemůže zasáhnout víc session, než kolikrát
nastal, ani víc, než kolik jich v dávce vůbec bylo.
## Co se o tvém rozhodnutí zapíše
| Rozhodnutí | Do `findings.jsonl` | Do `log/reflect.log` |
|---|---|---|
| složení patche (u každého nálezu bez patche, ještě před předložením) | `patch` + `patch_drafted_at` (= složeno při review, ne modelem) | `DRAFTED <id> [vzor] <soubor>` |
| `ok` | `applied: {at, sha, file}`, `patch` = návrh modelu | `APPLIED <id> [vzor] <soubor> — <sha>` |
| `uprav:` | navíc `applied.new_text` = tvoje verze (návrh modelu zůstává v `patch`) | `APPLIED-EDITED …` |
| `zamítni` | `rejected: {at, reason}`**důvod je povinný** | `REJECTED <id> [vzor] — <důvod>` |
| `přeskoč` | `skipped: {count, last}`, status zůstává `open` | `SKIPPED <id> [vzor] ×N` |
Povinný důvod u zamítnutí není otravování: je to jediná zpětná vazba na kvalitu analýzy.
Z prvních osmi nálezů jsi čtyři zamítl — bez důvodů se z toho čísla nedá poznat, co
v analytickém promptu změnit.
Na dotaz „co jsem už rozhodl" ti to `/reflect` vypíše z logu včetně revert příkazu.
## Jak vrátit změnu zpět
Workspace je git, takže každá aplikovaná oprava je samostatný commit:
```bash
git revert <sha>
```
SHA najdeš v `log/reflect.log` nebo u nálezu ve `findings.jsonl` (`applied.sha`).
Změna žije **jen na serveru** — do trackovacího repa (`src/nanobot`) se musí dotáhnout
zvlášť, jinak ji příští `rsync` skillu přepíše zpátky.
## Ruční spuštění
```bash
cd ~/.nanobot/workspace
# normální běh (to dělá cron)
~/.local/bin/uv run --script skills/reflect/scripts/reflect_auto.py
# jiné okno než výchozích 21 dní
~/.local/bin/uv run --script skills/reflect/scripts/reflect_auto.py --window-days 7
# jen prompty do tmp/, nevolat model; s --all přes celý korpus
~/.local/bin/uv run --script skills/reflect/scripts/reflect_auto.py --dry-run --all
# jak velký je korpus a na kolik dávek vyjde (nevolá model)
~/.local/bin/uv run --script skills/reflect/scripts/reflect_distill.py --stats > /dev/null
```
`--all` ignoruje cursor, takže by znovu přečetl už spočítané session a **nafoukl jim
počty**. V ostrém běhu ho skript odmítne — je jen na `--dry-run`.
**Přerušený běh o hotovou práci nepřijde.** Nálezy i cursor se zapisují po každé dávce
a nová dávka nezačne po 20 minutách (`--deadline-minutes`). S 21denním oknem to vyjde
zpravidla na jednu dávku, takže se tenhle strop ani neuplatní; kdyby zbyla nezpracovaná
dávka, Telegram to řekne i když nálezy nejsou žádné.
Plná cesta k `uv` je tu proto, že v neinteraktivním SSH není v `PATH`. Crontab si
`PATH` nastavuje sám, takže tam stačí `uv run …`.
## Záruky
Nejsou to sliby v promptu, ale kód:
- **Analýza (`reflect_auto.py`) nemá v sobě žádnou cestu k zápisu do cizího souboru.**
Navíc se před a po tahu porovná `git status` celého workspace — kdyby agent přesto něco
zapsal, nálezy se zahodí.
- **Editaci dělá výhradně `reflect_apply.py`, vždy jeden nález.** Agent soubor needituje
sám. Skript odmítne patch, jehož původní text v souboru není nebo je tam vícekrát —
nehádá, kam patřil. Odmítne i nález, který není `open` (tj. nebyl ti předložen).
- **Do `findings.jsonl` píše taky jen `reflect_apply.py`.** I patch složený při review jde
přes něj (`--set-patch`) a projde stejnou kontrolou; agent auditní stopu needituje.
- **Commituje se jen ten jeden dotčený soubor** (`git add -- <file>`), nikdy `git add -A`.
Rozdělaná práce Dreamu a jiných skillů se do commitu nedostane; když je rozdělaný přímo
ten soubor, udělá se nejdřív checkpoint commit, aby byl revert přesný.
- Skill vynechává vlastní session, takže neanalyzuje sám sebe.
- **Každé rozhodnutí nechá záznam**, i to, které nic nezmění: zamítnutí s důvodem,
přeskočení s počítadlem, `uprav:` odděleně od původního návrhu modelu.
Ověřeno testy v `tests/` — včetně toho, že `git revert` vrátí soubor do původního stavu.