Research

Der Never-Sleeping-Harness

12. August 2026 Tom Küstner Lesezeit 35 min

Was diese Notiz ist, und was nicht

Sie beschreibt ein Muster, das sich in mehreren Monaten produktiver Nutzung herausgebildet hat: wie man einen Coding-Agenten über Stunden autonom arbeiten lässt, ohne dass ein Mensch danebensitzt, und vor allem, welche Fehler dabei auftreten und woran man sie erkennt.

Sie ist bewusst setup-agnostisch. Alle Pfade, Namen und Hosts stehen als Platzhalter in spitzen Klammern; die konkrete Topologie einer Installation gehört in eine interne Betriebsdoku, nicht hierher. Was hier steht, gilt unabhängig von Betriebssystem, Hosting und Projektstruktur.

Sie ist kein Produktversprechen. Jede Aussage stammt aus eigener Messung in einem konkreten Aufbau. Wo etwas nur plausibel, aber ungeprüft ist, steht das ausdrücklich dabei, diese Unterscheidung ist der halbe Wert des Dokuments.

Ein Glossar der Fachbegriffe steht als Anhang D am Ende (Abschnitt 15); wer bei einem Begriff hängt, findet ihn dort mit Kurzdefinition und dem Grund, warum er hier zählt.

Platzhalter durchgehend: <PROJEKT> Projektverzeichnis · <AGENT-USER> unprivilegierter Konto, unter dem der Agent läuft · <CLI> die Agenten-CLI · <REPO> das Versionskontroll-Remote · <PUSH-CMD> das gehärtete Publikationswerkzeug.


Das Problem

Coding-Agenten mit CLI können längere Aufgaben selbstständig bearbeiten. In der Praxis stoßen sie dabei auf drei Wände:

  1. Turn-Limits. Ein Lauf endet nach einer festgelegten Zahl Werkzeug-Runden, unabhängig davon, ob die Aufgabe fertig ist.
  2. Nutzungs-Limits. Anbieter begrenzen Verbrauch pro Zeitfenster. Der Agent steht still, bis das Fenster zurückgesetzt wird, typischerweise Stunden.
  3. Kontext-Grenzen. Lange Konversationen laufen voll; der Agent verliert den Faden oder bricht ab.

Ein Mensch, der daneben sitzt und immer wieder „mach weiter” tippt, löst alle drei. Der Harness ist die Automatisierung genau dieses Menschen, mehr nicht. Deshalb der Name: Er schläft nicht, wenn das Modell schlafen muss.


Grundarchitektur

   ┌──────────────────────────────────────────────────────────┐
   │  Watcher (Shell-Skript, läuft im Hintergrund)            │
   │                                                          │
   │   while true:                                            │
   │     ├─ Poke: <CLI> im Print-Modus, EIN JSON zurück       │
   │     ├─ JSON auswerten (subtype · is_error · result)      │
   │     ├─ Fortschritt? → publizieren                        │
   │     ├─ Limit?       → schlafen bis Reset                 │
   │     ├─ Sentinel?    → sauber beenden                     │
   │     └─ Stillstand?  → abbrechen (nicht ewig weiterpoken) │
   └──────────────────────────────────────────────────────────┘
Standing Command bei jedem Poke identisch Poke frischer Prozess · Session wird fortgesetzt Antwort: ein JSON subtype · is_error · result Fehlerflag gesetzt? zuerst prüfen Sentinel im Text? Neue Version? Commit-Kennung Stillstands-Zähler +1 N erreicht → Abbruch Wartefall, kein Stillstand Reset-Zeit aus dem Klartext · schlafen · Session neu Sauberes Ende Exit 0 — nichts mehr greifbar Publizieren gehärtetes Werkzeug · Zähler zurück auf 0 Abbruch Exit 2 Turn-Ende weiter poken janein janein janein nächster Poke
Abb. M26 · Der Poke-Zyklus. Die Prüfreihenfolge ist der Inhalt: erst das Fehlerflag, dann der Sentinel, dann der Fortschritt. Orange markiert das getarnte Limit, der Pfad, der Abschnitt 6.4 gekostet hat.

Der zentrale Kunstgriff: Jeder „Poke” ist ein frischer Prozess, aber die Konversation überlebt ihn. Die meisten Agenten-CLIs legen Sessions persistent ab und können sie fortsetzen. Der Agent denkt also über Prozessgrenzen hinweg an derselben Aufgabe weiter, während der Watcher beliebig oft neu startet.

Warum Print-Modus (headless) und nicht die interaktive Oberfläche: Ein Skript braucht drei Dinge, die nur der nicht-interaktive Modus liefert, programmatischen Start, ein maschinenlesbares Ende (strukturierte Ausgabe statt Bildschirmtext) und ein Turn-Budget. Eine interaktive Sitzung müsste man per Screen-Scraping auswerten; das ist fragil und nicht auditierbar.

Grundgerüst eines Pokes (schematisch):

