Pipeline
Liest das Rezept und baut daraus das Video
Status: Richtung entschieden von Leon am 27.07.2026 (HZ-363). Kein Fork pro Kanal mehr – EINE parametrische Kette, durch die jeder Kanal mit seiner DNA/seinem Rezept läuft. Dies ist die einzige Quelle für den Migrationsplan.docs/fabrik_blaupause.mdH4 (Weg A/B) verweist hierher; dort steht nur noch das Modell, nicht mehr die Abwägung. Verlauf/Changelog →docs/bereiche/youtube.md. Tickets → HZ-363 + Folgestufen.
1 · Warum das kein Risiko ist: die Vielfalt ist Werte-Vielfalt, keine Graph-Vielfalt
Leons Sorge beim Entscheid: „kritisch bei so vielen Arten – Stock, KI, Pexels etc.? Aber man kann ja schon noch angeben, welcher Prompt, welche Maße, welche Stills."
Gemessen am echten n8n-Stand am 27.07.2026 (102 Workflows, 64 aktiv; Messung read-only über die Public-API, Node-Namen + Parameter-Hashes je Stufe):
| Stufe | Kanäle | Struktur-Übereinstimmung (Jaccard über Node-Namen) | Nodes |
|---|---|---|---|
| publisher | 8 | 1.00 – byte-gleich | 10 |
| p5 | 9 | 1.00 – byte-gleich | 8 |
| p3 | 15 | Median 1.00 (16 gemeinsame Nodes) | 3–17 |
| p4 | 12 | Median 0.79 – 21 Kern-Nodes von 36 | 21–36 |
| p1 | 15 | Median 0.50 | 12–19 |
| p2 | 15 | Median 0.39 (Union 162) | 31–78 |
Bei den 10 Zwillings-Kanälen (history, psych, cs, wisdom, geo, survival, doomed, worldcup, alvaro_horror, auswandern_rente_de) sind die Node-Namen je Stufe deckungsgleich. Abweichend sind nur Parameter-Werte:
| Stufe | gemeinsame Nodes | parameter-identisch | abweichend |
|---|---|---|---|
| p2 | 43 | 26 | 17 |
| p4 | 21 | 9 | 12 |
| p3 | 16 | 10 | 6 |
| p1 | 9 | 1 | 8 (fast nur Prompt-Texte) |
Und die „vielen Arten" gibt es nicht. Alle Kanäle tragen dieselbe Quellen-Kette – ApiMart-KI-Zweig + Pexels→Pixabay→Wikimedia→Unsplash, optional getty / harvest-gw / openverse. Pro Kanal ist nur eine andere Teilmenge scharf, und welche Quelle einen Beat bedient, entscheidet schon heute der Claude-Node „Stock Query Router" zur Laufzeit (media=ai|stock je Beat). Die Quellen-Liste ist bereits ein Rezept-Feld (recipe.harvest_sources, wf_fork/patch_harvest_sources.py).
Die beiden scheinbaren Sonderfälle sind keine:
- stickman (78 Nodes, 52 „eigene"): dieselbe KI-Kette 8× entrollt –
AI Image 1..8je
Submit→Wait→Status→Done?→Download→R2-Upload – statt in einem Loop.
- reddit: dieselbe ApiMart-Kette unter anderen Node-Namen („ApiMart - Submit Image" statt
„ApiMart Submit").
Was heute je Kanal wirklich abweicht, ist genau die Liste, die Leon behalten will: Webhook-Pfad · R2-Prefix · Trigger-URL · Voice-ID (Algrow-Node) · Router-Prompt = welche Stills · Skript-Prompt + Wortziel = welche Maße · Thumbnail-Prompt · Store-Key · Telegram-Text. Das sind Rezept-Felder, keine Workflow-Varianten.
2 · Das Muster existiert im Haus – n8n ist die letzte geforkte Schicht
- 6 Gateways laufen schon parametrisch geteilt:
pexels-search,video-db,claude-gen,
algrow, algrow-tts, tg-notify – je EIN Webhook-Pfad, von allen Kanälen gerufen.
- 3 Render-Container bedienen 12 Kanäle (gemessen 13.08.2026 aus
registry.json:renderer
allein 10, dazu renderer-alvaro und render-whatif; shorts und ztest_walk haben keinen). Der Kanal kommt als channel / recipe_key im Body; fehlt channel, gibt es 400 (Constraint #11, HZ-814).
- 0 Sub-Workflow-Nodes (
executeWorkflow) im ganzen System – verkettet wird über HTTP-Webhooks.
Die Straße braucht also kein neues Muster, nur konsequente Anwendung des vorhandenen.
Wartungslast heute: 71 wf_fork/patch_*.py (gezaehlt 02.08.2026), viele davon mit hartkodierten Kanal-Listen/WF-ID-Maps. Jede neue Stellschraube = 1 Patch × N Kanäle × Constraint #7 (deactivate+activate) je Workflow. Jeder neue Kanal = 6 weitere Workflows, die alle künftigen Patches mitschleppen.
3 · Zielbild
Ein Aufrufer (Studio-Knopf, Cron oder die Vorstufe) ruft einen Workflow je Stufe und gibt den Kanal als Pfadsegment mit – nicht als Query, nicht irgendwo im Body. Dessen erster Node holt sich das Rezept beim Bauplan-Gateway, das registry.json (Verdrahtung) und recipes/<slug>.json (Rezept) zu einer Auflösung zusammenlegt; alle weiteren Nodes lesen nur noch aus diesem einen Ergebnis. Aus 74 Kanal-Kopien werden so 6 Workflows. Als Skizze:
Die fünf Regeln, die den Umbau tragen
- Kanal-Identität nur aus dem Pfad. Sie steht beim Aufrufer an genau einer Stelle, erscheint im
Log, und unbekannter Kanal = Abbruch vor der ersten Geldstelle.
- Das Rezept wird nie mitgeschickt, immer nachgeschlagen. Ein Aufrufer kann kein Rezept fälschen.
- Kein stiller Default. Kein
||/??in irgendeinem Body. Fehlendes Feld →stopAndError+
tg-notify. (Lesson HZ-814: genau so überlebte "channel":"HISTORY" als Default monatelang.)
- Alias-Auflösung zentral. Jeder Kanal hat heute drei Schreibweisen – Registry-Key
geo,
store_channel: emptyearth, youtube.channel_param: GEO. (Bei cs sind store+YT bewusst history = der CS-Umbau, kein Bug.) Das Rezept liefert alle drei explizit; kein Node bastelt sie sich selbst zusammen.
- Der Aktiv-Schalter ist Konfiguration. Heute lebt „läuft / läuft nicht" im n8n-
active-Flag plus
node-disabled Cron (alle P1-Crons stehen auf 0 16 /2 *). 6 Workflows können keine 15×6 Zustände tragen → enabled + cadence je Kanal/Stufe in registry.json, bevor irgendetwas umgeschaltet wird. Sonst ist am Umschalttag jeder Registry-Kanal „an" = 15× ApiMart parallel.
DNA-getrieben, aber ratefrei (Leons Entscheid + sein eigener Einwand)
Leon: „schon mit DNA direkt – das Problem ist halt, dass die DNAs bei den meisten Channels noch nicht ausgereift sind; alvaro ist am fortgeschrittensten." Gemessen ist das noch schärfer:
| DNA-Reife (gemessen 27.07.) | Kanäle |
|---|---|
| DNA vorhanden | 4 – alvaro_horror, auswandern_rente_de, geo, history |
| keine DNA, Rezept ist 350–600-Byte-Stub | 11 – cs, psych, wisdom, survival, doomed, worldcup, reddit, stickman, aviation, business, maritime |
shots_per_min maschinenlesbar (Stand 05.08.) | 2 – own_history (7,5 · HZ-538) und alvaro_horror (4,0 · HZ-942, aus bildwechsel_s; die 0,4 aus HZ-926 war die Szenenrate und heißt jetzt szenenwechsel_pro_min), beide gemessen in pacing_num. Überall sonst weiter Prosa ("unverifiziert", "15-30 (veri…", "stark templ…"). Aktuelle Zahl nie hier ablesen, sondern messen: python3 channel_clone/beat_probe.py --stand |
Auflösung: Die Straße rechnet immer aus dem Rezept – nirgends ein Literal (H7-Gesetz). Aber sie rät nie. Jedes Rezept-Feld trägt eine provenance:
dna– aus der DNA gerechnet (heute: alvaro/geo/history/rente, soweit maschinenlesbar)pinned– heutiger Wert bewusst eingefroren, weil die DNA-Stelle fehlt/Prosa ist (sichtbare Lücke)- fehlt ganz → harter Stop, kein Default
Damit fährt ein reifer Kanal sofort DNA-getrieben, die Stub-Kanäle fahren sichtbar eingefroren weiter, und die Migration wartet nicht auf 11 DNA-Läufe. Die DNA-Reife wird messbar statt still zu Müll zu führen.
4 · Reihenfolge (Begründung = die Zahlen aus §1)
| # | Stufe | Warum hier | Risiko |
|---|---|---|---|
| 1 | publisher | 1.00 identisch · alle 8 AUS · Cron-Trigger → Constraint #7 greift nicht | null |
| 2 | p5 ✅ umgeschaltet 30.07. (wKmuUD6uT9bVy5t8, 13 Nodes, aktiv) – alle 9 Kanäle sind auf die Straße verdrahtet, Bauplan seit HZ-564 (04.08.2026) 1/9 fahrbar – nur history (nachgemessen 05.08.2026: whatif ist zwar upload_mode: api, fährt aber keine Straße – road: null, kein p5_callback – und gehört damit nicht in die 9er-Population; der Bauplan antwortet ihm 409 schalter_fehlt); die 8 Browser-Kanäle beantwortet /hq/api/bauplan?stage=p5 mit 409 stufe_nicht_zustaendig (dashboard/bauplan.py → NUR_UPLOAD_WEG), und die P5-Straße bricht darauf ab, statt eine schon verbuchte Zeile ein zweites Mal zu schreiben. Verbucht wird im Browser-Weg an genau einer Stelle: scaling/auto_upload.py → /hq/api/upload/done (Status + youtube_url + Kalender-Gate). Die alte Zahl 9/9 fahrbar war formal richtig und praktisch irreführend – sie zählte Kanäle, für die die Stufe nie gerufen wird. Verdrahtet ≠ gerufen: die P4 stößt den Upload nur bei upload_mode: api an – das ist unter den 9 Straßen-Kanälen nur noch history (gezählt 05.08.2026 in registry.json: api haben history und whatif, letzterer ist aber kein Straßen-Kanal); alle Browser-Kanäle – darunter alvaro – enden im Zweig „fertig, manueller Upload" und rufen p5 nie (gemessen 03.08.2026 an registry.json + dashboard/bauplan.py:350). Details unten in §4c | 1.00 identisch · sitzt hinter allen Geldstellen | niedrig |
| 3 | p3 | Median 1.00 · jede Divergenz ist ein Registry-Feld (Container-Host, Prefix, callback, recipe_key) · Render lokal = $0 prüfbar | niedrig |
| 4 | p4 | 0.79 · die Nicht-Kern-Nodes werden Rezept-Schalter (Thumbnail-Schule, eBook) · Upload erst nach upload_target_check.py grün | mittel (YouTube) |
| 5 | p2 ✅ gebaut + belegt 27.07. (qUTlurAFjer8cbiD, 110 Nodes, aktiv – gemessen 07.08.2026 per GET, 94 waren es am 02.08.; +6 Upload-Wächter aus wf_fork/patch_upload_guard.py, die build_p2_road.py mitbaut) | 0.39 · beide Geldstellen (ApiMart + TTS). Kein Merge: der fetteste P2 (Psych/Rente) ist der Stamm – eingefroren als wf_fork/stamm/p2_stamm_2026-08-07.json, 89 Nodes gemessen 07.08.2026 (HZ-922); die frühere Zahl 74 stammte aus einem Stand, den es nicht mehr gibt, dünne Kanäle sind Teilmenge – 26/43 parameter-identisch belegen das. Beleg: echtes P1-Skript durch die Straße = 90 Beats, 20/14/14/14/14/14, identisch zum Fork bei demselben Run, $0 (HZ-406). Quellen-Mix wirkt (HZ-418, Exec 1751): wikimedia 31 · pexels 26 · openverse 17 · pixabay 16 statt 90× pexels. Seit 30.07. fahrbar (HZ-525): 9/9 Kanäle, gemessen mit channel_clone/p2_strasse_probe.py (führt den jsCode des lebenden Straßen-Nodes gegen den echten Bauplan aus, $0). Der alte Blocker HZ-430 ist erledigt: drittes Beat-Modell staffel (front-loaded Fahrplan, zahlengenau aus dem Fork) + Router-Prompt aller 9 Kanäle eingefroren + p2_router_prompt/beat_regel sind Bauplan-PFLICHT. Umgeschaltet: doomed (Kanarienvogel) und seit 03.08.2026 alvaro_horror – der eine Kanal, der laut Leon-Entscheid 01.08. ausgemeistert wird; seit 03.08.2026 fährt alvaro p2+p3+p4 parametrisch aus dem Bauplan (p4 per HZ-754). p5 zählt hier nicht mit: der Rückweg ist zwar verdrahtet (p5_callback = road-p5?channel=alvaro_horror), wird für alvaro aber nicht gerufen – die Straßen-P4 stößt den Upload nur bei plan.upload_mode === 'api' an, alvaro steht auf browser (gemessen 03.08. am /hq/api/bauplan). Beleg vor dem Schalten: p2_strasse_probe.py → alvaro JA (2290 Zeichen Router-Prompt, staffel 1:6,rest:5), bauplan?channel=alvaro_horror&stage=p2 → ok:true, fehlend:[]. Die übrigen 6 bleiben bewusst Fork, nicht weil sie klemmen (9/9 wären fahrbar), sondern weil sie geparkt sind – ein Umschalten wäre Arbeit an Kanälen, die niemand fährt. history zusätzlich NICHT: dort schlägt die harte DNA-Dichte den Fork-Fahrplan. Neu gemessen 05.08.2026 (HZ-355, beat_probe.py --channel history --vergleich, $0): mit der seit HZ-355 PFLICHTIGEN Obergrenze beats_max=12 je Kapitel sind es 96 statt 48 Bilder (2×), nicht die früher hier genannten ~157 gegen ~38 (~4×) – die alte Zahl war die ungeklammerte Rechnung. Bleibt ein Entscheid (Kosten), kein Nebeneffekt | hoch |
| 6 | p1 | mechanisch am leichtesten (nur Prompts), aber P1 ist der Takt. Solange P1 geforkt bleibt, bleiben die Aktiv-Schalter je Kanal das Gaspedal. Vorbedingung wortziel ist seit 30.07. erfüllt: 9/9 (HZ-407, §4e) | mittel |
Canary = doomed (aktiv, kein Publikum, kein eBook-Funnel), danach alvaro. history / cs zuletzt – dort hängt der eBook-Funnel.
Blast-Radius-Kontrolle (ein Bug trifft dann ALLE Kanäle)
- Die Straße entsteht als NEUER Workflow mit neuem Pfad, die Forks bleiben aktiv. Umgeschaltet wird
beim Aufrufer: eine URL (wf_fork/patch_*.py-Muster). Der PUT trifft damit nie den Fork.
- Rückweg = dieselbe URL zurück. Fork wird erst nach zwei sauberen Läufen inaktiv gesetzt,
nie gelöscht.
- Kill-Switch:
enabled:falseje Kanal/Stufe → Straße antwortet 423 und bricht ab.
Global: Rezept-Gateway deaktivieren → alles fällt fail-closed aus (kein Lauf statt falscher Lauf).
- Rollback-Netz (Stand 07.08.2026, HZ-922): es entsteht beim Schreiben, nicht vorher.
Die alten Sammel-Ordner sind beide weg – channel_clone/backups/2026-07-27-hz363/ ist seit 05.08. gelöscht, wf_fork/backups_hz404/ war am 07.08. gemessen nicht vorhanden, obwohl zwei Skripte ihn als Rückweg NANNTEN. Jetzt sichert jedes schreibende Werkzeug den lebenden Stand selbst, unmittelbar vor dem PUT und innerhalb derselben Schreibsperre: patch_telegram_kanal.sicherung_schreiben() (EINE Quelle, build_p2_road.py delegiert dorthin) legt wf_fork/backups_hz404/<marke>__<wf-id>__<updatedAt>.json an, und der Rückweg-Text nennt den fertigen Befehl statt eines Verzeichnisses:
Ohne Datei nimmt er die jüngste Sicherung; mit --dry zeigt er nur, was er zurückspielen würde; gibt es keine, bricht er laut ab statt still nichts zu tun. Der PUT läuft unter der Schreibsperre und reaktiviert den Webhook (Constraint #7). Getestet am 07.08.2026 an der aktiven Straße qUTlurAFjer8cbiD: Sicherung 110 Nodes → --rollback --dry → echter --rollback → GET danach 110 Nodes, active=1. Der P2-Stamm ist davon getrennt und liegt eingefroren unter wf_fork/stamm/ (p2_stamm_2026-08-07.json, 89 Nodes) – eine Sicherung ist ein Rückweg, ein Stamm ist eine Vorlage; früher war beides dieselbe Datei und driftete deshalb still.
- Verifikation:
channel_clone/road_check.pydiffed den aufgelösten Rezept gegen die Werte in
den lebenden Fork-Workflows (read-only, Sekunden, $0). upload_target_check.py bleibt Pflicht-Gate vor jedem P4-Schritt. Doppelagenten-Abnahme nach Constraint #12.
4b · Stand: was am 27.07. gebaut und bewiesen wurde
Die Rezept-Auflösung steht – sie ist die Grundlage aller sechs Stufen:
| Stück | Datei | Wirkung |
|---|---|---|
| Auflösung Kanal+Stufe → alle Werte | dashboard/bauplan.py | eine Quelle, provenance je Feld, kein stiller Default |
| HTTP-Zugang | dashboard/app.py → /hq/api/bauplan, /hq/api/bauplan/alle | 404 unbekannt · 409 Lücke (mit Feldnamen) · 423 Kill-Switch |
| 7. Gateway | wf_fork/einmal/create_bauplan_gateway.py → n8n N4YwuXDZKNY7Wj48 (aktiv) | POST /webhook/bauplan, Guard bricht ohne channel/stage ab |
| Stufe 1 | wf_fork/build_publisher_road.py → n8n MXVBcASOgDmYY8MG (deaktiviert) | EIN Publisher für alle Kanäle statt 8 Klonen |
| Der Beweis | channel_clone/road_check.py | diffed Rezept gegen die lebenden Workflows, read-only, $0 |
| Schalter + 4. Alias | channel_clone/registry.json → road.enabled/road.auto, asset_prefix | Aktiv-Stand an EINER Stelle lesbar. Seit 03.08.2026 GEMESSEN, nicht gepflegt (registry_sync_active.py --apply, Cron */30): enabled.<p1-p5> = der Workflow, den der Kanal für die Stufe fährt, läuft in n8n. publisher bleibt handgepflegte Freigabe. Den WEG sagt weiterhin nur road.<pN_trigger>. |
Belege (alle $0, keine Läufe ausgelöst):
- Gateway gegen sechs Fälle:
history/p3→ 200 ·history/publisher→ 423 ·gibtsnicht/p2→ 404 ·
history/p2 → 409 (beats) · fehlendes stage bzw. channel → Guard bricht ab.
- Trockenlauf der Publisher-Straße gegen eine Kopie der Registry (nie am Live-Topf):
history würde mit seinen eigenen Werten fahren (render-history-remotion · channel=HISTORY · store=history), alvaro wird wegen upload_mode=browser korrekt übersprungen. Gegen die echte Registry fährt sie niemanden – alle 8 stehen auf enabled:false.
road_check.pyüber alle 15 Kanäle × 6 Stufen: 162 Werte stimmen mit dem Fork überein,
3 Drift (unten), 25 benannte Lücken.
- Reifegrad nach Stufen (Stand 27.07., Altstand) – der Nenner hat sich seither zweimal
bewegt: 15 Kanäle damals, 12 ab 29.07., 14 am 13.08.2026: publisher 8/8 · p5 9/9 · p4 12/12 · p3 12/15 wären fahrbar gewesen; p2 klemmte an beats (14) und bildquellen (12), p1 an wortziel (15). Die 3 fehlenden p3 waren die Stubs (aviation/business/maritime). wortziel ist seit 30.07. erledigt: 9/9 (§4e). Aktuelle Zahlen nie von hier abschreiben – python3 channel_clone/road_check.py --schuld bzw. /hq/api/bauplan/alle messen sie.
- Der Migrations-Handgriff funktioniert:
road_check.py --freeze doomed --applyhat die
gemessenen Ist-Werte (beats: 48, bildquellen: [ai, pexels, pixabay]) mit voller Herkunft ins Rezept geschrieben → doomed/p2 ist seither vollständig fahrbar.
Was der Bau selbst zutage gefördert hat
- Es sind VIER Schreibweisen pro Kanal, nicht drei. Neben Registry-Key,
store_channelund
youtube.channel_param gibt es den Asset-Prefix, und der weicht ab (gemessen 13.08.2026 aus channel_clone/registry.json, 4 von 14 Kanälen – die Zahl selbst nachrechnen, nicht von hier abschreiben): alvaro_horror→alvaro, auswandern_rente_de→rente, cole_explains→cole, tomorrow_engineered→tomorrow. Er ist nicht ableitbar und steht jetzt gemessen als asset_prefix in der Registry.
- Render-Container = Upload-Container (korrigiert 03.08.2026). Das eigene Feld
youtube.render_container bleibt, weil ein Upload grundsätzlich woanders hinzeigen KÖNNTE – aber es trägt denselben Wert wie render_container. Bei geo, doomed und survival stand dort render, während ihr P3 in renderer rendert; der Upload konnte nie gelingen, denn der fertige Film liegt in /tmp/render, containerlokal ohne Volume (gemessen per docker inspect) – wer nicht gerendert hat, findet die Datei nicht. Ein abweichendes Upload-Ziel ist erst mit gemeinsamem Ablageort sinnvoll. Wächter: python3 channel_clone/render_host_check.py (read-only, $0) meldet ein auseinanderlaufendes Registerpaar, BEVOR es die Workflows prüft.
- Zwei Upload-Wege.
alvaro_horrorlädt bewusst über den AdsPower-Browser (HZ-290) und hat
absichtlich kein channel_param. Ein pauschales Pflichtfeld hätte den Kanal blockiert – oder jemanden verleitet, genau das Leck aufzureißen, das dort zugemauert ist.
recipe.voiceist ein Key, keine Stimm-ID. Die Auflösung läuft über
channel_clone/voices.json (4.772er-Pool, Stand 03.08.2026), nicht über registry.voices – dort steht ein alter Mini-Pool, in dem kein einziger Rezept-Key vorkommt.
- „36 Bilder fix" stimmt so nicht. Gemessen: psych 24 · worldcup 36 · wisdom/doomed/survival 48 ·
geo 72. Der Wert ist überall unsteuerbar, aber nicht überall gleich – wer bei der Migration pauschal 36 einsetzt, ändert vier Kanäle.
- Drei echte Drifts im Ist-Zustand (nicht durch den Umbau entstanden, von
road_checkgefunden).
Nachtrag 05.08.2026: diese Drifts stehen in den Fork-Kopien, nicht auf der Straße – dort meldet --stage p2 10/10 parametrisch und 0 DRIFT. Sie enden mit dem Umschalten des Kanals.
history/p2: der Workflow spricht mit Johi (HRttR5MB…), Rezept und Registry sagen
oscar2 (3mmJ2Z…); dazu stability 0.5 statt 0.7 und similarity_boost 0.75 statt 0.85 (gemessen 05.08.2026).
- ~~
alvaro/p2: Workflow Viraj (1U02n4…), Rezeptedoardo(YqZLNY…).~~ behoben 02.08.2026
(apply_recipe --with-voice, Stand: Workflow trägt YqZLNY…; Beleg road_check --stage p2).
alvaro/publisher: ruftrender:8080, laut Registry wäre esrenderer:8080
(schlafend: Workflow aus, und der Kanal lädt ohnehin über den Browser).
4c · Stufe 2 (P5): was am 30.07. umgeschaltet wurde – HZ-403
Stand: EIN Workflow Road P5 (alle Kanäle) (wKmuUD6uT9bVy5t8, aktiv) verbucht den Upload-Rücklauf. In allen 9 P4 steht die Straße als callback_url (road-p5?channel=<kanal>), der Bauplan ist für alle 9 vollständig und scharf, die 7 Fork-P5 bleiben unangetastet und aktiv (Rückweg).
⚠️ „9 umgeschaltet" heißt nicht „9 rufen heute". Gemessen am 30.07.: bei 6 von 9 ist der NodeServer - Trigger YT Uploaddisabled(alvaro_horror, auswandern_rente_de, doomed, geo, psych, survival) – dort feuert die Adresse gar nicht, sie gilt beim Scharfschalten. Real rufen können heute cs, history, wisdom; und weil 8 Kanäle aufupload_mode: browserstehen, lädt praktisch nur history über den Render-Container (→ HZ-564: der Browser-Weg verbucht selbst und ruft P5 nie).
Werkzeuge:
| Stück | Datei | Wirkung | |
|---|---|---|---|
| Die Straße | wf_fork/build_p5_road.py | Webhook → Bauplan → Store-Lauf holen → Status + Telegram | |
| Umschalten/Rückweg | wf_fork/patch_p5_callback.py | `--channel X --road\ | --fork --apply, --status` zeigt wer wohin ruft |
| Messen | channel_clone/freeze_p5_callback.py | liest die callback_url aus jedem P4 → registry.road.p5_callback | |
| Prüfen | road_check.py --stage p4 (Feld p5_callback) | Migrationsstand gegen den LEBENDEN Workflow | |
| HQ | Fabrik → Straße, Spalte UMGESCHALTET | zeigt je Stufe, wie viele Kanäle wirklich die Straße rufen |
Belege (alle $0, kein Upload, kein Render):
- Guard-Fälle über den echten Webhook: unbekannter Kanal → Abbruch „Bauplan 404" ·
whatif (kein road-Block) → „409 schalter_fehlt" · fehlende runId → Abbruch im Eingang · fehlender Kanal → Abbruch im Eingang (n8n-Executions 1849/1851/1852/1853).
- Happy Path an echten Daten, wiederhergestellt: history-Zeile
2026-05-10-cadaver-synod-trial (Status Public) durch die Straße mit ihrer EIGENEN YouTube-URL → Store stand auf Published + identischer URL (Exec 1854), danach zurück auf Public gesetzt. Kein Testdatensatz im Live-Topf.
- Echter Weg des Aufrufers: `docker exec render curl -X POST
https://n8n.herz-automation.com/webhook/road-p5?channel=doomed → HTTP 200, Exec 1857 läuft bis „Telegram - Zeile fehlt" (der Fall, den der Fork verschluckte). (Seit 02.08.2026 endet dieser Zweig zusätzlich in „Stop and Error (Zeile fehlt)" – die Execution steht dann auf error, nicht mehr auf success. Gemessen 05.08.2026: bei upload_mode: browser – 9 von 12 Registereinträgen, u. a. alvaro_horror – wird dieser Zweig gar nicht mehr erreicht, weil Bauplan prüfen` drei Schritte davor abbricht („diese Stufe ist nur für api zuständig", Execution 2350). Ein Leerfall-Beweis für P5 geht nur über einen api-Kanal.)
road_check.py --stage p5→ 7 Werte gleich, 0 Drift ·--stage p4→ 35 Werte gleich, 0 Drift.
Was der Umbau zutage gefördert hat:
- doomed und survival hatten nie einen eigenen P5. Ihr P4 rief
yt-done-trigger, also
HISTORYs P5 – dort wurde ihre run_id im History-Store gesucht und nie gefunden. Der Lauf endete ohne Status, ohne Telegram, und die Execution stand auf success. Genau die HZ-814-Fehlerklasse, nur leiser. (stickman ruft ebenfalls dort hin, ist aber komplett aus.)
- P5 war im Bauplan falsch blockiert.
UPLOAD_STUFENenthieltp5, also verlangte die
Stufe die Upload-Weg-Felder. Seit dem Browser-Entscheid (HZ-552, 29.07.) haben 8 Kanäle upload_mode: browser ohne yt_id/profile_id → 5 von 7 Kanälen meldeten am 29.07. eine Lücke, die in keinem P5-Node vorkommt. P5 lädt nichts hoch, es verbucht – Feld entfernt, 9/9 fahrbar. Überholt am 04.08.2026 (HZ-564): „fahrbar" heißt jetzt „zuständig". Die Browser-Kanäle bekommen 409 stufe_nicht_zustaendig (nicht mehr 409 wegen fehlender Upload-Felder – das wäre wieder der falsche Blocker); verbucht wird dort scaling/auto_upload.py. Stand 1/9 (nachgemessen 05.08.2026, HZ-564-Nachbesserung: nur history. whatif ist zwar upload_mode: api, gehört aber gar nicht zur 9er-Population – road: null, kein p5_callback; der Bauplan antwortet ihm 409 schalter_fehlt, nicht 200).
- Die Fork-Meldungen waren inhaltlich uneinig: 4 Kanäle schrieben „LIVE (public)", 5
„hochgeladen (unlisted), wird 20:00 public". Der Callback liefert privacy mit – die Straße sagt jetzt, was wirklich gilt, statt einen von zwei Sätzen zu raten.
- n8n kann keine
:param-Webhook-Routen (gemessen an 2.20.7). Der Kanal steht deshalb als
Query in der URL, nicht als Pfadsegment – Regel 1 bleibt inhaltlich erfüllt. Details + PUT-Settings-Whitelist + alwaysOutputData: docs/kb/werkzeuge-rezepte__n8n-rest-fallen-beim-workflow-umbau.md.
Was die Doppelagenten-Abnahme (Constraint #12) am 30.07. noch gefunden und was davon sofort behoben wurde – zwei Prüfer, getrennte Aufträge („Defekte" / „Vollständigkeit"), disjunkte Funde:
- behoben:
errorWorkflowfehlte an der Straße (alle Fork-P5 haben2HLnYbG1J9LS1yVT) –
ohne ihn sieht einen Abbruch NIEMAND, weil der Webhook vorher 200 antwortet und Webhook-Executions nicht in der Public-API stehen. Genau die Stille, die das Ticket abschafft.
- behoben: Store-Ausfall sah wie „keine Zeile" aus. Ein Gateway-500 fiel durch den Filter und
wurde als „Rücklauf ohne Zeile" gemeldet – falsche Diagnose, Status ungeschrieben. Jetzt ein eigener, benannter Abbruch.
- behoben: fehlendes
successgalt alsfalse– ein kaputter POST hätte einen erfolgreichen
Upload auf Upload Failed gesetzt. Jetzt Abbruch (kein Default).
- behoben: Telegram-HTML. Fehlertexte enthalten
<HttpError 403 …>; ungeescapt lehnt Telegram
die Meldung ab (400) und sie fällt wegen onError=continue still weg – ausgerechnet im Fehlerfall. Titel und Fehlertext werden jetzt escaped, und die Meldung sagt es, wenn der Store-Schreiber scheiterte.
- behoben:
road.enabled.p5stand bei psych/rente/alvaro auffalse, ihr Callback zeigte
aber schon auf die Straße → 423, Rücklauf nie verbucht. P5 ist kein Gaspedal (es verbucht, es gibt kein Geld aus): alle 9 stehen jetzt auf true, Begründung in der Registry.
- behoben: exakter
run_id-Vergleich statt „erste Zeile", Query im Umschalt-Guard (ein zweites
--road --apply machte sonst einen sinnlosen PUT samt Webhook-Lücke), gefilterte Messung der Callback-URL, und eine Warnung beim Rückweg für doomed/survival (ihr Fork-Pfad ist HISTORYs P5 – zurückschalten baut den stillen Totlauf wieder ein).
- als Ticket weitergegeben, nicht hier gelöst: HZ-564 (Browser-Upload verbucht selbst, P5 wird
übersprungen) · HZ-565 (fork_channel.py forkt neue Kanäle weiter an der Straße vorbei) · HZ-566 (die Straßen-Workflows stehen in KEINER Registry – Massen-Patcher wie patch_filestore.py und road_check --stage p5 sehen nur die Fork-IDs) · HZ-567 (der Webhook ist unauthentifiziert; bei einem Fork traf das einen Kanal, jetzt alle) – die Sache ist am 04.08.2026 erledigt, siehe §4g – das Ticket HZ-567 selbst gibt es nicht mehr, es wurde am 01.08. beim Ausmisten geparkt (archive/todo_erledigt.log:454).
Offen (bewusst nicht in diesem Schritt): Die 7 Fork-P5 bleiben aktiv, bis zwei echte Läufe über die Straße verbucht sind – erst dann inaktiv setzen, nie löschen. Echte Läufe hängen daran, dass überhaupt wieder hochgeladen wird (Browser-Weg HZ-552).
4d · Stufe 3 (P3): was am 30.07. gebaut wurde – HZ-404
Stand: EIN Workflow Road P3 (alle Kanäle) (Vd659BAWRqpyOw4B, 24 Nodes, aktiv, Pfad POST /webhook/road-p3?channel=<kanal>) kann alle 9 Kanäle rendern; der Bauplan ist für 9/9 vollständig (8 scharf, auswandern_rente_de steht per Kill-Switch auf enabled.p3:false). Die 9 sind der Fork-Bestand vom 30.07. – heutiger Stand: python3 channel_clone/road_switch.py (13.08.2026: 8 Kanäle rufen für p3 die Straße, das Register führt 14).
Umgeschaltet am 30.07. (HZ-540): 8 von 9 Kanälen. history · psych · cs · wisdom · geo · doomed · survival · alvaro_horror rufen für P3 jetzt die Straße – im Fork-P2 (Node Trigger P3_Render), in registry.road.p3_trigger und am HQ-Knopf „Fortsetzen" (hq.config.json → p3_hooks), alle drei Aufrufer gleichzeitig, je Kanal ein JSON-Backup unter wf_fork/backups_hz404/ (der Ordner wurde zwischenzeitlich geleert und wird seit 07.08. von den Werkzeugen selbst wieder angelegt, siehe Rollback-Netz oben) und Constraint #7 (deactivate+activate) vom Werkzeug selbst erledigt. auswandern_rente_de bleibt bewusst auf dem Fork: sein Bauplan antwortet 423 (Kill-Switch), eine umgeschaltete Stufe hinter einem geschlossenen Kill-Switch wäre ein toter Lauf statt eines Rückwegs. Die Fork-P3-Workflows bleiben aktiv und unangetastet – sie sind der Rückweg (patch_p3_trigger.py --channel X --fork --apply).
| Stück | Datei | Wirkung | |
|---|---|---|---|
| Die Straße | wf_fork/build_p3_road.py | Webhook → Bauplan → Store-Lauf → Bilder → Untertitel → Render-Lock → Render | |
| Umschalten/Rückweg | wf_fork/patch_p3_trigger.py | `--channel X --road\ | --fork --apply, --status` zeigt alle drei Aufrufer |
| Messen | channel_clone/freeze_p3_render.py | misst je Kanal callback_url · hq_out · Anzeigename → registry.road.p4_callback / .p3_hq_out / .p3_titel (so heißen die Schlüssel wirklich) | |
| Prüfen | road_check.py --stage p3 (Felder p4_callback, hq_out) · --stage p2 (Feld p3_trigger) | Migrationsstand gegen den LEBENDEN Workflow | |
| HQ | Fabrik → Straße, Spalte UMGESCHALTET zählt p3 jetzt mit | seit 30.07. 8/9 = STRASSE (live aus /hq/api/bauplan/alle, keine gepflegte Liste) |
Beim Umschalten gefunden und mitrepariert (30.07., HZ-540): p3_hooks in hq.config.json ist nach store_channel gekeyt, nicht nach Kanal – history und cs teilen sich den Store history, und der eine Eintrag stand auf cs. Leons Knopf „Fortsetzen" hätte einen History-Lauf also mit dem CS-Rezept gerendert (im Fork stand dort cs-p3-trigger, der Fehler ist älter als die Straße). Jetzt auf road-p3?channel=history. Dass CS damit keinen eigenen Resume-Knopf hat, ist die ehrliche Restlücke: solange zwei Kanäle einen Store teilen, ist eine Store-Zeile nicht eindeutig einem Kanal zuzuordnen (eigenes Ticket).
$0-Beweis, dass die Straße wirklich fährt (Exec 1931/1932, 30.07. 09:09): ein POST auf road-p3?channel=history mit einer bereits gerenderten runId läuft bis „Schon gerendert?" und endet im Doppel-Schutz; derselbe POST mit channel=doomed und unbekannter runId endet in „Zeile fehlt". Beide lösen den Bauplan des richtigen Kanals auf (store_channel/r2_prefix history bzw. doomed) und starten keinen Render.
Der erste ECHTE Render über die Straße ist gefahren (HZ-570, 31.07.2026, Kanarienvogel doomed, $0): Store-Zeile 2026-07-31-hz570-canary (is_test=1) mit vorhandenen Assets aus downloads/testdata/doomed → Untertitel-Schleife über whisper-proxy (2 Kapitel, Wort-Zeitmarken im Payload) → Render-Lock geholt → renderer:8080 → downloads/doomed-video/…_video.mp4 (1920×1080 h264/aac, 122,6 s, Karaoke-Untertitel eingebrannt), Store auf Render Triggered. Bezahlt wurde nichts, weil Doomed P4_Upload für den Lauf deaktiviert war – der Renderer- Callback lief absichtlich in ein 404. Der Lauf hat einen Bug gefunden, der die Straße für JEDEN Kanal blockierte: der Node „Visuals holen" fährt mit responseFormat: "text", n8n legt den Inhalt dann unter data statt body ab – der Guard las nur body und meldete „visuals.json ist leer", obwohl die Datei gültig war (Exec 1994). Repariert in build_p3_road.py (Hülle wird nur ausgepackt, wenn sie eine ist; die Meldung nennt jetzt den erhaltenen Anfang), Straße per --update ersetzt, zweiter Lauf danach durchgerendert. Rezept: docs/kb/werkzeuge-rezepte__n8n-rest-fallen-beim-workflow-umbau.md §12.
Zwei Dinge, die dabei sichtbar wurden: (1) P3 gibt den Render-Lock nach einem gelungenen Anstoß NICHT frei – das macht P4 (build_p4_road.py, beide Zweige). Wer P4 zum Sparen abschaltet, muss den Lock danach von Hand freigeben, sonst blockiert er alle Kanäle bis zum TTL (11400 s). (2) Genau so lag beim Start dieses Tickets noch ein Lock von hz493-longrun seit ~10 h auf render:8080.
Offen bleibt allein der Abschnitt Render Triggered → Video Rendered: den schreibt P4, und P4 kauft dabei ein Thumbnail (doomed: 1 Variante, gpt-image-2 = $0,0140 aus data.cost, gemessen 13.08.2026). Das ist HZ-573; das gerenderte Video liegt schon da, ein POST auf doomed-p4-trigger mit {"runId":"2026-07-31-hz570-canary","success":true} genügt.
P3 hat DREI Aufrufer – wer nur den ersten umstellt, hat halb umgeschaltet: Fork-P2 (Node „Trigger P3_Render") · Straßen-P2 (liest plan.p3_trigger) · der HQ-Knopf „Fortsetzen" (dashboard/config/hq.config.json → p3_hooks, gekeyt nach store_channel, weshalb history und cs sich einen Eintrag teilen – patch_p3_trigger.py fasst ihn deshalb nur an, wenn der heutige Wert wirklich zu diesem Kanal gehört).
Belege (alle $0, kein Render, kein Upload):
--probe: 8 Kanäle fahrbar mit ihren eigenen Werten, rente 423 (Kill-Switch) – kein Kanal
fährt versehentlich fremde Werte.
--vergleich <kanal>: der Straßen-Body Feld für Feld gegen den Fork-Body. history und
alvaro_horror identisch; geo/rente siehe „gewollter Unterschied" unten.
- Guard-Fälle über den echten Webhook (n8n-Executions 1868–1874): unbekannter Kanal → Bauplan
404 · whatif → 409 · fehlende runId → Abbruch im Eingang · fehlender Kanal → Abbruch.
- Echte Daten, Doppel-Schutz (Exec 1876): history-Lauf
2026-05-10-cadaver-synod-trial
(Status Public) durch die Straße → Store-Zeile über den Bauplan-store_channel gefunden, Duplikat erkannt, Telegram, kein Render (Nodes „Visuals holen"/„Render starten" nie betreten). Kein Testdatensatz im Live-Topf, kein Status verändert.
- Echte Daten, Bild-Guard (Exec 1878): history-Lauf im Status
Script Draft→ Abbruch mit
Grund „visuals.json nicht gefunden (HTTP 404) unter history/<run>_visuals.json – hat P2 für diesen Lauf wirklich Bilder abgelegt?". Der Fork hätte an dieser Stelle eine Filestore-Fehlerseite als Bildliste an den Renderer geschickt.
- Untertitel ohne Deepgram:
whisper-proxymit?language=esliefert 146 Wörter in
Deepgram-Form ($0) – die Sprache kommt aus recipe.lang.
Was der Umbau zutage gefördert hat:
- Alle neun Fork-P3 tragen einen Fallback-runId. Im Node „Set Run-ID" steht
$json.body?.runId || "2026-05-10-cadaver-synod-trial" – ein Aufruf ohne runId rendert also stillschweigend eine alte History-Folge, auf JEDEM Kanal (der Fallback wurde mitgeforkt). Die Straße bricht stattdessen ab.
callback_urlist die teuerste Verwechslung der Stufe. Fehlt sie, nimmt der Renderer
seine Container-env RENDER_CALLBACK_URL = psych-p4-trigger und weckt PSYCHs P4 – und P4 kauft das Thumbnail. Deshalb ist p4_callback seit HZ-404 ein Pflichtfeld des Bauplans.
- Gewollter Unterschied bei geo und auswandern_rente_de: ihr Fork-P3 schickt keinen
recipe_key. Ihr Rezept (Bewegungen, Übergänge, BGM, Untertitel-Stil, bei rente die eBook-Einblendungen) erreicht den Renderer deshalb heute gar nicht – build_payload.py lädt recipes/<RECIPE_KEY>.json nur, wenn der Key im Request steht. Die Straße schickt ihn aus dem Bauplan; beim Umschalten ändert sich für diese beiden also sichtbar der Look (Ticket HZ-569).
hq_outist kein Ablageort, sondern Branding.render/renderer/server.py::_brand_key()
leitet den Kanal aus dem Suffix ab. Ein erfundener Pfad heißt: fremdes Abo-Overlay. Ehrlich dazu: nur 4 der 9 Kanäle setzen hq_out überhaupt (wisdom, geo, doomed, survival) – history, psych, cs, alvaro_horror und rente rendern deshalb schon heute mit dem psych-Branding. Das ist Altbestand (HZ-848), kein Schaden der Straße: sie schickt exakt den gemessenen Wert. Gelöst ist es damit NICHT – recipe.branding je Kanal wäre der Weg.
- Der gemeinsame Fehler-Handler war tot.
X_Error_Handlermeldeteactive: true, war
aber nie registriert (fehlende Telegram-Credential ⇒ n8n verweigert das Publizieren). Jeder „Abbruch mit Grund" verschwand damit spurlos – bei allen Workflows, nicht nur bei der Straße. Repariert und belegt (Exec 1875); Rezept in docs/kb/werkzeuge-rezepte__n8n-rest-fallen-beim-workflow-umbau.md §6.
Was die Doppelagenten-Abnahme (Constraint #12) gefunden hat – zwei Prüfer, getrennte Aufträge („Defekte" / „Vollständigkeit"), disjunkte Fundklassen; sofort behoben:
- Der Render-Lock blieb bei einem gescheiterten Anstoß liegen (bis TTL, 30 min) und
blockierte damit ALLE Kanäle. Jetzt gibt ein eigener Node ihn frei, bevor die Meldung rausgeht.
- Die Warteschleife hatte kein Ende: bei totem
render:8080kam nie einacquired, also
alle 60 s erneut – endlos, ohne Meldung, Execution ewig auf running. Jetzt Abbruch mit Grund; seit HZ-683 (06.08.2026) mit zwei gerechneten Grenzen statt fester 90 Versuche: bei belegter Sperre Sperrfrist + 10 Min Marge (render/lock_ttl.py warte_grenzen, aktuell 460 Versuche), bei totem Container 15.
- Der Lock lief auf 30 min TTL, ein Render darf 3 h dauern → nach 30 min hätte der nächste
Lauf ihn per Takeover geholt und parallel gerendert (genau das, was Constraint #5 verbietet). Jetzt wird ttl_seconds mitgeschickt.
- Pfadausbruch über die runId (
../) – sie landet ungefiltert in Filestore-URL und
Dateinamen. Jetzt Formprüfung; belegt mit Exec 1893.
- Telegram-HTML: interpolierte Store-Werte werden escaped (ein
&hätte die Meldung
im Fehlerfall still verschluckt), .first() statt .item in der Kapitel-Schleife, --vergleich liefert bei Abweichung Exit 1, Registry/HQ-Konfig werden atomar geschrieben, und der Rückweg merkt sich die volle Fork-URL, nicht nur den Pfad.
- Nicht hier gelöst, weil schon als Ticket unterwegs: der Webhook ist unauthentifiziert
(HZ-567 – mit der Straße wächst die Reichweite eines Fremd-POSTs von einem Kanal auf neun, erledigt 04.08.2026, siehe §4g), die Straßen-Workflows stehen in keiner Registry, weshalb Massen-Patcher sie nicht sehen (HZ-566), und fork_channel.py baut neue Kanäle weiter am System vorbei (HZ-565).
Offen (bewusst nicht in diesem Schritt): der erste echte Render über die Straße. Er ist nicht $0-neutral abschließbar: ein gelungener Render ruft P4, und P4 kauft ein Thumbnail. Deshalb Canary doomed als eigener, bewusst gestarteter Schritt (HZ-570) – Werkzeug und Rückweg stehen bereit.
4e · Stufe 4 (P4): was am 30.07. gebaut wurde – HZ-405
Stand: EIN Workflow Road P4 (alle Kanäle) (KdrF6f1rv2VgYHg6, 48 Nodes, aktiv, Pfad POST /webhook/road-p4?channel=<kanal>) kann alle 9 Kanäle weiterführen (Fork-Bestand vom 30.07.; am 13.08.2026 führen 11 der 14 Register-Kanäle ein P4, gemessen mit road_switch.py); der Bauplan ist für 9/9 vollständig (8 scharf, auswandern_rente_de steht per Kill-Switch auf enabled.p4:false). Umgeschaltet ist seit 03.08.2026 alvaro_horror (HZ-754) – der eine Kanal, der ausgemeistert wird; die übrigen acht Renderer rufen weiter ihren Fork-P4, der Rückweg ist unangetastet. Der Eingang ist damit zum ersten Mal wirklich befahren: ein echter Aufruf gegen den vorhandenen Lauf 2026-08-03-hq-173603001 lief durch die ganze Stufe (Bauplan → Store → Claude-Regie → 3 Varianten → 3 Bilder → Filestore → Ready for Manual Upload), im Trockenlauf-Mock für $0 und ohne Upload. Vorher hatte road-p4 null Aufrufer, obwohl road.enabled.p4 bei 8 Kanälen true sagte (Stand 03.08.2026 – der Wert war damals handgepflegt) – diese Lücke meldet road_check.py jetzt als Befund UNBELEGT.
07.08.2026 (HZ-712) – der Rückweg im Bild-Zweig war kaputt, gefunden vom ersten Takt-Trockenlauf. Der nächtliche Trockenlauf (n8n-Execution 2670, Lauf 2026-08-07-hq-000103001) belegte zuerst das Erwünschte: road-p4?channel=alvaro_horror → Bauplan HTTP 200 → Bild-Prompt beginnt mit You are generating a thumbnail image for alvaro_horror (2487 Zeichen, alvaros eigene Regie), 3 Varianten. Dann scheiterte eine Variante beim Mock, und der Zweitversuch Bild bestellen (2) bestellte ohne Prompt – er las $json.prompt, hing aber an 2. Versuch erlaubt?, wo $json die Status-Antwort des Bild-Diensts ist. ApiMart antwortet darauf mit 400 '\prompt\ parameter is required', und die ganze Stufe starb – samt der zwei Thumbnails, die schon im Filestore lagen. Fix in wf_fork/build_p4_road.py (bild_bestellen(..., prompt_ausdruck), Zweitversuch zeigt jetzt auf $('Bild-Prompt bauen').first().json.prompt), live per --update (Webhook nach Constraint 7 neu registriert). Gegenprobe: Execution 2773 success, ganze Stufe bis Store - bereit, 3/3 Thumbnails, $0 im Mock. Nicht durchlaufen ist dabei der reparierte Retry-Zweig selbst (alle drei Varianten gelangen) – er ist statisch am lebenden Node belegt, nicht im Lauf: der Mock (agent/mock_gateway.py) antwortet immer completed, der Fehlschlag von 2670 ist damit nicht willentlich wiederholbar. .first() statt .item (Abnahme-Befund 07.08.): Bild fertig? baut sein Item ohne pairedItem, .item waere nicht sicher aufloesbar; Bild-Prompt bauen liefert je Schleifenrunde genau ein Item (Execution 2773: 3 Runs a 1 Item), also ist .first() des letzten Laufs die aktuelle Variante. Zweite Gegenprobe nach der Nachschaerfung: Execution 2777 success, 3/3.
Nebenbefund gleicher Lauf: der Trockenlauf ließ die geteilten Straßen-Workflows im Mock stehen (patch_testmode … off kam nach dem Abbruch nicht mehr) – P2/P4-Straße standen von 02:09 bis 07.08. abends auf mock-gateway, Upload gesperrt, für alle Kanäle. Kein Geldschaden, aber ein bezahlter Lauf hätte in dieser Zeit Attrappen-Bilder bekommen. Von Hand zurückgestellt; ein Wächter dafür fehlt.
Korrektur zur Reihenfolge-Tabelle in §4: dort steht „p4 12/12". Das war der Stand vom 27.07., als die Registry noch 15 Kanäle führte. Am 13.08.2026 hat sie 14 Einträge, davon 11 mit P4 (shorts/whatif/ztest_walk haben keinen) – die Stufe ist also 11/11, nicht 12/12. Der Nenner wandert mit dem Register: nachrechnen, nicht abschreiben.
| Stück | Datei | Wirkung | |
|---|---|---|---|
| Die Straße | wf_fork/build_p4_road.py | Webhook → Render-Guard → Bauplan → Store-Lauf → Thumbnail-Schleife → Store „bereit" → Upload (nur per API-Weg) | |
| Umschalten/Rückweg | wf_fork/patch_p4_trigger.py | `--channel X --road\ | --fork --apply, --status` zeigt, wen der Renderer heute ruft |
| Messen + Einfrieren | channel_clone/freeze_p4_thumb.py | Thumbnail-Schule je Kanal → recipes/<x>.json (prompts.p4.regie, prompts.p4.bild_basis+stil, thumb_*) + registry.road.p4_* | |
| Prüfen | road_check.py --stage p4 (14 Felder) | Migrationsstand gegen den LEBENDEN Workflow, inkl. Bildmenge und Prompt-Längen | |
| Pflicht-Gate | channel_clone/upload_target_check.py | prüft jetzt wirklich – inkl. Straßen-Workflows und Kanälen ohne Fork (HZ-415) | |
| HQ | Fabrik → Straße: p4 in der Spalte UMGESCHALTET, je Kanal ein Chip N× BILD | die Bildmenge ist der Preis der Stufe und steht jetzt sichtbar in der Zeile |
Die 15 Nicht-Kern-Nodes sind gemessen drei Schalter, keine Struktur-Vielfalt (30.07., 9 Fork-P4, 21 gemeinsame Nodes): Varianten-Schleife (3 Kanäle: psych, rente, alvaro → 3 Bilder für den A/B/C-Test) · zweiter Bild-Versuch (6 Kanäle) · Render-Guard (8 Kanäle). Dazu je Kanal zwei Prompt-Texte. Alles davon steht jetzt im Rezept: thumb_varianten · thumb_retry · thumb_paket · thumb_modell · thumb_size · thumb_wait · thumb_regie_tokens/temp · prompts.p4.regie · prompts.p4.bild_basis + prompts.p4.stil (seit HZ-349 die umschaltbare Textführung, siehe channel_clone/thumb_stil.py), und in der Registry die vier gemessenen Verdrahtungs-Werte p4_status_ready · p4_video_slug · p4_privacy · p4_upload_thumbtext.
Ehrlich zum „eBook": in §4 steht „Nicht-Kern-Nodes werden Rezept-Schalter (Thumbnail-Schule, eBook)". In P4 gibt es keinen eBook-Node – nachgemessen an allen neun Forks. Die eBook-Einblendungen sind eine P3/Renderer-Sache (recipe.book), der einzige „Paket"-Node in P4 baut den Upload-Zettel (Video- + Thumbnail-Links + gepinnter Kommentar). Der Schalter heißt deshalb thumb_paket, nicht ebook.
Belege (alle $0, kein Bild gekauft, kein Upload):
--probe: 8 Kanäle fahrbar mit ihren eigenen Werten (1× bzw. 3× Bild, mit/ohne zweiten
Versuch, 30/90 s Poll), rente 423 (Kill-Switch).
--vergleich <kanal>: 14 Felder Fork gegen Straße, inkl. Bildmenge, Modell, Store-Status,
privacy, Upload-Ziel und Prompt-Längen.
- Guard-Fälle über den ECHTEN Webhook (n8n-Executions 1895–1905, aus der SQLite gelesen, weil
Webhook-Executions nicht in der Public-API stehen): unbekannter Kanal → „Bauplan 404" · whatif → „409 schalter_fehlt [road.enabled.p4]" · fehlende runId → Abbruch im Eingang · fehlender Kanal → Abbruch · success:false → Lock frei + Telegram · success fehlt → gilt als NICHT gelungen · unbekannte runId → Telegram „Zeile fehlt". In keiner der sieben Executions wurde ein Geld-Node betreten (nachgeprüft: Claude, Bild bestellen, Bild ablegen, Upload anstoßen kommen in keiner Execution vor). (Stand 31.07.; seit 02.08.2026 enden die Telegram-Fehlerzweige in einem Stop and Error, die Executions stehen dann auf error statt success.)
road_check.py --stage p4→ 119 Werte gleich, 0 Drift, 0 Lücke, 4 ENTSCHEID. Die vier
sind privacy bei cs/geo/doomed/survival: die Registry steht seit Leons Entscheid vom 30.07. auf public (_p4_privacy_entscheid), die vier Forks noch auf unlisted. Das ist keine Drift, sondern der gewollte Unterschied – die Straße tut das Neue, der Fork ist die veraltete Seite. Damit niemand ihn „aufräumt" (also den Entscheid rückwärts stellt), führt road_check seit 30.07. den dritten Befund ENTSCHEID: er zählt getrennt, setzt den Exit-Code NICHT und nennt den Registry-Schlüssel als Beleg. Bedingung dafür ist, dass der Bauplan-Wert wirklich der entschiedene Registry-Wert ist – eine Ausnahmeliste ohne Beleg wäre nur ein blinder Fleck (Rezept: KB §10).
upload_target_check.py→ Gesamt gelb, Exit 0, keine ROT-Zeile. Gelb ist hier das Pflicht-Gate erfüllt, nicht ein halber Erfolg: grün gibt es je Zeile nur, wenn der Node
scharf ist UND Credentials da sind UND der Container sie durchreicht – heute laden 8 von 9 Kanälen bewusst über den Browser (HZ-552), ihre Upload-Nodes sind korrekt gesperrt und können gar nichts falsch hochladen. Das Gate prüft auf ROT (Node kann feuern, Ziel unklar/falsch); eine Zeile davon gibt es nicht, deshalb Exit 0.
bauplan.PFLICHT["p4"]nachgezogen (war HZ-575): die fünf Felder, die vorher nur der
Guard IM Workflow verlangte (thumb_modell · thumb_size · thumb_wait · thumb_regie_tokens · thumb_regie_temp), stehen jetzt auch im Bauplan – vorher meldete er ok und die Lücke fiel erst zur Laufzeit auf, bei einer Geldstelle der falsche Zeitpunkt. Beide Listen sind jetzt deckungsgleich (12 Felder), der Kommentar im Bauer sagt das auch. Belegt ohne Lauf und ohne Geld: CHANNEL_CLONE_DIR auf eine Kopie von channel_clone/, dort thumb_wait aus recipes/geo.json gelöscht → 409 bauplan_luecke ['thumb_wait']. Mit allen echten Rezepten bleibt die Stufe 9/9 fahrbar (--probe, und das HQ zeigt für alle neun p4-Zeilen ok, fehlt: []).
- Nach dem erneuten Ausrollen (
--update, 48 Nodes, aktiv, Webhook neu registriert) laufen alle
sieben Guard-Fälle wieder durch (Executions 1943–1953): kein Bild gekauft, kein Upload.
Was der Umbau zutage gefördert hat:
- history hatte keinen Render-Guard – seit 04.08.2026 hat er einen. Sein Webhook ging
direkt auf „Set Run-ID": ein gescheiterter Render (success:false) hat dort trotzdem ein Thumbnail GEKAUFT und die Folge als fertig gemeldet – und history ist der einzige Kanal, der wirklich hochlädt. Die anderen acht hatten den Guard. Nachgezogen im lebenden Fork (wf_fork/einmal/patch_p4_render_guard.py, 4 Nodes, Rückweg in archive/p4_render_guard_2026-08-04T123636/), nicht erst beim Umschalten auf die Straße: Webhook → Render OK? → ja: Set Run-ID · nein: Lock freigeben (Fehler) → Telegram → Stop and Error. Belegt ($0, kein Bild): POST success:false auf p4-trigger → Execution 2254 error, gelaufen sind ausschließlich die vier Guard-Nodes, Set Run-ID und ApiMart - Submit Thumbnail nicht. Der Fehlerzweig gibt zusätzlich den Render-Lock frei (Punkt 2) – das haben die anderen acht Forks weiterhin nicht; sie sind geparkt, und auf der Straße gilt es ohnehin für alle.
- Der Render-Lock blieb nach einem gescheiterten Render liegen. „Lock Release" hängt in
allen Forks hinter „Set Run-ID", also nur im Erfolgsfall; der GLOBALE Lock blockierte danach bis zum TTL jeden anderen Kanal. Die Straße gibt ihn auf beiden Wegen frei.
- Der Upload-Node ist in 8 von 9 Forks „disabled" – und ein deaktivierter Node ist Passthrough (Constraint #7): im Lauf sieht er wie ein Erfolg aus. Die Straße hat für
Browser-Kanäle gar keinen Upload-Pfad, sondern entscheidet am Bauplan (upload_mode, youtube_param). Ein falsches Häkchen kann dort nichts mehr hochladen. Sichtbare Folge: cs lädt heute im Fork scharf auf das HISTORY-Konto, steht in der Registry aber auf upload_mode: browser (HZ-552). Auf der Straße würde cs nicht per API laden. Das ist gewollt (Registry ist die Quelle), aber es ist eine echte Verhaltensänderung und steht deshalb hier, statt beim Umschalten zu überraschen.
GitHub - Get Release Assetist toter Code. Alle neun Forks fragen eine Release
render-<runId> ab, die es seit dem Umzug auf den Filestore nicht mehr gibt; die Ausgabe liest kein folgender Node. Die Straße hat ihn nicht.
- Ein fehlgeschlagener Claude-Aufruf kaufte trotzdem ein Bild. Die drei Varianten-Kanäle
haben im Split-Node einen Default-Hook (IS THIS YOU? samt Psych-Szene) – bei einer unbrauchbaren Antwort wurde also für JEDEN Kanal ein bezahltes Bild mit psychs Satz erzeugt. Die Straße bricht stattdessen ab.
- Zwei Store-Status, kein ableitbarer: history/cs schreiben „Pending Review", die übrigen
„Ready for Manual Upload". Und privacy ist geteilt (5× public, 4× unlisted). Wer hier rechnet statt zu messen, macht eine Folge sofort öffentlich oder hält sie ewig zurück.
- Der HQ-Video-Ordner ist nicht der Kanalname: geo lädt aus
emptyearth-video, rente und
alvaro aus psychology-video (dort landet ihr Render wirklich – sie haben kein eigenes hq_out, Altbestand HZ-848), history hat gar keinen. Auf der Platte existieren genau fünf Video-Ordner; ein abgeleiteter Name wäre ein 404 im Download-Link.
- P4 war im Bauplan falsch blockiert – dieselbe Klasse wie bei P5 (HZ-403): die Stufe
verlangte für upload_mode: browser die Felder yt_id/adspower_profile, die in keinem P4-Node vorkommen (der Browser-Upload läuft später über scaling/yt_upload.py). 6 von 9 Kanälen meldeten deshalb 409 [yt_id, adspower_profile]. Feld je Stufe getrennt (UPLOAD_PFLICHT_STUFE) → 9/9 fahrbar.
- Der Upload-Zettel (HZ-48) wird im Fork gebaut und weggeworfen. „Build Upload Paket"
erzeugt einen Notion-Callout, und die einzige Verbindung des Nodes führt zu Telegram – der Callout wird nie geschrieben, und das Video-DB-Gateway hat für Blöcke auch keine Operation. Die Straße legt die Links stattdessen in die Telegram-Meldung (dort liest Leon sie) und verschweigt nicht, dass der Notion-Weg fehlt → HZ-572.
Was die Doppelagenten-Abnahme (Constraint #12) gefunden hat – zwei Prüfer, getrennte Aufträge („Defekte" / „Vollständigkeit"), disjunkte Fundklassen. Sofort behoben:
- „Treffer merken" setzte
ok:trueungeprüft. „Bild laden" und „Bild ablegen" laufen mit
onError=continue; ein gescheiterter Filestore-PUT hätte eine URL gemeldet, unter der nichts liegt – Store und Upload hätten ein bezahltes Thumbnail verbucht, das es nicht gibt. Jetzt wird der PUT-Status gelesen (fullResponse), sonst gilt die Variante als Fehlversuch.
- Hook und Bild konnten auseinanderlaufen: der Upload nahm den Text von Variante 1, das
Bild aber die erste ERFOLGREICHE Variante. Beides kommt jetzt aus derselben Bilanz.
- Eine gescheiterte Bestellung hätte 40 Abfragen ins Leere gepollt (20 Minuten Stille),
weil task_id undefined war. Jetzt Abbruch mit Grund bei der ersten Abfrage.
- Stille Defaults entfernt:
thumb_wait || 30,tokens || 500,temp ?? 0.8– die drei
sind jetzt Pflichtfelder des Straßen-Guards. Ebenso privacy: wer per API lädt und keine Sichtbarkeit im Bauplan hat, bricht ab, statt sie dem Render-Container zu überlassen.
freeze_p4_thumb.pyriet die Variantenzahl (3 if split else 1), wennslice(0, N)
nicht traf – ein geratenes 3 hätte die Bildkosten eines Kanals dauerhaft verdreifacht. Jetzt: melden und nicht schreiben.
- Die Straßen-Workflows stehen jetzt in der Registry (
road_workflows: id · name · bauer ·
pfad je Stufe, live gemessen) – der erste Teil von HZ-566. upload_target_check.py liest sie von dort. Nachgeholt am 31.07. (HZ-566, zweiter Teil): die Verbraucher lesen die IDs jetzt über channel_clone/road_ids.py – patch_filestore (Straße als eigenes Ziel), patch_testmode (Forks UND Straße, mit Halter-Register für die geteilte Straße; vorher hat ein „Trockenlauf" eines umgeschalteten Kanals über Road P2/P4 echt bezahlt und echt hochgeladen), patch_harvest_sources/patch_p3_recipe_key (sagen, dass der Wert dort parametrisch ist), road_check (zweite Tabelle für den fahrenden Workflow), wiring_tree (kein falsches kette_gebrochen mehr) und registry_sync_active (warnt, wenn eine Straße mit fahrenden Kanälen inaktiv ist). Lehre: docs/kb/werkzeuge-rezepte__werkzeug-strassenfest-machen.md.
- Nachgeholt am 30.07. (war HZ-575, weil
dashboard/bauplan.pybeim Bau von einem anderen
laufenden Chat belegt war): die fünf Felder stehen jetzt in bauplan.PFLICHT["p4"], die Lücke wird also als 409 bauplan_luecke gemeldet statt erst im Workflow aufzufallen. Beleg, Negativtest und die Lehre daraus stehen oben bei den Belegen bzw. in KB §8.
Offen (bewusst nicht in diesem Schritt): der erste echte Lauf über die P4-Straße. Er ist nicht $0-neutral: die Stufe kauft das Thumbnail (1× bzw. 3× gpt-image-2). Zwei Dinge sind deshalb noch unbelegt und gehören in den Canary-Schritt (HZ-573, zusammen mit dem P3-Canary HZ-570): dass n8n die Ausdrücke in Wait-amount und in den Claude-Optionen (maxTokens / temperature aus dem Rezept) zur Laufzeit auflöst, und dass die Bild-Kette bis zum abgelegten Thumbnail durchläuft. Werkzeug und Rückweg stehen bereit.
4e · Stufe 6 (P1): die Vorbedingung wortziel ist geklärt – HZ-407, 30.07.
Stand (30.07.): Der Bauplan löst p1 für 9 von 9 Kanälen mit lebendem P1-Workflow auf (vorher 4). Umgeschaltet ist nichts – P1 bleibt bewusst die letzte Stufe (§4), und ein Straßen-P1 existiert noch nicht. Geklärt ist das eine Feld, an dem die Stufe hing.
Der Befund ist die eigentliche Nachricht: die Wortziele haben nie gefehlt, sie wurden nur nicht gelesen. road_check.x_wortziel kannte genau eine Schreibweise (TARGET 1400 WORDS), im Haus gibt es drei – die Spannen-Form (~8-minute narration (~1150-1300 words)) und dieselbe Spanne in Landessprache (2200-2700 Wörter, ~6000-8000 palabras). Sechs Kanäle meldeten deshalb „Fork hat keinen Wert", obwohl ihr Prompt eine Länge vorschreibt. Rezept dazu: docs/kb/werkzeuge-rezepte__wortziel-aus-dem-p1-prompt-messen.md.
| Kanal | wortziel | Herkunft |
|---|---|---|
| history | 2650 | dna – est_length_min 18,1 × words_per_min 145,4 (Fork sagt 1400 → bekannter Drift, Leon-Entscheid offen) |
| geo | 1750 | dna – 10,3 × 170 (Fork sagt 1400 → sichtbar seit dieser Messung) |
| psych · cs | 1500 · 1400 | pinned seit 27.07., TARGET-Form, deckungsgleich mit dem Fork |
| wisdom · doomed · survival | 2550 · 1250 · 2000 | neu pinned 30.07., Spannen-Form aus dem lebenden Prompt |
| auswandern_rente_de · alvaro_horror | 2450 · 7000 | neu pinned 30.07., Spanne auf Deutsch bzw. Spanisch |
Preis der Ehrlichkeit: die DNA-Quote fällt von 14 % auf 13 % (road_check.py --schuld). Fünf neu eingefrorene Werte sind fünf sichtbare Schulden – die DNA dieser Kanäle gibt weder est_length_min noch words_per_min als Zahl her (Prosa: „unverifiziert", „~150-160 (abgeleitet…)"). Tilgen kann das nur die DNA-Reifung (HZ-538, geparkt), nicht dieser Schritt.
Die worldcup-Frage aus dem Ticket, beantwortet: worldcup existiert nicht mehr – weder in registry.json noch als n8n-Workflow; übrig ist recipes/worldcup.json ohne wortziel. Seine Prompt-Klasse bleibt gültig und ist die Falle der Stufe: rechnet ein Prompt pro Einheit (~120-180 words per match), gibt es kein messbares Gesamtziel. Dann liefert die Messung bewusst nichts – belegt am gesicherten worldcup-P1 vom 27.07.: drei Zahlenbereiche gefunden, alle drei mit benanntem Grund verworfen. Ein Kanal dieser Klasse muss seine Länge explizit ins Rezept schreiben (Einheiten × Wörter, ausgerechnet); sonst bricht der Bauplan ab, statt zu raten. Das ist die gewollte Antwort, kein Mangel.
| Stück | Datei | Wirkung |
|---|---|---|
| Messen + einfrieren | channel_clone/wortziel_messen.py | liest alle drei Schreibweisen aus dem lebenden P1, --apply schreibt mit voller Herkunft ins Rezept (Backup, atomar, 0644) |
| Prüfen | dashboard/bauplan.py → /hq/api/bauplan?channel=X&stage=p1 | 9/9 lösen auf; auswandern_rente_de antwortet 423 (Kill-Switch, kein Datenmangel) |
Was „9/9" genau heißt – und was nicht: gezählt sind die Kanäle, für die registry.json einen P1 führt. In n8n laufen zwei weitere aktive Skript-Workflows, die dort nicht eingetragen sind: Vq1mdkKiWlx7YaxD (WhatIf-1) und N1fCAQWlOi17zbV8 (Shorts S1). Für whatif/shorts/ztest_walk antwortet der Bauplan p1 weiter mit 409 – dort fehlt nicht nur wortziel, sondern auch r2_prefix, lang und p1_prompts. Das sind Stub-Kanäle, kein Versehen dieser Messung; die Straße fasst sie erst an, wenn sie ein Rezept haben.
Zwei weitere Kanäle sind in der Ist-Aufnahme blind, nicht im Bauplan: auswandern_rente_de und alvaro_horror haben in der Registry kein p1_script_node, weshalb recipe_from_live.py (Cron, alle 30 min) ihr Wortziel und ihre Sprache gar nicht erst liest – im HQ-Builder stehen sie deshalb leer, obwohl das Rezept 2450 bzw. 7000 kennt (Ticket HZ-581).
Erledigt 30.07. (HZ-580): road_check.py führte für dasselbe Feld einen zweiten, schwächeren Regex und kannte nur die TARGET-Form – für die sechs Spannen-Kanäle meldete --stage p1 deshalb dauerhaft „nur Plan", eine Lücke, die es nicht gab. x_wortziel ruft jetzt wortziel_messen.messe() auf (faul importiert, weil wortziel_messen umgekehrt road_check braucht); es gibt im Lesepfad nur noch EINEN Messer. Ergebnis: fünf der sechs Kanäle stimmen überein, und geo wurde als echte Abweichung sichtbar – sein lebender P1 schreibt ~1300-1500 words (gemessen 1400), die DNA rechnet 1750 (Ticket HZ-598). Diese Drift war vorher unsichtbar, weil der blinde Messer sie als „nur Plan" wegkürzte. Der history-Drift (1400 im Fork vs. 2650 aus der DNA) bleibt der zweite bekannte Fall.
4f · Der Gesamtstand: EINE gemessene Tafel statt sechs Einzelmeinungen – HZ-540, 30.07.
Bis hierher hatte jede Stufe ihr eigenes Werkzeug (wf_fork/patch_p3_trigger.py, patch_p4_trigger.py, patch_p5_callback.py), und jedes kannte genau seine eine Stufe. Zwei Dinge fehlten deshalb, und beide sind der Grund für channel_clone/road_switch.py:
1 · Niemand konnte den Gesamtstand nennen. Die Frage „wie viele (Kanal × Stufe) fahren heute wirklich die Straße?" – die einzige richtige Erfolgszahl dieser Migration – war nirgends beantwortbar. road_check.py --schuld misst etwas anderes (DNA gegen eingefroren) und rührt sich beim Umschalten überhaupt nicht; wer sie als Fortschrittszahl las, maß den falschen Vorgang. Die Tafel beantwortet sie in einem Befehl:
python3 channel_clone/road_switch.py # Kanal × Stufe, live gemessen, $0
Gemessen am 30.07.2026: 17 von 36 messbaren Paaren = 47 %. Das sind p3 (8/9) und p5 (9/9). Die p5-Zahl zählt verdrahtet, nicht fahrbar: seit HZ-564 (04.08.2026) ist p5 nur für die beiden api-Kanäle zuständig, die 8 Browser-Kanäle bekommen 409 stufe_nicht_zustaendig. 18 der 54 Paare sind nicht messbar und stehen bewusst auf – statt auf „Fork":
| Stufe | warum nicht messbar |
|---|---|
p1 | wird nicht von n8n gerufen, sondern vom HQ-Studio-Knopf bzw. einem Cron – es gibt keine URL im System, die man tauschen könnte |
publisher | hängt an einem eigenen Cron, nicht an einem Aufrufer |
Ein nicht messbares Paar als „Fork" zu zählen würde die Quote drücken und den Rest der Arbeit größer aussehen lassen, als er ist. Deshalb ist der Nenner die Zahl der messbaren Paare.
2 · Der Stand war nur die Absicht, nicht die Wirklichkeit. Die drei Werkzeuge pflegen registry.road.<feld> mit; gelesen wurde bisher aber genau dieses gepflegte Feld. Die Tafel liest stattdessen den lebenden n8n-Node und vergleicht ihn gegen die Registry – laufen beide auseinander, meldet sie drift statt sich für eine Seite zu entscheiden. Genau dieses stille Auseinanderlaufen ist der Fehler, gegen den die ganze Straße gebaut wird.
p2 war nie „nicht messbar", nur nie gemessen
Bei der Bestandsaufnahme fiel auf, dass p2 als unmessbar galt („kein freeze-Werkzeug für p2"). Das stimmte nicht: der Aufrufer ist der Node Trigger P2_Assets im P1, bei allen neun Kanälen gleich benannt – aber mit neun uneinheitlichen Pfaden:
p2-stock-trigger · alvaro-p2-trigger · rente-p2-trigger · cs-p2-trigger p2-doomed-trigger · p2-geo-trigger · psych-p2-trigger · p2-survival-trigger wisdom-p2-trigger
Dieselbe HZ-431-Klasse wie bei p3 und p5: nicht herleitbar, nur messbar. Seitdem steht p2 in der Tafel (heute 9/9 Fork) und hat damit denselben Umschalt-Handgriff wie p3/p4/p5.
Die Stimme ist seit 02.08.2026 kein Geschmack mehr, sondern gemessen (HZ-700). Hier stand bis dahin, die offenen p2-Drifts seien „zwei davon Stimmen, also Geschmack" und damit Leon-Entscheide. Das ist überholt: road_check.py prüft seit 02.08. alle drei Stimm-Felder (voice_id, voice_stability, voice_similarity_boost) als Stufe-p2-Felder – und seit 03.08. zusätzlich quellen_mix –, weil der Vortrag der Stimme eine Linse der Klon-Messung ist – bei alvaro_horror las der Fork monatelang 0.55/0.75 aus dem Psychology-Fork, während das Rezept 0.7/0.85 vorgab (Preset „ruhig", Quelle DNA audio_style), und niemandem fiel es auf. Ein Wert, der die Klon-Zahl bewegt, ist eine Messung, kein Entscheid.
Gemessen 05.08.2026 (python3 channel_clone/road_check.py --stage p2): auf der Straße kommen 10 von 10 Feldern parametrisch aus dem Bauplan, 0 DRIFT, 0 blinde Stelle – die Stimm-Felder eingeschlossen. Die verbleibenden 10 DRIFT stehen in der Fork-Tabelle, also in den alten Kanal-Kopien, nicht auf der Straße; drei davon sind die Stimm-Felder von history. Sie verschwinden mit dem Umschalten des jeweiligen Kanals, nicht mit einem Leon-Entscheid. Freigegeben ist das Umschalten von p2 damit trotzdem nicht: P2 ist die Geldstelle, und ein Umschalten kostet einen echten Lauf.
Der Handgriff – hin und zurück derselbe Weg
python3 channel_clone/road_switch.py --channel <k> --stage <p2|p3|p4|p5> --to road python3 channel_clone/road_switch.py --channel <k> --stage <p2|p3|p4|p5> --to road --apply python3 channel_clone/road_switch.py --channel <k> --stage <p2|p3|p4|p5> --to fork --apply
Angefasst wird genau eine URL-Zeile im Aufrufer-Workflow; kein Fork wird je gelöscht. Enthalten sind dieselben Sicherungen wie in den Stufen-Werkzeugen – JSON-Backup vor dem PUT (nach channel_clone/backups_road_switch/, weil nur dieser Ordner in beide Welten gemountet ist), Settings-Sieb gegen den 400er der Public-API, Constraint #7 (deactivate + activate nach jedem PUT auf einen aktiven Workflow) – plus zwei, die vorher fehlten:
- Gegenprobe per GET nach dem PUT. Die Erfolgsmeldung eines PUT ist kein Beweis; steht
nach dem Schreiben etwas anderes im Node, bricht das Werkzeug mit dem Backup-Pfad ab.
- Rückweg-Warnung.
doomedundsurvivalhaben nie einen eigenen P5 gehabt; ihr
Fork-Pfad yt-done-trigger ist HISTORYs P5. Zurückzuschalten baut den stillen Totlauf wieder ein, den HZ-403 gefunden hat – das Werkzeug sagt es, blockiert aber nicht (ein Rollback muss immer möglich bleiben).
Der Rückweg ist erprobt, nicht erhofft. Am 30.07. hin und zurück gefahren an auswandern_rente_de/p3 – dem einzigen Kanal, der komplett stillsteht (gemessen 30.07.2026: road.enabled überall false, P1–P4 aus), also am Kanarienvogel ohne Blast-Radius. Beide Richtungen mit bestandener Gegenprobe; Endzustand wieder Fork, road_check.py vorher wie nachher 261 gleich · 4 Drift · 0 Lücke.
Warum das Modul in channel_clone/ liegt und nicht in wf_fork/
Die drei Stufen-Werkzeuge laufen nur am Host: sie holen die n8n-IP über docker inspect und lesen /opt/herz/.env – und wf_fork/ ist im dashboard-Container nicht einmal gemountet. channel_clone/ ist es (als /data/channel_clone, rw), und dort liegt auch die Registry. road_switch.py spricht n8n deshalb über den stabilen Hostnamen n8n:5678, wenn der auflösbar ist, und fällt sonst auf docker inspect zurück. Damit ist es dasselbe Modul am Host und im Container – die Voraussetzung dafür, dass das HQ es je importieren kann.
Was die Doppelagenten-Abnahme gefunden hat (zwei Runden, alle acht umgesetzt)
Drei Befunde sind es wert, hier zu stehen, weil sie Fallen der Migration selbst sind und nicht Marotten dieses Moduls:
road.p4_callbackist eine volle URL,p3_triggerundp5_callbacksind Pfade. Wer
die drei Felder generisch gleich behandelt, schreibt bei p4 einen Pfad – und der Render-Container ruft ihn unverändert, ohne dass irgendwo ein Host davorgesetzt wird. Die Callback-Adresse wäre tot. Die Form gehört deshalb explizit zur Stufe (STUFEN[…]["form"]), genau wie wf_fork/patch_p4_trigger.py:195 es tut.
- Der HQ-Knopf hat eine EIGENE Kopie des p3-Pfads (
hq.config.json → p3_hooks, HZ-311).
Zieht sie beim Umschalten nicht mit, zeigt Leons „Fortsetzen" auf einen toten Fork-Webhook – ohne Fehlermeldung. Mit der cs/history-Falle: beide teilen den Store history, also nur überschreiben, wenn der heutige Wert wirklich zu diesem Kanal gehört.
- Zwischen
deactivateundactivateliegt ein Fenster, in dem die Stufe AUS ist. Stirbt
der Lauf dort, bleibt eine Produktionsstufe still liegen – schlimmer als der Grund, aus dem man da war. Beide Aufrufe brauchen eine eigene, benannte Fehlerbehandlung; scheitert schon das deactivate, trägt der Node die neue Adresse, während der Webhook weiter die alte Fassung fährt (Constraint #7 in seiner unangenehmsten Form).
Dazu: alle n8n-Aufrufe werfen einen sprechenden Fehler statt einer rohen HTTPError (stirbt einer NACH dem PUT, hat n8n den neuen Stand und die Registry den alten – selbst erzeugte Drift), der ganze Schaltvorgang liegt in einem flock (parallele Chats, kein git), die Stufen-Erkennung prüft an Segmentgrenzen statt als roher Teilstring, und ein fehlender N8N_API_KEY sagt das, statt als 401 zu erscheinen.
Offen: die HQ-Straßen-Tafel (Zeilen Kanal × Stufe mit Knopf hin und zurück) braucht zwei Endpunkte in dashboard/app.py. Die Datei war am 30.07. vom Zonen-Wächter gesperrt (anderer laufender Chat) – eigenes Ticket, das Modul ist dafür fertig und container-erprobt.
4g · Straßen-Webhooks von außen dicht – 04.08.2026 (die Sache aus HZ-567)
Der Befund, gemessen statt vermutet: https://n8n.herz-automation.com/webhook/road-p2…p5 antwortete aus dem offenen Internet (GET → 404 This webhook is not registered for GET requests – also n8n selbst, nicht Caddy). Ein POST von irgendwo hätte damit eine Stufe gestartet, bei P2 eine bezahlte (KI-Bild-Fallback + TTS), und seit der Straße für jeden der 9 Kanäle über einen Aufruf statt für einen einzelnen Fork. n8n läuft bewusst nicht hinter Cloudflare Access (das 100-s-Limit bräche die Webhooks), das Tor war also die einzige Stelle, an der sich das schließen lässt.
Warum eine Wand und kein Geheimnis: alle echten Aufrufer sitzen im selben Docker-Netz. Gemessen am 04.08.: P1-Fork → http://n8n:5678/webhook/road-p2, P2 → road-p3, Render-Container → road-p4 (registry.road.p4_callback). Nur der P5-Rückruf lief bei allen 9 Kanälen über die öffentliche Adresse – ohne Grund, denn jeder Renderer erreicht n8n intern (render · renderer · render-whatif · render-paradox, je HTTP 404 auf den internen GET, also Antwort von n8n). Ein Shared-Secret hätte an ~20 Aufrufstellen gepflegt werden müssen, um eine Tür offen zu halten, die niemand braucht.
Zwei Schritte, beide $0 und mit Rückweg:
wf_fork/einmal/patch_p5_callback_intern.py– der P5-Rückruf in allen 9 Fork-P4 von
https://n8n.herz-automation.com/… auf http://n8n:5678/…; nur der Host, Pfad und Query bleiben zeichengleich. PUT + deactivate/activate (Constraint #7) + GET-Gegenprobe, Sicherung in archive/p5_callback_intern_2026-08-04T123901/. patch_p5_callback.py liest den Host aus dem gemessenen Node, übernimmt den internen Weg also von selbst weiter.
caddy/Caddyfile– zweihandle-Blöcke vor dem Sammel-handle(Reihenfolge zählt:
handle schließt einander aus und greift in Schreibreihenfolge; ein respond auf Site-Ebene wäre nach dem Sammel-handle dran gewesen und nie gelaufen): /webhook/road- und /webhook-test/road- → 403. Danach docker restart caddy (Constraint #13, der Caddyfile ist als Datei gemountet).
Belegt, nach dem Umbau gemessen: von außen road-p2…p5 je 403 („road webhooks are internal only"), auch als POST – und die letzte Execution von Road P2 blieb die von 09:40, der Fremd-POST erzeugte keine. Von innen (dashboard-Container) antwortet n8n weiter mit 404-auf-GET, der Weg ist also offen. Unverändert erreichbar: n8n-UI (200), Fork-Webhooks (p4-trigger → 404 von n8n), HQ (302).
Was die Abnahme gefunden hat – der Fehler, der die Wand tödlich gemacht hätte: die Straßen-P4 baut ihre callback_url zusammen ('https://…/webhook/' + p5_callback) und war damit der einzige verbliebene öffentliche Aufrufer; das Patch-Skript hatte nur die 9 Fork-P4 abgesucht, und eine Suche nach road-p5 trifft einen zusammengesetzten Ausdruck nie. Ab dem nächsten API-Upload wäre P5 still nie wieder gelaufen. Behoben live (KdrF6f1rv2VgYHg6, Node „Upload anstoßen“) und im Bauer build_p4_road.py. Konsequenz im Werkzeug: patch_p5_callback_intern.py nimmt jetzt registry.road_workflows mit und sweept zum Schluss alle Workflows; findet es dort noch eine öffentliche Straßen-Adresse, bricht es ab – der Sweep ist die Freigabe für die Wand, nicht die Ersetzung. Heute: 0 Straßen-Treffer, übrig 4 Fork-Adressen (3× WhatIf, 1× CS P3_Render → cs-p4-trigger), die das 403 nicht trifft.
Bewusste Nebenwirkung: /webhook-test/road-* ist mitgesperrt – der „Test workflow“-Knopf der n8n-Oberfläche postet von außen und bekommt für die Straßen-Workflows jetzt 403. Wer im UI an der Straße baut, testet über den internen Weg oder nimmt den Riegel kurz heraus.
Was damit NICHT erledigt ist (Ticket HZ-794, nicht nebenbei): 49 aktive Webhook-Pfade ohne Authentifizierung (Zähldefinition: aktiver Workflow · nicht deaktivierter Webhook-Node · authentication fehlt oder none · road- NICHT mitgezählt; gemessen 04.08.2026 zu 56 aktiven Workflows und 55 Pfaden, davon 4 road- und 2 mit headerAuth – video-db und bauplan) – alle Fork-Stufen (alvaro-p2-trigger, psych-p2-trigger, …, jede davon eine Geldstelle) und die Gateways (claude-gen, algrow-tts, pexels-search). Eine pauschale Wand ist dort falsch – aitelefon-booking, demo-visit-alert und book-reviews werden von außen gerufen; das braucht eine Entscheidung je Pfad. Zweites Stück (HZ-795) ist erledigt (07.08.2026): render/renderer/server.py und render/server.py werten den Statuscode ihres Callbacks aus – nur 2xx gilt als verbucht, sonst log.error plus Eskalation, und die läuft über zwei Dienste (tg-notify via n8n, Notweg Discord ohne n8n). Details im Bereichs-Changelog docs/bereiche/youtube.md.
5 · Sonderfälle
Kriterium: gleiche 6 Stufen + gleicher Store + gleicher Render-Vertrag + läuft → mitmigrieren. Läuft nicht → aus dem Stamm neu instanziieren statt umbauen. Anderer Vertrag → eigener Stamm.
- aviation / business / maritime – 31-Node-Stubs, p1–p3 aktiv, kein p4/p5, kein Render-Container,
andere Datenschicht (dataTable statt Store): eigener Stamm gemeinsam mit den Shorts-WFs (S1–S3, dort liegt der Fork schon zweimal), nach dem Langform-Stamm. Bis dahin unangetastet.
- stickman (komplett aus): nicht einrollen – die 52 entrollten Nodes tragen nur einen Index,
keinen Kanal. Aus dem Stamm-P2-Loop neu instanziieren, 78-Node-Fork exportieren + stilllegen.
- reddit (aus; lebendes Geschwister
alvaro): nur Node-Namen weichen ab, und Namen tragen die
$('…')-Expressions → Umbenennen ist reiner Verlust. Stilllegen, Registry/Recipe behalten.
6 · Die drei Fallen, die im Bau bewusst adressiert werden
- Der Aktiv-Stand ist Konfiguration (siehe Regel 5) – deshalb entsteht das
enabled/cadence-Gate
in Stufe 1, wo es null Risiko hat, und nicht erst bei P2.
- Drei Kanal-Schreibweisen (Key /
store_channel/channel_param) – der Store hat
UNIQUE(channel, run_id) und Assets liegen unter <channel>/<runId>: eine Verwechslung überschreibt fremde Zeilen und Assets. Das Rezept liefert alle drei explizit.
- Observability schrumpft – 74 Execution-Ströme werden 6, und Webhook-Executions stehen nicht in
der Public-API (Constraint #8). Gegenmaßnahme im selben Schritt: Kanal UND Stufe in jedem tg-notify, Store-Write beim Stufen-Eintritt statt erst am Ende. Außerdem in Loops immer .first() statt .item (sonst „Can't determine which item" / falsches Pairing).
7 · Nicht Teil dieser Migration (eigene Tickets)
Render-Container-Konsolidierung (10 Container auf 7,6 GB RAM) · Shorts-Stamm · DNA-Reifung der 11 Stub-Kanäle (extract_dna.py, $0 über claude-proxy) · derive_format.py-Ausbau (Prosa→Zahl für est_length_min/shots_per_min, beats-Formel) · Weg A (ein Builder generiert die Workflows aus Rezept + Modul-Katalog, HZ-203).
Quelle im Repo: docs/pipeline_strasse_migration.md
Vision