runtime
This commit is contained in:
133
plans/2026-09-02_reflect-review-findings.md
Normal file
133
plans/2026-09-02_reflect-review-findings.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Reflect skill — review 2026-09-02
|
||||
|
||||
Stav: nálezy review, opravy navrženy, čekají na schválení implementace
|
||||
Datum: 2026-09-02
|
||||
|
||||
Review prošlo celý skill: SKILL.md, README.md, `reflect_apply.py`, `reflect_auto.py`,
|
||||
`reflect_distill.py` a všech 168 testů (prošly, 1.1 s). Celkový verdikt: nadprůměrně
|
||||
dobře napsaný — „záruky v kódu, ne v promptu" je proveden důsledně, testy kódují
|
||||
racionalu u každého assertu. Níže jsou problémy v pořadí závažnosti + návrh opravy
|
||||
u každého.
|
||||
|
||||
## 1. Fold přeloženého open nálezu zahodí drafted patch a skip count (bug)
|
||||
|
||||
`merge_findings` (reflect_auto.py ~522) dědí `regression_of` a `history`, ale ne
|
||||
`patch`, `patch_drafted_at` ani `skipped`. Jakmile noční běh znovu spatří stejný
|
||||
`open` pattern, `supersede` starý záznam zahodí a nový vzniká bez těchto polí.
|
||||
|
||||
Následky:
|
||||
|
||||
- patch složený při review přes `--set-patch` (agent ho pracně ověřil proti
|
||||
souboru) zmizí; SKILL.md krok 2 tvrdí „`patch_drafted_at` says an earlier
|
||||
review drafted it" — po jedné noci to neplatí
|
||||
- „deferred 2× already" z kroku 3 se vynuluje — nález se předkládá donebezedne
|
||||
bez viditelné historie odkladů, i když ho uživatel už několikrát odložil
|
||||
- audit log (`DRAFTED`, `SKIPPED`) ukazuje práci, na kterou store už neodkazuje
|
||||
|
||||
**Fix:** v `merge_findings` při foldu open/watch předchozího záznamu přenést:
|
||||
|
||||
```python
|
||||
if previous and previous["status"] == STATUS_OPEN:
|
||||
if previous.get("patch") and not item.get("patch"):
|
||||
# drafted during a review, verified against the file — do not throw it away
|
||||
patch = previous["patch"] # do Finding(...)
|
||||
patch_drafted_at = previous.get("patch_drafted_at")
|
||||
skipped = previous.get("skipped") # deferral history survives the fold
|
||||
```
|
||||
|
||||
Pole `patch_drafted_at` a `skipped` je potřeba přidat do `Finding` dataclass a
|
||||
`to_json()` (podmíněně jako ostatní volitelná pole). Nový patch z analýzy má
|
||||
přednost před starým draftem; jinak se drží draft z review. Testy: fold open
|
||||
nálezu s `skipped={count:2}` a drafted patchem → nový záznam obojí nese;
|
||||
nový patch z modelu draft nepřepisuje, ale nahrazuje.
|
||||
|
||||
## 2. `reflect_apply.py` nechrání vlastní store jako cíl patche (designová mezera)
|
||||
|
||||
`_resolve_target` odmítne cestu mimo workspace, ale klidně aplikuje patch na
|
||||
`reflect/findings.jsonl`, `reflect/state.json` nebo `log/reflect.log`. Celá
|
||||
filozofie skillu je „do audit trail píše jen reflect_apply" — ale reflect_apply
|
||||
sám může patchem přepsat audit trail (typicky změnit `rejected` záznam zpět na
|
||||
`open`, což oživí zamítnutý vzor). Stane se to „se schválením uživatele", které
|
||||
v diffu snadno přehlédne, že jde o store.
|
||||
|
||||
**Fix:** explicitní blocklist v `check_patch` / `_resolve_target`
|
||||
(reflect_apply.py):
|
||||
|
||||
```python
|
||||
PROTECTED = ("reflect/", "log/reflect.log")
|
||||
|
||||
def _resolve_target(workspace, relative):
|
||||
target = (workspace / relative).resolve()
|
||||
if not target.is_relative_to(workspace.resolve()):
|
||||
raise ApplyError(f"{relative} resolves outside the workspace")
|
||||
if target == (workspace / FINDINGS_REL).resolve() or \
|
||||
any(target.is_relative_to(workspace / prefix) for prefix in PROTECTED):
|
||||
raise ApplyError(f"{relative} is part of the reflect store — not a patch target")
|
||||
...
|
||||
```
|
||||
|
||||
Důvod zdůvodnit v chybové hlášce („the audit trail is never a patch target").
|
||||
Pozn.: `set_patch` tím kryje i draft, nejen apply. Test: patch s
|
||||
`file: reflect/findings.jsonl` → exit 2, store netčen.
|
||||
|
||||
## 3. Vakuózní assert v testu (test_reflect_auto.py)
|
||||
|
||||
`test_first_run_has_no_trend_to_show` tvrdí `"(minule" not in report`, ale
|
||||
`_rate_line` generuje anglické „(previous run …)". Assert nikdy nemůže selhat,
|
||||
test tedy nic nehlídá.
|
||||
|
||||
**Fix:** `assert "(previous" not in report`. Jednořádková změna.
|
||||
|
||||
## 4. Noise prefix `"cli"` chytá i `client*` (případné falešné vyřazení)
|
||||
|
||||
`key.startswith(NOISE_PREFIXES)` — session `client_xyz` (nebo cokoli začínající
|
||||
„cli") se tiše vyřadí z analýzy. Prefix-match na krátkých prefixech je
|
||||
přístřelen. Podobně base64 session jména obsahující `-`/`_` (jsou v urlsafe
|
||||
abecedě) se nedekódují kvůli heuristice `"_" in stem or "-" in stem` —
|
||||
legitimní stará session se pak chytne prefixem, nebo naopak neprojde.
|
||||
|
||||
**Fix:** noise match na hranici klíče: session klíče mají tvar
|
||||
`<prefix>_<rest>` resp. base64 bez `_`, takže matchovat
|
||||
`key == prefix or key.startswith(prefix + "_")`. Případně (jednodušeji)
|
||||
přejmenovat prefix na `cli_` v NOISE_PREFIXES, protože reálné machinery session
|
||||
jsou `cli_<…>`. Test: `client_abc` prochází, `cli_kimi-ollama-test` ne.
|
||||
|
||||
## Menší
|
||||
|
||||
### 5. Git fingerprint se nekontroluje při LLM error cestě
|
||||
|
||||
`_resolve_findings`: fingerprint se porovná jen po úspěšném tahu. Když tah
|
||||
skončí LLM errorem a agent v tu chvíli něco zapsal, guard se neprojeví a
|
||||
run pokračuje. Riziko je teoretické (error odpověď znamená, že k zápisu
|
||||
s nejvyšší pravděpodobností nedošlo), ale guard je zadarmadlo dokončit.
|
||||
|
||||
**Fix:** porovnat fingerprint i na začátku error větve
|
||||
(`if result.stop_reason == "error" or result.error:`) — stejná kontrola,
|
||||
stejná hláška.
|
||||
|
||||
### 6. `MIN_MESSAGES=5` počítá i tool výsledky
|
||||
|
||||
`distill_session` inkrementuje `message_count` pro user, assistant i tool
|
||||
role. Session s 1 user zprávou a 2 tool cally (celkem 5 záznamů) projde
|
||||
prahem, přestože je to jeden dotaz — nižší signál, ne rovnou chyba.
|
||||
|
||||
**Fix (volitelné):** počítat jen user + assistant zprávy
|
||||
(`message_count += 1` jen v těch dvou větvích). Tool results počítat jako
|
||||
součást tahu, ne jako zprávu. Existující testy `message_count == 6` adaptovat.
|
||||
|
||||
### 7. Neatomičnost mezi `git commit` patche a `_save(findings)`
|
||||
|
||||
Crash mezi commitem patche a zápisem store zanechá soubor patched, ale store
|
||||
`open` (bez audit linky). Re-apply se správně odmítne na chybějícím `old_text`,
|
||||
ale audit stopa pro ten commit chybí. U single-user workspace akceptovatelné;
|
||||
zmiňuju pro úplnost. Plná atomicita (např. save-first-then-commit s rollbackem
|
||||
store) nedoporučuju — přidá složitost pro okrajový scénář. Spíš uvážit
|
||||
pořadí: nejdřív `_save` + audit, pak commit; pak crash zanechá `applied`
|
||||
záznam bez commitu, což reflektuje hlášku `git revert` — ale zase commit
|
||||
refusu zanechá store applied bez commitu. Trade-off, nechává rozhodnutí
|
||||
na implementaci.
|
||||
|
||||
## Pořadí implementace
|
||||
|
||||
1 → 2 → 3 → 5 → 4 → (6, 7 volitelné). Položky 1–3 jsou přímé rozpory se
|
||||
zárukami deklarovanými v README („Záruky"), 5 a 4 jsou levné tvrdnutí guardů.
|
||||
70
plans/cook-skill-design.md
Normal file
70
plans/cook-skill-design.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Cook skill — final design
|
||||
|
||||
## Context
|
||||
|
||||
User wants a personal store for recipes and tea notes (origins, brewing
|
||||
parameters, tasting). Wiki/note rejected as too heavy. Agreed design:
|
||||
`cook/` data dir + thin `skills/cook/` skill manifest + a small safety
|
||||
script. This plan finalizes the design agreed question-by-question in chat
|
||||
(2026-09-08) and supersedes the quick-draft SKILL.md already written.
|
||||
|
||||
## Decisions (locked with user)
|
||||
|
||||
1. **Script, not raw file tools** — `cook.py` guards against accidental
|
||||
overwrite/delete; frontmatter always machine-generated (no drift).
|
||||
Keep it **minimal — shortest working code, no speculative features**.
|
||||
2. **Frontmatter** (free-form values, only `type` is fixed):
|
||||
`type: recept|caj`, `category`, `cuisine` (recepts only),
|
||||
`origin` (caj only), `tags` (user hashtags, no `#`), `added` (auto date).
|
||||
Can be lightened later.
|
||||
3. **Hashtags live in frontmatter `tags`**, not in body.
|
||||
4. **Search via script**: frontmatter filtering + fulltext body grep.
|
||||
Start simple; enrich later.
|
||||
5. **Slug collision**: `add` on existing slug → error; agent shows the
|
||||
existing item, user decides edit vs. new slug. No auto `-2` suffixes.
|
||||
6. **Assets**: `cook/assets/<slug>/` when documents/photos ever arrive;
|
||||
not created upfront.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Rewrite `skills/cook/SKILL.md` to the final design:
|
||||
- layout (`cook/recepty/`, `cook/caj/`, `cook/assets/` — created on demand)
|
||||
- all file mutations via `cook.py` subcommands; agent never `write_file`s
|
||||
into `cook/` directly except body edits via `edit_file`/`apply_patch`
|
||||
after `add` creates the skeleton
|
||||
- capture inline (same turn), commit after each change
|
||||
(`git add cook/ && git commit -m "cook: ..."`; never `git add -A`)
|
||||
- search/answer flow: `list`/`search` to narrow, then read files, answer
|
||||
from files only, no confabulation
|
||||
- edit/delete: two-turn confirm flow (show exact text → user confirms →
|
||||
apply → commit)
|
||||
- slug conventions: kebab-case; collision handling per decision 5
|
||||
2. Write `skills/cook/scripts/cook.py` — minimal (~150 lines), stdlib only:
|
||||
- `add <slug> --type recept|caj [--category C] [--cuisine C] [--origin O]
|
||||
[--tags a,b] [--body-file F]` → creates file with frontmatter;
|
||||
**exit 1 if slug exists**; reads body from stdin or `--body-file`
|
||||
- `edit <slug>` → prints file path; **exit 1 if missing** (no silent
|
||||
create); actual text edits done by agent with `edit_file` on the path
|
||||
- `list [--type T] [--category C] [--tag X]` → one line per item:
|
||||
`slug type category added title`
|
||||
- `show <slug>` → full file content
|
||||
- `search <text>` → fulltext grep across bodies, prints matching lines
|
||||
with slug context
|
||||
- `rename <old> <new>` → moves file (+ its assets dir if present);
|
||||
exit 1 if target exists
|
||||
- `delete <slug>` → removes file (and empty assets dir); only run
|
||||
after user confirmation per SKILL.md flow
|
||||
- `validate` → checks every file has parseable frontmatter and `type`
|
||||
- Frontmatter: minimal hand-rolled parse (no external deps).
|
||||
3. Test drive: `add` a sample recipe + sample tea, verify frontmatter,
|
||||
`list`, `search`, collision error, `validate`, then delete samples and
|
||||
commit the real state.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cook.py add` twice on same slug → second exits 1
|
||||
- `edit`/`show`/`rename`/`delete` on missing slug → exit 1
|
||||
- `list --tag chata` filters on frontmatter tags
|
||||
- `search <word>` finds body text
|
||||
- `validate` passes on script-created files, fails on a hand-mangled one
|
||||
- SKILL.md contains no leftover pipeline cruft (inbox/compile/lock)
|
||||
Reference in New Issue
Block a user