ausgabe=$(su - <AGENT-USER> -c "
    cd '<PROJEKT>' &&
    timeout $POKE_TIMEOUT <CLI> -p '$STANDING_CMD' \
        $RESUME_FLAG \
        --max-turns $MAX_TURNS \
        --output-format json
") || exit_code=$?

Aus der Ausgabe werden ausgelesen: Ergebnistyp, Fehlerflag, Ergebnistext und die Session-Kennung (für das Fortsetzen beim nächsten Poke).


Der Standing Command

Der Watcher schickt bei jedem Poke denselben Auftrag. Er ist die einzige Stellschraube, über die der Agent erfährt, was er tun darf. Vier Bestandteile:

  1. Woher die Arbeit kommt, die Aufgabenliste im Projekt und ihr Format.
  2. Welche Aufgaben greifbar sind, ein scharfes Prädikat (§4).
  3. Was verboten ist, und zwar als Berührungsverbot, nicht als Bau-Verbot (§6.2, die teuerste Lektion).
  4. Wann Schluss ist, ein eindeutiger Sentinel-String, den der Watcher sucht.

Formulierungsregeln, die sich bewährt haben:

  • Der Text muss frei von Anführungszeichen, Backticks und Dollarzeichen sein. Er durchläuft mehrere Shell-Ebenen; jedes Sonderzeichen ist ein Bruchrisiko.
  • Der Sentinel muss als einzige Ausgabe verlangt werden („gib SOFORT und als einzige Ausgabe exakt: …”). Sonst schreibt der Agent einen Aufsatz darüber, warum er fertig ist, und der Watcher findet den String nicht verlässlich.
  • Der Auftrag muss projektunabhängig bleiben. Projektspezifika gehören in die Agenten-Instruktionsdatei im Projektverzeichnis, die die CLI ohnehin lädt.

Das Auswahl-Prädikat: der wichtigste Entwurfsentscheid

Der Agent darf nicht „irgendetwas Sinnvolles” tun, sondern nur klar abgegrenzte Aufgaben. Bewährt hat sich ein zweidimensionales Prädikat in der Aufgabenliste:

DimensionZweck
StatusIst die Aufgabe jetzt handlungsbereit?
ArbeitsmodusDarf sie ohne Menschen bearbeitet werden?
greifbar  ⟺  Status ∈ {offen, in Arbeit}  UND  Modus ∈ {autonome Modi}

Entscheidend ist die zweite Dimension. Ohne sie muss man aus dem Aufgabentext erraten, ob etwas heikel ist. Mit ihr ist die Antwort ein Feldwert.

Was in die nicht-autonomen Modi gehört (Erfahrungswerte): Schema- und Datenbankänderungen · Architekturentscheidungen · alles, was Schreibpfade nach außen öffnet · alles mit unklarer Spezifikation. Faustregel: Wenn ein Fehler nicht durch einen Test auffiele, gehört die Aufgabe nicht in den autonomen Modus.

Ein zusätzlicher Status wie „strukturiert, aber noch nicht handlungsbereit” hält die Liste sauber: Aufgaben können vollständig beschrieben herumliegen, ohne dass der Loop sie anfasst.


Wann gilt eine Aufgabe als fertig?

Das ist die Kernfrage jeder Automatisierung mit einem nicht-deterministischen Akteur. Die Antwort lautet: Die Selbstauskunft des Agenten ist nicht das Kriterium. Vier Ebenen, nur zwei davon sind deterministisch:

EbeneWer urteiltdeterministisch?
Statuswechsel in der Aufgabenlisteder Agentnein, eine Behauptung
Automatisierte Gates (Tests, Typprüfung, Linter, Build)Programmeja
Akzeptanzkriterien der Aufgabeals Test formuliertja, wenn richtig geschrieben
Abnahme durch einen MenschenMenschnein, und genau dafür da

5.1 Akzeptanz als Test formulieren, nicht als Absicht

Hier entsteht der Determinismus, nicht durch bessere Prompts:

untauglich (Absicht)tauglich (Test)
„Die Liste soll mandantengetrennt sein”„Zugriff über Mandantengrenze hinweg: Test läuft rot gegen die alte Fassung
„Löschen soll sicher sein”„Löschen mit bestehendem Verweis wird abgewiesen, sichtbar, kein stiller Fehler”
„Barrierefreiheit ok”(kein Messwert ohne Zahl, unzulässig)
„Pfad-Traversal ist geschützt”„Traversal fällt an allen drei Grenzen einzeln (drei getrennte Rot-Belege)”

Zwei Regeln, die daraus folgen: kein Messwert ohne Zahl, und ein Regressionstest muss gegen die alte Fassung rot gelaufen und als rot berichtet worden sein, sonst ist er wertlos.

5.2 Was der Watcher selbst versteht (rein mechanisch)

Der Watcher hat kein Verständnis von „fertig”. Er kennt drei Signale:

  1. Sentinel, der vereinbarte String im Ergebnistext → sauberes Ende.
  2. Fortschritt = neue Version im Versionskontrollsystem (Vergleich der Commit-Kennung vor/nach dem Poke). „Der Agent hat etwas Freundliches geschrieben” ist kein Fortschritt.
  3. Fehlerflag, trennt echten Erfolg von getarnten Fehlern (§6.4).

Konsequenz für die Aufgabenplanung: Eine Aufgabe ohne committbares Ergebnis ist für den Watcher von Leerlauf ununterscheidbar. Reine Dokumentationsaufgaben gehören entweder mit einem Commit-Abschluss versehen, oder nicht in den autonomen Modus.

5.3 Gates sind notwendig, nicht hinreichend

In einem produktiven Lauf wurden mehrere Aufgaben mit vollständig grünen Gates abgeschlossen, und die anschließende Bedienprobe durch einen Menschen fand trotzdem zwei echte Defekte (ein zu enges Eingabelimit mit stiller Fehlermeldung und ein nicht funktionierender Schreibpfad). Die menschliche Abnahme ist keine Formalität, sie findet eine andere Fehlerklasse als jeder Test.


Fehlermodi, die teuer gelernten Lektionen

Dieser Abschnitt ist der eigentliche Wert der Notiz. Alle Punkte sind selbst erlebt und gemessen.

6.1 Der Loop, der nie lief

Ein Skript, das an einer Stelle bearbeitet wurde, hatte ein fehlendes Anführungszeichen, Syntaxfehler, das Skript startete nie. Wochenlang stand in der Dokumentation, es sei „einsatzbereit”.

Regel: Jede Änderung an einem Steuerungsskript wird durch einen Syntax-Check (bash -n o. ä.) geschleust, bevor sie ausgerollt wird. Und: „einsatzbereit” ist eine Messaussage, keine Absichtserklärung.

6.2 „Überspringen” heißt für einen Agenten nicht „nicht anfassen”

Der Standing Command wies an, nicht-autonome Aufgaben zu überspringen. Der Agent las das als Bau-Verbot und leistete stattdessen hilfsbereite Vorarbeit: Planentwürfe für gesperrte Aufgaben, eine neu vergebene Aufgaben-Kennung (faktisch eine Strukturänderung) und das Überschreiben einer vorhandenen, nicht versionierten Datei.

Bemerkenswert: Er hielt dabei gleichzeitig andere Regeln perfekt ein, kein einziger Commit über die gesamte Laufzeit, weil seine Instruktionsdatei ein Commit-Gate für diese Aufgabenklasse vorschrieb. Die Schutzringe hielten; die Reichweiten-Interpretation nicht.

Regel: Verbote für autonome Agenten müssen Berührung verbieten, nicht Vollzug. Der Wortlaut muss explizit ausschließen: keine Entwürfe, keine Notizen, keine neuen Dateien, keine neuen Kennungen, kein Vorbereiten für später. Hilfsbereitschaft füllt jede Lücke, die eine Formulierung offenlässt.

6.3 Leerlauf ohne Abbruchbedingung

Nach dem Abarbeiten aller greifbaren Aufgaben gab der Agent nicht den Sentinel, sondern begründete in über sechzig aufeinanderfolgenden Pokes, dass er warte, seine Lesart: Es gibt ja noch offene Aufgaben, nur keine für ihn. Der Watcher pokte weiter, weil er kein Abbruchkriterium hatte. Über Nacht wäre das ein stundenlanger Verbrauch ohne jedes Ergebnis gewesen.

Regel: Ein Stillstands-Wächter ist Pflicht: N aufeinanderfolgende erfolgreiche Pokes ohne Sentinel und ohne neue Version → sauberer Abbruch mit eigenem Exit-Code. Zusätzlich empfiehlt sich eine Gesamt-Notbremse.

6.4 Limits kommen getarnt

Die wichtigste Einzelmessung: Das Nutzungslimit wurde nicht als Fehlertyp gemeldet, sondern als erfolgreicher Lauf mit gesetztem Fehlerflag, und der Reset-Zeitpunkt stand nur als Klartext im Ergebnistext („… resets <Uhrzeit>”, im Wortlaut der Anbieter-Meldung). Das Feld, in dem man Reset-Informationen vermutet, war leer.

Die Folge war bitter-ironisch: Der frisch eingebaute Stillstands-Wächter aus §6.3 brach den Lauf ab, er konnte „Agent dreht leer” nicht von „Agent ist ausgesperrt und wartet” unterscheiden, weil beide als Erfolg ankamen.

Regel: Vor dem Zählen von Stillstand erst das Fehlerflag prüfen. Ist es gesetzt und trägt der Text ein Limit-Muster, ist das ein Wartefall, kein Stillstand: Reset-Zeit aus dem Klartext parsen, schlafen, Session neu beginnen, weitermachen. Für das Parsen gilt: Fail-safe, schlägt es fehl, eine feste Wartezeit nutzen statt abzubrechen.

Und übergreifend: Verlassen Sie sich nicht auf vermutete Feldnamen in API-Antworten. Schreiben Sie jede Antwort in eine fortlaufende Datei und lesen Sie beim ersten echten Vorfall nach, was wirklich ankommt. Eine Annahme, die als Tatsache dokumentiert wird, überlebt Monate unentdeckt.

6.5 Testparameter passend zum Testzweck wählen

Ein Leerlauftest mit sehr kleinem Turn-Budget schlug fehl, weil der Agent das Budget schon beim Lesen der Aufgabenliste verbrauchte, er kam nie bis zum Sentinel. Getestet wurde also etwas anderes als beabsichtigt.

Regel: Ein Test des Leerlaufs braucht ein Budget, das den Abschluss überhaupt erreichbar macht. Klein gewählte Budgets testen den Fortsetzungs-Pfad, nicht den Abschluss-Pfad.

6.6 Kleine Budgets sind teuer

Gemessen: Bei sehr kleinem Turn-Budget verbrauchte der Loop rund 2,4 % des Sitzungskontingents pro Minute, weil jeder Poke Anlaufkosten hat (Kontext neu laden, Aufgabenliste neu lesen). Größere Budgets erledigen mehr echte Arbeit pro Anlauf.

Regel: Für Produktivläufe großzügige Turn-Budgets, kleine nur zum Testen des Fortsetzungs-Pfads.

6.7 Der Publikationsschritt braucht seine eigene Prüfung

„Committet” ist nicht „veröffentlicht”. Ein Agent kann wahrheitsgemäß von einem Commit berichten, während das Remote unverändert bleibt. Umgekehrt kann die lokale Referenz auf das Remote veraltet sein, sodass ein Agent fälschlich meldet, etwas sei nicht veröffentlicht (real erlebt).

Regel: Der Stand am Remote wird unabhängig abgefragt (etwa per ls-remote), nicht aus dem lokalen Zustand abgeleitet, weder vom Agenten noch vom prüfenden Menschen.


Sicherheitsarchitektur

Ein autonom laufender Agent ist ein Prozess, der ohne Rückfrage Dateien schreibt. Die folgenden Ringe haben sich bewährt, sie hielten auch dann, als die Reichweiten-Regel aus §6.2 verletzt wurde:

  1. Eigener unprivilegierter Nutzer. Der Agent läuft nie unter dem Konto des Menschen; Rechte enden am Projektverzeichnis.
  2. Gehärtetes Publikationswerkzeug statt roher Push-Rechte. Der Agent ruft ein Skript, das Ziel und Zweig fest verdrahtet und Fremdziele ablehnt. Rohe Push-Befehle sind gesperrt.
  3. Nicht rückholbares gehört in die Konfiguration, nicht in den Prompt. Eine Grenze, die im Gespräch gesetzt wird, wirkt nur, solange sie im Kontext steht, und verschwindet mit der Kompaktierung. Sie ist keine Autorisierungsgrundlage. Alles Irreversible (Veröffentlichen, Löschen, Migrieren, Installieren) gehört als Regel in die Berechtigungskonfiguration.
  4. Append-only-Remotes. Kein Force-Push, kein Löschen von Zweigen. Der Agent kann Historie nicht umschreiben, auch nicht versehentlich.
  5. Ein Mensch als Tor für Veröffentlichung und Abnahme. Welche Schritte Menschen-gebunden bleiben, ist eine Risikoentscheidung; dass es welche gibt, nicht.

Diese Ringe sind nicht theoretisch: Im Vorfall aus §6.2 hat der Agent Regeln zur Reichweite gebrochen, aber über die gesamte Laufzeit keinen einzigen Commit und keine Veröffentlichung erzeugt. Der Schaden blieb auf den Arbeitsbaum begrenzt und war mit einem Befehl zurückzunehmen.


Beobachtbarkeit: vier Schichten

Ein Loop ohne Bildschirm ist nicht undurchsichtig, er ist zeitversetzt einsehbar:

  1. Watcher-Protokoll, je Poke eine Zeile: Ergebnistyp, Fehlerflag, Session-Kennung, Ergebnis-Anriss, Entscheidung des Watchers.
  2. Antwort-Historie, jede vollständige Antwort in eine fortlaufende Datei (eine Zeile JSON pro Poke). Nicht überschreiben: Genau dadurch ging uns die Antwort verloren, in der der Agent seinen eigenen Fehler protokollierte.
  3. Versionsgeschichte, eine Aufgabe, ein Commit, mit Kennung im Betreff. Das ist das eigentliche Audit, weil unabhängig nachprüfbar.
  4. Nachträgliche Sitzungsansicht, die meisten CLIs können eine Headless-Sitzung anhand ihrer Kennung im interaktiven Modus wieder öffnen: vollständiges, scrollbares Protokoll aller Werkzeugaufrufe. Ideal zur Nachschau, aber erst nach Ende des Laufs (zwei Schreiber auf einer Sitzung sind eine schlechte Idee).

Zusätzlich empfehlenswert: ein Protokoll der erteilten Aufträge im Projekt selbst, vom Agenten geführt. In unserem Fall hat er es sogar dann gewissenhaft weitergeführt, als er andere Regeln verletzte.

8.1 Instanzen wissen nichts voneinander

Mehrere Agenten-Sitzungen am selben Projekt haben kein gemeinsames Gedächtnis, sie teilen nur Dateisystem und Versionskontrolle. Eine parallel geöffnete interaktive Sitzung merkt vom Loop nichts, bis sie die Historie liest. Koordination läuft ausschließlich über Artefakte, nie über Bewusstsein.


Aufbau-Reihenfolge (Empfehlung)

  1. Aufgabenformat festlegen und schriftlich fixieren: Statusvokabular, Modusvokabular, Akzeptanz-als-Test, Zählwerk. Zuerst, weil alles daran hängt.
  2. Konsistenzprüfung automatisieren. Ein Test, der Statusorte und Zählwerk gegeneinander prüft und beim Auseinanderlaufen rot wird. Ohne ihn driftet die Liste lautlos.
  3. Gates einrichten, bevor irgendetwas autonom läuft. Ohne sie ist ein Statuswechsel eine reine Behauptung.
  4. Schutzringe aus §7 aufsetzen, vor dem ersten unbeaufsichtigten Lauf.
  5. Watcher bauen, mit Stillstands-Wächter und Fehlerflag-Prüfung von Anfang an. Beides nachzurüsten kostet je einen misslungenen Lauf.
  6. Leerlauftest: Der Loop muss beweisen, dass er nichts tut, wenn nichts zu tun ist, und sauber endet. Erst danach bekommt er Arbeit.
  7. Beaufsichtigter Kurzlauf mit einer echten Aufgabe: Fortsetzung nach Turn-Ende, Veröffentlichung, Abschluss.
  8. Unbeaufsichtigter Lauf, in einer ablösbaren Terminal-Sitzung, damit ein Verbindungsabbruch ihn nicht beendet.

Zur Betriebsfestigkeit: Eine Terminal-Sitzung überlebt keinen Neustart des Rechners. Wer den Loop wirklich dauerhaft betreiben will, braucht eine Dienst-Einheit mit Neustart-Regel. Für Läufe von einigen Stunden reicht die einfache Variante.


Ehrliche Grenzen

  • Der Agent bleibt nicht-deterministisch. Der Harness macht ihn nicht verlässlicher, sondern überprüfbarer. Wer keine automatisierten Gates hat, bekommt schneller mehr ungeprüften Code, kein erstrebenswertes Ergebnis.
  • Die menschliche Abnahme entfällt nicht (§5.3).
  • Anbieterverhalten ändert sich. Ergebnisformate und Limit-Semantik sind nicht vertraglich zugesichert. Genau deshalb: jede Antwort protokollieren und beim ersten Abweichen nachlesen.
  • Aufgabenzuschnitt bleibt Menschenarbeit. Der Loop arbeitet ab, was gut beschrieben ist. Eine schlecht spezifizierte Aufgabe wird durch Automatisierung nicht besser, sondern schneller falsch umgesetzt.
  • Erfahrungsbasis: ein Aufbau, ein Projekt, mehrere Monate. Die Fehlermodi in §6 sind belegt; ihre Vollständigkeit ist es nicht.

Kurzreferenz, die zwölf Regeln

  1. Syntaxprüfung vor jedem Ausrollen eines Steuerungsskripts.
  2. „Einsatzbereit” ist eine Messaussage, keine Absicht.
  3. Verbote formulieren als Berührungs-, nicht als Vollzugsverbot.
  4. Sentinel als einzige Ausgabe verlangen.
  5. Stillstands-Wächter von Anfang an; Fortschritt = neue Version.
  6. Fehlerflag vor dem Stillstandszähler prüfen.
  7. Reset-Zeit aus der realen Antwort lesen, nicht aus vermuteten Feldern.
  8. Jede Antwort protokollieren, anhängen, nicht überschreiben.
  9. Akzeptanz als Test formulieren; kein Messwert ohne Zahl.
  10. Gates sind notwendig, nicht hinreichend, Menschen nehmen ab.
  11. Remote-Stand unabhängig messen, nie aus lokalem Zustand ableiten.
  12. Irreversibles in die Konfiguration, nicht in den Prompt.


Anhang A: Referenz-Implementierung des Watchers

Das folgende Skript ist die produktiv eingesetzte Fassung, um setup-spezifische Angaben bereinigt. Es parst (bash -n), ist aber eine Vorlage, keine schlüsselfertige Software: Die Platzhalter aus der Legende müssen ersetzt und der Standing Command auf das eigene Aufgabenformat angepasst werden.

12.1 Legende der Platzhalter

Platzhalter im SkriptBedeutungErsetzen durch
AGENTUSERunprivilegiertes Konto, unter dem der Agent läuftden eigenen Dienstnutzer
agent-clidie Kommandozeile des Coding-Agentenden Aufrufnamen der eigenen CLI
publish-toolgehärtetes Publikationswerkzeug (Ziel und Zweig fest verdrahtet)eigenes Skript, siehe Abschnitt 7 Punkt 2
meinprojektVerzeichnisname des Projekts, steuert die Fallauswahleigenen Projektnamen
GATE1, GATE2, GATE3die automatisierten Prüfungeneigene Testbefehle, Linter, Typprüfung, Build
T-NNNKennungsformat der Aufgabeneigenes Format
CAP-NNNVerweis auf einen internen Befundeintragentfällt oder eigener Verweis

Weiter bewusst neutral gehalten: Zeitangaben in den Kommentaren, der Pfad für temporäre Dateien und der Wortlaut projektspezifischer Zusatzregeln.

Warum das Skript ohne Umlaute geschrieben ist: Kommentare wie Log-Ausgaben bleiben bewusst auf ASCII beschränkt. Der Loop läuft typischerweise unter einem Dienstkonto, dessen Umgebung nicht zwingend eine UTF-8-Zeichensatzeinstellung erbt; Umlaute in Protokollzeilen erscheinen dann verstümmelt. Der Fließtext dieses Dokuments verwendet dagegen durchgehend echte Umlaute.

12.2 Das Skript

#!/bin/bash
# watcher.sh - Never-Sleeping-Harness · V1.5 (Datum)
# Aenderungen ggü. V1.3 (Belege: Leertest-Log Testlauf-Log, CAP-NNN):
#   B6  Stall-Guard: STALL_LIMIT aufeinanderfolgende success-Pokes ohne Sentinel UND
#       ohne neuen Commit (HEAD-Vergleich) -> Stopp exit 2. Der Leertest lief 60+
#       identische "Ich halte an"-Pokes ohne Abbruch. error_max_turns zaehlt nicht
#       (Arbeit laeuft) und resettet nicht. Optional POKE_LIMIT (Gesamt-Notbremse,
#       default 0 = aus) -> exit 3.
#   B2b Standing-Command (meinprojekt) verschaerft: UEBERSPRINGEN hiess fuer CC "nicht
#       bauen", nicht "nicht anfassen" -> Plan-Entwuerfe, neue Task-ID T-064, eine
#       ueberschriebene untracked-Datei. Jetzt: Beruehrungsverbot + Sofort-Sentinel
#       als einzige Ausgabe, wenn kein greifbarer Task existiert.
#   B7  Quoting-Fragilitaet entschaerft (vorbestehend seit V1.2): agent-cli -p
#       '$STANDING_CMD' laeuft durch zwei Shell-Ebenen; einfache Anfuehrungszeichen
#       IM Command-Text ueberlebten nur zufaellig (Bash-Konkatenation). Der Text ist
#       jetzt frei von ' " ` $ - nur so ist das Argument stabil.
#   Forensik: JSON-Historie (append, JSONL) statt Ueberschreiben - der erste
#       success-Poke des Leertests (Bericht zur ueberschriebenen Datei) ging durch
#       das Ueberschreiben verloren. get_reset_epoch liest tail -1.
#   Push-Log ehrlich: "Nichts zu pushen" wird als No-op geloggt, nicht als Push.
#   B8  Session-Limit-Erkennung: die Agenten-CLI meldet ein Session-Limit als
#       subtype=success MIT is_error=true und result 'You've hit your session
#       limit · resets <Uhrzeit>' - NICHT als error_rate_limited (gemessen Datum,
#       run1.log; rate_limits war null). V1.5 zaehlte das als Stall und brach ab
#       (Stall-Guard und Limit-Warten arbeiteten gegeneinander). Jetzt: is_error
#       wird ausgelesen; bei true wird der result-Text auf ein Limit-/resets-Signal
#       geprueft, die Reset-Uhrzeit aus dem KLARTEXT geparst (einzige verlaessliche
#       Quelle), bis dahin geschlafen, Session-ID zurueckgesetzt, weiter. Kein Stall.
#       Zeitparser nutzt lokale TZ (VPS = Europe/Berlin); Parse-Fail -> sleep 3600.
# Exit-Codes: 0 = Sentinel · 1 = unbekannter subtype · 2 = Stall · 3 = POKE_LIMIT.
set -euo pipefail

PROJECT="${1:?Projektpfad fehlt}"
MAX_TURNS="${2:-12}"
SENTINEL="ALL-TASKS-DONE"
POKE_TIMEOUT="${POKE_TIMEOUT:-600}"
RESET_MARGIN=120
STALL_LIMIT="${STALL_LIMIT:-3}"
POKE_LIMIT="${POKE_LIMIT:-0}"
SESSION_FILE="/tmp/watcher-session-$$.txt"
LOG="/tmp/watcher-$$.log"
JSON_HIST="/tmp/watcher-history-$$.jsonl"

case "$(basename "$PROJECT")" in
  meinprojekt)
    STANDING_CMD="Arbeite an docs/tasks.md weiter (Projekt-Format: ID-Format T-NNN, Statusvokabular, Feld Modus). REICHWEITE: Du darfst AUSSCHLIESSLICH Tasks bearbeiten, die JETZT den Status offen oder in arbeit tragen UND deren Modus-Feld Direkt, Loop oder Goal-Mode ist. ALLES ANDERE IST TABU - nicht anfassen, auch nicht vorbereitend: keine Plan-Entwuerfe, keine Notizen, keine neuen Dateien, KEINE neuen Task-IDs, keine Schema-, ADR- oder Strukturaenderungen, kein Bearbeiten von Tasks mit Status geplant oder Modus Plan-Mode oder Auto + Browser-STOP. Die brauchen P1, und Vorarbeit fuer P1 ist ebenfalls TABU. Findest du keinen greifbaren Task, dann aendere NICHTS und gib SOFORT als einzige Ausgabe exakt: ALL-TASKS-DONE. Sonst: Bau gegen die Spec des Tasks. Lass ALLE Gates gruen laufen: GATE1, GATE2, GATE3. Committe atomar (kein git add -A). Bei erledigt-Flip: Quality-Write im selben Commit (projektspezifische Zusatzregel), beide Statusorte + Zaehlzeile + Drift-Lock konsistent. NICHT pushen (der Watcher pusht). Setz den Status, dann stopp."
    PUSH_CMD="publish-tool "$PROJECT""
    ;;
  *)
    STANDING_CMD="Arbeite an docs/TASKS.md weiter: Ist ein Task IN-PROGRESS, beende ihn (Gates laufen lassen, Status setzen). Sonst nimm den naechsten OPEN-Task. Schreib den Status (DONE/BLOCKED) nach TASKS.md, dann stopp. Sind keine OPEN-Tasks mehr offen, gib exakt aus: ALL-TASKS-DONE"
    PUSH_CMD="git push origin main 2>/dev/null"
    ;;
