Krok 7 fazy mailowej, warstwa wspoldzielona. Realizuje decyzje (a), (b), (f)
reconu kb/audits/mail-sync-2026-08-06.md (zatwierdzone przez operatora
2026-08-06): jeden adapter IMAP na oba konta, stan synca jako tabela w bazie.
Nowe moduly w packages/kb-mail:
- imap.py — ImapAccount/ImapClient nad stdlib imaplib (zero nowych zaleznosci).
Foldery otwierane READ-ONLY (EXAMINE) i pobierane przez BODY.PEEK[],
zeby job nie ustawial \Seen na skrzynce operatora. Wybor folderu po
atrybucie SPECIAL-USE, nigdy po nazwie — Gmail lokalizuje
"[Gmail]/All Mail". search_from_uid filtruje zakres po stronie
klienta, bo n:* zwraca ostatnia wiadomosc takze gdy przedzial pusty.
- sync_state.py — tabela mail_sync_state + czyste funkcje: plan_folder_sync
(pierwszy tick / przyrost / uniewaznienie UIDVALIDITY) i
contiguous_last_uid (kursor przesuwa sie tylko po nieprzerwanym
ciagu sukcesow — bledna wiadomosc jest ponawiana, nie przeskakiwana).
- headers.py / message.py — parse_headers(+fallback) z gmail-header-backfill oraz
message_id/parse_date/parse_attachments/eml_ref z gmail-bulk-import,
przeniesione zamiast skopiowane. Klucz dedup musi pochodzic z jednej
implementacji: kazdy insert przyrostowki trafia na 225 030 istniejacych
id. Stare joby re-eksportuja te nazwy — ich CLI i testy bez zmian.
kb_mail.db.insert_envelope zwraca teraz command tag (+ rows_affected,
envelope_source) — bez tego nie da sie odroznic zwyklego duplikatu od kolizji
Message-ID miedzy kontami (recon §2.4).
Migracja 005_mail_sync_state.sql: addytywna, klucz (account, folder).
Testy: 285 passed (111 kb-mail w tym 37 adaptera IMAP na fake serwerze i 26
planera kursora; 174 istniejace suity jobow bez zmian po ekstrakcji).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read-only recon (kb/audits/mail-sync-2026-08-06.md, OKF type: audit).
Stan wyjsciowy zmierzony na zywo: korpus gmail urywa sie 2026-06-19, dziura
48 dni ~ 1800 maili przy tempie ~37/dobe; zero kodu IMAP/JMAP w repo (tylko
dokumenty), brak modelu stanu synca — w bazie 3 tabele, zadnej z UID.
Ustalenia blokujace, ktore latwo przeoczyc (kazde zawodzi cicho, bez bledu):
- koperty bez entities[type=headers] daja prefiks chunka "(brak tematu) | ?"
(build_prefix), wiec poller musi pisac headers przy INSERCIE, nie backfillem
- DEFAULT_SUMMARYLESS_SOURCES = ("gmail",) — fastmail zembeduje sie i zniknie
z /search, bo galaz summaryless filtruje po source
- etap mailowy dopiety do kb-ingest.timer (03:30) zapali KbEmbedBacklogGrowing
na stale: SOLARIA wtedy spi (potwierdzone: kb_ingest_embed_skipped 1)
- envelope.id = goly Message-ID globalnie, wiec mail obecny na obu kontach
trafia do bazy raz, z source konta ktore wygralo wyscig
Architektura: fetch na PIHA co godzine (24/7, archiwum kanoniczne, bez GPU),
indeksowanie osobno bramkowane probe'em Ollamy (embed z PIHA zmierzony:
HTTP 200 w 8 ms, ~60 chunkow/dobe — rsync na SOLARIE zbedny). Spoiwem jest
kolejka "koperty bez chunkow", nie --since (ts to naglowek nadawcy).
Decyzje operatora (a)-(g) z rekomendacjami. Dwie korekty zalozen:
- POSTGRES_PASSWORD NIE lezy plaintextem w repo — service.yaml wymienia tylko
nazwy zmiennych, env.example ma placeholdery, skan sledzonych YAML: 0 trafien.
Rekomendacja uzywa istniejacego /opt/homelab/kb/.env (root:root 600,
czytany przez systemd przed zrzuceniem uprawnien)
- Fastmail przez IMAP, nie JMAP — domyka otwarta od czerwca decyzje
"unifikacja adaptera" (kb-mail-pillar.md §9); wymaga korekty §2/§7 tamtego
dokumentu po zatwierdzeniu
Zaleznosci z reconem multiagentowym: zadnych blokujacych. Dyspozytor to
subsystem B (osobny projekt); jawna zaleznosc to wiki-kompilat
(kb-m5-faza3.md:620 — "pelna wiki po przyrostowce").
Aktualizacja kb-m5-faza-mailowa.md: Krok 7 = WYKONANE + wskaznik do reconu.
Nic nie zaimplementowano, nie zdeployowano ani nie pobrano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warunek zdjecia M1 brzmial "R1 (prune filtrowany po restart policy / labelu
compose) wdrozony na tym nodzie". Zweryfikowane bezposrednio w kodzie dzialajacych
kontenerow, nie po datach deployu:
SOLARIA md5(/app/src/node_agent.py) = c9ac64e10b42b3e0ed9e4c168579bfaa,
identyczny z origin/master.
VPS rozni sie od origin/master wylacznie trescia komentarzy (5 linii
`docs/backlog.md` vs `kb/phases/backlog.md`, skutek migracji sciezek
w 9128530) — zero roznic funkcjonalnych.
Na obu nodach `_prune_stopped_containers` jest obecny (te same numery linii:
635/700/717/725), a jedyne wystapienia `containers.prune()` to tekst docstringa
i komentarza — brak wykonywalnego niefiltrowanego prune. Sprawdzone dodatkowo,
ze scripts/monitor/health-monitor.sh (ktory nadal ma niefiltrowane
`docker container prune -f` — R1 objelo tylko node_agent.py) nie jest wpiety w
zaden crontab ani timer na SOLARII i VPS, wiec node-agent byl faktycznie jedynym
zrodlem prune i cleanup byl na obu nodach realnie wylaczony.
SOLARIA: przywrocone jawne NODE_TYPE=ai_node — stan sprzed M1 (1cd6401),
zgodnie z konwencja pozostalych hostow, ktore wszystkie ustawiaja NODE_TYPE
jawnie (piha/lustro sd_card, chelsty-infra lte_node). solaria jest w AI_NODES,
wiec default dalby to samo, ale jawny wpis nie zalezy od hostname'u.
VPS: linia usunieta w calosci wraz z komentarzem TEMPORARY — dokladny stan
sprzed 11f3f80, gdzie NODE_TYPE nie bylo ustawione wcale. Potwierdzone, ze
default daje `standard`, nie None: base compose przekazuje `NODE_TYPE=${NODE_TYPE:-}`,
czyli pusty string, ktory jest falsy, wiec _resolve_node_type() schodzi do
rozpoznania po nazwie, a `vps` nie nalezy do LTE_NODES/SD_CARD_NODES/AI_NODES.
Sprawdzone na zlozonym `docker compose config` (NODE_TYPE: "") i uruchomieniem
_resolve_node_type() -> 'standard'. Rotacja filesystemu control-plane jest
bramkowana node_name == VPS_NODE_NAME, nie node_type, wiec dziala niezaleznie.
UWAGA DO DEPLOYU: na obu nodach brak /opt/homelab/state/last-docker-cleanup,
a przy braku markera _cleanup_rate_ok() zwraca True — pierwszy cleanup pojdzie
w pierwszym cyklu po restarcie (<=60 s), nie po 24 h jak na LUSTRO.
W chwili sprawdzenia zero kontenerow `exited` na obu nodach, wiec galaz
kontenerowa nie ma czego usunac; do sprzatniecia sa 4 dangling images na SOLARII
(~553 MB) i 1 na VPS (395 MB) plus build cache. humanai-mailer i humanai-landing
maja restart=unless-stopped, wiec sa chronione pierwsza galezia filtra R1 nawet
gdyby zostaly zatrzymane.
node-agent: 70 passed.
Refs docs/incidents/2026-07-30-ollama-solaria-vanish.md (§7, M1),
docs/sessions/2026-08-06.md (follow-up #5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wyciek plikow dispatch potwierdzony 2026-08-06 (session log, follow-up #1):
LUSTRO re-pullowalo te same dwie akcje co 60 s przez wiele dni, odbijajac sie
od bramki idempotencji, i nie zostawilo po sobie ani jednej linii w logach.
Przyczyna zlozona z dwoch niezaleznych defektow:
1. Executor tworzyl actions/dispatch/<node>/ z 0o755 (aerbot:aerbot). Rsync-pull
z noda uwierzytelnia sie jako inny uzytkownik, bedacy tylko *czlonkiem* tej
grupy. --remove-source-files musi zrobic unlink pliku, a unlink wymaga prawa
zapisu w katalogu nadrzednym, nie na samym pliku. Zrodlo przezywalo pobranie.
(dispatch/piha mialo historycznie 775 i dlatego dzialalo.)
2. node-agent traktowal rc=23 jako benign obok 0 i 24, wiec rsync zglaszal
porazke, a agent ja polykal.
Executor: _ensure_inbox_dir() = mkdir + bezwarunkowy os.chmod(0o775). chmod jest
bezwarunkowy z dwoch powodow: mkdir(mode=) jest maskowany przez umask procesu
(przy 0o022 daje dokladnie feralne 0o755), a inboxy zalozone przez wczesniejszy
build juz istnieja na flocie z 0o755. Naprawa w miejscu zapisu, a nie skanem przy
starcie: jedno idempotentne wywolanie na tej samej sciezce kodu, ktora pisze plik
dispatch, wiec nie da sie rozjechac z pisarzami. Blad chmod nie jest fatalny —
akcja i tak sie wykonuje, a nieskasowane zrodlo widac teraz po stronie noda.
Objete tez actions/deploy/<node>/ (deploy-runner): ten sam wzorzec drenowania
tym samym rsync-pullem, ten sam defekt, jedno wywolanie obok.
node-agent: klasyfikacja kodow wyjscia zamiast wspolnej listy benign.
Weryfikacja empiryczna rsync 3.4.1 pokazala, ze rc=23 pokrywa dwa rozne
przypadki, a rozroznia je dopiero stderr:
* `change_dir ... No such file or directory` — executor zaklada inbox dopiero
przy pierwszym dispatchu, wiec kazdy nod, do ktorego nic nie poszlo, dostaje
rc=23 co cykl. DEBUG — inaczej byloby po linii na minute z wiekszosci floty
i realny sygnal utonalby w szumie.
* `sender failed to remove <plik>: Permission denied` — wlasnie ten wyciek.
WARNING z pelnym stderr.
Pusty (ale istniejacy) inbox to rc=0, nie 23 — dotychczasowy komentarz w kodzie
mowil inaczej. rc=24 zostaje benign (wyscig z executorem piszacym inbox),
pozostale kody to teraz ERROR, nie WARNING. Zachowanie funkcjonalne bez zmian:
retry i idempotencja dzialaja jak dotad, zmienia sie wylacznie widocznosc.
Testy: 4 nowe w test_executor_dispatch.py (oba inboxy 0o775 pod umask 0o022,
naprawa istniejacego 0o755 in place, dispatch przezywa nieudany chmod), 5 w
test_action_dispatch.py na klasyfikacje rc. Zastapiony
test_pull_treats_empty_source_returncodes_as_non_error — kodyfikowal wlasnie to
zalozenie, ktore okazalo sie bugiem. Oba zestawy sprawdzone mutacja: bez chmod
padaja 3 testy executora, przy starej liscie benign pada test rc=23.
node-agent 70 passed, control-plane 173 passed.
Refs docs/sessions/2026-08-06.md (follow-up #1)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eval na pelnym korpusie 2026-08-06 (187 025 zembedowanych chunkow mailowych
w HNSW) dal PASS: kryterium 1 (regresja paperless) bez degradacji zadnego
istniejacego hitu we flat ani w hybrid, mailowe hit@3 5/5. Koszt hybrydy to
jedno dodatkowe zapytanie SQL na wyszukiwanie. Surowe wyniki:
eval-http-2026-08-06.json / eval-direct-2026-08-06.json w ~/kb/mail/ingest-logs
na PIHA (niecommitowane, artefakt runu).
- app/main.py: Query("cascade") -> Query("hybrid"); pattern bez zmian, wiec
jawne ?mode=cascade i ?mode=flat dzialaja dokladnie jak dotad.
- app/static/app.js: przy odznaczonym "tryb flat (debug)" UI nie wysyla juz
parametru mode w ogole -- dziedziczy default API. Default zdefiniowany
w jednym miejscu (serwer), nie zduplikowany w JS.
- testy: nowa klasa TestSearchEndpointModeDefault (TestClient bez lifespan,
fake pool/router) sprawdza kontrakt HTTP -- brak mode => tor hybrid
(weryfikowany po obecnosci koperty gmail osiagalnej wylacznie galezia
hybrid, nie po samej etykiecie), jawne mode=flat / mode=cascade => stare
tory, nieznany mode => 422. Frontend: buildSearchUrl pomija mode gdy brak.
- docs: kb/services/kb-query.md (tabela trybow + endpoint + przyklad
odpowiedzi + opis przelacznika w UI), env.example/service.yaml (komentarze
SUMMARY_MODEL; default mode nie jest konfigurowalny przez env),
kb/phases/kb-m5-faza-mailowa.md (DoD (d) SPELNIONE 2026-08-06 + wzmianki
w Kroku 3, Wyniku bramki, decyzjach Etapu B i tabeli planu).
Weryfikacja: pytest services/kb-query -> 46 passed; node --test
tests/frontend/app.test.js -> 6/6; docker build OK + smoke run (uvicorn
startuje, bez KB_DSN swiadomie konczy sie RuntimeError z env.example).
Deploy NIE wykonany -- operator wdraza z mastera na PIHA po mergu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regresja po zamknieciu Etapu B fazy mailowej dala FAIL wylacznie na
kryterium 3: kontrolka N2 "piaskownica plastikowa" spadla do 0.4924
(flat/hybrid) na mail przedszkolny "Materialy plastyczne" -- kolizja
leksykalna ponizej progu 0.50. Rownoczesnie odrzucone w Etapie A
zapytanie "piaskownica plac zabaw wspolnota" ma dzis realne odpowiedzi
(maile administracji wspolnoty holc.waw.pl, 2023). To nie regresja
retrievalu -- korpus urosl o tresc, ktorej w Etapie A nie bylo, wiec
kontrolka negatywna stracila waznosc.
- N2 wycofana; pelna historia decyzji (pilot, 2026-07-23, 2026-08-06)
zachowana jako komentarz w queries.yaml, nie skasowana.
- M5: "wymiana piasku w piaskownicy na placu zabaw wspolnoty",
expected_envelope 6ab218df-...@holc.waw.pl, d1=0.2858, hit3=y.
Wariant z "wymiana piasku" zamiast doslownego sformulowania operatora,
bo tamto daje 0.4562/0.4597 -- oba nad HIT_THRESHOLD 0.45.
- N3: "sterylizacja kota cennik kliniki weterynaryjnej", bar 0.50 bez
zmian, 0.5257/0.5799/0.5257. Dwaj odrzuceni kandydaci i lista tematow
majacych realna odpowiedz w korpusie udokumentowane w komentarzu.
- retrieval_eval.py: tylko komentarz -- mail_hit nadal ocenia sie
source-matchem, expected_envelope M5 jest dokumentacyjne.
Weryfikacja: retrieval_eval.py --transport http --base-url
http://192.168.31.5:8230 --gate-n 10 -> OVERALL PASS (exit 0),
kryteria 1/2/3 PASS, kryterium 4 = 5/5 (wymagane >= 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chunki z NUL wywalaly insert do postgresa (asyncpg CharacterNotInRepertoireError:
invalid byte sequence for encoding "UTF8": 0x00) - 3 przypadki na mailach z 2007
w plastrze offset 50000 Etapu B. sanitize_surrogates tego nie lapie, bo NUL to
poprawny code point, nie osierocony surogat.
Strip dzieje sie zaraz po strip_quotes, czyli PRZED chunk_text i przed wywolaniem
Ollamy - dzieki temu embedding liczy sie z dokladnie tego samego stringa, ktory
trafia do document_chunk.text. Sanityzacja dopiero przy insercie zostawialaby
wektor opisujacy tekst, ktorego DB nigdy nie zobaczyla.
Skala widoczna w progress/summary: nul_bytes_stripped (ile znakow) oraz
mails_nul_sanitized (ilu maili dotyczylo). Zadne z nich nie wchodzi do rownan
balansu i nie wplywa na exit code - to normalizacja, nie blad.
Ten sam strip w extract_threading (In-Reply-To / References): json.dumps zamienia
NUL na escape u0000, ktory jsonb odrzuca tym samym bledem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Poprzedni commit nauczyl gen_pages.py dopisywac token do linkow, ale nic
go nie podawalo — publikacja poszlaby stara sciezka i dalaby build
z golymi linkami, czyli stan sprzed fiksa.
kb-site nie ma skryptu deployu: generator wolany jest wylacznie recznie
z runbooka (kroki 2 i 7), wiec to tam token musi wejsc.
Zrodlo tokenu: /opt/homelab/config/kb-site/.env na wezle GENERUJACYM
(SATURN/SOLARIA), nie na PIHA. To swiadome odstepstwo od konwencji
config/<serwis>/ z CLAUDE.md — plik trzyma zwykle sekrety wezla, ktory
serwis uruchamia, a ten token jest potrzebny tam, gdzie serwis sie
generuje. Kontener nginx dalej nie ma zadnej konfiguracji ani sekretow;
odnotowane w service.yaml i env.example, zeby nikt nie szukal .env na PIHA.
Token idzie zmienna srodowiskowa (set -a; . plik; set +a), nie flaga
--access-token: argument z linii polecen laduje w historii shella i jest
widoczny w ps dla kazdego uzytkownika wezla.
Lancuch publikacji z kroku 7 dostal dwa nowe ogniwa przed scp: test -n
"$ACCESS_TOKEN" (pusty token = build nieklikalny) oraz grep -q 'key='
w index.html (token byl, ale nie dojechal do generatora). Oba zatrzymuja
publikacje tak samo jak --check.
Krok 6 weryfikuje teraz wlasciwa rzecz: wyciaga href ze spisu i pobiera
GO, zamiast recznie sklejac URL — czyli testuje to, co faktycznie bylo
zepsute. Doszedl tez negatywny test bramki (bez tokenu ma NIE byc 200).
Tabela problemow: "index sie otwiera, ale klikniecie daje 403" (build bez
tokenu) i "403 takze z tokenem" (rotacja tokenu w NPM rozjechana z plikiem
— stary build zostaje z martwym tokenem w kazdym linku).
kb/services/kb-site.md (public) — sekcja Access: token siedzi teraz
w tresci kazdej serwowanej strony, wiec jedna zapisana strona wydaje go
w calosci. Model zagrozen bez zmian (URL wejsciowy zawsze go niosl), ale
warto, zeby dokument mowil to wprost obok zdania "to obscurity, not
access control".
Test: sekwencje z krokow 2 i 7 przepuszczone na symulowanym pliku tokenu
(prod /opt/homelab nietkniety) — token obecny: Token: TAK, 4x key=
w index.html, lancuch dochodzi do tar; token pusty: staje na pierwszym
ogniwie, brak tgz; build bez tokenu przy ustawionej zmiennej: staje na
grep, brak tgz. check_okf.py exit 0, gen_pages --check exit 0,
service.yaml parsuje sie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wystawka KB stoi za bramka NPM na ?key=<token>. Linki generowane przez
gen_pages.py (index -> dokument, dokument <-> dokument, powrot do indexu)
tokenu nie nosily, wiec kazde klikniecie ze strony wpadalo w 403 —
dzialal wylacznie recznie sklejony URL do indexu.
Token podaje sie przy generacji: --access-token TOKEN albo zmienna
ACCESS_TOKEN. Nie ma go w repo w zadnej formie — to parametr runtime,
nie stala w kodzie. Bez tokenu generacja dziala jak dotad, z golymi
linkami (tryb lokalnego podgladu); wyjscie jest wtedy bajt w bajt takie
samo jak przed zmiana.
with_token() doklada ?key=... przed ewentualna kotwica i uzywa & gdy URL
ma juz wlasne query params (dzis nie ma — obrona na zapas). Token jedzie
przez urllib.parse.quote. Kotwice (#sekcja) i linki zewnetrzne zostaja
nietkniete. Wartosc nigdy nie leci na stdout — build() loguje tylko
TAK/NIE, bo logi z generacji bywaja wklejane.
--check: prawdziwy token (32+ hex) wygladal dla skanera dokladnie jak
wyciek `token-hex`. scan_line() wycina teraz wartosc `key=` WYLACZNIE
wewnatrz atrybutu href — ten sam token w tresci strony, po innym
parametrze niz key, albo poza href nadal jest raportowany jako wyciek.
Test: 11 stron public + index; z --access-token TEST123 wszystkie 12
linkow spisu, link doc->doc (agent-operating-procedures ->
action-approval-model) i kazdy powrot "All documents" niosa ?key=TEST123;
canonical swiadomie bez tokenu (metadana, nie nawigacja). Bez tokenu
diff vs HEAD pusty poza znacznikiem czasu. gen_pages --check exit 0 dla
generacji bez tokenu, z TEST123 i z realistycznym tokenem 64-hex;
check_okf.py exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uzasadnienie "backfill bez fallbacku na PIHA" bylo dotad tylko w docstringu
modulu. Czytelnik ogladajacy help(embed_batch) go nie widzial, a to wlasnie ta
funkcja jest miejscem, w ktorym ktos moglby "uzupelnic brakujacy failover".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batching /api/embed juz istnial (Krok 1 fazy mailowej, batch 64). Recon przed
Etapem B wykazal w torze backfillu blad blokujacy i dwie luki.
BUG (blokujacy dla Etapu B): flush_embed_buffer lapal wylacznie
aiohttp.ClientError, a wyczerpanie ClientTimeout(total=...) rzuca goly
builtins.TimeoutError, ktory NIE jest jego podklasa (zweryfikowane empirycznie
na aiohttp 3.14.3). Zawieszona Ollama — czyli jej udokumentowany failure mode,
"przyjmuje polaczenie i milczy" — wywalala caly run nieobsluzonym wyjatkiem,
bez breakera i bez flushu threadingu. Na plastrze 50k = utrata zarobionej pracy.
Klasy przejsciowe nazwane teraz jawnie w TRANSIENT_EMBED_ERRORS.
kb-retrieval:
- embed_batch(timeout_s=...) — bound per zadanie, skalowalny z batch size
- embed_batch_resilient() — retry z backoffem wykladniczym, a po ich wyczerpaniu
probe /api/tags rozstrzyga: backend zywy -> bisekcja izolujaca trujacy chunk
(jeden zly tekst kosztowal caly batch 64, bo /api/embed jest all-or-nothing);
backend martwy -> natychmiastowe gave_up bez bisekcji, ktora spalilaby 2n-1
zadan i opoznila breaker. EmbeddingDimensionError nigdy nie jest retry'owane.
- failed_indices wyprowadzane z wyniku, nie akumulowane per span — przy gave_up
w srodku bisekcji porzucone poddrzewo nigdy nie dochodzi do liscia.
mail-body-ingest:
- breaker liczy give-upy (backend padl), nie dowolne nieudane batche; porazka
czesciowa przy zywym backendzie nie przesuwa licznika, bo te chunki i tak
zlapie kolejny run przez idempotencje
- wiersze zembedowane w umierajacym batchu sa commitowane przed abortem
- parametryzacja: --batch-size/--embed-retries/--embed-backoff/--embed-timeout,
kazdy z odpowiednikiem env MAIL_INGEST_*; bledna wartosc env = glosny SystemExit
- metryka embed_ms_per_chunk (porownywalna miedzy runami, w odroznieniu od
sredniej per batch) + embed_requests_total/embed_calls jako sygnal zdrowia
mail-body-ingest-bench: nowy entry point, sweep batch size na realnych chunkach.
Read-only (SELECT + inferencja, zero sciezki zapisu), warmup przed pomiarem, ten
sam zbior chunkow dla kazdego rozmiaru. Czyni liczby z planu §1.4 odtwarzalnymi.
Fallback SOLARIA->PIHA dla backfillu SWIADOMIE nie powstaje (potwierdzone przez
operatora): 271k chunkow x 790 ms CPU ~ 60 h na 8 GB PIHA dzielonym z HA i
Paperlessem. Wlasciwa odpowiedzia na martwy backend jest exit 2 i wznowienie
plastra. Tor online (kb-query -> embed_router) zachowuje fallback — rozdzial
torow udokumentowany w docstringu embed.py i w kb/services/.
Testy: 117 zielonych (62 job + 22 klient embed + reszta pakietow), w tym
regresja na TimeoutError, bisekcja, ograniczony koszt przy martwym backendzie
i porazka czesciowa nieprzesuwajaca breakera. Bez uruchamiania backfillu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
solaria (powered off ~16 h/day by design) and lustro (nightly display
power-off) generated node_offline/node_stale/node_online alerts on every
daily cycle. Six of them have sat in actions/pending/ since 2026-06-17/18,
unapproved. Because an unapproved pending action suppresses its own dedup
ID indefinitely (recon D14, supervisor.py pending/approved/running check),
those stale alerts also meant a *real* future outage on either node would
generate nothing at all.
Suppression is data-driven from inventory/topology.yaml, not a hardcoded
node-name check:
- topology.yaml: new `duty_cycle` (+ `duty_cycle_reason`) on solaria and
lustro, mirroring the existing dormant/dormant_reason shape. vps and piha
deliberately do not carry it — an offline 24/7 node is a real incident.
- supervisor: _load_dormant_nodes() -> _load_node_policy(), loading both
dormant_nodes and duty_cycle_nodes from one topology read. dormant
behavior is byte-for-byte unchanged.
- supervisor: one guard in _route_node_event. Duty-cycle liveness events
are logged at INFO and return; no action is written.
duty_cycle is deliberately NOT dormant. A duty-cycle node stays fully
active: its services are still reconciled (missing_service -> redeploy),
its disk pressure still generates disk_cleanup, and its ha_* events still
route. Only the liveness alert is suppressed. Regression tests pin all
three.
Fail-loud: an unreadable topology leaves both sets empty, which disables
suppression and lets alerts through. A broken topology must never silently
mute the fleet.
Accepted trade-off: a genuine permanent outage of solaria or lustro no
longer alerts. It stays visible in the operator UI (which computes liveness
independently at read time) and in the event feed. An "offline longer than
the expected window" escalation is the natural follow-up and needs a
schedule in the topology field rather than a bare marker.
Tests: 169 passed in services/control-plane/tests (was 157; +12).
Runtime deployment is deliberately NOT part of this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"User-agent: * / Disallow: /" odbijalo nie tylko wyszukiwarki, ale kazdego
klienta respektujacego robots.txt (m.in. fetch asystentow AI) — takze takiego,
ktory znal token dostepu. Token mial WPUSZCZAC znajacych go, a robots.txt ich
WYPYCHAL.
Dzialalo tez przeciwko wlasnemu celowi: crawler zablokowany przed pobraniem
strony nigdy nie widzi meta noindex, a wyszukiwarka i tak potrafi wylistowac
sam URL, ktorego nie wolno jej bylo pobrac.
Ochrona przed indeksowaniem zostaje bez zmian: <meta name="robots"
content="noindex, nofollow"> w <head> kazdej strony (scripts/kb/gen_pages.py).
Plik mial jedna linie polityki, wiec bez niej tracil sens — usuniety razem
z bind-mountem w compose (katalog static/ zniknal jako pusty).
Test: docker compose config (exit 0, zostaje tylko named volume);
gen_pages.py --check — CZYSTO, 0 trafien.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dociagniecie do zmiany DEFAULT_BASE_URL z 24afb49 — po niej repo w szesciu
miejscach dalej podawalo stary adres.
- kb/runbooks/kb-site-deploy.md: wszystkie wystapienia + rekord Cloudflare
(Name: kb -> kb-e2a24af3). Runbook jest visibility: private, wiec slug
moze stac wprost.
- services/kb-site/{README.md,service.yaml,env.example,docker-compose.yml}
- hosts/piha/services.yaml: komentarz przy exposure
kb/services/kb-site.md swiadomie nietkniety — dokument publiczny, slug tam
nie wchodzi (opisuje adres jako "non-obvious subdomain").
check_okf.py exit 0, gen_pages.py --check exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warstwa "nie daj sie przypadkiem znalezc" dla publicznej wystawki KB:
- gen_pages.py: <meta name="robots" content="noindex, nofollow"> w <head>
kazdej generowanej strony (page_shell, wiec takze index).
- gen_pages.py: DEFAULT_BASE_URL -> https://kb-e2a24af3.okit.pl. Slug musi
zgadzac sie z rekordem DNS i vhostem w npm@PIHA (runbook kb-site-deploy).
- services/kb-site: static/robots.txt (Disallow: /) montowany ro na
/usr/share/nginx/html/robots.txt. Plik nie jest dokumentem KB, wiec jedzie
z repo, a nie z wolumenu podmienianego przy kazdej publikacji.
- kb/services/kb-site.md: sekcja "Access" — token w query paramie na warstwie
nginx/NPM (sekret zyje tylko w NPM, nie w repo) + obscure subdomain +
noindex. Explicit: to obscurity, nie kontrola dostepu — token w URL laduje
w access logach, historii przegladarki i naglowku Referer.
Bramka publikacji bez zmian: gen_pages.py --check exit 0 (22 wyciszone
whitelista, jak dotad).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recon read-only zlecony pod teze "executor odpala deploy-node.sh z argumentami,
ktore skrypt ignoruje, na sciezce nieistniejacej w kontenerze". Teza byla
prawdziwa dla mastera do 2026-08-03; dzis nie jest. 79bfe8c + da151fc zastapily
to dispatchem do jobs/deploy-runner/ (systemd na hoscie wezla).
Realny stan: luka deployowa w trzech miejscach naraz — kontener executora
zbudowany 2026-07-22 (wciaz stary kod), /opt/homelab/actions/deploy/ nie istnieje
na VPS, deploy-runner nie jest zainstalowany na zadnym wezle. Zero akcji
kiedykolwiek osiagnelo stan terminalny (completed 0 / failed 0); jedyne dwa
action_result to reczne testy z 2026-07-23, nie remediacje z incydentu.
Zweryfikowane wzgledem fbf165f: healthcheck_failed idzie do container_restart,
nie do redeploy. Zywe incydenty w world state maja wylacznie trigger_type
containers_not_running / healthcheck_failed — czyli zaden nie generuje redeployu;
dzialaja tylko dryfy missing_service (2 pending).
Najostrzejszy problem projektowy: jedyny zywy emiter service_unhealthy
(node_agent.py:1104-1115) ma zahardkodowane service="control-plane", a
control-plane ma wlasne deploy-local.sh — deploy-service.sh:93-96 konczy sie
exit 3, wiec runner odmowi. Po wdrozeniu fixu redeploy sterowany incydentem
nadal nie wykona sie ani razu.
Ubocznie: w obrazie executora nie ma binarki ssh, wiec disk_cleanup
(executor.py:392-400) jest martwy tym samym defektem. Osobny task.
Dokument konczy sie sekcja FIX SHAPE — pieciopunktowa lista decyzji do podjecia,
bez rekomendacji.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker na PIHA wyczerpal domyslne pule adresowe (~30 zywych stackow,
"all predefined address pools have been fully subnetted"), wiec
docker-compose nie mogl zalozyc kb-site_default i serwis nie wstawal.
Deklaracja networks: [proxy] na serwisie wylacza domniemana siec
domyslna i podpina kontener pod istniejacy bridge "proxy"
(192.168.0.0/20, tworzony poza tym stackiem) — zero nowych podsieci.
Bez wplywu na ruch: npm@PIHA siedzi na nginxproxymanager_default i
trafia do kb-site po opublikowanym porcie hosta 8250, nie po tej sieci.
Walidacja: yaml.safe_load + asercje ksztaltu (bez testu na PIHA).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decyzja operatora 2026-08-04: /opt/homelab to standardowa sciezka deploy rootu
homelaba, ta sama na kazdym wezle, opisana wprost w publicznej czesci modelu
(standards, service-model, observer, event-system). Nie ujawnia sekretow ani
topologii, wiec zostaje na stronie publicznej.
scripts/kb/check_whitelist.txt: wpis `/opt/homelab` z uzasadnieniem i data.
Zakres wyjatku jest waski — sprawdzone, ze wycisza wylacznie warianty
`/opt/homelab/...`; `/home/oskar/...`, `/opt/other/...`, adresy RFC1918 i porty
dalej zapalaja czerwone.
gen_pages.py: load_whitelist() obcina komentarz `#` w dowolnym miejscu linii,
nie tylko na jej poczatku — wyjatek ma stac obok uzasadnienia, a nie osobno.
Zaden ze skanowanych wzorcow nie zawiera `#`, wiec obciecie jest bezpieczne.
kb/runbooks/kb-site-deploy.md §2: zapisany aktualny stan bramki (exit 0, 22
trafienia wyciszone) zamiast opisu decyzji do podjecia.
Po zmianie: python3 scripts/kb/gen_pages.py --check -> CZYSTO, exit 0.
kb/services/kb-site.md (type: service, visibility: public) — opis samej
wystawki: co jest publikowane (regula fail-closed na `visibility`), jak
renderowane sa odnosniki do dokumentow nieopublikowanych, struktura builda,
stopka z commitem, bramka --check. Napisany tak, zeby sam przechodzil --check:
zero adresow, portow i sciezek hosta — szczegoly operacyjne siedza w runbooku.
kb/runbooks/kb-site-deploy.md (type: runbook, visibility: private) — pelna
procedura: deploy na PIHA, generacja + kontrola wyciekow jako bramka
publikacji, podmiana tresci w wolumenie helperem alpine (wzorzec z
services/narty27/README.md, rozszerzony z pliku na drzewo), rekord A w
Cloudflare + Pi-hole local DNS (split-horizon), proxy host i cert przez
scripts/npm/npm_api.py, weryfikacja, rutynowa aktualizacja jednym lancuchem
&&, tabela rollback/typowe problemy.
Dwie rzeczy zapisane wprost, bo latwo je przeoczyc:
- kolejnosc DNS -> NPM host -> cert (HTTP-01 wymaga dzialajacego vhosta),
- `rm -rf /content/*` przed rozpakowaniem — bez tego dokument przelaczony z
public na private zostaje w wolumenie i dalej jest serwowany.
Runbook notuje tez aktualny wynik --check (22 trafienia /opt/homelab w
dokumentach public) jako decyzje do podjecia przed pierwsza publikacja:
wyczyscic zrodla albo swiadomie wpisac wyjatek do whitelisty.
services/kb-site/ — nginx:alpine serwujacy wolumen kb-site_content
(:ro, /usr/share/nginx/html), pelny layout z CLAUDE.md: docker-compose.yml,
service.yaml, README-wskaznik, env.example (swiadomie pusty — brak sekretow),
healthcheck.sh. Wpis w hosts/piha/services.yaml.
Port hosta 8250, NIE 8240 z opisu zadania: 8240 jest juz zajete przez narty27
(services/narty27/docker-compose.yml). Blok statyczny PIHA to 8210 paperless,
8220 nextcloud, 8230 kb-query, 8240 narty27 -> 8250 to nastepny wolny.
Uzgodnione z operatorem.
exposure: public — w odroznieniu od narty27 ta wystawka ma byc dostepna z
internetu przez vhost npm@PIHA kb.okit.pl; bind :8250 jest upstreamem proxy,
nie punktem wejscia.
Tresc jest czystym artefaktem repo (wyjscie scripts/kb/gen_pages.py), zyje
wylacznie w wolumenie kb-site_kb-site_content — bez binda pod /opt/homelab/data
i bez zadania backupu: odtworzeniem jest regeneracja z kb/.
Sprawdzone lokalnie: docker compose config -q, bash -n healthcheck.sh,
yaml.safe_load na obu manifestach.
Tryb --check nie generuje niczego: skanuje build/kb-site/**/*.html linia po
linii i przerywa z exit 1 na pierwszym zestawie trafien. Wzorce:
ip-rfc1918 192.168./10./172.16-31.
ip-tailscale 100.64-127.
ip-public-v4 reszta poprawnych adresow v4 (loopback, link-local, multicast,
broadcast i pule dokumentacyjne RFC 5737 sa neutralne)
ip-v6 adresy z "::" albo >=4 grupami (3 grupy to zwykle godzina)
port :NNNN w zakresie 1024-65535
path-host /home/... i /opt/...
token-hex ciagi hex >=32 znakow
token-b64 ciagi base64-podobne >=40 znakow mieszajace cyfry i litery
(sciezki absolutne odsiane — raportuje je path-host)
Raport: plik:linia [wzorzec] trafienie, na koncu licznik per wzorzec.
scripts/kb/check_whitelist.txt — swiadome wyjatki, na start PUSTY (same
komentarze z opisem formatu). Wpis to `<fragment>` albo
`<sciezka strony>|<fragment>`; fragment dopasowuje sie jako podciag trafienia,
wiec jeden wpis `/opt/homelab` wycisza wszystkie warianty.
Uruchomione lokalnie na 11 wygenerowanych stronach: 22 trafienia, wszystkie
path-host (/opt/homelab/... w dokumentach public), zero IP, portow i tokenow.
Whitelist zostaje pusta — decyzja co z tymi sciezkami zrobic nalezy do
operatora (patrz kb/runbooks/kb-site-deploy.md).
scripts/kb/gen_pages.py — kb/**/*.md -> build/kb-site/ (index.html pogrupowany
per type + strona na dokument). Renderer markdown na samej bibliotece
standardowej, wzorowany na ~/narty-2027/saalbach-kb/gen_pages.py; parser
frontmattera wspoldzielony z check_okf.py, zeby lint i generator widzialy
frontmatter tak samo.
Kwalifikacja fail-closed: publikowany jest wylacznie dokument z jawnym
`visibility: public`. Brak frontmattera, niepoprawny YAML, brak pola albo inna
wartosc = private. Linki do dokumentow nieopublikowanych nie sa renderowane jako
linki — zostaje etykieta z dopiskiem [private]; render_inline ma druga bramke
(linkuje tylko http/mailto/kotwice/.html), wiec martwy odnosnik nie ma jak
przeciec na strone.
BASE_URL jest parametrem (--base-url, domyslnie https://kb.okit.pl) i trafia do
<link rel="canonical">. Stopka kazdej strony: data generacji + git rev-parse
--short HEAD.
check_okf.py: EXCLUDE_DIRS = ("build",) — wyjscie generatora nie jest zrodlem
i nie podlega lintowi. build/ dopisany do .gitignore.
Uruchomione lokalnie: 150 dokumentow kb/, 10 public, 140 pominietych.
Liczba przepisana z README sprzed circuit breakera; 1764052 dodalo 4 testy
(trip po N kolejnych, reset po sukcesie, 0 wylacza, flush threadingu przy
abortcie) i zaktualizowalo licznik w sekcji Tests, ale nie w DoD.
Potwierdzone przebiegiem: 55 passed w jobs/mail-body-ingest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Weryfikacja 0-odwolan z 01db57a liczyla tylko podzbior prefiksow — poza nim
zostalo 13 wskaznikow w plikach niemarkdownowych i w prozie dokumentow:
docs/kb/modules/05-faza3-plan.md -> kb/phases/kb-m5-faza3.md (2x systemd)
docs/kb/modules/05-faza4-plan.md -> kb/phases/kb-m5-faza4.md (kb-query app.js)
docs/kb/modules/DECYZJE-*.md -> kb/decisions/kb-dokumenty-otwarte.md
docs/backlog.md (npm panel admina) -> kb/decisions/backlog-aktywne.md
docs/backlog/ (uid/gid floty) -> kb/decisions/backlog-uid-gid-flota.md
docs/kb/modules/0X-*.md -> kb/phases/kb-m*.md
docs/incidents/2026-07-30-*.md -> kb/incidents/ (3x node-agent)
docs/architecture/RECON-multi*.md -> kb/subsystems/recon-multiagent.md
jobs/deploy-runner/README.md -> kb/services/job-deploy-runner.md
Wyjatek zamierzony: `docs/kb/modules/05-faza3-pilot-streszczen.md` w §11 planu
fazy 3 to nazwa artefaktu, ktory nigdy nie powstal — przepiety na docelowa
konwencje (kb/phases/kb-m5-faza3-pilot-streszczen.md), zeby przyszly plik
wyladowal w nowym drzewie, a nie w skasowanym katalogu.
Weryfikacja: skan po 67 sciezkach zmigrowanych w tej galezi (git grep -F na
kazdej) = 0 trafien poza docs/sessions (logi historyczne, celowo nietkniete);
0 martwych linkow markdown na 190 plikach; check_okf.py 190/190 ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stub z 2026-04-15 (65 linii) z pierwszego reconu, gdy homelab byl jednym
RPi5. Wszystkie "Unknown / needs clarification" dawno odpowiedziane przez
kb/subsystems/fleet-inventory.md i hosts/<node>/capabilities.yaml.
Kasowany, a nie migrowany, bo niesie publiczne IPv4/IPv6 i Tailscale IP
VPS-a przy zerowej wartosci merytorycznej — migracja oznaczalaby
przeniesienie tych danych do KB bez zadnego zysku.
Zero odwolan w repo. Tresc pozostaje w historii gita.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Naprawa kontraktu CLAUDE.md §Service Structure (opcja b). Migracja do KB
zabrala README z katalogow serwisow i hostow, przez co 0/26 katalogow
services/ spelnialo wymagany layout. Wskazniki przywracaja nawigacje,
nie duplikujac tresci.
31 wskaznikow, jednolity format, dokladnie 5 linii:
# <nazwa>
<jedno zdanie opisu>
Dokumentacja: [kb/...](../../kb/...)
Opis nie jest pisany od zera — wyciagany z kb-doca: pierwsze pelne zdanie
pierwszego akapitu (sklejane z zawinietych linii, ciete tylko tam, gdzie
backticki i nawiasy sa zbilansowane), a dla node'ow czlon tytulu H1 po
myslniku. Dla ha-mcp opis z H1, bo pierwszy akapit zaczyna sie od markera
statusu. Wiodace markery "**Status: ...**" sa zdejmowane.
26 x services/<svc>/README.md, 5 x hosts/<node>/README.md.
WYJATEK services/home-assistant/config/ken-legacy/README.md: pelne
ostrzezenie "historical archive, do not deploy" przywrocone doslownie
z historii (odzyskane z drzewa sprzed migracji) + link do kb-doca.
Ostrzezenie musi stac tam, gdzie chroni — w katalogu archiwum, nie tylko
w KB. Odwolanie do services/home-assistant/DESIGN.md przepiete na
kb/decisions/ha-configs-as-code.md + kb/incidents/2026-07-22-ha-dwie-instancje.md.
check_okf.py: POINTER_GLOBS + is_pointer() wykluczaja wskazniki ze scope'u
lintu. Wskazniki celowo NIE maja frontmattera OKF — to nawigacja, nie
dokumenty KB. Wykluczenie zapisane wprost, zeby poszerzenie SCOPE nie
zaczelo ich nagle walidowac.
Bez wskaznikow: hosts/chelsty-ha/ i hosts/lustro/ — nie maja dokumentow
w kb/nodes/ (luka odnotowana juz w reconie etapu 1). Utworzenie ich
wymagaloby napisania nowej dokumentacji, czyli wyjscia poza konwersje.
Lint: 190/190 ZGODNE. Weryfikacja 822 plikow: 0 martwych linkow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rozstrzygniecie 1. Monolit mieszal cztery typy OKF. Rozbity PER TYP po
granicy sekcji `##`:
10 x decision — pozycje backlogu (w tym backlog-aktywne 28 KB
i backlog-zamkniete 12 KB, ktore zostaja calosciami)
7 x incident — bugi/awarie dotad wtopione w backlog: cutover HA ken,
checkpoint observera, ha-diag-agent node=unknown,
deploy-local ghost-kontenery, paperless-worker config,
deploy-node nie przebudowuje obrazu, ollama bez sterownika
2 x phase — HA configs-as-code, monitoring floty Prometheus
kb/phases/backlog.md zostaje jako cienki indeks (type: phase, status: active):
oryginalna preambula + wygenerowany spis linkow do wszystkich 19 elementow.
23 przychodzace odwolania zostaja przepiete na ta sciezke w grupie 7.
NIE rozbijano po `###` (38 pozycji w "Aktywne" + 13 w "Zamkniete" = 51
plikow). Rozstrzygniecie mowi "rozbij per typ", a nie per pozycja;
rozdrobnienie do 51 plikow rozerwaloby czytelnosc backlogu.
Kontrola: preambula + 19 sekcji == oryginal z HEAD (multizbior niepustych
linii). Tresc pozycji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
czujniki-2026-07-30 (node-agent vs stability-agent)
lustro-shipping-2026-07-16 (event=dead prom=up, 1507 mismatchy)
prometheus-cutover-2026-07-06 (recon starego toru livenesci)
piha-slim-2026-07-02 (audyt odchudzania PIHA)
vps-stacki-2026-07-27 (audyt niezarzadzanych stackow na VPS)
ODSTEPSTWO OD RECONU — swiadome. Recon typowal te 5 plikow jako SPLIT
(audit+decision / audit+incident / audit+phase). Rozstrzygniecie 2 wprowadza
typ `audit` z polem as_of i mapuje kazdy z nich na JEDNA sciezke
kb/audits/<obszar>-<data>.md. Audyt jest spojna migawka stanu z konkretna
data — rozbicie go na "ustalenia" i "rekomendacje" rozerwaloby ten kontekst
i wymagaloby redakcji tresci, czego etap 2 zabrania. Zostaja w calosci.
Efekt: 29 SPLIT-ow z reconu realizowane jako 24 (10 service+runbook,
14 wielotypowych), 5 zamienionych na caloscowe dokumenty type: audit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/kb-overview.md — architektura 4 warstwy x 4 filary, zasady
przekrojowe, kolejnosc projektow, stan per zrodlo, konwencje katalogow
kb/decisions/kb-log-decyzji.md — sekcja "Decyzje — zamkniete vs otwarte"
Tresc sekcji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/control-plane.md — status: deprecated,
superseded_by: "przepisany tor redeploy, commity da151fc/79bfe8c 2026-08-03"
kb/runbooks/control-plane-deploy-recovery.md — Deployment + Recovery
Rozstrzygniecie 3: dokument NIE jest odswiezany, tylko oznaczony jako
nieaktualny. Ostatnia zmiana tresci 2026-05-27, czyli przed przepisaniem
toru redeployu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/service-lifecycle.md (visibility private wg rozstrzygniecia 6)
kb/runbooks/service-operational-recovery.md — Operational Recovery
Tresc sekcji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/deployment.md — konwencje deployu
kb/incidents/deploy-sh-vps-niszczy-control-plane.md — sekcja
"ZNANY BUG — deploy.sh vps niszczy control-plane (2026-06-25)"
UWAGA: "Recovery Workflow" to ### zagniezdzone w "Staged Deployment
Framework". Split mechaniczny tnie wylacznie po ##, a wyciagniecie tego
fragmentu wymagaloby przebudowy tresci — zostaje w dokumencie glownym.
Do rozwazenia jako osobny runbook w etapie redakcyjnym.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/decisions/ha-configs-as-code.md — decyzje projektowe HA configs-as-code
kb/incidents/2026-07-22-ha-dwie-instancje.md — sekcja "Incident log":
dwie instancje HA sterujace domem rownolegle po migracji
Incydent byl dotad wtopiony w dokument decyzyjny; teraz jest adresowalny
jako osobny wpis type: incident. Tresc nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/job-deploy-runner.md (How it works now)
kb/decisions/deploy-runner-uzasadnienie.md (What was broken)
kb/runbooks/deploy-runner-install.md (Install per node, Operating it, Tests)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/paperless.md (Stack, OIDC, Storage i backup, RAM na PIHA)
kb/decisions/paperless-split-ocr.md (split OCR serwis@PIHA + worker@SOLARIA)
kb/runbooks/paperless-cutover.md (cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/nextcloud.md (Stack, WebDAV dla ingestu, Storage i backup)
kb/decisions/nextcloud-host-piha.md (decyzja: host = PIHA)
kb/runbooks/nextcloud-cutover.md (OIDC przez Forgejo + cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/gokapi.md (Stack, Backup, Rejestracja w repo)
kb/decisions/gokapi-storage-e2e-siec.md (storage lokalny zamiast S3,
szyfrowanie E2E, bind tylko na Tailscale)
kb/runbooks/gokapi-cutover.md (cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/fleet-prometheus.md (Stack, Placement & exposure, Data, Next steps)
kb/decisions/fleet-prometheus-osobny-od-prom.md ("Why separate from the home prom")
kb/runbooks/fleet-prometheus-deploy.md (Configuration, Verify)
Wzajemne links. Tresc sekcji nietknieta; kontrola: multizbior niepustych
linii czesci == oryginal z HEAD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wzorzec mechaniczny: sekcje deploy/verify/install/testy wycinane do
kb/runbooks/<serwis>-*.md, reszta zostaje dokumentem type: service.
Wzajemne `links` w obie strony. Tresc sekcji nietknieta — przenoszone
doslownie, dodany wylacznie naglowek H1 nowego runbooka.
kb-query, paperless-worker, planner-agent, ha-diag-agent, ollama-piha,
narty27, home-assistant, ha-mcp, job-gmail-header-backfill, job-mail-body-ingest.
Weryfikacja: dla kazdego pliku multizbior niepustych linii
(main + runbook) == oryginal z HEAD. Zero zgubionych, zero dodanych.
Recon szacowal 13 splitow service+runbook; faktycznie 2-typowych jest 10,
pozostale 5 (paperless, nextcloud, gokapi, fleet-prometheus, deploy-runner)
sa 3-typowe i ida osobno jako splity wielotypowe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rozstrzygniecie 5: katalogi z kodem, ktore nie mialy README, dostaja stub
(frontmatter + stub: true + opis jednozdaniowy). NIE pisana pelna dokumentacja.
kb/services/: control-plane, node-agent, brain-watchdog, node-exporter,
job-gmail-bulk-import, pkg-kb-mail, pkg-kb-retrieval.
Kazdy stub podaje zrodlo opisu (service.yaml / docstring / CLAUDE.md /
pyproject.toml). Dla gmail-bulk-import i kb-retrieval zrodla brak — stub
mowi o tym wprost zamiast zmyslac opis.
Dodatkowo kb/subsystems/repo-operating-contract.md — wskaznik na CLAUDE.md
(rozstrzygniecie 4). CLAUDE.md zostaje w korzeniu jako zywa konfiguracja
narzedzia; kb-doc niesie pole contradicts: brak katalogow services/joplin,
services/outline, services/ai-cluster deklarowanych w sekcji
"Repo-managed services on VPS". Kontekst PR2 feat/vps-service-migration,
swiadomie nienaprawiane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nowy typ `audit` (rozstrzygniecie 2) — migawka stanu z pola `as_of`,
nie opis stanu biezacego.
monitoring-coverage-2026-07-14, ha-automatyzacje-2026-07-23.
Pozostale 5 audytow/reconow jest wielotypowych — wychodza w grupie SPLIT-ow.
git mv + frontmatter, tresc nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>