diff --git a/docs/superpowers/plans/2026-07-20-ausbaustufe-9.md b/docs/superpowers/plans/2026-07-20-ausbaustufe-9.md
new file mode 100644
index 0000000..af5d778
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-20-ausbaustufe-9.md
@@ -0,0 +1,196 @@
+# Ausbaustufe 9 Implementation Plan — Vorschlags-Algorithmus v2 (v0.9.0)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** `suggest_recurring` erkennt monatliche/vierteljährliche/jährliche Serien mit letztem Betrag, Aktiv-Check, Betrags-Clustern, Umfirmierungs-Merge und robustem Bestandsabgleich; GUI zeigt Rhythmus/Start/Hinweis; Release v0.9.0 mit Live-Gate gegen die echten Daten.
+
+**Architecture:** Vollständiger Rewrite von `app/services/suggestions.py` (reine Session-in/dict-out-Funktion, Parameter als Modul-Konstanten); `SuggestionOut`-Erweiterung in `routers/planning.py`; Template-Anpassung der Vorschlags-Tabelle. Kein Datenmodell-/Migrationsbedarf.
+
+**Tech Stack:** SQLAlchemy 2, Pydantic v2, Jinja2, pytest (synthetische Daten).
+
+**Spec:** `docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md` — die dortigen Abschnitte „Algorithmus" (8 Schritte, Konstanten) und „Tests" sind bindend und Teil dieses Plans.
+
+## Global Constraints
+
+- Beträge `Decimal` (keine float-Arithmetik, auch nicht in Toleranzvergleichen — relative Differenzen als `Decimal`-Quotienten).
+- `date.today()` nur an EINER Stelle (Parameter `today: date | None = None` der Hauptfunktion, Default heute) — Tests injizieren ein festes Datum.
+- Tests ausschließlich mit synthetischen Daten (DATENSCHUTZ: keine echten Namen/Beträge aus der Live-DB in Tests/Commits).
+- GUI deutsch, TT.MM.JJJJ, `|eur`, `|de_label`; API Punkt-Dezimal.
+- Fable-Testagent-Gate je Task VOR Commit; Ledger-Eintrag je Task.
+- Testlauf: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` — Basis 187 passed, muss grün bleiben (drei bestehende Suggestion-Tests DÜRFEN an die neue Semantik angepasst werden, siehe Task 1 Step 4).
+
+---
+
+### Task 1: Algorithmus-Rewrite + API-Schema
+
+**Files:**
+- Rewrite: `app/services/suggestions.py`
+- Modify: `app/routers/planning.py` (`SuggestionOut`)
+- Modify: `tests/test_planning_api.py`
+
+**Interfaces:**
+- Produces: `suggest_recurring(session, today: date | None = None) -> list[dict]` mit Keys `name, amount (Decimal), rhythm, due_day, start_date (date|None), category_id, hinweis (str)`; `SuggestionOut` mit denselben Feldern (`start_date: date | None = None`, `hinweis: str = ""`). Task 2 rendert genau diese Felder.
+
+- [ ] **Step 1: Failing Tests** — in `tests/test_planning_api.py` die drei bestehenden Suggestion-Tests ERSETZEN/ERWEITERN durch die Spec-Fälle (Helper zum Anlegen synthetischer Buchungen schreiben; `dedup_hash` eindeutig, `status="confirmed"`; ein Account genügt; `today=date(2026, 7, 20)` in alle Aufrufe injizieren):
+
+```python
+from datetime import date
+from decimal import Decimal
+
+def _tx(db, acc_id, d, amount, cp, cat=None):
+ db.add(Transaction(account_id=acc_id, booking_date=d, amount=Decimal(amount),
+ purpose="p", counterparty=cp, category_id=cat,
+ status="confirmed", dedup_hash=f"h-{cp}-{d}-{amount}"))
+
+TODAY = date(2026, 7, 20)
+
+def test_suggest_letzter_betrag_bei_preiserhoehung(db):
+ acc = _acc(db) # Helper: Account anlegen, gibt id zurueck
+ for d, a in [(date(2026, 3, 1), "-190.65"), (date(2026, 4, 1), "-202.94"),
+ (date(2026, 5, 4), "-202.94"), (date(2026, 6, 1), "-202.94"),
+ (date(2026, 7, 1), "-202.94")]:
+ _tx(db, acc, d, a, "Entis Lebensversicherung AG")
+ db.commit()
+ out = suggest_recurring(db, today=TODAY)
+ assert len(out) == 1
+ s = out[0]
+ assert s["amount"] == Decimal("-202.94") and s["rhythm"] == "monthly"
+ assert s["due_day"] == 1 and s["start_date"] is None
+
+def test_suggest_quartal_mit_phase(db):
+ acc = _acc(db)
+ for d in [date(2025, 9, 15), date(2025, 12, 15), date(2026, 3, 16), date(2026, 6, 15)]:
+ _tx(db, acc, d, "-55.08", "Rundfunk ARD ZDF")
+ db.commit()
+ out = suggest_recurring(db, today=TODAY)
+ assert len(out) == 1
+ assert out[0]["rhythm"] == "quarterly"
+ assert out[0]["start_date"] == date(2026, 6, 15) and out[0]["due_day"] == 15
+
+def test_suggest_jahr_mit_zwei_belegen(db):
+ acc = _acc(db)
+ for d, a in [(date(2025, 6, 16), "-409.92"), (date(2026, 6, 16), "-467.33")]:
+ _tx(db, acc, d, a, "Kraftfahrer-Schutz e.V.")
+ db.commit()
+ out = suggest_recurring(db, today=TODAY)
+ assert len(out) == 1
+ assert out[0]["rhythm"] == "yearly" and out[0]["amount"] == Decimal("-467.33")
+ assert out[0]["start_date"] == date(2026, 6, 16)
+ assert "409.92" in out[0]["hinweis"] # Betrag zuletzt gestiegen
+
+def test_suggest_tote_serie_kein_vorschlag(db):
+ acc = _acc(db)
+ for m in (9, 10, 11, 12):
+ _tx(db, acc, date(2025, m, 1), "-35.00", "WWK Alt")
+ db.commit()
+ assert suggest_recurring(db, today=TODAY) == []
+
+def test_suggest_umfirmierung_merge(db):
+ acc = _acc(db)
+ for m in (11, 12):
+ _tx(db, acc, date(2025, m, 1), "-190.65", "Heidelberger Leben")
+ for m in (1, 2, 3):
+ _tx(db, acc, date(2026, m, 2), "-190.65", "Entis Lebensversicherung")
+ for m in (4, 5, 6, 7):
+ _tx(db, acc, date(2026, m, 1), "-202.94", "Entis Lebensversicherung")
+ db.commit()
+ out = suggest_recurring(db, today=TODAY)
+ assert len(out) == 1
+ assert "Entis" in out[0]["name"] and out[0]["amount"] == Decimal("-202.94")
+
+def test_suggest_bestandsabgleich_trotz_preisdrift(db):
+ acc = _acc(db)
+ db.add(RecurringItem(name="Entis Lebensversicherung AG", amount=Decimal("-190.65"),
+ rhythm="monthly", due_day=1))
+ for m in (4, 5, 6, 7):
+ _tx(db, acc, date(2026, m, 1), "-202.94", "Entis Lebensversicherung AG")
+ db.commit()
+ assert suggest_recurring(db, today=TODAY) == [] # Namens-Match schlaegt an
+
+def test_suggest_zwei_vertraege_getrennt(db):
+ acc = _acc(db)
+ for m in (4, 5, 6, 7):
+ _tx(db, acc, date(2026, m, 1), "-346.23", "Heidelberger LV")
+ _tx(db, acc, date(2026, m, 2), "-145.21", "Heidelberger LV")
+ db.commit()
+ out = suggest_recurring(db, today=TODAY)
+ assert len(out) == 2
+ assert {s["amount"] for s in out} == {Decimal("-346.23"), Decimal("-145.21")}
+```
+
+(`_acc`-Helper analog bestehender Tests; `RecurringItem`/`Transaction`-Importe existieren.) Die drei Alt-Tests (`three_consecutive_months_with_year_wrap`, `two_months_no_suggestion`, `excludes_existing_recurring_item`) an die neue Signatur/Semantik anpassen: feste `today`-Injektion; Daten ggf. ins Fenster schieben; der Exclusion-Test bleibt inhaltlich gültig (Name-Match).
+
+Run: `.venv/bin/python -m pytest tests/test_planning_api.py -q` → neue Tests FAIL
+
+- [ ] **Step 2: Rewrite `app/services/suggestions.py`** gemäß Spec-Abschnitt „Algorithmus" (8 Schritte, Konstanten `WINDOW_DAYS=460`, Rhythmus-Tabelle monthly 25-36/≥3, quarterly 80-105/≥3, yearly 330-400/≥2, `STEP_DAYS={"monthly":30,"quarterly":91,"yearly":365}`, `ACTIVITY_FACTOR` 7/4 als `Fraction` oder Tage-Vergleich ganzzahlig, Cluster 35 %, Merge 25 %, Bestand 10 % — alle Toleranzvergleiche als `Decimal`). Struktur: `_norm`, `_rel_diff`, `_amount_clusters` (greedy gegen letztes Mitglied, gleiches Vorzeichen), `_classify` (Median der Abstände), Merge-Pass je Konto über alle Serien, `_covered_by_existing`, Hauptfunktion `suggest_recurring(session, today=None)`. Deutsche Docstrings/Kommentare zur Begründung der Toleranzen.
+
+- [ ] **Step 3: `SuggestionOut` erweitern** — `routers/planning.py`:
+
+```python
+class SuggestionOut(BaseModel):
+ name: str
+ amount: Decimal
+ rhythm: str
+ due_day: int
+ start_date: date | None = None
+ category_id: int | None = None
+ hinweis: str = ""
+```
+
+- [ ] **Step 4: Tests + Suite grün** — `.venv/bin/python -m pytest -q` → PASS (Alt-Test-Anpassungen im Report begründen).
+
+- [ ] **Step 5: Fable-Testagent-Abnahme** (Faktencheck: Toleranz-Arithmetik Decimal-rein; Aktiv-Check-Grenzen; Merge-Bedingungen; keine `date.today()`-Streuung; Alt-Test-Anpassungen berechtigt). Erst nach VERIFIED weiter.
+
+- [ ] **Step 6: Commit** — `git add finance/app/services/suggestions.py finance/app/routers/planning.py finance/tests/test_planning_api.py && git commit -m "feat: Vorschlags-Algorithmus v2 (Rhythmen, letzter Betrag, Aktiv-Check, Merge)"`
+
+---
+
+### Task 2: GUI — Rhythmus/Start/Hinweis in der Vorschlags-Tabelle
+
+**Files:**
+- Modify: `app/templates/planning.html` (Fieldset „Vorschläge aus Buchungen")
+- Modify: `tests/test_gui.py`
+
+**Interfaces:**
+- Consumes: `SuggestionOut`-Felder aus Task 1; Filter `|eur`/`|de_label`.
+
+- [ ] **Step 1: Failing GUI-Test** — in `tests/test_gui.py` (synthetische Serie seeden, `/planung` laden):
+
+```python
+def test_vorschlaege_zeigen_rhythmus_und_start(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ acc = Account(bank="dkb", iban="DE-SUG-1", name="S", type="giro")
+ db.add(acc)
+ db.flush()
+ for d in (date(2025, 9, 15), date(2025, 12, 15), date(2026, 3, 16), date(2026, 6, 15)):
+ db.add(Transaction(account_id=acc.id, booking_date=d, amount=Decimal("-55.08"),
+ purpose="p", counterparty="Rundfunk Synth", status="confirmed",
+ dedup_hash=f"sug-{d}"))
+ db.commit()
+ r = client.get("/planung").text
+ assert "vierteljährlich" in r # de_label des Rhythmus
+ assert "15.06.2026" in r # Start-Spalte TT.MM.JJJJ
+ assert 'name="start_date"' in r # hidden input der Uebernahme
+```
+
+WICHTIG: Der Test hängt von `date.today()` der App ab (Aktiv-Check!) — Serie so legen, dass sie um den echten Testlauf-Zeitpunkt herum aktiv ist, oder (besser) `suggest_recurring` in `planung_page` unverändert lassen und den Test mit relativen Daten um `date.today()` konstruieren (letzte Buchung ≤ 45 Tage vor heute, Quartalsschritte rückwärts). Die Variante mit relativen Daten umsetzen; die obigen Fixdaten sind als Muster zu verstehen und auf `date.today()`-relative Werte umzustellen (inkl. erwartetem Start-String via `.strftime('%d.%m.%Y')`).
+
+- [ ] **Step 2: Template** — Vorschlags-Tabelle: Kopf `Name | Betrag | Rhythmus | Fälligkeitstag | Start | (Aktion)`; Zellen `{{ s.rhythm|de_label }}`, `{{ s.start_date.strftime('%d.%m.%Y') if s.start_date else '–' }}`; Betrag-Zelle ergänzt `{% if s.hinweis %}{{ s.hinweis }}{% endif %}`; Übernahme-Formular: hidden inputs unverändert plus `` (json-form macht leer → null). Hinweistext unter dem Fieldset: „Erkannt werden monatliche, vierteljährliche und jährliche Serien; Betrag = jeweils letzte Buchung."
+
+- [ ] **Step 3: Suite grün**; **Step 4: Fable-Abnahme** (Live-Approximation: Rendering + Übernahme-Roundtrip eines Quartals-Vorschlags inkl. start_date); **Step 5: Commit** `feat: Vorschlaege mit Rhythmus, Start und Hinweis`.
+
+---
+
+### Task 3: Release v0.9.0 + Live-Gate gegen echte Daten
+
+- [ ] **Step 1:** Suite final; `VERSION` → 0.9.0; Commit; `./create_pod_finance.sh`; `/api/version` == 0.9.0.
+- [ ] **Step 2: Fable-Release-Gate (LIVE, lesend):** `GET /api/recurring/suggestions` gegen die echte DB. Prüfen: (a) KEINER der bestehenden ~41 Fixposten wird erneut vorgeschlagen (Bestandsabgleich wirkt, auch bei gedrifteten Beträgen); (b) keine bekannten toten Serien (gelöschte PayPal-−4,99-Serie, ausgelaufene WWK-−35-Police) im Ergebnis; (c) verbleibende Vorschläge einzeln gegen die Buchungen plausibilisieren (echte aktive Serie? korrekte Werte?). Ergebnisliste NUR im Chat/Bericht, nie committen. Bei Fehlklassifikationen: Befund zurück an Task 1 (Toleranzen), Fix + Re-Gate.
+- [ ] **Step 3:** Ledger (generisch) + Plan-Häkchen + Push; Kandidatenliste dem Nutzer berichten.
+
+---
+
+## Self-Review
+
+- Spec-Abdeckung: Algorithmus/Schema → Task 1; GUI → Task 2; Release/Live-Gate → Task 3. Testfälle der Spec vollständig in Task 1 Step 1 kodiert.
+- Platzhalter: Task 1 Step 2 verweist bewusst auf den bindenden Spec-Abschnitt (8 nummerierte Schritte + Konstanten) statt den vollen Code zu duplizieren; alle Schnittstellen/Konstanten sind exakt benannt.
+- Typ-Konsistenz: `suggest_recurring(session, today)`-Signatur = Testaufrufe; `SuggestionOut`-Felder = Template-Zugriffe (`s.rhythm`, `s.start_date`, `s.hinweis`).
diff --git a/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md b/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md
new file mode 100644
index 0000000..d2ec70b
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md
@@ -0,0 +1,81 @@
+# Design — Ausbaustufe 9: Vorschlags-Algorithmus v2 (v0.9.0)
+
+> Status: vom Nutzer freigegeben (Chat 2026-07-20, Direktdurchlauf). Anlass:
+> Der bisherige `suggest_recurring` gruppiert nach exaktem Betrag (jede
+> Preiserhöhung zerreißt die Serie), erkennt nur monatliche Serien, prüft
+> keine Aktivität (schlägt tote Serien vor) und nutzt Median-Werte. Ein
+> manueller Vollabgleich (A8-Datenpflege, siehe Ledger) fand 16 fehlende
+> Posten — der Algorithmus soll solche Serien künftig selbst finden.
+
+## Anforderungen (Nutzer)
+
+- Betrag = **letzte** Buchung, nicht Median (Preissteigerungen relevant).
+- Erkennung **monatlich, vierteljährlich, jährlich**.
+- Empfänger-Gruppierung robust (Schreibweisen, Preisänderungen,
+ Umfirmierungen); mehrere Verträge desselben Anbieters getrennt.
+- Keine „Leichen": abgerissene Serien werden nicht vorgeschlagen.
+- Kein Wiedervorschlagen bereits gepflegter Posten (auch bei zwischenzeitlich
+ geändertem Betrag).
+
+## Algorithmus (`app/services/suggestions.py`, vollständiger Rewrite)
+
+Parameter als Modul-Konstanten (Toleranzen zentral änderbar):
+`WINDOW_DAYS=460` (~15 Monate), Rhythmen mit Intervallgrenzen und
+Mindestbelegen: monthly 25–36 Tage/≥3, quarterly 80–105/≥3, yearly
+330–400/≥2; `ACTIVITY_FACTOR=1.75`; Betrags-Cluster-Toleranz 35 %;
+Merge-Toleranz 25 %; Bestandsabgleich-Toleranz 10 %.
+
+1. **Datenbasis:** bestätigte Buchungen der letzten `WINDOW_DAYS`, je Konto.
+2. **Gruppierung:** Schlüssel = (account_id, normalisierter Empfänger)
+ (`casefold`, Whitespace kollabiert).
+3. **Betrags-Cluster** innerhalb der Gruppe (chronologisch, greedy gegen das
+ jeweils letzte Cluster-Mitglied, gleiches Vorzeichen, relative Differenz
+ ≤ 35 %) — trennt parallele Verträge, hält Preisdrift zusammen.
+4. **Rhythmus je Cluster:** Median der Buchungsabstände gegen die
+ Intervallgrenzen; Mindestbelege je Rhythmus.
+5. **Aktiv-Check:** letzte Buchung ≤ `ACTIVITY_FACTOR` × Rhythmus-Schrittweite
+ (30/91/365 Tage) her, sonst kein Vorschlag.
+6. **Umfirmierungs-Merge** (über Gruppengrenzen, je Konto): Serie A endet,
+ Serie B beginnt danach (Lücke 0,4–1,6 Schrittweiten), gleicher Rhythmus,
+ Fälligkeitstag ±3, Betrag ±25 % → eine Serie; Name/Betrag der neueren.
+7. **Vorschlagswerte:** Name = Empfänger-Schreibweise der neuesten Buchung;
+ Betrag = neueste Buchung; Fälligkeitstag = Tag der neuesten Buchung;
+ `start_date` = Datum der neuesten Buchung bei quarterly/yearly (Phase!),
+ sonst None; Kategorie = häufigste in der Serie; `hinweis` = Text
+ „Betrag zuletzt gestiegen (vorher X)" wenn die vorletzte Buchung
+ betragskleiner war, sonst leer.
+8. **Bestandsabgleich:** Vorschlag entfällt, wenn ein `RecurringItem`
+ existiert mit (a) Namens-Substring-Match (normalisiert, in beide
+ Richtungen) ODER (b) gleichem Rhythmus + Fälligkeitstag ±2 + Betrag
+ ±10 %.
+
+## API/GUI
+
+- `SuggestionOut` (routers/planning.py): + `start_date: date | None`,
+ + `hinweis: str = ""`.
+- Vorschlags-Tabelle (planning.html): Spalten Rhythmus (`|de_label`) und
+ Start (TT.MM.JJJJ bzw. „–"); `hinweis` als `muted`-Text hinter dem Betrag;
+ „Vorschlag übernehmen" überträgt `start_date` mit (hidden input).
+- Hinweistext unter der Tabelle aktualisiert: monatliche/vierteljährliche/
+ jährliche Serien, Betrag = letzte Buchung.
+
+## Tests (synthetische Daten, keine Fixtures)
+
+Preiserhöhungs-Serie → letzter Betrag + hinweis; Quartals-/Jahres-Serie mit
+korrektem start_date; tote Serie (letzte Buchung zu alt) → kein Vorschlag;
+Umbenennungs-Merge → ein Vorschlag mit neuem Namen; Bestandsabgleich:
+existierender Posten mit altem Betrag verhindert Wiedervorschlag; zwei
+parallele Verträge eines Anbieters → zwei getrennte Vorschläge; bestehende
+drei Suggestion-Tests an die neue Semantik anpassen.
+
+## Release
+
+`VERSION` → 0.9.0, Redeploy, **Live-Gate gegen echte Daten**: kein einziger
+der bestehenden Fixposten darf erneut vorgeschlagen werden; keine als
+beendet bekannten Serien (z.B. gelöschte PayPal-Leiche, ausgelaufene
+WWK-Police) im Ergebnis; verbleibende Vorschläge werden dem Nutzer als
+Kandidatenliste berichtet (nur Chat, kein Commit). Fable-Gate je Task.
+
+**Außerhalb des Scopes:** halbjährliche Rhythmen (nicht im Datenmodell),
+automatische Übernahme ohne Nutzer-Klick, Einnahmen-Prognose des
+Geschäftskontos.