esac

log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG" >&2; }

repo_head() {
    su - AGENTUSER -c "cd '$PROJECT' && git rev-parse HEAD" 2>/dev/null || echo ""
}

poke() {
    local sid_flag=""
    if [[ -f "$SESSION_FILE" ]]; then
        local sid
        sid=$(cat "$SESSION_FILE")
        [[ -n "$sid" ]] && sid_flag="--resume $sid"
    fi
    local out
    local exit_code=0
    out=$(su - AGENTUSER -c "
        cd '$PROJECT' &&
        timeout $POKE_TIMEOUT agent-cli -p '$STANDING_CMD' \
            $sid_flag \
            --max-turns $MAX_TURNS \
            --permission-mode acceptEdits \
            --output-format json \
            2>/tmp/watcher-poke-err.txt
    ") || exit_code=$?
    printf '%s\n' "$out" >> "$JSON_HIST" 2>/dev/null || true
    local subtype result new_sid
    subtype=$(echo "$out" | jq -r '.subtype // "unknown"' 2>/dev/null || echo "unknown")
    is_error=$(echo "$out" | jq -r '.is_error // false' 2>/dev/null || echo "false")
    result=$(echo "$out" | jq -r '.result // ""' 2>/dev/null || echo "")
    new_sid=$(echo "$out" | jq -r '.session_id // ""' 2>/dev/null || echo "")
    [[ -n "$new_sid" ]] && echo "$new_sid" > "$SESSION_FILE"
    log "exit=$exit_code subtype=$subtype is_error=$is_error sid=${new_sid:0:8}..."
    [[ -n "$result" ]] && log "result: ${result:0:120}"
    # Feld-Trenner \x1f, damit Freitext im result nichts zerreisst
    printf '%s\x1f%s\x1f%s' "$subtype" "$is_error" "$result"
}

parse_reset_epoch() {
    # Reset-Uhrzeit aus dem KLARTEXT des result ("resets <Uhrzeit>") -> Epoch.
    # Nutzt lokale Systemzeitzone (VPS = Europe/Berlin). Fail -> leer (Aufrufer: 3600).
    local text="$1" clock ampm hm target_h target_m today epoch now_epoch
    clock=$(printf '%s' "$text" | grep -oiE 'resets ([0-9]{1,2}(:[0-9]{2})?)(am|pm)' | head -1 | sed 's/[Rr]esets //')
    [[ -z "$clock" ]] && return 0
    ampm=$(printf '%s' "$clock" | grep -oiE '(am|pm)$' | tr 'A-Z' 'a-z')
    hm=${clock%$ampm}
    target_h=${hm%%:*}
    if [[ "$hm" == *:* ]]; then target_m=${hm##*:}; else target_m=0; fi
    target_h=$((10#$target_h)); target_m=$((10#$target_m))
    [[ "$ampm" == "pm" && "$target_h" -ne 12 ]] && target_h=$((target_h+12))
    [[ "$ampm" == "am" && "$target_h" -eq 12 ]] && target_h=0
    today=$(date +%Y-%m-%d)
    epoch=$(date -d "$today $target_h:$target_m" +%s 2>/dev/null) || return 0
    now_epoch=$(date +%s)
    (( epoch <= now_epoch )) && epoch=$((epoch + 86400))
    echo "$epoch"
}

get_reset_epoch() {
    # Quelle: JSON des letzten Pokes (JSONL-Historie). Feld-Kandidat laut offizieller
    # Statusline-Doku: rate_limits.five_hour.resets_at (Unix epoch). [ANNAHME] fuer
    # das -p-Result-JSON - beim ersten echten Limit in $JSON_HIST verifizieren.
    [[ -f "$JSON_HIST" ]] || return 0
    local raw
    raw=$(tail -1 "$JSON_HIST" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null || true)
    [[ -z "$raw" ]] && return 0
    if [[ "$raw" =~ ^[0-9]+$ ]]; then
        echo "$raw"
    else
        date -d "$raw" +%s 2>/dev/null || true
    fi
}

log "=== Watcher Start: $PROJECT (V1.5) ==="
log "max_turns=$MAX_TURNS timeout=${POKE_TIMEOUT}s stall_limit=$STALL_LIMIT poke_limit=$POKE_LIMIT json_hist=$JSON_HIST"

stall_count=0
poke_count=0
last_head=$(repo_head)
log "Basis-HEAD: ${last_head:0:7}"

while true; do
    poke_count=$((poke_count + 1))
    if (( POKE_LIMIT > 0 && poke_count > POKE_LIMIT )); then
        log "POKE_LIMIT ($POKE_LIMIT) erreicht -> Stopp (exit 3)."
        exit 3
    fi

    result_line=$(poke)
    IFS=$'\x1f' read -r subtype is_error result_text <<< "$result_line"

    case "$subtype" in
        success)
            if [[ "$is_error" == "true" ]]; then
                # Getarntes Limit/Fehler (subtype=success, is_error=true). Haeufigster
                # Fall: Session-Limit "resets <Uhrzeit>". KEIN Stall, KEIN Fortschritt.
                if echo "$result_text" | grep -qiE "session limit|hit your.*limit|resets [0-9]"; then
                    reset_epoch=$(parse_reset_epoch "$result_text")
                    if [[ -n "$reset_epoch" ]]; then
                        now=$(date +%s); sleep_secs=$(( reset_epoch - now + RESET_MARGIN ))
                        (( sleep_secs < 0 )) && sleep_secs=0
                        log "Session-Limit ($result_text) -> sleep ${sleep_secs}s bis Reset."
                        sleep "$sleep_secs"
                    else
                        log "Session-Limit erkannt, Reset-Zeit nicht parsebar ($result_text) -> sleep 3600s."
                        sleep 3600
                    fi
                    rm -f "$SESSION_FILE"   # frische Session nach Reset
                    log "Reset abgewartet -> naechster Poke."
                else
                    log "is_error=true, kein Limit-Muster ($result_text) -> naechster Poke."
                fi
            elif echo "$result_text" | grep -q "$SENTINEL"; then
                log "ALL-TASKS-DONE -- fertig."
                rm -f "$SESSION_FILE"
                exit 0
            elif echo "$result_text" | grep -qi "prompt is too long\|context.*too long\|context window"; then
                log "Kontext zu lang - Session-ID zuruecksetzen, neuer Poke ohne --resume."
                rm -f "$SESSION_FILE"
            else
                new_head=$(repo_head)
                if [[ -n "$new_head" && "$new_head" == "$last_head" ]]; then
                    stall_count=$((stall_count + 1))
                    log "Turn fertig, kein Sentinel, kein neuer Commit (Stall $stall_count/$STALL_LIMIT)."
                    if (( stall_count >= STALL_LIMIT )); then
                        log "STALL: $STALL_LIMIT success-Pokes ohne Sentinel und ohne Commit -> Stopp (exit 2). Letzte Antworten: $JSON_HIST"
                        exit 2
                    fi
                else
                    stall_count=0
                    last_head="$new_head"
                    log "Turn fertig, neuer Commit ${new_head:0:7} -> Push."
                fi
                push_rc=0
                push_out=$(su - AGENTUSER -c "cd '$PROJECT' && $PUSH_CMD" 2>&1) || push_rc=$?
                push_tail=$(printf '%s\n' "$push_out" | tail -1)
                if (( push_rc == 0 )); then
                    if printf '%s' "$push_out" | grep -qi "Nichts zu pushen"; then
                        log "Auto-Push: No-op (keine neuen Commits)."
                    else
                        log "Auto-Push OK: $push_tail"
                    fi
                else
                    log "Auto-Push fehlgeschlagen (kein Blocker): $push_tail"
                fi
            fi
            ;;
        error_max_turns)
            log "Cap-Overflow -> naechster Poke."
            ;;
        error_rate_limited|rate_limited)
            log "Rate-Limit erreicht."
            reset_epoch=$(get_reset_epoch)
            if [[ -n "$reset_epoch" ]]; then
                now=$(date +%s)
                sleep_secs=$(( reset_epoch - now + RESET_MARGIN ))
                if (( sleep_secs > 0 )); then
                    log "Sleep ${sleep_secs}s bis Reset."
                    sleep "$sleep_secs"
                fi
            else
                log "[ANNAHME] resets_at nicht im Poke-JSON gefunden -> tail -1 $JSON_HIST ansehen, Feldnamen in get_reset_epoch eintragen. Fallback: sleep 3600s."
                sleep 3600
            fi
            log "Reset abgewartet -> naechster Poke."
            ;;
        *)
            log "Unbekannter subtype '$subtype' -- Stopp."
            cat /tmp/watcher-poke-err.txt >> "$LOG" 2>/dev/null || true
            exit 1
            ;;
    esac

    sleep 2
