Autopilot
Wer nachts die Tickets abarbeitet – und was ihn daran hindert
Die Systemkarte. Was existiert, was tut es, wo liegt es. Erhoben am 31.07.2026 aus dem laufenden Code, nicht aus dem Gedächtnis. Warum etwas so gebaut ist (Entscheidungen, Fehlschläge, Lehren): design.md. Diese Datei wiederholt das nicht – sie beantwortet „was ist da".In einem Satz
Ein Prozess auf dem Host arbeitet Tickets aus TODO.md selbstständig ab: er wählt eins, lässt einen frischen claude -p-Lauf daran arbeiten, nimmt das Ergebnis von Prüf-Agenten abnehmen und entscheidet dann, ob es erledigt ist oder zu Leon geht.
Die Grundidee: das Ticket-System ist das Gedächtnis, nicht der Chat. Jeder Lauf startet mit leerem Kontext – was er wissen muss, steht im Ticket, in kapitel.json und in den Doku-Dateien.
1. Starten, anhalten, übernehmen
| Ich will … | Wie | Was passiert |
|---|---|---|
| starten | Knopf Start auf #/autopilot | schreibt einen Wunsch; der Cron-Wachposten (autopilot_watch.py --wache, jede Minute) macht daraus einen Prozess |
| nach dem laufenden Ticket anhalten | ◔ Nach diesem Ticket anhalten | Marke archive/autopilot/auslaufen; das laufende Ticket wird samt Abnahme fertig. Nochmal drücken nimmt es zurück |
| sofort stoppen | Not-Aus | archive/autopilot.stop; bricht mitten im Schritt ab und rollt zurück, wo möglich |
| selbst weitermachen | Übernehmen | hält an, gibt [wip] frei und zeigt claude --resume <session-id> – du sitzt danach im selben Chat |
Wichtig: ändert sich scripts/autopilot.py, während er läuft, beendet er sich nach dem aktuellen Ticket selbst (Python lädt sein Modul nur beim Start). Dann ist ein Start nötig.
2. Ein Durchgang, Schritt für Schritt
1 Not-Aus / Auslaufen? → sauber beenden 2 Ressourcen ok? → RAM ≥ MIN_RAM_MB, Swap ≥ MIN_SWAP_MB, sonst warten 3 Kontingent ok? → sonst Konto wechseln, sonst warten (Abschnitt 5) 4 Ticket wählen → ein Sonnet-Lauf wählt frei aus allen offenen (2–3 min) 5 Schnappschuss → tar der berührten Zone nach archive/autopilot/snap/ 6 Modus lesen → Detailzeile „Modus: auto|plan" 7 [wip] setzen → über /hq/api/todo/status 8 Lauf → claude -p, eigene Session-ID, Log als jsonl 9 Abnahme → agent_loop.py, VIER Rollen (Abschnitt 6) 10 Urteil → done · nachbessern · test · leerlauf · freigabe · abbruch 11 Bericht + Journal → ein Abschlussbericht ans Ticket, Ereignis ins Journal → nächstes Ticket
3. Welches Ticket er nimmt
Zuerst der Planer (HZ-1044, seit 09.08.2026 – er ist das, was man umgangssprachlich den „Orchestrator" nennt). Ein eigener Lauf auf claude-fable-5 liest das Zielbild und legt eine Queue an: welche Tickets als nächstes drankommen, in welcher Reihenfolge, und welche davon Stärke: schwer bekommen (das hebt sie beim Arbeiten auf Opus). Er darf dabei bis zu PLANER_NEUE_MAX = 3 neue Tickets schneiden. Liegt eine Queue vor, hat sie Vorrang vor der Wahl – der Wähler springt dann gar nicht erst an.
Er läuft ereignisgesteuert, nicht nach Uhr, und das ist Absicht:
| Größe | Wert | wofür |
|---|---|---|
PLANER_LAEUFE_MAX | 3 je 24 h | Kontingentschutz – Fable ist teuer, er soll denken, nicht takten |
PLANER_QUEUE_MIN | 2 | weniger freie Einträge → Plan gilt als „dünn" → neu planen |
PLANER_ALTER_H | 48 | so alt darf ein Plan höchstens werden, auch ohne Ereignis |
PLANER_QUEUE_MAX | 5 | so lang wird die Queue höchstens |
PLANER_TIMEOUT_S | 900 | Fable liest mehr Quellen als der Wähler |
Sein Zustand steht in archive/autopilot/plan.json (stand, queue, laeufe_24h, notiz). Eine leere Queue heißt nicht „kaputt": sie heißt entweder „abgearbeitet" oder „Tagesdeckel erreicht, wartet". Wer das verwechselt, sucht einen Fehler, wo eine Regel greift – gemessen 11.08.2026, als drei Läufe vom Vortag den Deckel noch 23,5 h lang belegten.
Dann erst die Wahl. Ein eigener claude -p-Lauf (Sonnet, nur Read Grep Glob) bekommt die Kandidaten und die Vision-Schritte als Maßstab und begründet seine Wahl. Fällt er aus, greift die feste Fabrik-Reihenfolge aus kapitel.json → weg_schritte.
Beides hängt an derselben Datei wie die Vision-Seite. Zeigt ein Schritt dort auf ein Ticket, das es nicht mehr gibt, kann ihn weder Planer noch Wähler greifen – die Reihenfolge sieht voll aus und ist leer. Gemessen 11.08.2026: 10 von 13 System-Schritten waren so unerreichbar (HZ-1117).
Ausgeschlossen ist ein Ticket, wenn (frei_fuer_laeufer()):
| Grund | Funktion |
|---|---|
steht auf [park] | – |
steht auf [test] und darf nicht nachgearbeitet werden | nacharbeit_erlaubt() |
steht auf [wip] (ein anderer Chat sitzt dran) | – |
| ein anderer Läufer arbeitet im selben Gebiet | gebiet_frei() |
| in den letzten 2 h im selben Modus schon drangewesen (slot-uebergreifend) | schon_dran() / archive/autopilot/schon_dran.json |
| gesperrt (gescheitert / wartet auf Freigabe) | gesperrt_bis() |
heute schon ANLAUF_MAX mal angefasst | zu_oft_versucht() |
| hat einen offenen Blocker | offener_blocker() |
| baut am eigenen Code des Läufers | baut_am_eigenen_code() |
| alle im Ticket genannten Dateien hält eine lebende Fremdsitzung | pfade_fremd_belegt() |
| kostet Geld ohne Freigabe | kostet_geld() + freigegebene() |
| ist ein Dach mit Subtickets | – |
| ist ein reiner Leon-Handgriff | nur_fuer_leon() |
| Bereich im HQ abgewählt | bereiche_aus() |
| „nur Vision" an und Ticket nicht auf dem Weg | nur_vision() / vision_nummern() |
Kernzahl-Vorrang (klon_stillstand() + klon_vorrang(), seit 03.08.2026): wurde seit 24 h kein neuer Gegenstand klon-gemessen, bleiben nur noch Tickets in der Auswahl, die eine Messung erzeugen oder eine gemessene Lücke schließen (klon-luecke:-Marke, Trockenlauf, Artefakt-Takt, ganze Folge). Erkannt wird das über die Aufgabenzeilen (aufgabenzeilen()), nicht über den ganzen Ticketkörper – die angehängte Historie enthält fast immer das Gegenteil („Vision-Bezug: bewegt die Klon-% nicht"). Der Filter endet von selbst mit der nächsten Messung an einem neuen Gegenstand, lässt die volle Liste stehen, wenn er sie leerräumen würde, und verengt bei unlesbarem Ledger gar nicht. Anlass: am 03.08. liefen 53 Läufe über 10,3 h, die Kernzahl stand die ganze Zeit auf dem Vortagswert.
Der Läufer legt nichts mehr zur Abnahme vor (Leon-Entscheid 03.08.2026: „alles automatisch, nur Video zählt"). Ein Lauf endet auf done (grüne Abnahme + Wirkungsbeleg), auf zurueck (Befunde offen oder kein Artefakt → Ticket bleibt in der Arbeit, gesperrt bis zum nächsten Anlauf) oder auf park (3× ohne Dateiänderung – raus aus dem Kreis statt weiterzudrehen). [test] setzt nur noch der Ermittler, wenn er misst, dass wirklich ein Mensch nötig ist: Optik, Geschmack, ein Handgriff außerhalb des Servers. Anlass: von 9 wartenden Tickets wollten 5 nur ein Nicken („sag nur, ob die Zeile reicht"), und 8 von 9 hatte der Ermittler längst auf To-do entschieden – der nächste Lauf setzte sie jedes Mal zurück auf [test].
[test]-Nacharbeit (nacharbeit_erlaubt(), gezählt aus dem Journal, verfällt nicht): 0 Enden = Leon hat es gesetzt → nur anfassen, wenn es einen Zielbild-Schritt blockiert · genau 1 = ein Nacharbeits-Durchgang · ≥ 2 = endgültig beim Menschen.
4. Was er nicht darf (Sperren im Werkzeug, nicht als Bitte)
Durchgesetzt von agent/policy_hook.py, scharf über HERZ_AUTOPILOT=1:
| Sperre | Was frei bleibt |
|---|---|
Geld – ApiMart, Higgsfield, HeyGen, YTSeller, letterXpress, --confirm-spend … (PAID_PATTERNS) | Preis- und Guthaben-Abfragen |
Grundfesten löschen – /opt/herz, /, /home, /etc | jedes rm -r innerhalb des Repos (mit automatischem tar davor) |
Global-Docker – compose down, system prune, Volumes, Netze | docker stop/rm/up -d <name> |
TODO.md als Datei (Bindmount-Inode-Falle) | alles über /hq/api/todo/* |
Selbst abhaken – /hq/api/todo/done (check_autopilot_abhaken) | status, note, add |
Fremde Arbeit – zone_guard.py verweigert (exit 2) Schreibzugriff auf Dateien, an denen ein anderer Chat sitzt | eigene Zone |
Nicht gesperrt, aber beobachtet: CLAUDE.md, zones.json, autopilot.py, policy_hook.py darf er ändern – der Läufer vergleicht Prüfsummen und meldet jede Änderung (regeldatei_geaendert), schreibt aber nie zurück.
Messgrundlagen (kapitel.json, registry.json) werden vor/nach jedem Lauf verglichen. Eine Änderung wird nicht verhindert – echter Fortschritt ändert sie zu Recht –, sondern der Abnahme als Frage vorgelegt: sachlich gedeckt oder nur Zahlenkosmetik?
5. Wann er aufhört und wann er weitermacht
Zwei verschiedene Bremsen:
Eigene Zählung (budget_stand()): summiert Ausgabe-Tokens aus dem Journal über BUDGET_FENSTER_S (5 h). Ab BUDGET_SCHWELLE (90 %) der gelernten Grenze wird pausiert. Die Grenze wird bei einem echten Anschlag gelernt (budget_lernen()) – je Konto, weil max und team verschiedene Tarife sind.
Echte Kontozahl (konto_stand()) schlägt die Eigenzählung. Sie kommt aus /hq/api/claude-konten und wird verworfen, wenn ihr session_reset hinter uns liegt – sonst hält eine veraltete Zahl den Läufer endlos in der Pause (der Fehler vom 31.07., HZ-629).
Statt zu schlafen: Konto wechseln (konto_wechseln()). Zwischen zwei Tickets, nie mitten im Lauf. Ein Konto gilt nur als frei, wenn Session und Woche unter der Schwelle liegen. Höchstens WECHSEL_PRO_SITZUNG (2) Wechsel je Sitzung. Welche Konten er nehmen darf, steuern die Chips „Womit er arbeiten darf" (konten_aus in bereiche.json, leer = alle).
Abbruch eines Laufs nur bei: Stille (STILLE_MAX_S = 40 min ohne jedes Ereignis) oder harter Obergrenze (TIMEOUT_S = 4 h). Nicht bei Wiederholung – ein Kriterium, das echte Arbeit tötet, ist schlechter als keins.
6. Abnahme und Urteil
Vier Prüfer laufen parallel (scripts/agent_loop.py, Sonnet, getrennte Aufträge):
| Rolle | Sucht |
|---|---|
defekte | Race Conditions, Fehlerpfade, nicht freigegebene Ressourcen, Sicherheit |
vollstaendigkeit | was noch am alten Zustand hängt und still bricht |
vision | ob die Arbeit das Zielbild bewegt – oder nur etwas schaltbar macht |
werte | jede Zahl und Beschriftung gegen ihre Quelle (die häufigste Fehlerklasse) |
Exitcode: 0 sauber · 1 mindestens ein Befund hält auf · 2 Prüfer gescheitert. Aufhaltend ist nur schwere=hoch mit zuversicht hoch|mittel.
Mögliche Urteile:
| Ergebnis | Bedeutung |
|---|---|
done | grüne Abnahme und Lauf-Beweis und mindestens eine geänderte Datei |
nachbessern | Befunde gefunden → derselbe Lauf bekommt sie als Auftrag zurück (eine Runde) |
test | geht zu Leon: Befunde bleiben, Prüfer gescheitert, oder kein BEWEIS |
leerlauf | nichts geändert → zurück auf To-do + Sperre (ab dem 3. Mal doch [test]) |
freigabe | an einer bezahlten Stelle gestoppt → Freigabe-Liste im HQ |
abbruch | Stille oder Zeitdeckel → Rollback, Ticket gesperrt |
kontingent | Plan-Limit → warten oder Konto wechseln |
7. Was er dir meldet
Discord (scripts/melden.py, Kanal #alerts, als Embed mit Feldern und HQ-Knopf) – nur was ohne dich nicht weitergeht:
| Anlass | Drossel |
|---|---|
| Freigabe nötig (Betrag, Dienst, Zweck) | 1 h je Ticket |
| Ticket dreht sich im Kreis | 1 h je Ticket |
| Autopilot steht still (eigener Code geändert) | 1 h |
| Zielbild hat sich bewegt – Schritt zu, Kernzahl bewegt, erster DNA-Beweis | 1 Tag |
| Stillstand – 3 gearbeitete Tage ohne jede Bewegung | 1 Tag |
Gemerkt wird eine Meldung erst, nachdem sie angekommen ist – sonst schluckt die Drossel genau die, die niemand erhalten hat.
HQ (#/autopilot): Kennzahlen-Reihe · „Wo wir im Zielbild stehen" (mit Bedeutung je Zeile) · Freigaben · „Deine Abnahme schließt einen Zielbild-Schritt" · „Wartet auf deinen Test" · Live-Log · Verlauf. Dazu die Chips woran und womit er arbeiten darf.
8. Wo was liegt
| Datei | Rolle |
|---|---|
scripts/autopilot.py | der Läufer: Schleife, Wahl, Lauf, Abnahme, Urteil, Journal, Meldungen |
scripts/autopilot_watch.py | Cron-Wachposten – macht aus dem Start-Knopf einen Prozess |
scripts/autopilot-settings.json | Einstellungen des Kind-Laufs (hängt den Policy-Hook ein) |
scripts/agent_loop.py | die vier Prüfer als Befehl |
scripts/melden.py | der eine Melde-Weg (Discord, Telegram als Rückfall) |
scripts/claude_konto.py | Konten umschalten (vom Läufer als Unterprozess gerufen) |
agent/policy_hook.py | die Sperren |
dashboard/app.py | /hq/api/autopilot/* |
dashboard/ui/85_autopilot.js | die Seite + Cockpit-Band |
Zustand in archive/autopilot/:
| Datei | Inhalt |
|---|---|
journal.jsonl | jedes Ereignis (das HQ liest daraus), mit slot und gebiet |
laeufer/<slot>.json | was dieser Läufer gerade tut, alle 30 s ein Lebenszeichen |
laeufer/<slot>.auslaufen | Marke: dieser Läufer hört nach seinem Ticket auf |
gebiete.json | welcher Slot welche Pfade hält (Riegel gegen zwei Läufer im selben Gebiet) |
bereiche.json | abgewählte Bereiche · nur_vision · abgewählte Konten · laeufer_soll |
budget.json | gelernte Kontingent-Grenze (global + je Konto), Bremsschwelle, zaehlung_ab |
sperren.json · anlaeufe.json | gescheiterte Tickets, Anlauf-Zähler |
freigaben.json | Geld-Freigaben (offen/erteilt/verbraucht) |
zielbild_letzt.json · zielbild_bericht.json | Vergleichsstand und abgelegter Bericht |
abnahme_letzte.json | Befunde der letzten Abnahme |
snap/ · geloescht/ · logs/ | Schnappschüsse, Lösch-Sicherungen, Lauf-Logs |
Journal-Ereignisse: lauf_start · lauf_ende · start · aus · halt · auslaufen · budget_pause · budget_gelernt · kontingent_warten · konto_gewechselt · neustart_noetig · durchgang_fehler · regeldatei_geaendert · regelbruch · fremdaenderung · messgrundlage_geaendert · doku_geschrieben · freigabe_eingeloest · zielbild_gemeldet · zielbild_stillstand · abnahme_wiederholt
9. Die Stellschrauben
Alle in scripts/autopilot.py, oben beieinander:
| Konstante | Wert | Wofür | |
|---|---|---|---|
BUDGET_SCHWELLE | 0.90 | ab hier Pause bzw. Kontowechsel | |
BUDGET_FENSTER_S | 5 h | Fenster der Eigenzählung | |
WECHSEL_PRO_SITZUNG | 2 | Deckel gegen das Konto-Karussell | |
ANLAUF_MAX / ANLAUF_FENSTER | 3 / 24 h | wie oft ein Ticket am Tag drankommt | |
SPERRE_S | 2 h | Sperre nach einem gescheiterten Anlauf | |
STILLE_MAX_S | 40 min | ohne jedes Ereignis → hängt (Stand 06.08.2026, Code: scripts/autopilot.py) | |
TIMEOUT_S | 4 h | harte Obergrenze je Lauf | |
LANGLAEUFER_S | 45 min | ab hier ein Hinweis ins Journal (kein Abbruch) | |
| _(kein Deckel mehr)_ | – | Denktiefe ist seit 05.08.2026 keine Tagesquote, sondern fest am Modus: plan → xhigh, alles andere → low (denktiefe()) | |
MIN_RAM_MB / MIN_SWAP_MB | 1200 / 300 | darunter startet er nicht (7,6-GB-Host) | |
STILLSTAND_TAGE / _LAEUFE | 3 / 3 | wann der Stillstands-Alarm feuert | |
MELDUNG_DECKEL_S | 24 h | höchstens eine Zielbild-Meldung pro Tag | |
MODELL / MODELL_SCHWER | sonnet-5 / opus-5 | Arbeiter: günstiger Standard / teure Ausnahme. Modell und Denk-Aufwand kommen seit HZ-1062 (11.08.2026) aus EINER Tabelle STAFFEL (Stufe → Modell/Effort) über staerke_fuer(t, modus, anlauf); die früheren zwei Funktionen modell_fuer()/effort_fuer() sind gelöscht. Stufen: mechanisch/normal → sonnet/low · schwer → opus/medium · eskalation (ab dem 2. Anlauf) → opus/medium · plan → opus/xhigh. Rangfolge: Eskalation → Modus: plan → Fables Queue-Feld staerke bzw. Ticketzeile Staerke: schwer → Stufe des Wahl-Laufs (stufe, mechanisch\ | normal) → Standard normal. leicht/mechanisch ist nie ein Veto, nur der Verzicht auf ein Upgrade. Geprüft von scripts/autopilot_selbsttest.py (Test staffel, 15 Prüfungen) |
STAFFEL_AUS | leer | =1 stellt den Stand vom 11.08.2026 her: Effort low außer im Plan-Modus, Modell wie zuvor. Rückfahrkarte für HZ-1062 | |
MODELL_PLANER / MODELL_WAHL | fable-5 / sonnet-5 | Fable ordnet an (was UND wer), Sonnet wählt/ermittelt |
10. Bekannte Grenzen
- Der laufende Prozess kennt nur den Code seines Starts. Jede Änderung an
autopilot.py
wirkt erst nach einem Neustart; er beendet sich dafür selbst, aber erst nach dem laufenden Ticket – in einer Pause merkt er es nicht.
- Fremde Arbeit im selben Zeitfenster geht in die Abnahme ein (
geaenderte_dateien()misst
über die Uhrzeit). Folge ist Rauschen, kein Schaden.
- Medien im Schnappschuss sind ausgeschlossen (82 MB → 14 MB). Löscht ein Lauf ein Bild, holt
der Rollback es nicht zurück.
- Die Wochen-Grenze bremst nicht. Gebremst wird nach der Session (Leons Entscheid, weil die
Woche über Tage läuft) – bei voller Woche läuft er bis Claude hart stoppt. Die Seite warnt.
- Erledigt (05.08.2026, HZ-725): der Bericht zeigt die Klon-% als Kernzahl, die DNA-Quote
nur noch als Nebenzahl (zielbild_bericht() → klon_messen() → klon_verlauf.stabiler_stand, also derselbe Median wie HQ und Vision-Seite). Die Zeile nennt Quelle, Alter, n Urteile aus messungen Ledger-Zeilen, die Spanne zwischen den Messungen und die Spanne innerhalb der jüngsten (die vier Linsen untereinander – am 05.08. 60 Punkte, während die Spanne zwischen den Zeilen 0 war), Vorbehalte sowie entwertete Zeilen. Erhoben wird clone_score per Cron (scripts/clone_score_watch.py, jede Minute, ein Lauf zur Zeit per flock, nur Kanäle aus clone_score_watch.json). Vorher stand hier „der Bericht zeigt die DNA-Quote" (Stand 03.08.) – seit HZ-665 falsch.
- Der Ermittler durfte
clone_score.pybis 03.08.2026 nicht aufrufen – er stand in
ERMITTLER_ABGELEHNT. Belegt ist inzwischen: --stand [--json] liest nur und kostet $0. Aufnahme in die Allowlist per scripts/patch_ermittler_clone_score.py --apply (ein Befehl, mit Sicherung und Gegenprobe) – ein unbeaufsichtigter Lauf darf autopilot.py laut Auftragstext nicht selbst ändern, der Policy-Hook hindert ihn seit 29.07.2026 aber nicht mehr daran (AUTOPILOT_RO enthält nur noch TODO.md).
Verwandtes
design.md– warum es so gebaut ist: Entscheidungen, Fehlschläge, Lehren je Runde../architektur.md– Systemkarte des ganzen Hauses../kb/werkzeuge-rezepte__bauer-pruefer-loop-muster.md– das Prüfer-Muster../kb/werkzeuge-rezepte__hq-design-standard-nexus.md– wie die HQ-Seite auszusehen hat- https://vision.herz-automation.com – das Zielbild, auf das der Läufer hinarbeitet
Quelle im Repo: docs/autopilot/README.md
Vision