done

12.3 Die drei Stellen, an denen die meiste Arbeit steckt

  1. Der Standing Command (Zeile mit STANDING_CMD). Er trägt das Auswahlprädikat, das Berührungsverbot und die Sentinel-Anweisung. Rund die Hälfte aller Nachbesserungen betraf diesen einen String.
  2. Der Erfolgszweig mit der Reihenfolge: erst Fehlerflag prüfen, dann Sentinel, dann Fortschritt gegen die Versionskennung. Diese Reihenfolge ist nicht beliebig, sie ist das Ergebnis von Abschnitt 6.4.
  3. parse_reset_epoch. Die Uhrzeit kommt als Klartext, die Zeitzone ist die des Systems. Der Rückfallpfad (feste Wartezeit) ist Pflicht, sonst hängt der Loop bei einem unerwarteten Format.

Anhang B: Beispielhafte Aufgabenliste

Die Datei liegt im Projekt und ist die einzige Arbeitsquelle des Loops. Sie zeigt drei Aufgaben in drei verschiedenen Zuständen, damit das Prädikat aus Abschnitt 4 sichtbar wird.

# tasks.md

Format der Tabellenzeile: `| STATUS | ID | Titel |`
Statusvokabular: `offen` `in arbeit` `erledigt` `geplant` `entfällt`
Modusvokabular: `Plan-Mode` `Auto + Stopp` `Direkt` `Loop` `Goal-Mode`
Autonom bearbeitbar sind ausschließlich: Status `offen` oder `in arbeit`
in Verbindung mit Modus `Direkt`, `Loop` oder `Goal-Mode`.

## Übersicht

| Status   | ID    | Titel                                             |
|----------|-------|---------------------------------------------------|
| erledigt | T-101 | Datenzugriffsschicht für Kundenliste             |
| offen    | T-102 | Kundenliste als Übersichtsseite                  |
| offen    | T-103 | Mandantenfähigkeit im Datenmodell                |

*3 Aufgaben, 1 `erledigt`, 2 `offen`.*

---

### T-101: Datenzugriffsschicht für Kundenliste

- **Status:** erledigt (Datum)
- **Modus:** Loop
- **Absicht:** Lese- und Schreibzugriffe auf die Kundentabelle bündeln.
- **Deps:** keine
- **Akzeptanz:** jede Abfrage filtert nach Mandant, belegt durch einen Test, der
  gegen die ungefilterte Fassung rot läuft; alle Gates grün.
- **Ergebnis (Datum):** Zugriffsschicht mit sechs Testfällen, jeder gegen einen
  zweiten Mandanten geprüft. Nicht enthalten: die Oberfläche, das ist T-102.

### T-102: Kundenliste als Übersichtsseite

- **Status:** offen
- **Modus:** Loop
- **Absicht:** Die Kunden des aktiven Mandanten anzeigen, anlegen, bearbeiten,
  löschen.
- **Deps:** T-101 (erledigt)
- **Schreibpfad:** über die Zugriffsschicht aus T-101, kein direkter Zugriff.
- **Akzeptanz:** Liste zeigt ausschließlich Kunden des aktiven Mandanten
  (Test rot gegen ungefiltert); Anlegen und Ändern bleiben nach Neuladen
  erhalten; Löschen eines Kunden mit offenen Belegen wird abgewiesen und die
  Ablehnung ist sichtbar, kein stiller Fehler; alle Gates grün.

### T-103: Mandantenfähigkeit im Datenmodell

- **Status:** offen
- **Modus:** Plan-Mode
- **Absicht:** Mandantenspalte in allen Tabellen, Migration, Fremdschlüssel.
- **Deps:** keine
- **Warum nicht autonom:** Schemaänderung mit Migration. Ein Fehler fällt hier
  nicht durch einen Test auf, sondern erst im Betrieb an den Daten. Entwurf und
  Freigabe durch einen Menschen, danach Umsetzung.
- **Akzeptanz:** Migration ist vorwärtsgerichtet und ohne Datenverlust; jede
  Tabelle trägt die Mandantenspalte; Zugriff über Mandantengrenzen hinweg
  scheitert, belegt durch je einen Test pro Tabelle.

Wie der Loop diese Liste liest: Greifbar ist genau T-102. T-101 ist erledigt, also fertig. T-103 trägt zwar den Status offen, aber den Modus Plan-Mode, ist also für den autonomen Betrieb gesperrt und bleibt liegen. Ist T-102 abgeschlossen, findet der Agent keine greifbare Aufgabe mehr und gibt den Sentinel aus, woraufhin sich der Loop beendet.

Die drei Zählebenen (Statuszeile in der Tabelle, Detailblock, Zählzeile) müssen übereinstimmen. Eine automatisierte Konsistenzprüfung, die genau das vergleicht und beim Auseinanderlaufen rot wird, ist dringend zu empfehlen: Zahlen in einer Zählzeile driften lautlos, und ein Statuswort, das der Parser nicht erfasst, lässt eine Aufgabe unbemerkt aus der Prüfsicht verschwinden.


Anhang C: Start des Loops

14.1 Beaufsichtigter Kurzlauf zum Testen

# Schritt 1: Syntaxpruefung, immer zuerst
bash -n ./watcher.sh && echo PARSE-OK

# Schritt 2: Leerlauftest. Erwartung: Sentinel und Ende ohne jede Aenderung.
# Kleines Turn-Budget genuegt hier nicht immer, siehe Abschnitt 6.5:
# das Budget muss reichen, damit der Agent die Liste lesen UND antworten kann.
./watcher.sh /pfad/zum/projekt 12

# Schritt 3: erster echter Lauf mit einer Aufgabe, beaufsichtigt.
# Kleines Budget erzwingt hier absichtlich mehrere Fortsetzungen,
# damit der Resume-Pfad einmal beobachtet wird:
POKE_TIMEOUT=3600 ./watcher.sh /pfad/zum/projekt 2

14.2 Unbeaufsichtigter Lauf

# In einer abloesbaren Terminalsitzung starten, damit ein
# Verbindungsabbruch den Lauf nicht beendet:
tmux new -s harness

POKE_TIMEOUT=3600 STALL_LIMIT=5 POKE_LIMIT=0   ./watcher.sh /pfad/zum/projekt 12 2>&1 | tee /tmp/harness-lauf.log

# Ctrl-b d loest die Sitzung ab, der Loop laeuft weiter.
# Spaeter wieder ansehen:  tmux attach -t harness

14.3 Die Stellschrauben

GrößeWirkungEmpfehlung
zweites Argument (MAX_TURNS)Werkzeugrunden je Pokeproduktiv großzügig, klein nur zum Testen des Resume-Pfads
POKE_TIMEOUTAbbruch eines einzelnen Pokesgroßzügig, sonst wird ein laufender Bauschritt gekillt
STALL_LIMITerfolgreiche Pokes ohne Fortschritt bis zum Abbruchdrei bis fünf
POKE_LIMITharte Obergrenze aller Pokes0 (aus) im Normalbetrieb, gesetzt bei Experimenten

14.4 Während und nach dem Lauf

Während des Laufs geben das Watcher-Protokoll und die Antwort-Historie Auskunft. Nach dem Lauf lässt sich die Sitzung anhand ihrer Kennung, die in jeder Protokollzeile steht, im interaktiven Modus der CLI wieder öffnen und vollständig durchsehen. Während der Loop läuft, sollte man das nicht tun.



Anhang D: Glossar

Die Begriffe sind nach Themenfeldern geordnet, nicht alphabetisch: Wer den Loop verstehen will, liest von oben nach unten. Jede Karte nennt die Bedeutung, den Grund, warum der Begriff in diesem Dokument zählt, und wo nötig die häufigste Verwechslung.

15.1 Betriebsarten und Grundmechanik

Headless

Kurz: Betrieb eines Programms ohne Bildschirmoberfläche, gesteuert ausschließlich über Aufrufparameter und Rückgabewerte. Warum es zählt: Nur so kann ein Skript einen Agenten starten und sein Ergebnis auswerten. Eine interaktive Oberfläche müsste man vom Bildschirm ablesen, was nicht auditierbar ist. Verwechslungsgefahr: Headless heißt nicht “unbeobachtbar”. Der Loop ist zeitversetzt vollständig einsehbar, siehe Abschnitt 8.

Kurz: Die Betriebsart einer Agenten-Kommandozeile, in der ein Auftrag übergeben, abgearbeitet und eine strukturierte Antwort zurückgegeben wird, worauf der Prozess endet. Warum es zählt: Er liefert die drei Dinge, die ein Loop braucht: programmatischen Start, maschinenlesbares Ende, Turn-Budget.

Poke

Kurz: Ein einzelner Anstoß des Agenten durch den Watcher, also ein vollständiger Start-Arbeit-Ende-Zyklus. Warum es zählt: Der Poke ist die Recheneinheit des Loops. Alle Kennzahlen (Verbrauch, Fortschritt, Stillstand) werden je Poke gemessen. Merkhilfe: Ein Poke ist das automatisierte Äquivalent zu einem Menschen, der “mach weiter” tippt.

Standing Command

Kurz: Der immer gleiche Auftragstext, den der Watcher bei jedem Poke übergibt. Warum es zählt: Er ist die einzige Stelle, an der festgelegt wird, was der Agent darf. Rund die Hälfte aller Nachbesserungen betraf diesen Text. Siehe: Abschnitt 3.

Sentinel

Kurz: Eine vereinbarte Zeichenkette, die der Agent ausgibt, wenn es nichts mehr zu tun gibt, und auf die der Watcher prüft. Warum es zählt: Sie ist das einzige Abschlusssignal, das der Loop versteht. Deshalb muss sie als einzige Ausgabe verlangt werden, sonst geht sie in einer Begründung unter.

15.2 Zeit, Kontingent und Grenzen

Turn

Kurz: Eine Werkzeugrunde des Agenten, etwa eine Datei lesen, einen Test starten, eine Datei schreiben. Verwechslungsgefahr: Ein Turn ist kein Dialogschritt mit einem Menschen. Eine einzige Aufgabe verbraucht oft Dutzende Turns.

Turn-Budget

Kurz: Obergrenze der Werkzeugrunden je Poke. Warum es zählt: Zu klein gewählt, wird der Agent abgeschnitten, bevor er antworten kann, und ein Test misst dann etwas anderes als beabsichtigt. Zu klein ist außerdem teuer, weil jeder Poke Anlaufkosten hat. Siehe: Abschnitte 6.5 und 6.6.

Rate-Limit

Kurz: Anbieterseitige Begrenzung des Verbrauchs innerhalb eines Zeitfensters. Warum es zählt: Es ist der Grund, warum es diesen Loop gibt.

Session-Limit

Kurz: Eine besondere Ausprägung der Verbrauchsgrenze, die die laufende Sitzung stoppt, bis ein Zurücksetzungszeitpunkt erreicht ist. Warum es zählt: Es kam in der Messung getarnt an, nämlich als erfolgreicher Lauf mit gesetztem Fehlerflag, und der Zeitpunkt stand nur im Klartext. Wer nur auf einen Fehlertyp prüft, übersieht es. Siehe: Abschnitt 6.4.

Reset

Kurz: Der Zeitpunkt, zu dem ein verbrauchtes Kontingent wieder bereitsteht. Warum es zählt: Bis dorthin schläft der Loop, statt abzubrechen. Genau diese Brücke unterscheidet ihn von einer simplen Wiederholschleife.

Kontext-Kompaktierung

Kurz: Das Zusammenfassen älterer Gesprächsteile, wenn der Kontext eines Modells voll läuft. Warum es zählt: Sie ist der Grund, warum eine im Gespräch gesetzte Grenze keine Autorisierungsgrundlage ist. Was zusammengefasst wurde, wirkt nicht mehr zuverlässig. Siehe: Abschnitt 7 Punkt 3.

15.3 Steuerung und Sicherungen

Prädikat (Auswahlprädikat)

Kurz: Die logische Bedingung, die entscheidet, welche Aufgaben der Agent anfassen darf. Warum es zählt: Zweidimensional formuliert (Status und Modus) macht es aus einer Ermessensfrage einen Feldwert. Siehe: Abschnitt 4.

Stillstands-Wächter

Kurz: Eine Abbruchbedingung, die greift, wenn mehrere erfolgreiche Pokes nacheinander weder den Sentinel liefern noch eine neue Version erzeugen. Warum es zählt: Ohne ihn läuft ein Leerlauf unbegrenzt weiter und verbrennt Kontingent ohne Ergebnis. Verwechslungsgefahr: Er darf nicht anspringen, wenn der Agent nur auf einen Reset wartet. Deshalb wird zuerst das Fehlerflag geprüft.

Fail-safe

Kurz: Verhalten, das im Fehlerfall in den ungefährlichen Zustand fällt, statt zu blockieren oder abzubrechen. Beispiel hier: Lässt sich der Zurücksetzungszeitpunkt nicht aus dem Text lesen, wartet der Loop eine feste Zeit, statt zu enden.

Exit-Code

Kurz: Der Rückgabewert eines Programms, null bedeutet Erfolg. Warum es zählt: Verschiedene Abbruchgründe bekommen verschiedene Werte, damit eine Überwachung unterscheiden kann, ob der Loop fertig wurde oder aufgab.

Sandbox

Kurz: Eine eingeschränkte Ausführungsumgebung, in der ein Prozess nur auf das zugreifen kann, was er braucht. Warum es zählt: Der Agent läuft unter einem eigenen, unprivilegierten Konto. Als eine Reichweiten-Regel verletzt wurde, blieb der Schaden dadurch auf den Arbeitsbaum begrenzt.

15.4 Versionskontrolle und Publikation

Arbeitsbaum

Kurz: Der Stand der Dateien im Projektverzeichnis, also das, was noch nicht in die Versionsgeschichte aufgenommen wurde. Warum es zählt: Änderungen im Arbeitsbaum sind mit einem Befehl zurücknehmbar. Genau deshalb ist ein Commit-Gate eine wirksame Sicherung.

Commit

Kurz: Ein festgeschriebener, benannter Stand in der lokalen Versionsgeschichte. Warum es zählt: Ein neuer Commit ist das einzige Fortschrittssignal, das der Watcher mechanisch prüft.

Publizieren

Kurz: Das Übertragen lokaler Commits auf das gemeinsame Remote. Verwechslungsgefahr: “Committet” ist nicht “publiziert”. Beide Richtungen des Irrtums sind aufgetreten, siehe Abschnitt 6.7.

Remote

Kurz: Die gemeinsam genutzte Ablage der Versionsgeschichte außerhalb des Arbeitsplatzes. Warum es zählt: Ihr Stand wird unabhängig abgefragt und nie aus dem lokalen Zustand abgeleitet.

Append-only

Kurz: Eine Ablage, die nur Hinzufügen erlaubt, kein Überschreiben und kein Löschen von Historie. Warum es zählt: Der Agent kann Vergangenheit nicht umschreiben, auch nicht versehentlich. Korrekturen sind zusätzliche Einträge.

Force-Push

Kurz: Ein Publikationsvorgang, der die Historie am Remote überschreibt. Warum es zählt: Er ist der gefährlichste Einzelbefehl im Umfeld eines autonomen Agenten und wird serverseitig gesperrt, nicht per Absprache.

15.5 Qualität und Nachweis

Gate

Kurz: Eine automatisierte Prüfung, die bestanden werden muss, bevor ein Stand als fertig gelten darf, etwa Tests, Typprüfung, Linter, Build. Warum es zählt: Gates sind der deterministische Teil der Fertig-Entscheidung. Programme urteilen, nicht Formulierungen. Verwechslungsgefahr: Notwendig, aber nicht hinreichend. Siehe 5.3.

Akzeptanzkriterium

Kurz: Die Bedingung, unter der eine Aufgabe als erfüllt gilt. Warum es zählt: Nur wenn es als Test formuliert ist, wird aus einer Absicht ein prüfbarer Sachverhalt. Siehe: Abschnitt 5.1.

Regressionstest

Kurz: Ein Test, der sicherstellt, dass ein behobener Fehler nicht zurückkehrt. Warum es zählt: Er ist nur dann etwas wert, wenn er gegen die alte Fassung rot gelaufen ist und das berichtet wurde. Ein Test, der nie rot war, beweist nichts.

Determinismus

Kurz: Gleiche Eingabe, gleiches Ergebnis. Warum es zählt: Ein Sprachmodell ist nicht deterministisch. Die Lösung besteht nicht darin, es dazu zu machen, sondern darin, die Entscheidung “fertig” an deterministische Instanzen zu hängen.

Drift

Kurz: Das langsame Auseinanderlaufen zweier Angaben, die übereinstimmen müssten, etwa eine Zählzeile und die tatsächliche Verteilung. Warum es zählt: Drift ist lautlos. Nur eine automatisierte Konsistenzprüfung macht sie sichtbar.

Audit-Trail

Kurz: Die lückenlose, nachträglich prüfbare Spur dessen, was geschehen ist. Warum es zählt: Bei autonomer Arbeit ersetzt er die Beobachtung in Echtzeit. Vier Schichten, siehe Abschnitt 8.

15.6 Formate und Werkzeuge

JSON

Kurz: Ein textbasiertes Datenformat aus benannten Feldern. Warum es zählt: Die Antwort jedes Pokes kommt so, weshalb sie sich maschinell auswerten lässt statt per Textsuche im Bildschirminhalt.

JSONL

Kurz: Eine Datei, die je Zeile ein vollständiges JSON-Objekt enthält. Warum es zählt: Ideal für eine fortlaufende Antwort-Historie, weil Anhängen genügt. Überschreiben statt Anhängen kostete uns genau die Antwort, in der ein Fehler protokolliert war.

Epoch-Zeit

Kurz: Zeitangabe als Sekunden seit einem festen Nullpunkt. Warum es zählt: Rechnen mit Zeit ist damit trivial. Der Loop rechnet die im Klartext gelesene Uhrzeit zuerst in dieses Format um.

Ablösbare Terminalsitzung

Kurz: Eine Sitzung, die weiterläuft, wenn die Verbindung getrennt wird, und später wieder betreten werden kann. Warum es zählt: Ohne sie beendet ein Verbindungsabbruch den Loop. Grenze: Sie überlebt keinen Neustart des Rechners. Für Dauerbetrieb braucht es eine Dienst-Einheit mit Neustart-Regel.

Idempotenz

Kurz: Eine Operation, die mehrfach ausgeführt dasselbe Ergebnis liefert wie einmal ausgeführt. Warum es zählt: Der Publikationsschritt läuft nach jedem Poke. Ohne Idempotenz wäre das gefährlich, so ist es folgenlos, wenn es nichts zu publizieren gibt.


Diese Notiz beschreibt ein Betriebsmuster, keine Software. Sie enthält bewusst keine Angaben zu Hosts, Konten, Adressen oder Verzeichnisstrukturen einer konkreten Installation; solche Details gehören in eine interne Betriebsdoku.

Nichts verpassen

Neue Research-Artikel per E-Mail, sobald sie erscheinen.