31 Commits

Author SHA1 Message Date
Hitonabi 76d2d7c74b deploy: neues Curator-Monitoring-Skript mit 7-Tage-Schwellenwert
Ampel / ampel (push) Failing after 22s
2026-08-02 04:04:21 +02:00
Hitonabi 7638f0244d fix(cron): correct implementation for cron spam prevention via wrapper script and deploy origin
Ampel / ampel (push) Failing after 21s
2026-07-27 17:12:39 +02:00
Hitonabi 4985977492 fix(cron): use --name flag for hermes cron create
Ampel / ampel (push) Failing after 22s
2026-07-27 17:10:43 +02:00
Hitonabi f65ff48599 fix(cron): pipe alle nächtlichen crons durch notify.sh zur verhinderung von telegram spam
Ampel / ampel (push) Failing after 22s
2026-07-27 17:09:26 +02:00
Hitonabi 87f446c5ae feat(pruefstand): Etappe E5 Autonomie für Auto-Adoption und täglichen Rhythmus
Ampel / ampel (push) Failing after 22s
2026-07-26 21:28:50 +02:00
Hitonabi f524ef4375 chore(prompt): Morgen-Digest detailreicher und gehaltvoller gemacht
Ampel / ampel (push) Successful in 22s
2026-07-26 21:16:02 +02:00
Hitonabi 605c9d7dd1 feat: Morgen-Digest für gebündelte nächtliche Telegram-Nachrichten
Ampel / ampel (push) Successful in 22s
2026-07-26 21:10:31 +02:00
Hitonabi 9791f44644 UI: GovernorCard aus dem Cockpit entfernt
Ampel / ampel (push) Successful in 22s
2026-07-26 21:03:47 +02:00
Hitonabi 1e3e0066af Governor: Hard ceiling respects tool chains (OpenCode fix)
Ampel / ampel (push) Successful in 23s
2026-07-26 20:50:45 +02:00
Hitonabi 2365f7aac6 Warm-Waechter lief 11 Tage im Leerlauf: Reranker war nie erreichbar
Ampel / ampel (push) Successful in 22s
Beim Nachsehen wegen einer haengenden Gitea-Karte im Steward-Log gefunden:
600 vergebliche warmup.sh-Laeufe in 24 h, alle 90 Sekunden. Aelteste Meldung
dieser Art: 15.07.2026 — also elf Tage, nicht erst seit dem Umbau.

Ursache: Der Reranker steckt in der ko-residenten Gruppe `brains` und gilt dem
Waechter damit als warm-pflichtig. Er antwortet aber NUR auf /v1/rerank — und
genau diesen Endpunkt kannte warmup.sh nicht. Das Modell konnte also nie warm
werden, der Waechter sah es ewig als "fehlend" und rief alle 90 s ein Skript auf,
das daran nichts aendern konnte. Mit Devstral in derselben Gruppe habe ich die
Falle vorgestern verdoppelt.

Zwei Korrekturen:
1. warmup.sh waermt jetzt auch Reranker (MC_WARMUP_RERANK, Default "reranker").
2. warmer.py bekommt MC_WARMSET als Override der Gruppen-Ableitung. Ko-Residenz
   ("duerfen gleichzeitig liegen") und Warm-Pflicht ("muessen immer liegen") sind
   zwei verschiedene Aussagen; die Gruppe kann nur die erste ausdruecken. Seit
   Coder (TTL 90 min) und Kritiker (TTL 30 min) in `brains` liegen, haette der
   Waechter sonst gegen ihre TTLs gearbeitet und nachts 62 GB wieder hochgeladen.

Steward bekommt MC_WARMSET = embed + reranker + hermes (Drop-in auf der Box).
Falle dabei, live erlebt: systemd trennt `Environment=` an Leerzeichen — ohne
Anfuehrungszeichen kam nur das erste Modell an und das Warm-Ziel war still zu
klein. Der erste Fix sah deshalb erfolgreich aus (Schleife weg), war aber nur
eine kaputte Messung. Jetzt gequotet und im Prozess-Environ geprueft.

Belegt: alle drei Ziel-Modelle warm, 0 vergebliche Ausloesungen des neuen
Prozesses, Coder geladen aber korrekt kein Warm-Ziel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:13:26 +02:00
Hitonabi 56b374a0aa Ideen: Auswahl war bei leerem Feld unsichtbar
Ampel / ampel (push) Successful in 22s
User: "dein Umbau ist immer noch nicht da. Oder denke ICH falsch?" — er dachte
nicht falsch. Der ganze Auswahl-Block hing an `text.trim().length >= 3`, also
sah man bei leerem Eingabefeld NICHTS davon. Kein Hinweis, dass es ueberhaupt
eine Wahl gibt, keine Moeglichkeit sie vorher einzustellen, und nach jedem
Neuladen war alles wieder weg.

Meine Begruendung war, die Auswahl sei eine Entscheidung ZU dem Text und habe
ohne Text nichts zu suchen. Das ist logisch sauber und praktisch falsch: ein
Mechanismus, den man nicht sieht, existiert fuer den Nutzer nicht.

Jetzt immer sichtbar. Ohne Einordnung steht davor "VOREINGESTELLT" — die Zeile
ist dann eine Einstellung, keine Aussage ueber einen Text, den es noch nicht
gibt. Sobald die Box gelesen hat, verschwindet das Wort und das Etikett
"gelesen" erscheint. Aendern kann man beides jederzeit, auch vor dem Tippen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 11:50:00 +02:00
Hitonabi eef7fdb4df Ideen: Kopfzeile beschrieb eine Bedienung, die es nicht mehr gibt
Ampel / ampel (push) Successful in 23s
Zwei Fehler in der prominentesten Zeile der Seite, vom User gefunden:

1. "fuer Zed" statt "für Zed" — verschluckter Umlaut aus dem Anlegen der Seite.
2. "Je nach Modus baut sie es selbst, denkt es nur durch, oder legt dir ein
   startklares Repo an" — die Modi sind seit gestern weg, der Entscheidungsbaum
   ist seit heute zugeklappt. Der Satz erklaerte also eine Bedienung, die es
   nicht mehr gibt. Genau die Sorte Text-Leiche, die entsteht, wenn man die
   Mechanik umbaut und die Beschreibung stehen laesst.

Jetzt: "Schreib hin, was entstehen soll. Die Box liest mit und schlaegt vor, wie
es weitergeht — aendern kannst du das immer." Das ist, was tatsaechlich passiert.

Restliche Oberflaeche gegengeprueft: alle anderen "Modus"-Treffer sind
Variablennamen oder unverwandt (Dev-Modus, Experten-Modus), und ausser diesem
"fuer" gibt es keine weiteren ASCII-Ersatzschreibweisen in sichtbarem Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 11:43:15 +02:00
Hitonabi cd60974ff9 Ideen: Entscheidungsbaum zugeklappt statt Assistent
Ampel / ampel (push) Successful in 24s
Der vorgeschlagene Wizard hatte den richtigen BAUM — Wer baut das?, und nur im
Zed-Ast: wie startest du? — aber den falschen Mechanismus fuer eine taegliche
Zehn-Sekunden-Handlung. Ein Assistent haette gefragt, was der Einordner in
~500 ms schon weiss.

Jetzt: derselbe Baum, zugeklappt.
  Normalfall  Textfeld + EINE Zeile ("Du baust selbst — ich arbeite erst ein
              Konzept aus.") + Enter. Nichts steht zwischen dir und der Idee.
  "anders"    klappt genau die Wizard-Reihenfolge auf: erst Wer baut das?, und
              NUR im Zed-Ast eingerueckt darunter Wie startest du?
              Bei "die Box" folgt keine Rueckfrage — dann geht es direkt los.

Das Etikett heisst "gelesen" und erscheint nur, wenn die Box den Text wirklich
eingeordnet hat. Ohne Einordnung steht dort die Voreinstellung, und die darf sich
nicht als Verstaendnis ausgeben.

Der Entwurf steht und faellt damit, dass der Einordner meistens richtig liegt —
5 von 5 im Test ist ein huebsches, aber winziges Ergebnis. Deshalb zaehlt die
Seite jetzt mit, wie oft "anders" geklickt wird (localStorage, zwei Zahlen, kein
Endpunkt) und zeigt die Quote beim Aufklappen. Bleibt sie unter etwa jedem
fuenften Mal, war der Weg richtig; liegt sie darueber, bauen wir den Assistenten
mit Daten statt mit Meinung.

Nebenbei: das jetzt tote `vorgeschlagen`-Prop aus <Wahl> entfernt (4 Aufrufer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:16:17 +02:00
Hitonabi 20d4f40ed9 Ideen: zwei Bereiche, drei Karten — Durchdenken nach vorn
Ampel / ampel (push) Successful in 24s
User-Befund, und er trifft einen echten Modellierungsfehler von gestern: "Eine
durchgedachte Idee wird ja sowieso mit meinem Ja zu einem IDE-Repo." Stimmt —
/ideen/weiterfuehren macht aus der durchdachten Idee GENAU die IDE-Projekt-Karte,
die der Repo-Weg direkt anlegt. Es sind zwei Stufen EINES Weges, keine zwei Achsen.

Der Haken von gestern war deshalb falsch: "Die Box, allein" + "nur denken" war
anklickbar, submit() setzte dann aber still welt:"ide" und schob einen in die
Zed-Welt, ohne das zu sagen. Genau die versteckte Umschaltung, die anderswo in
diesem Repo als Fassade gilt.

Jetzt GENAU EINE Auswahl aus drei, in zwei Bereichen:
  Du baust selbst, in Zed   [Erst durchdenken] [Direkt ein Repo]
  Oder die Box macht es     [Die Box baut allein]
Die falsche Kombination ist damit strukturell unmoeglich, nicht nur unwahrscheinlich.

Nebengewinn: beim Durchdenken entfallen Tiefe und Ziel am Eingang. Die
Weiter-Leiste fragt beides ohnehin beim "gefaellt mir" — und dann weiss man, was
das Ding ueberhaupt wird. Vorher entschied man LXC-oder-Docker, bevor ein Konzept
existierte.

Ausgewaehlt ist immer eine KARTE, nie ein Bereich — der Bereich gruppiert nur, was
denselben Weg geht. Voreinstellung ist "Erst durchdenken": die Wahl, die am
wenigsten anlegt. Der Einordner ueberschreibt sie ohnehin in ~500 ms.

API unveraendert; alle drei Abbildungen gegen den alten Vertrag geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:08:23 +02:00
Hitonabi cb649aac34 Ideen 2.0: schreiben zuerst, die Box waehlt vor
Ampel / ampel (push) Successful in 24s
Die Seite verlangte eine technische Weichenstellung, BEVOR man gesagt hat was man
will — mit Begriffen aus dem Maschinenraum (BOX / IDE: SIMPEL / IDE: GRUENDLICH).
Deshalb klebten 116 Woerter Beipackzettel unter einem einzigen Eingabefeld.

Zwei unabhaengige Entscheidungen, vorher in vier sich ausschliessende Knoepfe gepresst:
  wer       — die Box allein, oder ein Repo fuer Zed
  nurDenken — erst nur ein Konzept (passt zu beidem, jetzt ein Haken)
Die Knoepfe nennen das ERGEBNIS statt des Werkzeugs, und der erklaerende Satz steht
an der Option statt in einer Legende darunter.

Neu: POST /api/ideen/einordnen — das dauerwarme Hirn liest den getippten Text und
waehlt vor (gemessen ~500 ms, 5 von 5 Testfaellen richtig, inkl. "Lohnt sich...?"
-> nur denken). Bewusst ein VORSCHLAG mit sichtbarem Etikett: er setzt nur die
Auswahl, die ohnehin dasteht. Faellt er aus, bleibt schlicht die Voreinstellung —
das Tippen wird nie blockiert, es gibt keinen Ladebalken und keine Fehlermeldung.

Tiefe und Ziel erscheinen nur noch beim Repo-Weg, weil sie nur dort wirken. Ein
Schalter der nichts tut ist schlimmer als keiner.

Leerer Zustand zeigt drei anklickbare Beispiele statt "wirf der Box eine Idee zu".
Kopfzeile nennt nur noch den Zustand — "Ideen" stand dreimal uebereinander.

Die API bleibt unveraendert; die Abbildung passiert in submit().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:58:51 +02:00
Hitonabi d18aac0e1b Persoenliche Arbeitsregeln erreichen endlich den Coding-Agenten
Ampel / ampel (push) Successful in 25s
Befund beim Nachsehen wegen Zeds "Skills Support"-Ankuendigung: die 46 Zeilen
Arbeitsregeln in AppData\Roaming\Zed\AGENTS.md haben OpenCode NIE erreicht.
Zeds Doku ist eindeutig — AGENTS.md und Skills gelten nur fuer Zeds NATIVEN
Agenten, nicht fuer ACP-Agenten wie OpenCode. Hier wird ueber ACP gearbeitet.
Gute Regeln (Plan vor Code, kleine Schritte, beweisen statt behaupten,
Faulheits-Leiter, Kontext-Waechter) liefen also ins Leere.

Sie liegen jetzt in ~/.config/opencode/AGENTS.md — auf dem PC UND auf der Box,
also am Tag in Zed und nachts im Cron. Vorlage im Repo unter deploy/opencode/.

Beim Umzug korrigiert:
- Zeds eingebaute Commit-Vorlage entfernt. Sie war in dieselbe Datei gerutscht
  und sagt "Only return the commit message in your response" — in dauerhaft
  aktiven Regeln ein Fehlerherd.
- "Zum Bauen oben auf Coder umschalten" gestrichen: seit der Mannschafts-
  Umstellung laufen plan und build auf demselben Modell. Das war MEINE
  Regression aus Stufe 4, hier faellig geworden.
- Kontext-Waechter auf den Governor umgeschrieben (der kennt den Fuellstand
  wirklich, Zeds Anzeige war Heuristik).
- Neu: Pruef-Tor (VERIFY, inkl. "Test abschwaechen gilt als Betrug"),
  die Subagenten-Mannschaft, der Werkzeug-Zaun und ein Abschnitt fuer
  UNBEAUFSICHTIGTE Laeufe — nachts sagt niemand "los", dort darf der Agent
  nicht auf Freigabe warten.

Bewiesen: in einem leeren Ordner (nur globale Regeln wirksam) antwortet der
Agent wortgenau aus der neuen Datei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:38:08 +02:00
Hitonabi 6c14705798 Ideen: fertige Karten sammelweise wegraeumen
Ampel / ampel (push) Successful in 22s
Einzeln ging es schon (X je Karte). Bei fuenf abgeschlossenen Projekt-Ketten und
18 fertigen Karten waren das aber ~18 Klicks — praktisch raeumt dann niemand auf,
und die drei relevanten Karten gehen in der Liste unter.

Neu:
  POST /api/ideen/archivieren-alle {projekt?}  -> services.archive_done()
  Kopfzeile:      "18 fertige wegraeumen" (alle, auch die in Ketten)
  Je Projekt:     "wegraeumen" neben "N fertig"

Sicherheitseigenschaft, bewusst im Dienst statt in der UI: archive_done fasst
AUSSCHLIESSLICH status=done an — laufende, haengende, wartende und in Ausarbeitung
befindliche Karten nie, auch nicht ohne Projekt-Filter. Ein Sammel-Knopf darf
niemals versehentlich Arbeit abraeumen. Der Dialog sagt ausserdem ehrlich, dass
nichts geloescht wird: es ist Hermes' Kanban-Archiv, die Karten bleiben lesbar.

Getestet ohne Nebenwirkung: Filter auf ein nicht existierendes Projekt liefert
archiviert=0, die 19 Karten der Queue bleiben unangetastet. Das echte Wegraeumen
ist ein User-Klick, kein Deploy-Schritt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:54:35 +02:00
Hitonabi 991c9a2f82 Ideen-Queue bekommt eine eigene Seite
Ampel / ampel (push) Successful in 22s
Die Queue war 703 der 1147 Zeilen in AuftragsbuchView — die MEHRHEIT einer Seite,
die nach etwas anderem benannt ist. Und sie laufen gegenlaeufig: Auftragsbuch =
die Box schlaegt DIR vor (Eingang), Ideen = DU beauftragst die Box (Ausgang).
Seit dem Umbau ist Idee -> Konzept -> Repo -> Zed der Haupteingang des Systems
und gehoert in die Seitenleiste, nicht in einen Unterabschnitt.

Umzug (reines Verschieben, kein Logik-Umbau):
  AuftragsbuchView  1147 -> 406 Zeilen   Bundle 51,0 -> 17,8 KB
  IdeenView         neu, 785 Zeilen      Bundle 32,7 KB, laedt nur bei Bedarf
  fmtWhen           -> lib/format.ts (dessen Kopf genau davor warnt: "vorher in
                      einzelnen Views dupliziert")
  SectionLabel      -> components/ui/ (beide Seiten brauchen sie jetzt)

Zwei inhaltliche Aenderungen:
1. Die flache Einzelkarten-Liste wird gruppiert — mit der Ordnung, die
   ProjektGruppe fuer Ketten LAENGST benutzt (AKTIV_RANG): aktiv zuerst,
   Wartende und Fertige eingeklappt. Bei 19 Karten, davon 16 fertig, gingen die
   drei relevanten in der Erstellzeit-Liste unter.
2. Die Kopfzeile sagt jetzt "1 braucht dich · 2 in Arbeit · 16 fertig" statt
   einer nackten Gesamtzahl.

Das Auftragsbuch zaehlte im gruenen "Nichts zu entscheiden" bisher haengende
Ideen mit, damit es nicht luegt, waehrend direkt darunter eine Karte haengt.
Nach der Trennung wird daraus ein anklickbarer Querverweis auf die Ideen-Seite —
dieselbe Ehrlichkeit, jetzt mit Weg dorthin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:41:10 +02:00
Hitonabi ad49dd65ca Update-Kette repariert: Boot-Warmliste zurueckgenommen + Tool-Smoke mit Retry
Ampel / ampel (push) Successful in 22s
MEINE REGRESSION. Das systemd-Drop-in vom Umbau setzte
MC_WARMUP_MODELS="fast coder kritiker". warmup.sh waermt aber erst ALLE Chat-
Modelle, dann Embeddings und ERST DANN Hermes' ~70-KB-Agenten-Prompt — genau das,
was der Tool-Smoke braucht. Coder (48 GB) + Kritiker davorzuschieben kostete ~35 s
und trieb den Tool-Smoke ueber sein 90-s-Limit: Job faf1137a262c FAILED mit
"Tool-Smoke ohne verwertbares Ergebnis" (leere Antwort = curl-Timeout), obwohl
Agent und Modelle in Ordnung waren. Warm gemessen dauert derselbe Aufruf 12 s.

Zwei Korrekturen:
1. Boot-Warmliste zurueck auf "fast" (Drop-in auf der Box). Lucys Hirn und der
   Agenten-Prompt haben Vorrang; der Coder waermt beim ersten Auftrag selbst
   (23 s einmalig, dann 90 min warm).
2. Tool-Smoke bekommt 3 Versuche + 120 s statt 1 Versuch + 90 s — dieselbe Lehre,
   die der Voice-Smoke zehn Zeilen tiefer laengst umgesetzt hatte ("erster Turn
   zahlt den Session-Kaltstart"). Ein Versuch war ein Fehlalarm-Generator.

Bewiesen: llama-swap kalt neu gestartet, Postcheck sofort danach -> GEHIRN OK,
Tool-Smoke im ersten Versuch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:19:30 +02:00
Hitonabi c28aa4a6a2 README aktualisiert + Kritiker auf 16k gedeckelt (Prefill-Befund)
Ampel / ampel (push) Successful in 22s
README war auf dem Stand "Final-Version eingefroren" und kannte weder Governor,
Coding-Bahn, CI-Ampel noch den Weg einer Idee. Neu geschrieben mit den echten
Ports/Diensten, den gemessenen Modell-Rollen und den vier Qualitaets-Toren.
Alle Verweise gegen den Baum geprueft.

Dabei aufgefallen: das eigene VERDIKT vom 24.07. ("Devstral bricht beim 32K-Prefill
auf 63 t/s ein") widerlegt meine Begruendung "ein Kritiker liest viel und schreibt
wenig, also stoert die Langsamkeit nicht" — es ist genau das LESEN, das einbricht.
Nachgemessen: Prefill 265-318 t/s (Coder 754). Der Kritiker ist deshalb in
llama-swap UND in beiden opencode.json auf 16384 gedeckelt; schlimmster Fall je
Review jetzt unter einer Minute statt 8+ Minuten.

VERDIKTE um zwei Eintraege ergaenzt: die enge Kritiker-Rolle mit Deckel, und die
llama-swap-Gruppenregel (eine Gruppe resident, persistent:false, OOM-Grenze).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:04:44 +02:00
Hitonabi 8b76c0f749 Ampel-Rot auf main behoben: ungenutzter Import in curator_archive_logic.py
Ampel / ampel (push) Successful in 22s
Der Merge hat die Ampel zum ERSTEN MAL auf `main` laufen lassen — sie kam am
24.07. auf dem Branch dazu und war nie auf main. Sie hat sofort einen echten
Altfehler aus der autonomen Auftragsbuch-Arbeit gefunden: F401, `os` importiert
aber nirgends benutzt (deploy/curator_archive_logic.py:11).

Nicht von diesem Umbau verursacht — aber jetzt sichtbar, und deshalb behoben.
Alle drei Ampel-Gates lokal gruen: ruff · compileall · Frontend-Build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:51:50 +02:00
Hermes Worker a934deee41 Doku: Qwen-AgentWorld-35B als abgelehnt für Vision-Rolle vermerken (t_9daf088f)
Ampel / ampel (push) Failing after 22s
2026-07-25 21:46:07 +02:00
Hitonabi 543eee95d0 Merge governor-phase0: Umbau Stufe 1-5 (Ampel #3 gruen) 2026-07-25 21:45:56 +02:00
Auftragsbuch 00f2b71a63 Auftragsbuch: 'doku/curator-autos' angenommen (Ein-Klick-Gate) 2026-07-25 04:10:55 +02:00
Hitonabi 2cecd06c5b doku: Curator Auto-Archivierung Cron dokumentieren 2026-07-25 03:53:02 +02:00
Auftragsbuch 080eaa5ff5 Auftragsbuch: 'doku/pruefstand-coder-backend-24-07' angenommen (Ein-Klick-Gate) 2026-07-24 16:16:13 +02:00
Auftragsbuch 7dd44a3630 Auftragsbuch: 'feature/curator-auto-archiv' angenommen (Ein-Klick-Gate) 2026-07-24 16:16:12 +02:00
Hitonabi 554db10a6f VERDIKTE: Pruefstand 24.07 — Vulkan bleibt (ROCm-Recheck erledigt), Coder-Next bleibt (Devstral 4-8x langsamer), Reliabilitaet=Harness 2026-07-24 14:44:22 +02:00
Hitonabi ce030d6817 feat: Curator Auto-Archivierung (14-Tage-Inaktivität) 2026-07-24 13:46:49 +02:00
Auftragsbuch 8996c0d012 Auftragsbuch: 'wartung/curator-idle-watchdog' angenommen (Ein-Klick-Gate) 2026-07-24 09:58:38 +02:00
Hitonabi a1de1ec2ac feat: Curator Idle Watchdog implementieren
- Skript deploy/curator-idle-watchdog.sh für 14-Tage-Idle-Erkennung
- Spezifikation deploy/specs/curator-idle-watchdog.md
- Wöchentliche Prüfung (Sonntag 00:00), Trigger bei Überschreitung
2026-07-24 08:53:19 +02:00
65 changed files with 3140 additions and 1591 deletions
+128 -50
View File
@@ -1,72 +1,150 @@
# Mission Control 2.0
Komponierbarer Local-AI-Stack für den Bosgame M5 (Strix Halo). Läuft **live auf der Box**
als sudo-freier systemd-User-Dienst (`:9001`) und ist als **Final-Version eingefroren**
Pflege übernehmen ab jetzt die selbst-wartenden Box-KIs (Auto-Update mit Fangnetz + die
„Werkstatt"), nicht mehr manuelle Releases.
Steuerzentrale des lokalen KI-Stacks auf dem **Bosgame M5** (AMD Ryzen AI MAX+ 395 „Strix Halo",
122 GB Unified Memory, **keine dedizierte GPU** — llama.cpp über **Vulkan/RADV**). Läuft live als
sudo-freier systemd-User-Dienst und ist auf **Autonomie** ausgelegt: Die Box wartet sich selbst,
prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
**Schichten:** Engine (llama.cpp **Vulkan/RADV** auf Strix Halo) · Router (**llama-swap**,
Ko-Residenz-Warm-Set) · **Builtin-Routing-Gateway** in MC2 (`model: auto`, kein externer
LiteLLM — scheitert auf Python 3.14) · Mission Control 2.0 (FastAPI + React/shadcn) ·
**Hermes Agent** (api_server-Gateway :8642, Tools/MCP/Telegram/cron) · auto-lernendes
**Gedächtnis** (Mem0-Sidecar, semantisch). Jede Schicht hinter stabilem Vertrag austauschbar.
> **100 % lokal.** Kein Cloud-Modell, kein externer Dienst im Datenpfad.
> **Sprachassistentin Lucy** (Voice + 3D-Avatar) ist in ein **eigenes Repo** ausgelagert
> (`Hitonabi/lucy`) und spricht über den Hermes-Gateway. Aus MC2 selbst sind die Voice-/
> Web-Reste entfernt — MC2 ist die Steuerzentrale, kein Sprach-Frontend.
## Die drei Bahnen
## Status: **live + eingefroren (MC2 Final)**
| Bahn | Was sie tut | Wo |
|---|---|---|
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
| **Coding** | Zed + OpenCode am Tag, `opencode-lauf.sh` in der Nacht — **ein Regelwerk, zwei Auslöser** | Governor `:8100` |
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
Der ganze Stack ist auf der Box in Betrieb und auf **Autonomie** ausgelegt (Ziel: läuft auch
ohne Betreuung monatelang weiter):
Lucy holt ihr Hirn über MC2, spricht aber nie durch den Governor — ein Gespräch ist keine
Bau-Sitzung und wird nie unterbrochen.
- **Modelle & Routing** — Discover (live HF + Fit/Caps), Engine-Write (register, `groups`/
Ko-Residenz, vocab-geprüfte Spec-Drafts), Gateway `model: auto` + Fallbacks.
- **System & Wartung** — Status (CPU/RAM/GPU/Disk), **ehrliche Speicher-Zahlen** (echte KV-Bytes
aus GGUF-Architektur), Logs mit Fehler-Filter, Dienst-Neustart (sudo-frei).
- **Updates mit Fangnetz** — OS/Engine/Router/Hermes per Timer: Backup → Update → Postcheck →
bei Fehler **Auto-Rollback + Pin**; verständliche Zusammenfassung („Musst du etwas tun?") in
Lucys Stimme. Eigene Software wird eingefroren, nicht geupdatet.
- **Gedächtnis** — Mem0-Sidecar (auto-lernend, semantisch), 4 Kategorien, Auto-Dedupe.
- **Selbsterhaltung** — Health-Wächter (`sentry`), Warm-Set-Wächter (`warmer`), tägliche
Self-Smokes, Lucy-Watchdog + Alarmkette; **Werkstatt** (Hermes wartet den Stack via Gitea-
Branch → Gate → Deploy selbst).
## Ports & Dienste
API: `health · models · discover · fit · models/register · groups · routing · system/* ·
maintenance/* · connect · memory/* · agent/* · voice/announce · reminders/*`. MCP: `mcp/`.
Box-Runbooks: [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md), [`docs/RUNBOOK.md`](docs/RUNBOOK.md),
[`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) + `deploy/` (Units, `deploy.sh`, `backup.sh`, `warmup.sh`).
| Port | Dienst | Erreichbar |
|---|---|---|
| `9001` | **MC2** (FastAPI + React) — Cockpit, API, Konsole | LAN |
| `8100` | **Governor** — Token-Wächter vor dem Gateway | LAN |
| `8080` | **llama-swap** — Modell-Router | LAN |
| `9010` | `mc2-gateway``/v1`-Datenpfad (`model: auto`, Bild-Weiche) | loopback |
| `8642` · `9119` | Hermes-Gateway · Hermes-Web-UI | loopback |
| `8765` · `8650` | Mem0-Sidecar · Voice-Sidecar (STT/TTS) | loopback |
| `7682` | ttyd — Box-Konsole, same-origin unter `/console/` | loopback |
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` · `governor` ·
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
## Der Weg einer Idee
```
Idee in MC2 → Gitea-Repo (mit CI-Ampel + VERIFY) → in Zed bauen → Ampel → main
└── oder nachts: Hermes-Cron → opencode-lauf.sh → dieselben Regeln
```
Jedes neue Repo wird von [`deploy/gitea-repo-create.sh`](deploy/gitea-repo-create.sh) **mit beiden
Wächtern geboren**:
- **`.gitea/workflows/ci.yml`** — die Außen-Prüfung nach dem Push (Gitea Actions)
- **`VERIFY`** — die Innen-Prüfung: eine Zeile, wie man das Projekt testet. Das OpenCode-Plugin
führt sie nach jeder Etappe aus und gibt rote Tests dem Agenten **sofort** zurück, statt sie
erst der CI zu zeigen. Keine `VERIFY`-Datei = Prüf-Tor aus.
## Modelle & Rollen
Gemessen auf der Box (25.07.2026, Vulkan, Q4):
| Rolle | Modell | Tempo | Aufgabe |
|---|---|---|---|
| `coder` | Qwen3-Coder-Next (80B-A3B) | **51,5 t/s** · Prefill **754 t/s** | Plant und baut — Kopf der Coding-Mannschaft |
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6 t/s** | Lucys Hirn **und** der Sucher-Subagent. Immer warm |
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen) |
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen |
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30). **Nie tagsüber** — verdrängt das warme Set |
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
auf dieser bandbreitenbegrenzten Box bricht sein Prefill mit wachsendem Kontext ein (bei 32k
gemessene 63 t/s ≈ **9 Minuten nur zum Lesen**). Mit dem 16k-Deckel bleibt der schlimmste Fall
je Review unter einer Minute. Deshalb ist er der **Gegenleser für Etappen**, nicht der Coder —
als Coder ist er auf dieser Box disqualifiziert (siehe VERDIKTE).
‼️ **Speicher-Regel:** `Warm-Set + größtes On-Demand-Modell ≤ ~115 GB`. Die ko-residente Gruppe
(`groups.brains` in der llama-swap-Config) muss **`persistent: false`** sein — sonst räumt
llama-swap vor einem großen Modell nicht ab und der Kernel schießt Prozesse ab. Details und
Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
## Qualitäts-Tore
1. **Werkzeug-Zaun** ([`deploy/opencode/plugin/`](deploy/opencode/plugin/)) — blockt `git push`,
`rm -rf`, `sudo`, `curl | sh` und Fremd-Hosts im Agentenlauf.
2. **Prüf-Tor**`VERIFY` nach jeder Etappe; rot → der Agent repariert selbst weiter (max. 3 Runden).
3. **Governor** ([`deploy/governor/`](deploy/governor/)) — zählt Tokens ehrlich (aus den echten
`usage.prompt_tokens` jeder Antwort, selbstkalibrierend). Bei ~45.000: Savepoint schreiben und
Sitzung sauber beenden. Bei ~50.000: harter Riegel. Sichtbar als Kachel im Cockpit.
4. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft bei jedem Push.
Gemeinsame Lehre dahinter: **Wissen lebt in Datei + git, nicht im schrumpfenden Chat-Kontext.**
Kontext-Komprimierung löscht still die Regeln mit.
## Selbsterhaltung
- **Health-Wächter** (`sentry`) + **Warm-Set-Wächter** (`warmer`, per Env abschaltbar)
- **Updates mit Fangnetz** — Backup → Update → Postcheck → bei Fehler **Auto-Rollback + Pin**
- **Melde-Briefkasten** (`/api/voice/announce`) — alles, was die Box von sich aus sagen will;
Lucy pollt ihn und spricht es. Absender `loop` = Coding-Ereignisse
- **Gedächtnis** — Mem0-Sidecar, auto-lernend, semantisch, mit Auto-Dedupe
- Tägliche Self-Smokes, Zeitmaschine (Snapshot + Ein-Klick-Restore), Chronik der autonomen Taten
## API (Auswahl)
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
memory/* · agent/* · **governor** · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
## Entwickeln
**Backend:**
```bash
# Backend
cd backend
python -m venv .venv && .venv/Scripts/python -m pip install -r requirements.txt # Windows
.venv/Scripts/python -m uvicorn app:app --port 9000
# Frontend (Dev, proxyt /api → :9000)
cd frontend && npm install && npm run dev # http://localhost:5173
# Frontend (Build → wird vom Backend ausgeliefert)
cd frontend && npm run build # → frontend/dist
```
**Frontend (Dev, proxyt /api → :9000):**
```bash
cd frontend
npm install
npm run dev # http://localhost:5173
```
**Frontend (Build → wird vom Backend ausgeliefert):**
```bash
cd frontend && npm run build # → frontend/dist
```
`frontend/dist` **wird mitcommittet** — die Box hat kein Node; Deploy ist ein `git pull` plus
Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `npm run build`.
## Env-Vars (Auswahl)
| Variable | Default | Zweck |
|---|---|---|
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine |
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap Config |
| `MC_GATEWAY_URL` | `http://127.0.0.1:$MC_PORT` | Builtin-Gateway (Teil von MC2, kein externer Dienst) |
| `MC_PORT` | `9000` | MC-Backend-Port |
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | Aktive Engine-Binary (Vulkan-Build) |
| `MC_ENGINE_REPO` | `ggml-org/llama.cpp` | Quelle für Engine-Update-Check |
| `MC_DRAFTS_DIR` | `$MODELS/drafts` | Spec-Draft-Modelle (vocab-geprüft) |
| `MC_SPEC_TYPE` | `draft-simple` | Speculative-Decoding-Typ (llama.cpp) |
| `MC_PORT` | `9000` | MC-Backend-Port (**die Unit auf der Box setzt `9001`**) |
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine/Router |
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap-Config |
| `MC_V1_UPSTREAM` | | gesetzt → `/v1` geht an `mc2-gateway` (`:9010`) statt in-process |
| `MC_GOVERNOR_URL` | `http://127.0.0.1:8100` | Token-Wächter für die Cockpit-Kachel |
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | aktive Engine (Vulkan-Build) |
| `MC_REWARM_ENABLED` | `1` | Warm-Set-Wächter (auf der Box bewusst `0`) |
| `MC_DRAFTS_DIR` · `MC_SPEC_TYPE` | `$MODELS/drafts` · `draft-simple` | Spekulatives Dekodieren |
Governor-eigene Schalter (`GOV_THRESHOLD`, `GOV_HARD_CEILING`, `GOV_EXEMPT_MODELS`, …):
[`deploy/governor/README.md`](deploy/governor/README.md).
## Weiterlesen
| Dokument | Inhalt |
|---|---|
| [`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) | Wie man MC2 benutzt |
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) · [`docs/DISASTER_RECOVERY.md`](docs/DISASTER_RECOVERY.md) | Betrieb, Störungen, Wiederherstellung |
| [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md) | Hermes-Agent, Profile, Crons |
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
| [`deploy/opencode/README.md`](deploy/opencode/README.md) | Coding-Bahn: Mannschaft, Verteilung, Fallen |
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
+25
View File
@@ -26,6 +26,15 @@ class AntwortIn(BaseModel):
antwort: str
class ArchivAlleIn(BaseModel):
# Leer = alle fertigen Karten der Queue; gesetzt = nur die dieses Projekts.
projekt: str = ""
class EinordnenIn(BaseModel):
text: str
@router.get("/ideen")
def list_queue() -> dict:
return ideen.list_queue()
@@ -114,6 +123,22 @@ def archive(body: TaskIn) -> dict:
return res
@router.post("/ideen/einordnen")
def einordnen(body: EinordnenIn) -> dict:
"""Einordnungs-VORSCHLAG beim Tippen. Wirft nie — scheitert es, kommt `ok: false`
und die Oberfläche behält ihre Voreinstellung."""
return ideen.einordnen(body.text)
@router.post("/ideen/archivieren-alle")
def archive_alle(body: ArchivAlleIn) -> dict:
"""Fertige Karten sammelweise wegräumen. Fasst ausschließlich `done` an."""
res = ideen.archive_done(body.projekt)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Archivieren fehlgeschlagen."))
return res
@router.get("/ideen/{id}/log")
def get_log(id: str) -> dict:
return ideen.log_of(id)
+98
View File
@@ -22,6 +22,9 @@ import subprocess
import time
from pathlib import Path
import httpx
from config import GATEWAY_URL, V1_UPSTREAM
log = logging.getLogger(__name__)
HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
@@ -509,6 +512,101 @@ def archive_task(task_id: str) -> dict:
return {"ok": True}
_EINORDNEN_SYS = (
"Du ordnest eine frisch getippte Projekt-Idee ein. Antworte AUSSCHLIESSLICH mit einem "
"JSON-Objekt, ohne Prosa, ohne Codezaun:\n"
'{"wer":"box"|"zed","tiefe":"schlank"|"gruendlich","nur_denken":true|false}\n\n'
"wer=box: Aufgabe am eigenen Heimlabor des Nutzers — seine Server, seine bestehenden "
"Dienste (Mission Control, Lucy, Rippy), Wartung, Doku, Automatisierung. Die Box baut selbst.\n"
"wer=zed: ein NEUES eigenstaendiges Programm/Werkzeug/App, das der Nutzer danach selbst "
"programmieren will. Er bekommt nur ein vorbereitetes Repo.\n"
"tiefe=gruendlich: gross, vage oder mit vielen offenen Entwurfsfragen. "
"tiefe=schlank: klein und klar umrissen.\n"
"nur_denken=true: der Text stellt eine FRAGE oder waegt ab (\"lohnt sich\", \"waere es sinnvoll\", "
"\"brauche ich\") statt einen Auftrag zu geben."
)
def einordnen(text: str) -> dict:
"""Grober Einordnungs-VORSCHLAG für eine frisch getippte Idee.
Nur eine Vorauswahl in der Oberfläche — der Mensch sieht und überschreibt sie. Deshalb
bewusst anspruchslos: ein einziger kurzer Aufruf ans dauerwarme Hirn (69,6 t/s gemessen),
Schnellspur-Marker gegen das Nachdenken, hartes Timeout. Scheitert irgendetwas, kommt
`{"ok": False}` zurück und die Oberfläche behält schlicht ihre Voreinstellung — eine
Einordnung darf das Tippen NIE blockieren oder eine Fehlermeldung erzeugen.
"""
text = (text or "").strip()
if len(text) < 12:
return {"ok": False}
ziel_url = (V1_UPSTREAM or GATEWAY_URL).rstrip("/")
marker = os.environ.get("MC_NO_THINK_MARKER", "[[MC:NO_THINK]]")
try:
r = httpx.post(f"{ziel_url}/v1/chat/completions", timeout=12.0, json={
"model": "hermes", "temperature": 0, "max_tokens": 60, "stream": False,
"messages": [
{"role": "system", "content": _EINORDNEN_SYS},
{"role": "user", "content": f"{marker} {text[:600]}"},
],
})
r.raise_for_status()
inhalt = (r.json()["choices"][0]["message"]["content"] or "").strip()
except Exception:
log.debug("ideen: einordnen fehlgeschlagen", exc_info=True)
return {"ok": False}
# Modelle packen JSON gern in einen Codezaun oder schreiben einen Satz davor.
m = re.search(r"\{.*\}", inhalt, re.DOTALL)
if not m:
return {"ok": False}
try:
d = json.loads(m.group(0))
except Exception:
return {"ok": False}
wer = d.get("wer") if d.get("wer") in ("box", "zed") else None
tiefe = d.get("tiefe") if d.get("tiefe") in ("schlank", "gruendlich") else None
if wer is None and tiefe is None:
return {"ok": False}
return {"ok": True, "wer": wer or "zed", "tiefe": tiefe or "schlank",
"nur_denken": bool(d.get("nur_denken"))}
def archive_done(projekt: str = "") -> dict:
"""Alle FERTIGEN Karten wegräumen — optional nur die eines Projekts.
Sammel-Aufräumen (25.07.2026): Bei fünf abgeschlossenen Projekt-Ketten wären das
sonst ~18 Einzelklicks. Wie beim Einzel-Archivieren wird nichts gelöscht — die
Karten wandern ins Kanban-Archiv und bleiben dort lesbar.
‼️ Bewusst NUR `done`. Laufende, hängende, wartende und in Ausarbeitung befindliche
Karten werden nie angefasst, auch nicht bei leerem `projekt`-Filter — ein Sammel-
Knopf darf niemals versehentlich Arbeit abräumen.
"""
if not _available():
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
q = list_queue()
if not q.get("available"):
return {"ok": False, "error": q.get("fehler") or "Queue nicht lesbar."}
ziel = [i for i in q.get("items", [])
if i.get("status") == "done" and (not projekt or i.get("projekt") == projekt)]
if not ziel:
return {"ok": True, "archiviert": 0, "fehlgeschlagen": []}
ok, fehler = 0, []
for item in ziel:
r = archive_task(item.get("id", ""))
if r.get("ok"):
ok += 1
else:
# Die Items aus list_queue tragen "titel" (deutsch), nicht das rohe "title".
fehler.append({"id": item.get("id"), "titel": _kurz(item.get("titel") or "", 60),
"error": r.get("error", "")})
_list_cache["ts"] = 0.0
return {"ok": True, "archiviert": ok, "fehlgeschlagen": fehler}
def ergebnis_of(task_id: str) -> dict:
"""Das ERGEBNIS einer fertigen Aufgabe — die Abschluss-Zusammenfassung des Workers.
+12 -1
View File
@@ -73,7 +73,18 @@ def _run_warmup() -> bool:
def _warmset_members() -> set[str]:
"""Soll-Warm-Set = Mitglieder aller ko-residenten Gruppen (swap:false bzw. persist/persistent)."""
"""Soll-Warm-Set. `MC_WARMSET` (leerzeichengetrennt) schlägt die Gruppen-Ableitung.
Warum ein Override (26.07.2026): Seit die ko-residente Gruppe auch Modelle MIT TTL
enthält (Coder 90 min, Kritiker 30 min), ist „alle Gruppen-Mitglieder" das falsche Ziel.
Der Wächter würde gegen die TTLs arbeiten und nachts 62 GB wieder hochladen, die
absichtlich freigegeben wurden. Ko-Residenz („dürfen gleichzeitig liegen") und
Warm-Pflicht („müssen immer liegen") sind zwei verschiedene Aussagen — die Gruppe kann
nur die erste ausdrücken.
"""
explizit = os.environ.get("MC_WARMSET", "").split()
if explizit:
return set(explizit)
try:
from services import llamaswap
groups = llamaswap.list_groups() or {}
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# curator-archive-watchdog.sh
# Archiviert Skills nach 14 Tagen Inaktivität (nächster Durchlauf um 3:00 Uhr)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_SCRIPT="$SCRIPT_DIR/curator_archive_logic.py"
# Python-Skript ausführen
python3 "$PYTHON_SCRIPT"
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# curator-idle-watchdog.sh
# Prüft den Curator-Idle-Status und triggert Reaktivierung nach 14 Tagen
set -euo pipefail
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
LOG_FILE="$LOG_DIR/idle-watchdog-$(date +%Y%m%d).log"
THRESHOLD_DAYS=14
mkdir -p "$LOG_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Curator-Status auslesen
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
log "FEHLER: hermes curator status fehlgeschlagen"
exit 1
}
# Prüfen, ob Curator ENABLED ist
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
exit 0
fi
# Letzten Lauf extrahieren (Format: "last run: 17d ago")
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
if [[ -z "$LAST_RUN_LINE" ]]; then
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
exit 1
fi
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
# Prüfen, ob "d ago" vorhanden ist
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
else
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen) → überspringe"
exit 0
fi
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
# Threshold prüfen
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage) → kein Trigger nötig"
exit 0
fi
# Reaktivierung triggern
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS) → triggere 'hermes curator run'"
if hermes curator run 2>&1 | tee -a "$LOG_FILE"; then
log "ERFOLG: Curator-Reaktivierung gestartet"
else
log "FEHLER: Curator-Reaktivierung fehlgeschlagen"
exit 1
fi
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
# curator-monitor.sh
# Prüft den Curator-Idle-Status mit 7-Tage-Schwellenwert.
# GIBT AUS: PASS oder FAIL mit Detaillierung.
# EXIT 0 = OK (Curator aktiv innerhalb von 7 Tagen)
# EXIT 1 = FAIL (Curator >7 Tage idle) Monitoring-Skript erkannt Problem
#
# --dry-run DAYS Simuliert eine >DAYS-alte Curator-Auszeit (für Tests)
set -euo pipefail
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
LOG_FILE="$LOG_DIR/monitor-$(date +%Y%m%d).log"
THRESHOLD_DAYS=7
mkdir -p "$LOG_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# --- Argumente parsen ---
DRY_RUN=false
DRY_RUN_DAYS=""
for arg in "$@"; do
case "$arg" in
--dry-run)
DRY_RUN=true
if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
DRY_RUN_DAYS="$2"
shift
else
DRY_RUN_DAYS=8 # Default-Testwert
fi
;;
esac
done
# --- LIVE-PRÜFUNG oder DRY-RUN ---
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY-RUN: Simuliere Curator-Auszeit von $DRY_RUN_DAYS Tagen"
DAYS_SINCE_LAST_RUN="$DRY_RUN_DAYS"
else
# Live-Status abfragen
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
log "FEHLER: hermes curator status fehlgeschlagen"
echo "FAIL: hermes curator status fehlgeschlagen"
exit 1
}
# Prüfen, ob Curator ENABLED ist
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
echo "PASS: Curator ist nicht ENABLED (keine Aktivität nötig)"
exit 0
fi
# Letzten Lauf extrahieren
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
if [[ -z "$LAST_RUN_LINE" ]]; then
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
echo "FAIL: Konnte 'last run' nicht extrahieren"
exit 1
fi
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
# Tage extrahieren
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
else
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen)"
echo "PASS: Curator ist aktuell aktiv oder <1 Tag idle"
exit 0
fi
fi
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
# --- MONITORING-LOGIK (keine Reaktivierung!) ---
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage)"
echo "PASS: Curator ist aktiv ($DAYS_SINCE_LAST_RUN Tage idle)"
exit 0
else
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS)"
echo "FAIL: Curator-Cron ist für $DAYS_SINCE_LAST_RUN Tage stumm (>= $THRESHOLD_DAYS-Tage-Schwellenwert)"
exit 1
fi
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
Curator Auto-Archivierung: Archiviert Skills nach 14 Tagen Inaktivität.
Liest `.curator_state` und `.usage.json` aus ~/.hermes/skills/, prüft
auf Inaktivität (>14 Tage last_used_at), schließt aus (pinned, system)
und verschiebt in .archive/<Kategorie>/.
"""
import json
import shutil
from datetime import datetime, timezone
from pathlib import Path
HERMES_SKILLS_DIR = Path.home() / ".hermes" / "skills"
CURATOR_STATE_FILE = HERMES_SKILLS_DIR / ".curator_state"
USAGE_JSON_FILE = HERMES_SKILLS_DIR / ".usage.json"
ARCHIVE_DIR = HERMES_SKILLS_DIR / ".archive"
LOG_DIR = Path.home() / ".hermes" / "logs" / "curator"
def load_json(path: Path) -> dict:
"""JSON-Datei laden."""
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path: Path, data: dict) -> None:
"""JSON-Datei speichern (mit Pretty-Print)."""
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
def is_stale(skill_usage: dict, threshold_days: int = 14) -> bool:
"""Prüft, ob ein Skill älter als threshold_days ist."""
last_used = skill_usage.get("last_used_at")
if not last_used:
return False
# ISO-Format mit optionaler Zeitzone
last_used_str = last_used.replace("Z", "+00:00")
try:
last_used_dt = datetime.fromisoformat(last_used_str)
except ValueError:
return False
now = datetime.now(timezone.utc)
delta = now - last_used_dt
return delta.days >= threshold_days
def is_excluded(skill_name: str, excluded: list) -> bool:
"""Prüft, ob ein Skill ausgeschlossen ist."""
return skill_name in excluded
def is_pinned(skill_usage: dict) -> bool:
"""Prüft, ob ein Skill angepinnt ist."""
return skill_usage.get("pinned", False) is True
def get_skill_category(skill_name: str) -> str:
"""
Ermittelt die Kategorie eines Skills basierend auf dem Verzeichnisnamen.
Gibt 'unknown' zurück, wenn keine Kategorie gefunden wird.
"""
# Versuche, die Kategorie aus dem Verzeichnis zu ermitteln
# z.B. 'betrieb-playbook' → 'betrieb-playbook' (direkt unter skills/)
# 'apple' (in .archive/apple/) → 'apple'
# Falls der Skill direkt in skills/ liegt ohne Unterverzeichnis:
return "unknown"
def ensure_archive_category(category: str) -> Path:
"""Erstellt das Archiv-Verzeichnis für eine Kategorie, falls nötig."""
category_path = ARCHIVE_DIR / category
category_path.mkdir(parents=True, exist_ok=True)
return category_path
def move_skill_to_archive(skill_name: str, category: str) -> Path:
"""
Verschiebt ein Skill-Verzeichnis in das Archiv.
Gibt den neuen Pfad zurück.
"""
source = HERMES_SKILLS_DIR / skill_name
target_category = ensure_archive_category(category)
target = target_category / skill_name
if source.exists():
shutil.move(str(source), str(target))
return target
def archive_skill(skill_name: str, usage_data: dict) -> dict:
"""
Archiviert einen Skill:
- Setzt state auf 'archived'
- Setzt archived_at auf aktuelle Zeit
- Verschiebt das Verzeichnis nach .archive/
Gibt das aktualisierte Usage-Dict zurück.
"""
usage_data[skill_name]["state"] = "archived"
usage_data[skill_name]["archived_at"] = datetime.now(timezone.utc).isoformat()
return usage_data
def log_archive_action(skill_name: str, category: str, success: bool, message: str) -> None:
"""Protokolliert eine Archivierungsaktion."""
LOG_DIR.mkdir(parents=True, exist_ok=True)
log_file = LOG_DIR / f"archive-{datetime.now(timezone.utc).date().isoformat()}.log"
timestamp = datetime.now(timezone.utc).isoformat()
status = "SUCCESS" if success else "FAILED"
log_line = f"[{timestamp}] {status} | {skill_name} ({category}) | {message}\n"
with open(log_file, "a", encoding="utf-8") as f:
f.write(log_line)
def main() -> None:
"""Hauptfunktion: Archiviert stale Skills."""
# 1. Config laden
if not CURATOR_STATE_FILE.exists():
print(f"Fehler: {CURATOR_STATE_FILE} nicht gefunden.")
return
curator_state = load_json(CURATOR_STATE_FILE)
archivierung = curator_state.get("archivierung", {})
if not archivierung.get("enabled", False):
print("Archivierung ist nicht aktiviert.")
return
threshold_days = archivierung.get("threshold_days", 14)
excluded_skills = archivierung.get("excluded_skills", [])
# 2. Usage-Daten laden
if not USAGE_JSON_FILE.exists():
print(f"Fehler: {USAGE_JSON_FILE} nicht gefunden.")
return
usage_data = load_json(USAGE_JSON_FILE)
# 3. Skills prüfen
archived_count = 0
skipped_count = 0
for skill_name, skill_usage in usage_data.items():
# Ausschlusskriterien prüfen
if is_excluded(skill_name, excluded_skills):
print(f"Übersprungen (Exkludiert): {skill_name}")
skipped_count += 1
continue
if is_pinned(skill_usage):
print(f"Übersprungen (Gepinnt): {skill_name}")
skipped_count += 1
continue
# Inaktivität prüfen
if not is_stale(skill_usage, threshold_days):
print(f"Übersprungen (Aktiv): {skill_name}")
skipped_count += 1
continue
# Archivierung durchführen
category = get_skill_category(skill_name) or "unknown"
print(f"Archiviere: {skill_name}{category}")
try:
# Usage aktualisieren
usage_data = archive_skill(skill_name, usage_data)
# Verzeichnis verschieben
move_skill_to_archive(skill_name, category)
# Erfolg protokollieren
log_archive_action(skill_name, category, True, "Skill archiviert")
archived_count += 1
except Exception as e:
log_archive_action(skill_name, category, False, str(e))
print(f"Fehler beim Archivieren von {skill_name}: {e}")
# 4. Usage-Daten speichern
save_json(USAGE_JSON_FILE, usage_data)
# 5. Archivierungs-Zeitstempel aktualisieren
curator_state["archivierung"]["last_archive_run_at"] = datetime.now(timezone.utc).isoformat()
save_json(CURATOR_STATE_FILE, curator_state)
print(f"\nArchivierung abgeschlossen: {archived_count} archiviert, {skipped_count} übersprungen.")
if __name__ == "__main__":
main()
+6
View File
@@ -89,6 +89,9 @@ cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.t
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
# Morgen-Digest (Nächtliche Telegram-Zusammenfassung, 26.07.2026): fasst alle Meldungen zwischen 0 und 7 Uhr zusammen.
cp "$SRC/deploy/mc2-morgen-digest.service" "$HOME/.config/systemd/user/mc2-morgen-digest.service"
cp "$SRC/deploy/mc2-morgen-digest.timer" "$HOME/.config/systemd/user/mc2-morgen-digest.timer"
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
mkdir -p "$HOME/.hermes/scripts"
@@ -105,6 +108,8 @@ cp "$SRC/deploy/fremdblick.sh" "$HOME/.hermes/scripts/fremdblick.sh"
cp "$SRC/deploy/worker.sh" "$HOME/.hermes/scripts/worker.sh"
# Chef-Gutachter-Cron (Orchestrator-Finale): gpt-oss urteilt nachts über die autonome Arbeit.
cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh"
# Wrapper für nächtliche Crons, um Telegram-Spam in den Morgen-Digest zu leiten.
cp "$SRC/deploy/night-cron-wrapper.sh" "$HOME/.hermes/scripts/night-cron-wrapper.sh"
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
# Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen").
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
@@ -248,6 +253,7 @@ systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
systemctl --user enable --now mc2-morgen-digest.timer >/dev/null 2>&1 || true
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
# Alte Crons löschen
hermes cron remove traum || true
hermes cron remove chef-gutachter || true
hermes cron remove release-radar || true
hermes cron remove idle-radar || true
hermes cron remove selbst-inventur || true
hermes cron remove karten-gutachter || true
hermes cron remove pruefstand || true
hermes cron remove trend-radar || true
# Neue Crons registrieren mit Wrapper und --deliver origin.
# Dadurch schweigt Hermes cron selbst, und der Wrapper nutzt notify.sh für den Morgen-Digest.
hermes cron create --name traum --deliver origin "15 3 * * *" "Traum dreaming-feed.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name chef-gutachter --deliver origin "30 4 * * *" "Chef-Gutachter chef-gutachter-feed.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name release-radar --deliver origin "15 5 * * 1" "Release-Radar hermes-release-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name idle-radar --deliver origin "45 3 * * *" "Idle-Radar idle-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name selbst-inventur --deliver origin "35 3 * * *" "Selbst-Inventur selbst-inventur.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name karten-gutachter --deliver origin "0 4 * * *" "Karten-Gutachter karten-gutachter.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name pruefstand --deliver origin "0 2 * * *" "Prüfstand pruefstand.sh" --script night-cron-wrapper.sh --no-agent
hermes cron create --name trend-radar --deliver origin "15 5 * * 6" "Trend-Radar trend-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
echo "Crons erfolgreich gefixt!"
+36 -9
View File
@@ -433,18 +433,45 @@ class Handler(BaseHTTPRequestHandler):
if base in EXEMPT_MODELS:
return "passthrough", body, est, streaming, model, chars
if HARD_CEILING > 0 and est >= HARD_CEILING:
if not isinstance(messages, list):
return "passthrough", body, est, streaming, model, chars
has_warned = False
marker = "[GOVERNOR — SITZUNGS-LIMIT ERREICHT]"
for m in reversed(messages):
if m.get("role") == "user" and isinstance(m.get("content"), str) and marker in m["content"]:
has_warned = True
break
is_safe = safe_to_append(messages)
# 1. Absolutes Not-Aus bei komplettem Amoklauf
if HARD_CEILING > 0 and est >= HARD_CEILING + 10000:
return "hard", body, est, streaming, model, chars
if est >= THRESHOLD and isinstance(messages, list):
if not safe_to_append(messages):
# Mitten im Werkzeug-Austausch: nicht dazwischenfunken, naechste Runde.
# 2. Sind wir am Limit?
if est >= THRESHOLD:
if not is_safe:
# Agent arbeitet gerade an einer Tool-Kette. Auf keinen Fall abbrechen!
return "defer", body, est, streaming, model, chars
directive = (DIRECTIVE.replace("{est}", str(est))
.replace("{threshold}", str(THRESHOLD)))
messages.append({"role": "user", "content": directive})
data["messages"] = messages
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
# Es ist sicher (Tool-Kette beendet oder Agent wartet auf Input).
if HARD_CEILING > 0 and est >= HARD_CEILING:
if has_warned:
# Wir haben ihn schon gewarnt, er macht trotzdem weiter -> harter Schnitt.
return "hard", body, est, streaming, model, chars
else:
# Er hat wegen einer langen Tool-Kette direkt das Hard-Limit ueberschritten.
# Gib ihm trotzdem noch den einen Finalisier-Zug (Soft).
pass
# Soft-Limit greift
if not has_warned:
directive = (DIRECTIVE.replace("{est}", str(est))
.replace("{threshold}", str(THRESHOLD)))
messages.append({"role": "user", "content": directive})
data["messages"] = messages
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
return "passthrough", body, est, streaming, model, chars
+15 -5
View File
@@ -72,12 +72,22 @@ fi
# --- Approval-/Tool-Ketten (Antwort muss kommen und darf nicht in pending_approval hängen). ---
API_KEY="$(grep -E '^API_SERVER_KEY=' "$HERMES/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
if [ -n "$API_KEY" ]; then
TOOL_RESP=$(curl -sf -m 90 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-H "X-Hermes-Session-Id: postcheck-tool-smoke" \
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
# Mit Retry — GENAU wie der Voice-Smoke darunter, und aus demselben Grund: direkt nach
# einem Update ist das Modell kalt. Ein Kaltstart kostet ~20-30 s Laden PLUS ~37 s
# Prefill für Hermes' ~70-KB-Systemprompt (33 Tool-Schemas) — zusammen mit zwei
# Inferenz-Runden sprengt das die 90 s des ersten Versuchs. Warm gemessen: 12 s.
# Ein Versuch allein war ein Fehlalarm-Generator (live erlebt 25.07., Job faf1137a).
TOOL_RESP=""
for try in 1 2 3; do
TOOL_RESP=$(curl -sf -m 120 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-H "X-Hermes-Session-Id: postcheck-tool-smoke-$try" \
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
echo "$TOOL_RESP" | grep -qi "postcheck-ok" && break
[ "$try" -lt 3 ] && sleep 10
done
if echo "$TOOL_RESP" | grep -qi "postcheck-ok"; then
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis"
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis (Versuch $try)"
elif echo "$TOOL_RESP" | grep -qi "pending_approval"; then
echo "FAIL · Tool-Smoke hängt in pending_approval (approvals.mode prüfen!)"; fail=1
else
+7
View File
@@ -0,0 +1,7 @@
[Unit]
Description=MC2 Morgen-Digest (Nächtliche Telegram-Zusammenfassung)
[Service]
Type=oneshot
ExecStart=/bin/bash %h/mission-control-v2/deploy/morgen-digest.sh
TimeoutStartSec=300
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Timer für MC2 Morgen-Digest (07:00 Uhr)
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Morgen-Digest: Sammelt die zurückgehaltenen nächtlichen Meldungen (00:00 - 06:59)
# und lässt Lucy (LLM) eine Telegram-Zusammenfassung daraus bauen.
set -uo pipefail
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
if [ ! -f "$NIGHT_QUEUE" ]; then
# Keine nächtlichen Meldungen
exit 0
fi
# Wenn die Datei existiert, aber leer ist, löschen und beenden.
if [ ! -s "$NIGHT_QUEUE" ]; then
rm -f "$NIGHT_QUEUE"
exit 0
fi
EVIDENCE="$(cat "$NIGHT_QUEUE")"
PROMPT="Du bist Lucy, die KI-Assistenz der Mission-Control-Box.
Hier sind die Systemmeldungen der vergangenen Nacht.
Der Commander möchte ordentlich Futter und Details zum Frühstück! Fasse die Meldungen zu einem gehaltvollen Morgen-Digest zusammen.
Beachte dabei:
- Nenne konkrete Fakten, Namen und Versionen.
- Welche Modelle wurden z.B. vom Radar entdeckt oder evaluiert? Was waren die Ergebnisse?
- Welche Updates liefen genau und was haben sie Neues gebracht? Was muss sich der Commander noch anschauen?
- Strukturiere die Nachricht übersichtlich (z.B. mit kleinen Emojis oder Bulletpoints), aber bleibe in deiner Rolle als Lucy.
Nächtliche Meldungen:
$EVIDENCE"
# LLM aufrufen (llama-swap / gpt-oss-120b)
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
MODEL="${CHEF_MODEL:-gpt-oss-120b}"
REQ="$(jq -n --arg m "$MODEL" --arg sys "Du bist Lucy." --arg usr "$PROMPT" --argjson mt 1200 \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.4}')"
DIGEST="$(curl -s -m 180 "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$REQ" \
| python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
msg = d["choices"][0]["message"]
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
except Exception:
pass'
)"
if [ -n "$DIGEST" ]; then
# Sende den Digest via hermes send (Telegram)
bash -lc 'hermes send --to telegram --subject "[🌅 Morgen-Digest]" -- "$1"' _ "$DIGEST"
if [ $? -eq 0 ]; then
# Erfolgreich gesendet, Queue leeren
rm -f "$NIGHT_QUEUE"
else
echo "Fehler beim Senden des Morgen-Digests an Telegram. Queue bleibt erhalten." >&2
fi
else
echo "Leerer Digest vom LLM. Queue bleibt erhalten." >&2
fi
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# Erwartet den Job-Namen und das Skript als ein String oder zwei Argumente
if [ $# -eq 1 ]; then
read -r JOB_NAME SCRIPT_NAME <<< "$1"
else
JOB_NAME="$1"
SCRIPT_NAME="$2"
fi
if [ -z "$SCRIPT_NAME" ]; then
SCRIPT_NAME="$JOB_NAME"
fi
# Pfad zusammenbauen
SCRIPT_PATH="$HOME/mission-control-v2/deploy/$SCRIPT_NAME"
if [ ! -f "$SCRIPT_PATH" ]; then
# Versuche pruefstand/
SCRIPT_PATH="$HOME/mission-control-v2/deploy/pruefstand/$SCRIPT_NAME"
fi
bash "$SCRIPT_PATH" | bash "$HOME/mission-control-v2/deploy/notify.sh" -s "$JOB_NAME"
+19 -2
View File
@@ -22,8 +22,8 @@ shift $((OPTIND - 1))
MSG="${1:-}"
[ -z "$MSG" ] && [ ! -t 0 ] && MSG="$(cat)"
if [ -z "$MSG" ]; then
echo "usage: notify.sh [-s subject] <nachricht>" >&2
exit 2
# Bei leerer Pipe-Eingabe lautlos beenden (wichtig für Cron-Skripte ohne Ausgabe)
exit 0
fi
TS="$(date '+%Y-%m-%d %H:%M:%S')"
@@ -41,6 +41,23 @@ PY
)" >/dev/null 2>&1 || true
fi
# Nachtweiche: Zwischen 00:00 und 06:59 Uhr (inklusive) fangen wir Telegram-Nachrichten ab
# und sammeln sie für den Morgen-Digest, ES SEI DENN es ist ein Alarm.
HOUR="$(date +%H)"
if [ "$HOUR" -ge 0 ] && [ "$HOUR" -lt 7 ]; then
case "${SUBJECT,,}" in
*alarm*|*notfall*)
;; # Alarme gehen immer sofort durch
*)
# Sammeln für Morgen-Digest
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
echo "- ${SUBJECT:+$SUBJECT: }$MSG" >> "$NIGHT_QUEUE"
echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG"
exit 0
;;
esac
fi
# Primärweg: Telegram via hermes send (Login-Shell-PATH, falls aus Timer/cron aufgerufen).
if [ -n "$SUBJECT" ]; then
SEND_OUT="$(bash -lc 'hermes send --to telegram --subject "$1" -- "$2"' _ "$SUBJECT" "$MSG" 2>&1)"
+82
View File
@@ -0,0 +1,82 @@
# Persönliche Regeln (gelten für ALLE Projekte)
## So arbeitest du mit mir
- Antworte auf **Deutsch**, Fazit zuerst, Technik in Alltagssprache. Ich bin **kein Entwickler**.
- **Plan vor Code** bei allem Größeren: erst Vorgehen zeigen, auf mein **„los"** warten.
- **Kleine Schritte**, eine Sache zur Zeit. Nichts Ungefragtes ausführen; **frag**, bevor du löschst
oder überschreibst.
- **Beweisen statt behaupten** — am Ende live testen, nicht „sollte gehen".
- Nimm **Standard-Wege**, keine exotische Technik, außer ich will es ausdrücklich anders.
- **Geheimnisse** (Schlüssel/Passwörter/Tokens) nie in den Code — in `.env` (+ `.gitignore`); warnen,
bevor so etwas in einen Commit wandert.
- **Nicht festfahren:** klemmt dieselbe Sache nach 2 Versuchen, anhalten, sichern, erklären und fragen.
- **Bau-Prinzipien sind DEIN Job, nicht meine Frage:** KISS, YAGNI, Zuständigkeiten trennen, DRY,
sprechende Namen, schlank starten. Frag mich nie danach — frag mich nur nach Zielen in Alltagssprache.
- **Faulheits-Leiter vor jeder neuen Zeile/Abhängigkeit:** (1) muss das überhaupt sein? (2) gibt's das
schon (wiederverwenden)? (3) reicht Bordmittel/Standard-Bibliothek? (4) erst dann minimal selbst bauen.
Faul bei Lösungen, nie beim Verstehen; Sicherheitsnetze (Prüfung/Fehlerbehandlung/keine Geheimnisse)
bleiben. „Prüf auf Über-Engineering" = Diff auf unnötige Abhängigkeiten/Abstraktion/Verdachts-Features durchsehen.
## Deine Mannschaft — delegiere, statt alles selbst zu tun
- **`explore`** (Subagent): Code durchsuchen, Dateien finden, Fragen zum Bestand beantworten. Läuft auf
dem ohnehin warmen Hirn und kostet praktisch nichts. **Nutze ihn**, statt dich selbst durch den Baum
zu wühlen — dein eigener Kontext bleibt dadurch sauber und du hältst länger durch.
- **`review`** (Subagent): kritisches Gegenlesen nach jeder Etappe. Läuft bewusst auf einer **fremden
Modellfamilie**, damit er andere blinde Flecken hat als du. Nimm seine Befunde ernst.
- **`plan`**: nur lesen, keine Änderungen.
- Es gibt **keinen Modellwechsel mehr**. Früher galt „zum Bauen oben auf Coder umschalten" — Planen und
Bauen laufen inzwischen auf demselben Modell. Bitte mich nie, etwas umzustellen.
## Das Prüf-Tor (`VERIFY`)
- Im Projekt liegt eine Datei `VERIFY` mit **einer Zeile: wie man dieses Projekt testet**. Sobald du
„fertig" meldest, wird sie automatisch ausgeführt.
- **Rot heißt nicht fertig.** Der Fehler kommt zu dir zurück, du behebst die **Ursache** — nicht das
Symptom. Einen Test abschwächen, überspringen oder umschreiben, damit er grün wird, gilt als Betrug.
- Fehlt die Datei, ist das Prüf-Tor aus. Dann **frag mich**, was der richtige Testbefehl wäre, statt
ungeprüft weiterzubauen.
## Kontext-Wächter
- Ein Token-Wächter zählt jede Sitzung mit und schneidet sie bei ~45.000 Tokens. Du musst nichts messen
— die Ansage kommt von allein.
- Kommt sie: **`SAVEPOINT.md` ehrlich fertigschreiben und anhalten.** Ehrlich heißt: nur was **wirklich
im Code steht**. Nichts aus Absicht, Plan oder git-Nachrichten ableiten. Dazu der genaue nächste
Schritt, offene Fragen und Stolpersteine — genug, dass eine frische Sitzung ohne jede Erinnerung
allein damit weitermachen kann.
- Merkst du vorher schon, dass du dich wiederholst oder abdriftest: sag es proaktiv und schlag einen
frischen Chat vor.
## Wenn niemand da ist (unbeaufsichtigte Läufe)
Nachts startet ein Cron dieselbe Umgebung ohne Menschen davor. Erkennst du das — der Auftrag kam als
ein Block, auf Rückfragen antwortet niemand: **warte nicht auf ein „los"**. Halte dich an `KONZEPT`/
`SAVEPOINT.md`, bau in kleinen Etappen und lass das Prüf-Tor entscheiden. Was du ohne Antwort nicht
sauber entscheiden kannst, schreibst du als offene Frage in `SAVEPOINT.md`, statt zu raten.
## Was du nie tust
`git push`, Historie umschreiben, rekursiv löschen, `sudo`, Pakete veröffentlichen. Das ist gezäunt und
wird geblockt — nicht weil du es falsch machst, sondern weil Veröffentlichen meine Entscheidung ist.
Brauchst du so etwas wirklich: sag es mir, ich mache es selbst.
## Bestehendes Projekt fortsetzen?
Ordner nicht leer / neue Sitzung: zuerst **`SAVEPOINT.md` + `AGENTS.md` lesen**, mir in 2 Sätzen sagen
„hier stehen wir / nächster Schritt", dann weitermachen — nicht neu planen.
## Neues Projekt? → Projekt-Start-Ablauf
Wenn ein **neues** Projekt beginnt (leerer/neuer Ordner, „bau mir…", „neues Projekt…", „ich hätte
gern…"): folge diesem Ablauf. **Keine Datei, kein Code vor meiner Freigabe.**
1. **Verstehen:** erst herausfinden, *was für ein* Projekt (Web-App / kleines Skript /
Box-Lucy-Erweiterung / Experiment / Datensache), dann **36 typ-passende Fragen** + „woran ist
es fertig?".
2. **Plan vorlegen** (Ziel · Vorgehen · Technik + warum · Fertig-Kriterien · Risiken) → **STOPP**,
auf mein „los" warten.
3. **Erst dann Gerüst:** `git init` + die Projektdateien (`AGENTS.md`, `SAVEPOINT.md`, `.aiexclude`,
`VERIFY`) + `README`, dann **Sicherungspunkt „start"** (erster Commit).
4. In **kleinen Schritten** bauen, an jedem Meilenstein + vor riskanten Änderungen einen
**Sicherungspunkt** (Commit mit klarem Namen) setzen, am Ende **live testen**.
## Bei großem Meilenstein UND Session-Ende („Feierabend"/„sichern")
Immer fortschreiben: **`SAVEPOINT.md`** (Stand · Fertiges · nächste Schritte · offene Fragen — die
Übergabe an die nächste Sitzung), **`AGENTS.md`** (falls Konventionen sich änderten), **`.aiexclude`**
(neue Geheimnisse/große Dateien), **`VERIFY`** (falls sich der Testbefehl geändert hat), dann
Sicherungspunkt setzen.
Volle Fassung des Projekt-Start-Ablaufs: `F:\Coding Stuff\projekt-start\SKILL.md`
+20
View File
@@ -10,9 +10,29 @@ und denselben Token-Wächter. Der einzige Unterschied ist die Adresse des Govern
|---|---|---|
| `opencode.pc.json` | `~/.config/opencode/opencode.json` | — |
| `opencode.box.json` | — | `~/.config/opencode/opencode.json` |
| `AGENTS.global.md` | `~/.config/opencode/AGENTS.md` | `~/.config/opencode/AGENTS.md` |
| `plugin/mc2-governor.ts` | `~/.config/opencode/plugin/` | `~/.config/opencode/plugin/` |
| `VERIFY.template` | — | wird von `gitea-repo-create.sh` in **jedes neue Repo** als `VERIFY` gesät |
### Warum die Regeln hier liegen und nicht in Zed
Die persönlichen Arbeitsregeln standen bis 25.07.2026 in `AppData\Roaming\Zed\AGENTS.md` — und
haben dort **nie den Coding-Agenten erreicht**. Zeds eigene Doku ist eindeutig: `AGENTS.md` und
Skills gelten **nur für Zeds nativen Agenten**, nicht für ACP-Agenten wie OpenCode. Da hier über
ACP gearbeitet wird, liefen die Regeln jahrelang ins Leere.
OpenCode liest stattdessen (in dieser Reihenfolge): `AGENTS.md` im Projekt (aufwärts gesucht) und
`~/.config/opencode/AGENTS.md` global. Beide gelten zusammen — Projektregeln ergänzen die globalen.
Beim Umzug bewusst geändert:
- **Zeds Commit-Vorlage entfernt.** Sie war in dieselbe Datei gerutscht und enthielt „Only return
the commit message in your response" — in einer dauerhaft aktiven Regeldatei ein Fehlerherd.
- **Modellwähler-Absatz gestrichen.** „Zum Bauen oben auf Coder umschalten" stimmt seit der
Mannschafts-Umstellung nicht mehr: `plan` und `build` laufen auf demselben Modell.
- **Kontext-Wächter umgeschrieben** auf den Governor, der den Füllstand wirklich kennt.
- **Neu:** Prüf-Tor (`VERIFY`), die Subagenten-Mannschaft und ein Abschnitt für **unbeaufsichtigte
Läufe** — nachts sagt niemand „los", also darf der Agent dort nicht auf Freigabe warten.
Der Governor selbst liegt in `../governor/` (Proxy + systemd-Unit), der unbeaufsichtigte
Läufer in `../opencode-lauf.sh`.
+29 -8
View File
@@ -11,19 +11,31 @@
"models": {
"coder": {
"name": "coder — Bauen + Planen (Qwen3-Coder-Next, 51,5 t/s)",
"limit": { "context": 131072, "output": 16384 }
"limit": {
"context": 131072,
"output": 16384
}
},
"hermes": {
"name": "hermes — Suchen (Qwen3.6-35B, immer warm, 69,6 t/s)",
"limit": { "context": 65536, "output": 8192 }
"limit": {
"context": 65536,
"output": 8192
}
},
"kritiker": {
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
"limit": { "context": 65536, "output": 8192 }
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
"limit": {
"context": 16384,
"output": 4096
}
},
"heavy": {
"name": "heavy — Nacht-Gutachter (gpt-oss-120b, verdraengt das warme Set!)",
"limit": { "context": 32768, "output": 8192 }
"limit": {
"context": 32768,
"output": 8192
}
}
}
}
@@ -33,7 +45,10 @@
"agent": {
"plan": {
"model": "aibox/coder",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
},
"build": {
"model": "aibox/coder",
@@ -55,13 +70,19 @@
"mode": "subagent",
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, laeuft auf dem immer warmen Hirn",
"model": "aibox/hermes",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
},
"review": {
"mode": "subagent",
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, aendert nichts. Laeuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
"model": "aibox/kritiker",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
}
}
}
+29 -8
View File
@@ -11,19 +11,31 @@
"models": {
"heavy": {
"name": "heavy — Planer (gpt-oss-120b, 32k)",
"limit": { "context": 32768, "output": 8192 }
"limit": {
"context": 32768,
"output": 8192
}
},
"coder": {
"name": "coder — Bauen (Qwen3-Coder-Next, 131k)",
"limit": { "context": 131072, "output": 16384 }
"limit": {
"context": 131072,
"output": 16384
}
},
"hermes": {
"name": "hermes — Erkunden (Qwen3.6, immer warm, 69,6 t/s)",
"limit": { "context": 65536, "output": 8192 }
"limit": {
"context": 65536,
"output": 8192
}
},
"kritiker": {
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
"limit": { "context": 65536, "output": 8192 }
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
"limit": {
"context": 16384,
"output": 4096
}
}
}
}
@@ -33,7 +45,10 @@
"agent": {
"plan": {
"model": "aibox/coder",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
},
"build": {
"model": "aibox/coder",
@@ -55,13 +70,19 @@
"mode": "subagent",
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, läuft auf dem immer warmen Hirn",
"model": "aibox/hermes",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
},
"review": {
"mode": "subagent",
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, ändert nichts. Läuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
"model": "aibox/kritiker",
"permission": { "edit": "deny", "bash": "deny" }
"permission": {
"edit": "deny",
"bash": "deny"
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import sys, re, shutil, time
if len(sys.argv) < 5:
print("Nutzung: auto_adopt.py <config_path> <rolle> <new_hf_id> <new_gguf_path> [new_mmproj_path]")
sys.exit(1)
config_path = sys.argv[1]
rolle = sys.argv[2]
new_hf_id = sys.argv[3]
new_gguf_path = sys.argv[4]
new_mmproj_path = sys.argv[5] if len(sys.argv) > 5 else ""
shutil.copy(config_path, config_path + f".bak.{time.strftime('%Y%m%d%H%M%S')}")
content = open(config_path, "r", encoding="utf-8").read()
blocks = re.split(r'\n ([a-zA-Z0-9_.-]+):\n', "\n" + content)
new_content = blocks[0].lstrip("\n")
adopted = False
for i in range(1, len(blocks), 2):
m_name = blocks[i]
m_content = blocks[i+1]
if re.search(r'aliases:\s*\n(?:\s+- [^\n]+\n)*\s+- ' + re.escape(rolle) + r'\b', m_content):
m_content = re.sub(r'(-m\s+)[^\s]+', r'\g<1>' + new_gguf_path, m_content)
if new_mmproj_path:
if "--mmproj" in m_content:
m_content = re.sub(r'(--mmproj\s+)[^\s]+', r'\g<1>' + new_mmproj_path, m_content)
else:
m_content = re.sub(r'(-m\s+[^\s]+)', r'\g<1> --mmproj ' + new_mmproj_path, m_content)
else:
m_content = re.sub(r'\s*--mmproj\s+[^\s]+', '', m_content)
adopted = True
print(f"Auto-Adoption: {rolle} -> {new_hf_id} (ersetzt in {m_name})")
new_content += f" {m_name}:\n{m_content}"
with open(config_path, "w", encoding="utf-8") as f:
f.write(new_content)
if not adopted:
print(f"Warnung: Rolle {rolle} in der config nicht gefunden. Auto-Adoption übersprungen.")
sys.exit(1)
+40 -8
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Prüfstand (17.07.2026): das "volle Programm" für Modell-Kandidaten — echte Coding-,
# Agenten-, Recherche-, Deutsch- und Vision-Prüfungen (harness.py) plus Tempo, verglichen
# gegen die BASELINE der Amtsinhaber. Läuft als Cron (So 01:00, --no-agent: stdout wird
# wörtlich zugestellt; LEERER stdout = stille Nacht).
# Prüfstand-Runner (Täglich 02:00, ehemals Wöchentlich): Baut die Umgebung für den
# Harness (Modell-Sandkasten, GPU-RAM-Check) und zieht die Fäden.
# Läuft als Cron (Täglich 02:00, nach Radar/Checkout), nimmt Kandidaten aus der Queue
# und vergleicht sie gegen die BASELINE der Amtsinhaber.
#
# Modi:
# (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten
@@ -27,7 +27,7 @@ BENCH_BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
BENCH_PORT="${BENCH_PORT:-5899}"
LIVE="http://127.0.0.1:8080/v1"
JUDGE_MODEL="${PRUEFSTAND_JUDGE:-gpt-oss-120b}"
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-35}"
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-100}"
DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}"
mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse"
@@ -342,7 +342,9 @@ PY
# ---- Steckbrief: Kandidat vs. Amtsinhaber
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" <<'PY'
local SIEGER_FLAG="$STATE/tmp-sieger-${KEY}"
rm -f "$SIEGER_FLAG"
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" "$SIEGER_FLAG" <<'PY'
import json, sys, time
kand = json.load(open(sys.argv[1]))
try:
@@ -378,14 +380,15 @@ if faelle:
tempo = (f"Tempo {round(100*tg_k/tg_b-100):+d} % vs. Amtsinhaber" if tg_k and tg_b
else "Tempo-Vergleich unvollständig")
if qk >= qb and tg_k >= 0.8 * (tg_b or tg_k):
urteil = "KANDIDAT SIEHT STARK AUS — Karte prüfen lohnt sich."
urteil = "KANDIDAT SIEHT STARK AUS — Auto-Adoption greift (Etappe E5)."
open(sys.argv[7], "w").write("1")
elif qk >= qb:
urteil = "Qualität hält mit, aber deutlich langsamer — vermutlich kein Aufsteiger."
else:
urteil = "Qualität unter Amtsinhaber — kein Aufsteiger."
zeilen.append(f"**Einordnung:** Bestehensquote {qk:.0%} vs. {qb:.0%} · {tempo}. {urteil}")
zeilen.append("")
zeilen.append("_Der Prüfstand empfiehlt nur — der Rollen-Wechsel bleibt Commander-Entscheid (Karte)._")
zeilen.append("_Auto-Adoption (Etappe E5): Der Prüfstand tauscht Sieger-Modelle nun selbstständig aus._")
text = "\n".join(z for z in zeilen if z is not None)
open(ziel, "w", encoding="utf-8").write(text + "\n")
print(text)
@@ -402,6 +405,35 @@ PY
briefkasten "Prüfstand" "Kandidat ${HF_ID} (Rolle ${ROLLE}) geprüft — Steckbrief im Auftrags-Kanban und Vault (pruefstand/${KEY}.md)."
mv "$SPEC" "$STATE/done/${KEY}.spec.json" 2>/dev/null || true
cp "$OUT" "$STATE/done/${KEY}.json" 2>/dev/null || echo "{\"status\":\"geprueft\"}" > "$STATE/done/${KEY}.json"
# ---- Auto-Adoption (Etappe E5)
if [ -f "$SIEGER_FLAG" ]; then
echo "Kandidat hat gewonnen — starte Auto-Adoption für Rolle $ROLLE..."
local FINAL_DIR="/srv/models/${HF_ID//\//-}-GGUF"
sudo -n mkdir -p "$FINAL_DIR"
sudo -n mv "$ZIEL" "$FINAL_DIR/"
local FINAL_GGUF="$FINAL_DIR/$(basename "$PFAD")"
local FINAL_MMPROJ=""
if [ -n "$MM_ZIEL" ]; then
sudo -n mv "$MM_ZIEL" "$FINAL_DIR/"
FINAL_MMPROJ="$FINAL_DIR/$(basename "$MMPROJ")"
fi
sudo -n chown -R hitonabi:hitonabi "$FINAL_DIR" 2>/dev/null || true
# Config updaten
local CONFIG="$MC/deploy/llama-swap.config.yaml"
python3 "$PSD/auto_adopt.py" "$CONFIG" "$ROLLE" "$HF_ID" "$FINAL_GGUF" "$FINAL_MMPROJ"
# Reload llama-swap (via worker oder systemctl)
if systemctl --user is-active --quiet llama-swap; then
systemctl --user restart llama-swap
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde erfolgreich durch '$HF_ID' ersetzt (Neustart erledigt)."
else
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde durch '$HF_ID' ersetzt (llama-swap war inaktiv)."
fi
rm -f "$SIEGER_FLAG"
fi
rm -rf "${CACHE:?}/$KEY"
echo
echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)"
+287
View File
@@ -0,0 +1,287 @@
# Spezifikation: Auto-Archivierung nach 14 Tagen Inaktivität
**Datum:** 2026-07-24
**Task:** t_a401f387
**Status:** Analyse abgeschlossen → Spezifikation erstellt
**Herkunft:** Aus der Ideen-Queue (Idle-Radar, Traum-Notiz vom 18.07.2026)
---
## 1. Kontext
### 1.1 Ziel
Reduziere den Clutter im Skill-Index und unterstütze den Curator bei der Pflege.
### 1.2 Aktueller Zustand
| Komponente | Status |
|------------|--------|
| Aktive Skills | 11 |
| Archivierte Skills | 15 Kategorien |
| Usage-Tracking | Vorhanden (`.usage.json`), aber leer |
| `.curator_state` | `paused: true` |
| 14-Tage-Regel | **Nicht implementiert** |
### 1.3 Messlatte
- **Stale-Skills:** Skills mit `state: "stale"` in `.usage.json` (bisher kein Eintrag)
- **Inaktivität:** Letzte Nutzung (`last_used_at`) liegt >14 Tage zurück
- **Archivierung:** Verschieben in `.archive/<Kategorie>/` und Setzen von `archived_at`
---
## 2. Architektur
### 2.1 Komponenten
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Curator-Auto-Archivierung │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Idle-Radar │ → │ Archivierungs- │ → │ Curator-Indexer│ │
│ │ (Trigger) │ │ Watchdog │ │ (Datenpflege) │ │
│ └─────────────────┘ └─────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 Datenfluss
1. **Idle-Radar** erkennt Inaktivität (nächtlicher Scan)
2. **Archivierungs-Watchdog** prüft 14-Tage-Regel
3. **Curator-Indexer** aktualisiert `.usage.json` mit `archived_at`
4. Skill-Verzeichnis wird nach `.archive/<Kategorie>/` verschoben
---
## 3. Dateistruktur
### 3.1 Aktiv-Verzeichnis (`~/.hermes/skills/`)
- Normale Skill-Ordner (`betrieb-playbook/`, `wartung/`, ...)
- `.usage.json` (Nutzungsdaten)
- `.curator_state` (Curator-Status)
### 3.2 Archiv-Verzeichnis (`~/.hermes/skills/.archive/`)
```
.archive/
├── apple/ # Kategorie: Apple-Tools
├── creative/ # Kategorie: Kreatives
├── data-science/ # Kategorie: Datenanalyse
├── github/ # Kategorie: GitHub
├── media/ # Kategorie: Medien
├── mlops/ # Kategorie: MLOps
├── research/ # Kategorie: Recherche
├── software-development/ # Kategorie: Dev-Tools
└── ... # Weitere Kategorien
```
### 3.3 Skill-Datei nach Archivierung
```
~/.hermes/skills/.archive/software-development/gitea-workflow/
├── SKILL.md
└── ... (weitere Dateien)
```
---
## 4. Konfiguration
### 4.1 `.curator_state`
```json
{
"last_report_path": "/home/hitonabi/.hermes/logs/curator/20260720-182035",
"last_run_at": "2026-07-20T18:20:35.806573+00:00",
"last_run_duration_seconds": 882.931897,
"last_run_summary": "auto: no changes; llm: Context length exceeded: max compression attempts (3) reached.",
"last_run_summary_shown_at": "2026-07-20T18:20:35.806573+00:00",
"paused": false, // ← aktiviert für Auto-Archivierung
"run_count": 11,
"archivierung": {
"enabled": true, // ← neuer Schlüssel
"threshold_days": 14, // ← neuer Schlüssel
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
### 4.2 `.usage.json` Archivierter Skill
```json
{
"gitea-workflow": {
"archived_at": "2026-07-24T03:00:00.000000+00:00", // ← neuer Schlüssel
"created_at": "2026-06-30T20:00:58.871579+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-06-23T19:00:00.000000+00:00",
"last_viewed_at": null,
"patch_count": 0,
"pinned": false,
"state": "archived", // ← geändert von "active"
"use_count": 0,
"view_count": 0
}
}
```
---
## 5. Trigger-Mechanismus
### 5.1 Zeitlicher Trigger
- **Cron-Job:** `0 3 * * *` (täglich um 3:00 Uhr)
- **Skript:** `~/.hermes/scripts/curator-archive-watchdog.sh`
### 5.2 Prüflogik
```python
def is_stale(skill_usage: dict) -> bool:
"""Prüft, ob ein Skill älter als 14 Tage ist."""
last_used = skill_usage.get("last_used_at")
if not last_used:
return False
last_used_dt = datetime.fromisoformat(last_used.replace("Z", "+00:00"))
now = datetime.now(last_used_dt.tzinfo)
days_inactive = (now - last_used_dt).days
return days_inactive >= 14
```
### 5.3 Ausschlusskriterien
- **Pinned:** `pinned: true` → nie archivieren
- **Verwendung im Live-Repo:** Skills aus `~/mission-control-v2/deploy/skills/` nicht archivieren
- **System-Skills:** `betrieb-playbook`, `wartung`, `orchestrator` nicht archivieren (Konfigurierbar in `.curator_state`)
---
## 6. Archivierungs-Workload
### 6.1 Schritte
1. **Scan:** Alle Skills in `~/.hermes/skills/` durchlaufen
2. **Prüfung:** `is_stale()` für jeden Skill aufrufen
3. **Ausschluss:** Pinned/System-Skills überspringen
4. **Verschieben:** Skill-Verzeichnis nach `.archive/<Kategorie>/`
5. **Index aktualisieren:** `.usage.json` mit `archived_at` und `state: "archived"` schreiben
6. **Protokoll:** Log-Eintrag in `~/.hermes/logs/curator/`
### 6.2 Beispiel-Archivierung
```bash
# Vorher
~/.hermes/skills/skill-automatisch-verknuepfen/SKILL.md
# Nachher
~/.hermes/skills/.archive/software-development/skill-automatisch-verknuepfen/SKILL.md
```
---
## 7. Datenformat
### 7.1 `.usage.json` Aktiv
```json
{
"name": {
"archived_at": null,
"created_at": "2026-07-03T07:50:53.782473+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
"patch_count": 0,
"pinned": false,
"state": "active",
"use_count": 2,
"view_count": 2
}
}
```
### 7.2 `.usage.json` Archiviert
```json
{
"name": {
"archived_at": "2026-07-24T03:00:00.000000+00:00",
"created_at": "2026-07-03T07:50:53.782473+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
"patch_count": 0,
"pinned": false,
"state": "archived",
"use_count": 2,
"view_count": 2
}
}
```
---
## 8. Config-Interface
### 8.1 `.curator_state` Erweiterung
```json
{
"archivierung": {
"enabled": true,
"threshold_days": 14,
"excluded_skills": ["betrieb-playbook", "wartung", "orchestrator"],
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
### 8.2 `.usage.json` Erweiterung
```json
{
"name": {
"archived_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
---
## 9. Akzeptanzkriterien
| Kriterium | Status |
|-----------|--------|
| Klare Architektur | ✅ (siehe Abschnitt 2) |
| Definierte Dateistruktur | ✅ (`.archive/<Kategorie>/`) |
| Config-Interface | ✅ (`.curator_state` + `.usage.json`) |
| Keine Code-Änderungen | ✅ (nur Spezifikation) |
---
## 10. Nächste Schritte
### 10.1 Implementierung
1. **Skript erstellen:** `~/.hermes/scripts/curator-archive-watchdog.sh`
2. **Cron-Job anlegen:** `0 3 * * * curator-archive-watchdog.sh`
3. **`.curator_state` erweitern:** `archivierung`-Block hinzufügen
4. **`.usage.json` erweitern:** `archived_at`-Feld einfügen
### 10.2 Test
1. Manuell einen Skill archivieren (Handbetrieb)
2. Prüfen, ob `archived_at` gesetzt wird
3. Prüfen, ob `.archive/<Kategorie>/` angelegt wird
4. Prüfen, ob `state: "archived"` in `.usage.json` steht
---
## 11. Anhang
### 11.1 Begriffe
| Begriff | Bedeutung |
|---------|-----------|
| **Stale** | Skill mit `last_used_at > 14 Tage` |
| **Archivierung** | Verschieben in `.archive/<Kategorie>/` |
| **Curator** | Der automatische Wartungsdienst |
| **Idle-Radar** | Das System, das Inaktivität erkennt |
### 11.2 Quellen
- `~/.hermes/skills/.curator_state`
- `~/.hermes/skills/.usage.json`
- `~/.hermes/skills/.archive/`
- `~/wissens-vault/eigenbau-landkarte.md`
---
**Ende der Spezifikation**
+76
View File
@@ -0,0 +1,76 @@
# Curator Idle Watchdog Spezifikation
## Ziel
Automatische Reaktivierung des Hermes Curator nach **14 Tagen Idle**, damit der Skill-Graph nicht dauerhaft isoliert bleibt.
## Problem
- Curator läuft normalerweise alle 7 Tage (`interval: every 7d`)
- Wenn der Curator ausfällt (z. B. aufgrund von `paused: true`), bleiben Skills isoliert
- Derzeitige Beobachtung: Curator blockiert seit 16 Tagen (Stand 24.07.2026)
- Keine automatische Erkennung → keine automatische Wiederherstellung
## Lösung: Watchdog-Skript
### `curator-idle-watchdog.sh`
**Zweck:** Cron-basierte Prüfung, ob der Curator zu lange offline war.
**Ablauf:**
1. `hermes curator status` ausführen
2. "last run"-Zeit extrahieren (z. B. `17d ago`)
3. Tage berechnen und mit `THRESHOLD_DAYS=14` vergleichen
4. Bei Überschreitung → `hermes curator run` triggern
5. Alle Aktionen nach `~/.hermes/logs/curator/idle-watchdog-YYYYMMDD.log` protokollieren
### Cron-Eintrag
**Zeitplan:** Sonntag 00:00 (wöchentlich)
```bash
0 0 * * 0 ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
```
**Logik:**
- Prüfung nur, wenn Curator `ENABLED` ist
- Wenn `last run` < 14 Tage → nichts tun
- Wenn `last run` ≥ 14 Tage → `hermes curator run` aufrufen
- Fehler protokollieren, aber nicht blockieren
## Implementierung
### Dateien
- Skript: `~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
- Cron: `/etc/cron.d/curator-idle-watchdog` (oder user-level via `crontab -e`)
- Logs: `~/.hermes/logs/curator/idle-watchdog-*.log`
### Akzeptanzkriterien
- [ ] Skript existiert und ist ausführbar (`chmod +x`)
- [ ] Cron-Eintrag ist aktiv (wöchentliche Prüfung Sonntag 00:00)
- [ ] Reaktivierung triggert `hermes curator run`
- [ ] Protokollierung in `~/.hermes/logs/curator/` funktioniert
- [ ] Keine Änderungen an `main` oder `~/mission-control-v2`
## Test
### Manueller Test
```bash
# Simuliere 15-Tage-Idle
sed -i 's/last run:.*$/last run: 15d ago/' <(hermes curator status)
# Watchdog ausführen
~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
```
### Erwartetes Verhalten
1. Log-Eintrag: `WARNUNG: Curator ist für 15 Tage idle (>= 14) → triggere 'hermes curator run'`
2. `hermes curator run` wird ausgeführt
3. Log-Eintrag: `ERFOLG: Curator-Reaktivierung gestartet`
## Rollback-Plan
- Cron-Eintrag löschen: `crontab -e` → Zeile entfernen
- Skript löschen: `rm ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
- Logs archivieren: `tar -czf idle-watchdog-logs-$(date +%Y%m%d).tar.gz ~/.hermes/logs/curator/idle-watchdog-*.log`
## Anmerkungen
- 14 Tage Threshold = Kompromiss zwischen zu häufigem Trigger (z. B. bei kurzen Wartungen) und zu spätem Erkennen
- Watchdog prüft nur bei `ENABLED` Curator (kein Fehlalarm bei `paused`)
- Keine Abhängigkeit zu externen Diensten (reines Hermes-System)
+12
View File
@@ -15,6 +15,11 @@ set -u
URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
BRAINS="${MC_WARMUP_MODELS:-fast}"
EMBEDS="${MC_WARMUP_EMBED:-embed}"
# Reranker laufen im --reranking-Modus und antworten NUR auf /v1/rerank — weder Chat noch
# Embeddings wecken sie. Solange das hier fehlte, galt der Reranker dem Waechter dauerhaft
# als "fehlend": er rief warmup.sh alle 90 s auf, ohne dass sich je etwas aendern konnte
# (600 vergebliche Laeufe in 24 h, aelteste Meldung 15.07.2026).
RERANKS="${MC_WARMUP_RERANK:-reranker}"
if [ "${1:-}" != "--inner" ]; then
setsid "$0" --inner >/dev/null 2>&1 &
@@ -41,6 +46,13 @@ for m in $EMBEDS; do # Embedding-Modelle über /v1/emb
>/dev/null 2>&1 || true
done
for m in $RERANKS; do # Reranker über /v1/rerank (eigener Endpunkt!)
curl -s -m 120 -X POST "$URL/v1/rerank" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$m\",\"query\":\"ping\",\"documents\":[\"ping\"]}" \
>/dev/null 2>&1 || true
done
# Agenten-Prompt vorkauen: Hermes' Systemprompt (~70 KB, 33 Tool-Schemas) kostet nach
# jedem Modell-(Neu-)Laden sonst 30-40 s Prefill BEIM ERSTEN USER-CALL (gemessen 03.07.:
# kalt 37 s, mit gefülltem Prompt-Cache ~6 s). Ein Wegwerf-Turn füllt den -cram-Cache.
+27
View File
@@ -129,6 +129,33 @@ erweitert nur der Commander, nie das Skript.
legt er zusätzlich EINE rohe Idee in die Ideen-Queue („Probe bauen?", idempotent je
Release-Tag) — der Blick nach draußen füttert damit denselben Kreislauf.
## Curator Auto-Archivierung (Cron `0 3 * * *` — täglich um 03:00)
**Zweck:** Automatische Archivierung inaktiver Skills nach 14 Tagen ohne Nutzung.
**Skripte:**
- `~/.hermes/scripts/curator-archive-watchdog.sh` — Cron-Einstieg, ruft das Python-Skript auf.
- `~/.hermes/scripts/curator_archive_logic.py` — Hauptlogik. Liest `.curator_state` und `.usage.json` aus `~/.hermes/skills/`, prüft auf Inaktivität (>14 Tage seit letztem Gebrauch), überspringt gepinnte und exkludierte Skills, verschiebt Skills nach `.archive/<Kategorie>/`.
- `~/.hermes/scripts/curator-idle-watchdog.sh` — Zusätzlicher Watchdog (Sonntag 00:00, Cron `0 0 * * 0`): prüft, ob der Curator selbst ≥14 Tage idle ist und triggert ggf. eine Reaktivierung.
**Konfiguration** (`~/.hermes/skills/.curator_state`):
- `archivierung.enabled: false` — Archivierung muss explizit aktiviert werden.
- `archivierung.threshold_days: 14` — Tage ohne Nutzung bis zur Archivierung.
- `archivierung.excluded_skills: [...]` — Liste der nie zu archivierenden Skills.
- `archivierung.last_archive_run_at` — Zeitstempel des letzten Durchlaufs.
**Ablauf im Durchlauf:**
1. Prüft, ob Archivierung aktiviert ist.
2. Läuft alle Skills durch `.usage.json`.
3. Überspringt: gepinnte Skills, exkludierte Skills, aktive Skills (<14 Tage).
4. Archiviert: State auf "archived" setzen, Verzeichnis nach `.archive/<Kategorie>/` verschieben, Usage-Datei aktualisieren.
5. Protokolliert in `~/.hermes/logs/curator/archive-<Datum>.log`.
**Sicherheit:**
- Archivierung standardmäßig deaktiviert — passiert nie ohne explizite Freigabe.
- Gepinnte und exkludierte Skills werden nie berührt.
- Archivierung ist reversibel: Skills können manuell aus `.archive/` zurückverschoben werden.
## Leitplanken
- Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses
+49 -2
View File
@@ -51,10 +51,14 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
⚠️ `llama-quantize` verwirft DFlash-Sondertensoren bei Nicht-Standard-Archs.
- **coder-Lane: Qwen3-Coder-Next, FINAL** („bleiben bei dem was wir haben, nachweislich
besser"). Qwen3.6-27B dense gebencht + verworfen (tg 12,7 t/s = 47× langsamer).
- **hipEngine + strix-halo-toolboxes/TheRock: beobachten, nicht adoptieren.** hipEngine
|- **hipEngine + strix-halo-toolboxes/TheRock: beobachten, nicht adoptieren.** hipEngine
(natives HIP, nur Qwen3.6, >2× Prefill) ist jung; kyuz0-Toolboxes lösen ein Problem, das
die Box nicht hat. Radar/Reminder wachen; Adoption nur nach eigenem brain-bench.
- **dist/ im Git ist ABSICHT.** Box hat kein Node; Deploy = `git reset --hard` + committetes
|- **Vision-Rolle: GLM-4.6V-Flash bleibt Amtsinhaber.** Qwen-AgentWorld-35B geprüft (Prüfstand
t_9daf088f, Speed: tg 26,8 t/s / pp 339,1, Vision: 0/1 korrekt). Qualität deutlich unter
GLM-4.6V-Flash, kein Aufsteiger — **abgelehnt**. Wiedervorlage nur bei neuer Version mit
nachweislich > 50 % Vision-Rate.
|- **dist/ im Git ist ABSICHT.** Box hat kein Node; Deploy = `git reset --hard` + committetes
`frontend/dist`. NICHT gitignoren.
## Lucy (Voice-App)
@@ -99,3 +103,46 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
(deprecated Python, Server blockiert, Hybrid unsupported). Zweck/Umfang offen.
- **Aussortiert (tot):** Kyutai-Streaming-STT (nur EN/FR) · Unmute-Voll-Stack · LXC 105 (alt) ·
WebUI-Update-Checks · openWakeWord (Wake-Gate ist STT-basiert).
## Prüfstand 24.07.2026 — Coder-Backend + Modell (ROCm-Recheck erledigt)
Gemessen mit llama-bench (Vulkan/RADV, Q4_K_M, -fa on) auf der Box (Ryzen AI Max+ 395):
| Test | Coder-Next 80B-A3B | Devstral Small 2 24B dense |
| -------------- | ------------------ | -------------------------- |
| pp2048 @ 0 | 702 t/s | 468 t/s |
| tg64 @ 0 | 59 t/s | 15 t/s |
| pp2048 @ 8192 | 638 t/s | 315 t/s |
| tg64 @ 8192 | 55 t/s | 14 t/s |
| pp2048 @ 32768 | 488 t/s | 63 t/s (7,7x langsamer) |
| tg64 @ 32768 | 47 t/s | 11 t/s (4,2x langsamer) |
- **Backend bleibt Vulkan (ROCm-Recheck 24.07. erledigt).** Vulkan-Prefill haelt bei 32K
noch 488 t/s — der einzige ROCm-Vorteil (Langkontext-Prefill) ist auf dieser Box kein
Engpass. Der Autor des fuehrenden ROCm/Strix-Halo-Rezepts ist im Juli 2026 selbst zu
Vulkan zurueckgekehrt (+28% Decode). Kein ROCm-Bau — Produktiv-Risiko fuer null Gewinn.
Wiedervorlage nur bei neuem Engine-Langkontext-Support oder einem reinen HIP-Modell.
- **Coder bleibt Qwen3-Coder-Next — jetzt gemessen bestaetigt.** Devstral Small 2 (einziger
neuer agentisch trainierter Dense-Kandidat) ist 4x langsamer bei Generierung und bricht
beim 32K-Prefill auf 63 t/s ein (~9 min um einen 32K-Kontext zu lesen). Fuer agentisches
Coden auf dieser Box disqualifiziert — Reliabilitaet war nicht einmal noetig als Argument.
- **Der Coder ist nicht langsam (59 t/s).** Das Fassaden-/Halluzinations-Problem ist kein
Speed- und kein Backend-Problem und wird weder durch Modelltausch noch ROCm geloest. Es
ist Reliabilitaet = Modellklasse + Harness. Naechster Hebel: Post-Edit-Verify-Hook,
kleine Etappen/frische Threads, Eskalation der harten 20% an Frontier.
- **Devstral Small 2 ist der KRITIKER, nicht der Coder — mit hartem 16k-Deckel (25.07.).**
Das Verdikt „als Coder disqualifiziert" (24.07.) bleibt unveraendert richtig. Neu ist nur die
ENGE Rolle: Gegenlesen einer Etappe. Gemessen am 25.07. auf der Box: Generierung 15,0 t/s
(Coder 51,5), Prefill 265-318 t/s (Coder 754). Der Prefill bricht mit der Kontextlaenge ein
(32k -> 63 t/s = ~9 min nur zum Lesen), deshalb ist der Kritiker in llama-swap UND in beiden
opencode.json auf **16384** gedeckelt: schlimmster Fall je Review bleibt unter einer Minute.
Der Nutzen liegt nicht im Tempo, sondern in der FREMDEN Modellfamilie — ein Mistral-Modell hat
andere blinde Flecken als der Qwen-Coder. Alternative GLM-4.7-Flash (MoE, schnellerer Prefill)
bleibt eine Zeile Config entfernt, kostet aber ~9 Punkte SWE-bench (59,2 vs. 68,0).
- **llama-swap haelt nur EINE Gruppe resident, und `persistent: true` ist gefaehrlich (25.07.).**
Zwei ko-residente Gruppen verdraengen sich gegenseitig — alles, was gleichzeitig warm sein muss,
gehoert in DIESELBE Gruppe (`brains`). `persistent: true` hindert llama-swap daran, vor einem
grossen On-Demand-Modell abzuraeumen: mit 107 GB Warm-Set + Vision (20 GB) folgte ein
**Kernel-OOM** (llama-server abgeschossen). Regel: `Warm-Set + groesstes On-Demand-Modell
<= ~115 GB` UND `persistent: false`. Danach lud vision in 10 s statt 117 s + Absturz.
Kontext-Halbierung spart bei Qwen3-Next uebrigens **0 GB** (Hybrid-Attention, winziger KV).
@@ -1,4 +1,4 @@
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-Cm0NCQeJ.js";/**
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as m,ag as S,r as c,j as e,ah as z,L as g,U as M,ai as C,ad as D,aj as A,ak as B,al as Z,e as E,am as L,u as R,b as q,g as k,q as T}from"./index-Cm0NCQeJ.js";import{R as j}from"./rotate-ccw-DQJileoK.js";/**
import{c as m,ai as S,r as c,j as e,aj as z,L as g,U as M,ak as C,ae as D,al as A,am as B,an as Z,e as E,ao as L,u as R,b as q,g as k,q as T}from"./index-BChEIiQl.js";import{R as j}from"./rotate-ccw-CPYnjQ6M.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-Cm0NCQeJ.js";import{F as O}from"./folder-open-CvG8JCKh.js";import{C as R}from"./circle-x-DT2INJve.js";import{C as W}from"./copy-BzlMSkzh.js";/**
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-BChEIiQl.js";import{F as O}from"./folder-open-CtWva2VF.js";import{C as R}from"./circle-x-D8NCr-a2.js";import{C as W}from"./copy-CRj5M_pY.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as h,an as b,r as i,j as e,L as g,ao as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-Cm0NCQeJ.js";import{B as S}from"./book-open-CMR6fip9.js";import{C as z}from"./circle-x-DT2INJve.js";/**
import{c as h,ap as b,r as i,j as e,L as g,aq as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-BChEIiQl.js";import{B as S}from"./book-open-2v7qByfb.js";import{C as z}from"./circle-x-D8NCr-a2.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-Cm0NCQeJ.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-BChEIiQl.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
@@ -1,5 +1,5 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-B853GRcI.js","assets/index-Cm0NCQeJ.js","assets/index-Bkns39Uj.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-Cm0NCQeJ.js";import{C as ee}from"./copy-BzlMSkzh.js";import{S as _e}from"./send-Bc-GXqQz.js";import{B as Ge}from"./book-open-CMR6fip9.js";/**
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-B3HC-zB_.js","assets/index-BChEIiQl.js","assets/index-Bm-JldFM.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-BChEIiQl.js";import{C as ee}from"./copy-CRj5M_pY.js";import{S as _e}from"./send-CS1kRsDf.js";import{B as Ge}from"./book-open-2v7qByfb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-B853GRcI.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-B3HC-zB_.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
1) wer ich bin und woran ich gerade arbeite,
2) wie ich angesprochen werden möchte,
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
@@ -1,4 +1,4 @@
import{c as q,u as oe,a as Fe,b as ge,j as e,f as ue,d as Re,e as w,g as K,q as P,h as Ie,r as N,C as Y,L as Oe,i as Pe,B as le,k as Te,E as Be,l as de,m as Ge,n as $e,o as He,p as ee,X as pe,P as Qe,H as fe,T as re,s as Ue,t as Ve,v as V,w as We,S as ye,x as Ze,y as Je,z as Xe,A as Ye,D as De,F as et,G as tt}from"./index-Cm0NCQeJ.js";import{C as rt}from"./code-xml-p3YDy76e.js";import{R as st}from"./rotate-ccw-DQJileoK.js";import{C as be,Z as ie,L as nt,a as at}from"./zap-BOjMx7d9.js";import{L as ot}from"./layers-CKJvqHFy.js";/**
import{c as q,u as oe,a as Fe,b as ge,j as e,f as ue,d as Re,e as w,g as K,q as P,h as Ie,r as N,C as Y,L as Oe,i as Pe,B as le,k as Te,E as Be,l as de,m as Ge,n as $e,o as He,p as ee,X as pe,P as Qe,H as fe,T as re,s as Ue,t as Ve,v as V,w as We,S as ye,x as Ze,y as Je,z as Xe,A as Ye,D as De,F as et,G as tt}from"./index-BChEIiQl.js";import{C as rt}from"./code-xml-owp4HQPY.js";import{R as st}from"./rotate-ccw-CPYnjQ6M.js";import{C as be,Z as ie,L as nt,a as at}from"./zap-I2SMIyr4.js";import{L as ot}from"./layers-C62c1z5s.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+6
View File
@@ -0,0 +1,6 @@
import{c as s,j as e}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const r=s("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);function c({icon:a,children:t}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(a,{className:"h-3.5 w-3.5"})," ",t]})}export{r as H,c as S};
@@ -1,4 +1,4 @@
import{ap as y,r as x,g as L,j as e,L as v,S,ae as W,U as C,e as w,aq as E}from"./index-Cm0NCQeJ.js";import{F as M}from"./folder-open-CvG8JCKh.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
import{ar as y,r as x,g as L,j as e,L as v,S,ah as W,U as C,e as w,as as E}from"./index-BChEIiQl.js";import{F as M}from"./folder-open-CtWva2VF.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
@@ -1,4 +1,4 @@
import{c as a}from"./index-Cm0NCQeJ.js";/**
import{c as a}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-Cm0NCQeJ.js";/**
import{c}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./index-Cm0NCQeJ.js";/**
import{c as e}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-Cm0NCQeJ.js";/**
import{c}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-Cm0NCQeJ.js";/**
import{c as a}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as a}from"./index-Cm0NCQeJ.js";/**
import{c as a}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
-6
View File
@@ -1,6 +0,0 @@
import{c as t}from"./index-Cm0NCQeJ.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const e=t("Lightbulb",[["path",{d:"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5",key:"1gvzjb"}],["path",{d:"M9 18h6",key:"x1upvd"}],["path",{d:"M10 22h4",key:"ceow96"}]]);export{e as L};
@@ -1,4 +1,4 @@
import{c as t}from"./index-Cm0NCQeJ.js";/**
import{c as t}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-Cm0NCQeJ.js";/**
import{c as a}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-Cm0NCQeJ.js";/**
import{c as a}from"./index-BChEIiQl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-Cm0NCQeJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bkns39Uj.css">
<script type="module" crossorigin src="/assets/index-BChEIiQl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bm-JldFM.css">
</head>
<body>
<div id="root"></div>
+2
View File
@@ -20,6 +20,7 @@ const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default:
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
const IdeenView = lazy(() => import("@/views/IdeenView").then((m) => ({ default: m.IdeenView })))
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView })))
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
@@ -201,6 +202,7 @@ export default function App() {
<main className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
<Suspense fallback={<ViewLoading />}>
{view === "dashboard" && <CockpitView onNavigate={(v) => navigate(v as ViewId)} />}
{view === "ideen" && <IdeenView />}
{view === "auftraege" && <AuftragsbuchView />}
{view === "models" && <ModelsView />}
{view === "connect" && <ConnectView />}
@@ -1,180 +0,0 @@
import { Fuel, ShieldAlert, Check } from "lucide-react"
import { useGovernor } from "@/lib/queries"
import type { GovernorRun } from "@/lib/api"
// ── Token-Wächter („Tankuhr mit Handbremse") ────────────────────────────────
// Zeigt in Klartext, wie voll die laufende Coding-Sitzung ist. Zwei Marken:
// Soft = ab hier bittet der Governor um einen Savepoint (Sitzung sauber beenden)
// Hart = ab hier antwortet er selbst und lässt das Modell gar nicht mehr ran
// Die Zeile „Schätzung vs. Wahrheit" ist der Ehrlichkeits-Nachweis: bis 25.07. lag der
// Governor bei werkzeugdichten Agenten um Faktor 3 daneben, weil er Werkzeug-Schemata
// nicht mitzählte. Jetzt lernt er aus den echten prompt_tokens jeder Antwort.
const fmtNum = (n: number | null | undefined) =>
n == null ? "" : n.toLocaleString("de-DE")
function ago(ts: number): string {
const s = Math.max(0, Date.now() / 1000 - ts)
if (s < 60) return `vor ${Math.round(s)} s`
if (s < 3600) return `vor ${Math.round(s / 60)} min`
return `vor ${Math.round(s / 3600)} h`
}
const ACTION_META: Record<string, { label: string; tone: string }> = {
passthrough: { label: "frei", tone: "text-emerald-400" },
defer: { label: "wartet", tone: "text-amber-400" },
soft: { label: "Savepoint", tone: "text-amber-400" },
hard: { label: "STOPP", tone: "text-rose-400" },
}
/** Abweichung Schätzung↔Wahrheit in Prozent — der Ehrlichkeits-Messwert. */
function drift(run: GovernorRun | undefined): number | null {
if (!run?.est || !run?.exact) return null
return ((run.est - run.exact) / run.exact) * 100
}
export function GovernorCard() {
const { data } = useGovernor()
if (!data?.reachable) {
return (
<div className="mc-card p-5">
<div className="flex items-center gap-2">
<Fuel className="h-4.5 w-4.5 text-muted-foreground" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
</div>
<div className="mt-3 text-xs text-muted-foreground">
Nicht erreichbar. Die Coding-Sitzungen laufen dann ohne Sitzungs-Bremse.
<div className="mt-1 font-mono text-[10px] text-muted-foreground/60">
{data?.error ?? "keine Antwort von :8100"}
</div>
</div>
</div>
)
}
const soft = data.soft ?? 0
const hard = data.hard ?? null
const runs = data.recent ?? []
// Nur echte Bau-Läufe füllen die Tankuhr; Lucys Gespräche sind ausgenommen.
const lastBuild = runs.find((r) => !(data.exempt_models ?? []).includes(r.model))
const füllung = lastBuild?.exact ?? lastBuild?.est ?? 0
const skala = Math.max(hard ?? soft * 1.2, soft * 1.2, füllung)
const pct = Math.min(100, (füllung / skala) * 100)
const abw = drift(runs.find((r) => r.est != null && r.exact != null))
const c = data.counters
const tone =
hard && füllung >= hard ? "bg-rose-500"
: füllung >= soft ? "bg-amber-500"
: "bg-emerald-500"
return (
<div className="mc-card p-5">
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<Fuel className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
</span>
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="font-space text-3xl font-bold tracking-tight tabular-nums text-foreground">
{fmtNum(füllung)}
</span>
<span className="text-xs text-muted-foreground">
Tokens in der letzten Bau-Anfrage
</span>
</div>
</div>
<div className="shrink-0 pt-1 text-right">
<div className="text-[11px] font-semibold uppercase tracking-wider text-amber-400">
Savepoint ab {fmtNum(soft)}
</div>
{hard != null && (
<div className="text-[11px] font-semibold uppercase tracking-wider text-rose-400">
Stopp ab {fmtNum(hard)}
</div>
)}
</div>
</div>
{/* Tankuhr */}
<div className="relative h-3 overflow-hidden rounded-sm bg-muted/25">
<div className={`h-full ${tone} transition-all`} style={{ width: `${pct}%` }} />
<div
className="absolute inset-y-0 w-px bg-amber-400/80"
style={{ left: `${Math.min(100, (soft / skala) * 100)}%` }}
title={`Savepoint-Marke ${fmtNum(soft)}`}
/>
{hard != null && (
<div
className="absolute inset-y-0 w-px bg-rose-400/80"
style={{ left: `${Math.min(100, (hard / skala) * 100)}%` }}
title={`Harter Stopp ${fmtNum(hard)}`}
/>
)}
</div>
{/* Zählerstand */}
<div className="mt-3 grid grid-cols-4 gap-2 text-center">
{[
{ k: "Anfragen", v: fmtNum(c?.chat) },
{ k: "Savepoints", v: fmtNum(c?.soft) },
{ k: "Stopps", v: fmtNum(c?.hard) },
{ k: "Tokens ges.", v: fmtNum(c?.tokens_prompt) },
].map((x) => (
<div key={x.k} className="rounded-sm bg-muted/20 py-1.5">
<div className="font-mono text-sm font-semibold tabular-nums text-foreground">{x.v}</div>
<div className="text-[10px] uppercase tracking-wider text-muted-foreground/70">{x.k}</div>
</div>
))}
</div>
{/* Ehrlichkeits-Nachweis */}
{abw != null && (
<div className="mt-3 flex items-center gap-2 rounded-sm bg-muted/15 px-2.5 py-1.5">
{Math.abs(abw) < 15
? <Check className="h-3.5 w-3.5 shrink-0 text-emerald-400" />
: <ShieldAlert className="h-3.5 w-3.5 shrink-0 text-amber-400" />}
<span className="text-[11px] text-muted-foreground">
Schätzung lag{" "}
<span className={Math.abs(abw) < 15 ? "font-semibold text-emerald-400" : "font-semibold text-amber-400"}>
{abw > 0 ? "+" : ""}{abw.toFixed(1)} %
</span>{" "}
neben der Wahrheit er kalibriert sich aus jeder Antwort selbst nach.
</span>
</div>
)}
{/* Letzte Läufe */}
{runs.length > 0 && (
<div className="mt-3 space-y-1">
{runs.slice(0, 5).map((r, i) => {
const m = ACTION_META[r.action] ?? { label: r.action, tone: "text-muted-foreground" }
return (
<div key={`${r.t}-${i}`} className="flex items-center gap-2 text-[11px]">
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(r.t)}</span>
<span className="w-20 shrink-0 truncate font-medium text-foreground">{r.model || "?"}</span>
<span className="flex-1 font-mono tabular-nums text-muted-foreground">
{fmtNum(r.exact ?? r.est)}
{r.exact != null && r.est != null && (
<span className="text-muted-foreground/50"> (geschätzt {fmtNum(r.est)})</span>
)}
</span>
<span className={`shrink-0 font-semibold ${m.tone}`}>{m.label}</span>
</div>
)
})}
</div>
)}
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
Alle Coding-Agenten laufen durch den Wächter (:8100). Lucys Alltagsmodelle sind
ausgenommen ein Gespräch ist keine Bau-Sitzung und wird nie unterbrochen.
</div>
</div>
)
}
@@ -0,0 +1,12 @@
import type { LucideIcon } from "lucide-react"
// Kleine Abschnitts-Überschrift innerhalb einer Seite. Lag vorher in AuftragsbuchView;
// seit der Ideen-Queue eine eigene Seite hat (25.07.2026) brauchen beide sie — also
// einmal hier statt zweimal dupliziert.
export function SectionLabel({ icon: Icon, children }: { icon: LucideIcon; children: React.ReactNode }) {
return (
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
<Icon className="h-3.5 w-3.5" /> {children}
</p>
)
}
+6
View File
@@ -30,3 +30,9 @@ export function fmtEta(s?: number): string {
export function fmtCtx(c: number | null): string {
return c ? `${Math.round(c / 1024)}k` : "—"
}
/** Unix-Sekunden → "25.07., 21:04"; "—" bei 0/null/undefined. */
export function fmtWhen(ts: number | null | undefined): string {
if (!ts) return "—"
return new Date(ts * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })
}
+7 -1
View File
@@ -10,10 +10,11 @@ import {
Sparkles,
SquareTerminal,
HelpCircle,
Lightbulb,
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "auftraege" | "eigenleben" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type ViewId = "dashboard" | "ideen" | "auftraege" | "eigenleben" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
@@ -28,6 +29,11 @@ export interface NavItem {
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
export const NAV: NavItem[] = [
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
// Der Haupteingang: Idee → Konzept → Repo → Zed. Steht bewusst direkt unter dem
// Cockpit — hier fängt jede Arbeit an (eigene Seite seit 25.07.2026).
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
{ id: "ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
{ id: "eigenleben", label: "Skills", hint: "Fähigkeiten der Box — aktiv/inaktiv, Herkunft & Vorschlags-Bilanz", icon: Sparkles, group: "Operativ" },
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
+24 -765
View File
@@ -1,14 +1,14 @@
import { useEffect, useState } from "react"
import { useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Layers, Lightbulb, Send, HelpCircle,
RotateCcw, Square, ArrowUp, ArrowDown, Code2, FileText, GitFork,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, ArrowRight,
} from "lucide-react"
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type IdeenKonzept, type IdeenProjekt, type SkillKandidat } from "@/lib/api"
import { Markdown } from "@/lib/markdown"
import { useAuftragsbuch, useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
import { fmtWhen } from "@/lib/format"
import { SectionLabel } from "@/components/ui/SectionLabel"
// Auftragsbuch = die Vorschlags-Inbox der Box. Werkstatt/Orchestrator liefern propose-only-
// Branches, der Traum liefert Skill-Kandidaten — hier fällt die Entscheidung per Klick.
@@ -31,10 +31,6 @@ const STATE_UI: Record<string, { label: string; cls: string; spin?: boolean }> =
abgelehnt: { label: "abgelehnt", cls: "text-muted-foreground border-border/60 bg-background/30" },
}
function fmtWhen(ts: number | null | undefined): string {
if (!ts) return "—"
return new Date(ts * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })
}
export function AuftragsbuchView() {
const { data, isLoading, refetch, isFetching } = useAuftragsbuch()
@@ -62,8 +58,11 @@ export function AuftragsbuchView() {
const items = data?.items ?? []
const kandidaten = data?.skill_kandidaten ?? []
// Hängende Ideen leben seit 25.07. auf der eigenen Ideen-Seite. Das grüne „Nichts zu
// entscheiden" darf trotzdem nicht behaupten, alles sei erledigt — es zeigt jetzt einen
// Querverweis statt die Karte selbst (vorher stand sie direkt darunter).
const blockedIdeen = ideenData?.available ? ideenData.items.filter((i) => i.status === "blocked").length : 0
const empty = items.length === 0 && kandidaten.length === 0 && blockedIdeen === 0
const empty = items.length === 0 && kandidaten.length === 0
return (
<div className="space-y-6">
@@ -104,7 +103,20 @@ export function AuftragsbuchView() {
</div>
)}
<IdeenSection />
{blockedIdeen > 0 && (
<button
onClick={() => window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "ideen" } }))}
className="flex w-full items-center gap-3 rounded-2xl border border-red-500/30 bg-red-500/5 p-4 text-left transition-all hover:border-red-500/50 cursor-pointer">
<Lightbulb className="h-5 w-5 shrink-0 text-red-300" />
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">
{blockedIdeen === 1 ? "Eine Idee hängt und braucht dich" : `${blockedIdeen} Ideen hängen und brauchen dich`}
</p>
<p className="text-xs text-muted-foreground">Hier ist nichts zu tun aber drüben in den Ideen wartet eine Frage auf deine Antwort.</p>
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-red-300/70" />
</button>
)}
{items.length > 0 && (
<section className="space-y-3">
@@ -129,759 +141,6 @@ export function AuftragsbuchView() {
)
}
function SectionLabel({ icon: Icon, children }: { icon: any; children: React.ReactNode }) {
return (
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
<Icon className="h-3.5 w-3.5" /> {children}
</p>
)
}
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
// Eine Queue, drei Türen (Zentrale/Telegram/Lucy). Idee rein → die Box arbeitet sie
// selbst aus (Specifier) und baut (Werkstatt-Profil) → Code kommt als Karte zurück.
const IDEEN_STATE: Record<string, { label: string; cls: string; spin?: boolean }> = {
triage: { label: "wird ausgearbeitet …", cls: "text-violet-300 border-violet-500/40 bg-violet-500/10", spin: true },
todo: { label: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
scheduled: { label: "geplant", cls: "text-muted-foreground border-border/60 bg-background/30" },
ready: { label: "gleich dran", cls: "text-sky-300/80 border-sky-500/30 bg-sky-500/5" },
running: { label: "in Arbeit …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
review: { label: "im Review", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
blocked: { label: "hängt — braucht dich", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
done: { label: "fertig ✓", cls: "text-emerald-300 border-emerald-500/40 bg-emerald-500/10" },
}
// Welche Welt hat die Karte durchlaufen? Vorher stand auf Idee UND IDE-Projekt dasselbe
// „IDE" — man sah der fertigen Karte nicht an, ob sie nur durchdacht oder als Repo
// vorbereitet wurde (User 21.07.). Jetzt drei ehrliche Marken.
const ART_UI: Record<string, { label: string; cls: string; icon: typeof Hammer; title: string }> = {
box: {
label: "BOX", icon: Hammer,
cls: "border-border/60 bg-background/40 text-muted-foreground",
title: "Box-Welt: die Werkstatt baut das selbst. Das Ergebnis kommt als Patch-Vorschlag zurück — nichts geht ohne deinen Klick live.",
},
idee: {
label: "IDEE", icon: Lightbulb,
cls: "border-sky-500/40 bg-sky-500/10 text-sky-300",
title: "Nur durchdacht: die Box hat die Produkt-Idee mit voller Denk-Kaskade zu einem Konzept ausgearbeitet. Kein Gerüst, kein Bau — du liest es und entscheidest, ob ein Projekt draus wird.",
},
projekt: {
label: "IDE", icon: Code2,
cls: "border-violet-500/40 bg-violet-500/10 text-violet-300",
title: "IDE-Welt: die Box bereitet nur vor (Konzept + Repo + Doku, kein Code). Gebaut wird von dir in Zed.",
},
}
// Wie die Frage einer hängenden Karte eingeleitet wird — je nach Block-Grund des Workers.
const FRAGE_HINT: Record<string, string> = {
needs_input: "Die Box hat eine Frage:",
capability: "Die Box kam hier nicht weiter:",
transient: "Die Aufgabe hakt:",
}
function IdeenSection() {
const { data, isLoading } = useIdeen()
const qc = useQueryClient()
const { showAlert, showConfirm, dialogElement } = useDialog()
const [text, setText] = useState("")
// Intake-Modus: Box baut · Idee (nur durchdenken) · IDE simpel/gründlich (Projekt vorbereiten).
const [modus, setModus] = useState<"box" | "idee" | "simpel" | "gruendlich">("box")
const isIdee = modus === "idee" // Produkt-Idee prüfen
const isProjekt = modus === "simpel" || modus === "gruendlich" // Projekt für Zed
const isIde = isIdee || isProjekt // beides → projektstart
// Ziel-Plattform des Projekts (nur IDE-Welt): läuft es als LXC-Dienst oder in Docker?
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
const [sending, setSending] = useState(false)
const [busy, setBusy] = useState<Record<string, boolean>>({})
const [openLog, setOpenLog] = useState<string | null>(null)
const [openKonzept, setOpenKonzept] = useState<string | null>(null)
const [antworten, setAntworten] = useState<Record<string, string>>({})
const items = data?.items ?? []
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
// Polling-Log nur aktivieren, wenn Idee triage/running und geöffnet
const logEnabled = openLog !== null && (data?.items.find((i) => i.id === openLog)?.status === "triage" || data?.items.find((i) => i.id === openLog)?.status === "running")
const logData = useIdeenLog(openLog ?? "", logEnabled)
async function submit() {
const t = text.trim()
if (t.length < 3 || sending) return
setSending(true)
try {
// Lange Eingaben: Kurzfassung als Titel, alles als Notiz — der Specifier der Box
// macht daraus ohnehin einen sauberen Auftrag.
const titel = t.length > 160 ? t.slice(0, 157) + "…" : t
await api("/api/ideen", { method: "POST", body: JSON.stringify({
titel, notiz: t.length > 160 ? t : "",
welt: isIde ? "ide" : "box",
tiefe: isProjekt ? modus : "",
ziel: isProjekt ? ziel : "", // eine Idee braucht noch keine Plattform
art: isIdee ? "idee" : "projekt",
}) })
setText("")
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setSending(false)
}
}
async function archive(id: string) {
setBusy((b) => ({ ...b, [id]: true }))
try {
await api("/api/ideen/archivieren", { method: "POST", body: JSON.stringify({ id }) })
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, [id]: false }))
}
}
// Karten-Steuerung (Stopp / Neuer Versuch / Prio) — ein Helfer für alle drei Endpunkte.
async function steuer(prefix: string, id: string, pfad: string, body: object) {
setBusy((b) => ({ ...b, [prefix + id]: true }))
try {
await api(pfad, { method: "POST", body: JSON.stringify(body) })
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, [prefix + id]: false }))
}
}
// Hängende Aufgabe beantworten: die Antwort geht als Kommentar an die Karte, der Worker
// macht damit weiter (Ausweg aus der Sackgasse „fragt, aber niemand kann antworten").
async function answer(id: string) {
const t = (antworten[id] || "").trim()
if (t.length < 1 || busy["ans:" + id]) return
setBusy((b) => ({ ...b, ["ans:" + id]: true }))
try {
await api("/api/ideen/antwort", { method: "POST", body: JSON.stringify({ id, antwort: t }) })
setAntworten((a) => ({ ...a, [id]: "" }))
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, ["ans:" + id]: false }))
}
}
// EINE Zeilen-Darstellung für alle Sichten (Projekt-Gruppen + Einzelkarten) — damit
// Antwortfeld, Live-Log und Ergebnis-Panel überall identisch funktionieren.
const zeile = (it: IdeenItem) => {
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
const isOpen = openLog === it.id
// Aufklappbar: laufende Karten zeigen das Live-Log, FERTIGE das Ergebnis
// (User-Wunsch 15.07.: „Dinge werden getestet, ich sehe das Ergebnis nicht").
const canOpen = it.status === "triage" || it.status === "running" || it.status === "done"
const blocked = it.status === "blocked"
const answering = !!busy["ans:" + it.id]
// Hänger-Verdacht: läuft, aber seit >10 min kein Lebenszeichen (Heartbeat) — der
// Prozess kann trotzdem „leben" (wartet z. B. auf einen selbst gestarteten Server).
const haengt = it.status === "running" && (it.puls_alter ?? 0) > 600
const wartend = it.status === "todo" || it.status === "ready" || it.status === "scheduled"
return (
<li key={it.id} className={cn("flex flex-col gap-2 rounded-xl border bg-background/30 px-3 py-2", blocked ? "border-red-500/30" : haengt ? "border-amber-500/40" : "border-border/40")}>
<div className="flex items-center gap-2">
<span className={cn("flex shrink-0 items-center gap-1.5 rounded-lg border px-2 py-0.5 text-[10px] font-bold", ui.cls)}>
{ui.spin && <Loader2 className="h-3 w-3 animate-spin" />}
{ui.label}
</span>
{haengt && (
<span className="flex shrink-0 items-center gap-1 rounded-lg border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-bold text-amber-300"
title="Der Worker lebt, gibt aber kein Lebenszeichen mehr — vermutlich wartet er auf etwas, das nie kommt. „Neuer Versuch“ startet die Karte frisch.">
<AlertTriangle className="h-3 w-3" /> {Math.round((it.puls_alter ?? 0) / 60)} min ohne Regung
</span>
)}
{(it.prio ?? 0) !== 0 && (
<span className="shrink-0 rounded bg-background/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"
title="Priorität — höher kommt zuerst dran (bei gleicher Prio: Erstellzeit)">P{it.prio}</span>
)}
{(() => {
const a = ART_UI[it.art || (it.welt === "ide" ? "projekt" : "box")]
if (!a) return null
const Icon = a.icon
return (
<span className={cn("flex shrink-0 items-center gap-1 rounded-lg border px-2 py-0.5 text-[10px] font-bold", a.cls)} title={a.title}>
<Icon className="h-3 w-3" /> {a.label}
</span>
)
})()}
{/* Tooltip = der volle TITEL (der ist abgeschnitten), nicht der Auftragstext
der beginnt mit Vorspann-Textbausteinen und half beim Lesen nie. */}
<span className="min-w-0 flex-1 truncate text-xs text-foreground" title={it.titel}>{it.titel}</span>
<span className="flex shrink-0 items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(it.erstellt)}</span>
{wartend && (
<span className="flex shrink-0 items-center">
<button
disabled={!!busy["pr:" + it.id]}
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "hoch" })}
title="Vorziehen (Prio +1)"
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
<ArrowUp className="h-3.5 w-3.5" />
</button>
<button
disabled={!!busy["pr:" + it.id]}
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "runter" })}
title="Zurückstellen (Prio 1)"
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
<ArrowDown className="h-3.5 w-3.5" />
</button>
</span>
)}
{(it.status === "running" || blocked) && (
<button
disabled={!!busy["rt:" + it.id]}
onClick={() =>
showConfirm("Neuer Versuch?",
it.status === "running"
? `Der laufende Worker von '${it.titel.slice(0, 60)}' wird beendet (samt gestarteter Test-Prozesse) und die Karte frisch gestartet. Bisheriger Zwischenstand im Workspace bleibt liegen.`
: `Die Karte '${it.titel.slice(0, 60)}' wird entsperrt und läuft weiter, sobald ein Platz frei ist.`,
() => steuer("rt:", it.id, "/api/ideen/retry", { id: it.id }))}
title="Neuer Versuch — Karte frisch starten"
className={cn("shrink-0 rounded-md p-1 transition-colors cursor-pointer disabled:opacity-50",
haengt ? "text-amber-300 hover:text-amber-200" : "text-muted-foreground/60 hover:text-foreground")}>
{busy["rt:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RotateCcw className="h-3.5 w-3.5" />}
</button>
)}
{(it.status === "running" || wartend || it.status === "triage") && (
<button
disabled={!!busy["st:" + it.id]}
onClick={() =>
showConfirm("Karte stoppen?",
`'${it.titel.slice(0, 60)}' wird angehalten${it.status === "running" ? " und der laufende Worker beendet" : ""}. Sie bleibt als gestoppt im Board und kann jederzeit per „Neuer Versuch“ weiterlaufen.`,
() => steuer("st:", it.id, "/api/ideen/stopp", { id: it.id }))}
title="Stoppen — Karte anhalten"
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-red-300 cursor-pointer disabled:opacity-50">
{busy["st:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Square className="h-3.5 w-3.5" />}
</button>
)}
{(it.status === "done" || blocked) && (
<button
disabled={!!busy[it.id]}
onClick={() => archive(it.id)}
title={blocked ? "Aufgeben und aus der Liste räumen (wandert ins Kanban-Archiv)" : "Aus der Liste räumen (wandert ins Kanban-Archiv)"}
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
{busy[it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
</button>
)}
{it.welt === "ide" && (it.status === "running" || it.status === "done") && (
<button
onClick={() => setOpenKonzept(openKonzept === it.id ? null : it.id)}
title="Konzept ansehen (das ausgearbeitete KONZEPT.md)"
className={cn("flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-bold transition-colors cursor-pointer",
openKonzept === it.id ? "text-violet-300" : "text-violet-300/70 hover:text-violet-200")}>
<FileText className="h-3.5 w-3.5" /> Konzept
</button>
)}
{canOpen && (
<button
onClick={() => setOpenLog(isOpen ? null : it.id)}
title={it.status === "done" ? "Ergebnis anzeigen" : "Worker-Log anzeigen"}
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer">
{isOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
</button>
)}
</div>
{openKonzept === it.id && (
<KonzeptPanel
id={it.id}
istIdee={it.art === "idee" && it.status === "done"}
onWeiter={reload}
/>
)}
{it.status === "running" && it.notiz && (
<p className="-mt-1 ml-1 text-[11px] italic leading-relaxed text-sky-200/80" title="Jüngste Zwischenmeldung des Workers (Heartbeat)">
{it.notiz}
</p>
)}
{(it.status === "todo" || it.status === "scheduled" || it.status === "ready") && (it.wartet_auf?.length ?? 0) > 0 && (
<p className="-mt-1 ml-1 text-[10px] text-muted-foreground/70">
wartet auf: {it.wartet_auf!.join(" · ")}
</p>
)}
{blocked && (
<div className="space-y-2 border-t border-red-500/20 pt-2">
<p className="flex items-start gap-1.5 text-[11px] leading-relaxed text-red-200/90">
<HelpCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-300" />
<span>
<b>{FRAGE_HINT[it.frage_kind || ""] ?? "Die Aufgabe hängt und wartet auf dich:"}</b>{" "}
{it.frage
? <span className="text-foreground/90">{it.frage}</span>
: <span className="text-muted-foreground italic">kein genauer Grund hinterlegt antworte einfach mit einer Entscheidung, dann läuft sie weiter.</span>}
</span>
</p>
<div className="flex gap-2">
<input
value={antworten[it.id] || ""}
onChange={(e) => setAntworten((a) => ({ ...a, [it.id]: e.target.value }))}
onKeyDown={(e) => { if (e.key === "Enter") answer(it.id) }}
placeholder="Deine Antwort — die Box macht damit weiter"
maxLength={2000}
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
/>
<button
onClick={() => answer(it.id)}
disabled={answering || (antworten[it.id] || "").trim().length < 1}
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-emerald-300 transition-all hover:bg-emerald-500/20 cursor-pointer disabled:opacity-50">
{answering ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} Antworten &amp; weiter
</button>
</div>
</div>
)}
{isOpen && canOpen && it.status !== "done" && (
<div className="ml-2 -mt-1">
<div className="rounded-lg border border-border/50 bg-background/50 p-2">
{logEnabled && logData.isLoading ? (
<p className="text-[10px] text-muted-foreground animate-pulse">Log wird geladen </p>
) : logData.error ? (
<p className="text-[10px] text-red-400">Log nicht ladbar</p>
) : logData.data?.available && logData.data.lines.length > 0 ? (
<pre className="max-h-64 overflow-auto font-mono text-[10px] leading-relaxed text-foreground/80 whitespace-pre-wrap">
{logData.data.lines.join("\n")}
</pre>
) : (
<p className="text-[10px] text-muted-foreground">Kein Log verfügbar</p>
)}
</div>
</div>
)}
{isOpen && it.status === "done" && <ErgebnisPanel id={it.id} />}
</li>
)
}
// Windows-Dev / Queue nicht verfügbar → Sektion still weglassen (wie der Rest der Seite).
if (data && !data.available) return null
return (
<section className="space-y-3">
<SectionLabel icon={Lightbulb}>
Ideen-Queue{data ? `${data.offen} offen` : ""}
</SectionLabel>
<div className="mc-card p-4 space-y-3">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
className="h-9 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
/>
<button
onClick={submit}
disabled={sending || text.trim().length < 3}
className={cn("flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-3 text-[11px] font-bold uppercase tracking-wide transition-all cursor-pointer disabled:opacity-50",
modus === "gruendlich" ? "border-amber-500/40 bg-amber-500/10 text-amber-300 hover:bg-amber-500/20"
: modus === "simpel" ? "border-violet-500/40 bg-violet-500/10 text-violet-300 hover:bg-violet-500/20"
: isIdee ? "border-sky-500/40 bg-sky-500/10 text-sky-300 hover:bg-sky-500/20"
: "border-primary/40 bg-primary/10 text-primary hover:bg-primary/20")}>
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : isIdee ? <Lightbulb className="h-3.5 w-3.5" /> : isProjekt ? <Code2 className="h-3.5 w-3.5" /> : <Send className="h-3.5 w-3.5" />}
{isIdee ? "Durchdenken" : isProjekt ? "Vorbereiten" : "In die Queue"}
</button>
</div>
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/70">Modus</span>
<div className="flex rounded-lg border border-border/60 p-0.5">
<button
onClick={() => setModus("box")}
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "box" ? "bg-primary/15 text-primary" : "text-muted-foreground/60 hover:text-foreground")}>
<Hammer className="h-3 w-3" /> Box
</button>
<button
onClick={() => setModus("idee")}
title="Produkt-Idee erst einmal durchdenken lassen: Was ist es, für wen, was gibt es schon, was spricht dagegen? Ergebnis ist ein Konzept — kein Projekt, kein Bau-Druck."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "idee" ? "bg-sky-500/15 text-sky-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Lightbulb className="h-3 w-3" /> Idee
</button>
<button
onClick={() => setModus("simpel")}
title="IDE-Projekt, schnell vorbereitet: knappes Konzept, wenige Denk-Runden. Für kleine, klar umrissene Sachen."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "simpel" ? "bg-violet-500/15 text-violet-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Code2 className="h-3 w-3" /> IDE: simpel
</button>
<button
onClick={() => setModus("gruendlich")}
title="IDE-Projekt, gründlich durchdacht: volle Kaskade mit mehreren Rollen und Härtetests. Für große/ambitionierte Projekte."
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
modus === "gruendlich" ? "bg-amber-500/15 text-amber-300" : "text-muted-foreground/60 hover:text-foreground")}>
<Code2 className="h-3 w-3" /> IDE: gründlich
</button>
</div>
</div>
{/* Immer sichtbare Orientierung der Text darunter erklärt nur den GEWÄHLTEN
Modus, die Wahl selbst muss man aber vorher treffen können (User 21.07.). */}
<p className="text-[10px] leading-relaxed text-muted-foreground/60">
<b className="text-primary/80">Box</b>, wenn die Box es selbst bauen soll (dein Heimlabor,
MC2, Lucy Ergebnis kommt als Patch-Vorschlag zurück). ·{" "}
<b className="text-sky-300/80">Idee</b>, wenn du noch nicht weißt, ob es das überhaupt wert
ist erst durchdenken, entscheiden kannst du danach. ·{" "}
<b className="text-violet-300/80">IDE</b>, wenn du es selbst in Zed bauen willst und dafür
ein startklares Repo brauchst.
</p>
{isProjekt && (
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
<div className="flex rounded-lg border border-border/40 p-0.5">
<button
onClick={() => setZiel("lxc")}
title="Läuft als Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker). Wird vom Backup-Server automatisch mitgesichert."
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === "lxc" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
LXC
</button>
<button
onClick={() => setZiel("docker")}
title="Natives Docker nach Standard-Praxis: Dockerfile + compose, portabel, über Gitea-/GitLab-Pipelines baubar. Für Projekte, die bewusst außerhalb der LXC-Welt leben."
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === "docker" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
Docker
</button>
</div>
<span className="text-[10px] text-muted-foreground/50">
{ziel === "lxc" ? "Dienst im Proxmox-Container (deine Heimat)" : "natives Docker, portabel & pipeline-tauglich"}
</span>
</div>
)}
<p className="text-[11px] text-muted-foreground">
{isIdee ? (
<>
<b className="text-sky-300/90">Idee durchdenken:</b> Die Box arbeitet deine Produkt-Idee
mit Produkt-Blick aus was ist es genau, für wen, was gibt es dafür schon, was wäre der
kleinste sinnvolle Wurf und was spricht dagegen. Volle Denk-Kaskade mit Kritiker, Ergebnis
ist <b>nur ein Konzept</b> (kein Gerüst, keine Roadmap, kein Bau-Druck) du liest es hier
und entscheidest danach, ob ein Projekt draus wird.
</>
) : isProjekt ? (
<>
<b className={modus === "gruendlich" ? "text-amber-300/90" : "text-violet-300/90"}>IDE-Welt{modus === "gruendlich" ? " · gründlich" : " · simpel"}:</b>{" "}
Die Box denkt das Konzept durch{modus === "gruendlich"
? " — volle Kaskade: mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Kritiker-Härtetest (für große/ambitionierte Projekte, dauert entsprechend)"
: " — schlank: 2 Rollen, 1 Denk-Runde (für kleine, klar umrissene Projekte, ein paar Minuten)"}, legt
ein leeres Gitea-Repo an und füllt es nur mit den wichtigen Dokumenten (Konzept, Roadmap, AGENTS.md,
.aiexclude) <b>kein Code</b>. Danach öffnest du das Repo in Zed und baust selbst.
</>
) : (
<>
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
aus und baut Code-Ergebnisse erscheinen als Karte unter Patch-Vorschläge, nichts davon geht ohne
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
</>
)}
</p>
{isLoading && (
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen </p>
)}
{data?.fehler && (
<p className="text-xs text-amber-300">Queue gerade nicht lesbar neuer Versuch läuft.</p>
)}
{items.length > 0 && (
<div className="space-y-3">
{(data?.projekte ?? []).map((p) => (
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)} zeile={zeile} />
))}
{(() => {
// Einzelkarten (keine Kette) — die klassische flache Liste wie bisher.
const keys = new Set((data?.projekte ?? []).map((p) => p.key))
const einzeln = items.filter((i) => !i.projekt || !keys.has(i.projekt))
return einzeln.length > 0 ? <ul className="space-y-1.5">{einzeln.map((it) => zeile(it))}</ul> : null
})()}
</div>
)}
{data?.available && !isLoading && items.length === 0 && (
<p className="text-xs text-muted-foreground/70">Queue ist leer wirf der Box eine Idee zu.</p>
)}
</div>
{dialogElement}
</section>
)
}
// Karten-Familie (verkettete Karten = ein Projekt) als Block: Fortschritt oben, dann die
// AKTIVEN Karten in Ausführungs-Reihenfolge, Wartende und Fertige zusammengeklappt.
// Ersetzt für Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
const AKTIV_RANG: Record<string, number> = { blocked: 0, running: 1, review: 2, ready: 3, triage: 4 }
function ProjektGruppe({ projekt, karten, zeile }: {
projekt: IdeenProjekt
karten: IdeenItem[]
zeile: (it: IdeenItem) => React.ReactNode
}) {
const [zeigWartende, setZeigWartende] = useState(false)
const [zeigFertige, setZeigFertige] = useState(false)
const aktiv = karten
.filter((i) => AKTIV_RANG[i.status] !== undefined)
.sort((a, b) => (AKTIV_RANG[a.status] - AKTIV_RANG[b.status]) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
const wartend = karten
.filter((i) => i.status === "todo" || i.status === "scheduled")
.sort((a, b) => ((a.wartet_auf?.length ? 1 : 0) - (b.wartet_auf?.length ? 1 : 0)) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
const fertig = karten
.filter((i) => i.status === "done")
.sort((a, b) => (b.fertig ?? 0) - (a.fertig ?? 0))
const anteil = projekt.gesamt > 0 ? Math.round((projekt.fertig / projekt.gesamt) * 100) : 0
return (
<div className="space-y-2 rounded-xl border border-border/50 bg-background/20 p-3">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 shrink-0 text-primary/70" />
<p className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground" title={projekt.titel}>{projekt.titel}</p>
<span className="shrink-0 text-[10px] text-muted-foreground">{projekt.fertig} von {projekt.gesamt} fertig</span>
</div>
<div className="h-1 overflow-hidden rounded-full bg-background/60" title={`${anteil}%`}>
<div className="h-1 rounded-full bg-primary/70 transition-all" style={{ width: `${anteil}%` }} />
</div>
{aktiv.length > 0 && <ul className="space-y-1.5">{aktiv.map((it) => zeile(it))}</ul>}
{wartend.length > 0 && (
<div>
<button
onClick={() => setZeigWartende((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{zeigWartende ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{wartend.length} Karte{wartend.length === 1 ? "" : "n"} in der Kette laufen automatisch der Reihe nach
</button>
{zeigWartende && <ul className="mt-1.5 space-y-1.5">{wartend.map((it) => zeile(it))}</ul>}
</div>
)}
{fertig.length > 0 && (
<div>
<button
onClick={() => setZeigFertige((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{fertig.length} fertig
</button>
{zeigFertige && <ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>}
</div>
)}
</div>
)
}
// Konzept einer IDE-Karte: holt es aus dem Gitea-Repo — oder, wenn der Worker keins
// angelegt hat, aus seiner Datei in ~/konzepte (Bug-Fund 21.07.: dann stand hier vorher
// „noch kein Repo hinterlegt", obwohl das Konzept längst fertig war).
// Bei einer fertig durchdachten IDEE hängt unten der Weg nach vorn: „Gefällt mir".
function KonzeptPanel({ id, istIdee, onWeiter }: {
id: string
istIdee?: boolean
onWeiter?: () => void
}) {
const [data, setData] = useState<IdeenKonzept | null>(null)
const [fehler, setFehler] = useState(false)
const [versuch, setVersuch] = useState(0)
useEffect(() => {
let weg = false
setData(null); setFehler(false)
api<IdeenKonzept>(`/api/ideen/${id}/konzept`)
.then((r) => { if (!weg) setData(r) })
.catch(() => { if (!weg) setFehler(true) })
return () => { weg = true }
}, [id, versuch])
return (
<div className="ml-2 -mt-1">
<div className="rounded-lg border border-violet-500/25 bg-violet-500/[0.04] p-3">
<div className="mb-2 flex items-center gap-2">
<p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-violet-300">
<FileText className="h-3 w-3" /> Konzept
</p>
{data?.repo && (
<a href={`https://git.tobisniceshomelab.ddnsfree.com/${data.repo}`}
className="flex items-center gap-1 text-[10px] text-violet-300/70 hover:text-violet-200">
<GitFork className="h-3 w-3" /> {data.repo}
</a>
)}
{!data?.repo && data?.quelle === "datei" && (
<span className="truncate font-mono text-[10px] text-muted-foreground/70"
title="Der Worker hat kein Repo angelegt — das Konzept liegt als Datei auf der Box.">
{data.pfad}
</span>
)}
<button onClick={() => setVersuch((v) => v + 1)} title="Neu laden"
className="ml-auto rounded p-0.5 text-muted-foreground/50 hover:text-foreground cursor-pointer">
<RefreshCw className="h-3 w-3" />
</button>
</div>
{fehler ? (
<p className="text-[11px] text-red-400">Konzept nicht ladbar später nochmal versuchen.</p>
) : data === null ? (
<p className="text-[10px] text-muted-foreground animate-pulse">Konzept wird geladen </p>
) : data.konzept ? (
<div className="max-h-[28rem] overflow-y-auto pr-1">
<Markdown text={data.konzept} />
</div>
) : (
<p className="text-[11px] text-muted-foreground italic">
{data.error || "Noch kein Konzept — das Projekt wird gerade vorbereitet."}
</p>
)}
{istIdee && data?.konzept && <WeiterLeiste id={id} onFertig={onWeiter} />}
</div>
</div>
)
}
// Was nach dem Lesen passieren kann. Zwei Wege, bewusst nebeneinander statt hintereinander:
// nachschärfen (ein Punkt passt nicht — häufiger Fall, darum oben und offen) oder annehmen.
function WeiterLeiste({ id, onFertig }: { id: string; onFertig?: () => void }) {
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
const [tiefe, setTiefe] = useState<"simpel" | "gruendlich">("simpel")
const [busy, setBusy] = useState(false)
const [neu, setNeu] = useState<{ id: string; art: "projekt" | "runde" } | null>(null)
const [fehler, setFehler] = useState("")
const [hinweis, setHinweis] = useState("")
const ruf = async (pfad: string, rumpf: object, art: "projekt" | "runde") => {
setBusy(true); setFehler("")
try {
const r = await api<{ id: string }>(pfad, { method: "POST", body: JSON.stringify(rumpf) })
setNeu({ id: r.id, art })
onFertig?.()
} catch (e: any) {
setFehler(String(e?.message || e))
} finally {
setBusy(false)
}
}
if (neu) {
return (
<p className="mt-3 border-t border-violet-500/20 pt-2 text-[11px] text-emerald-300">
{neu.art === "projekt" ? (
<>Projekt-Karte <span className="font-mono">{neu.id}</span> ist angelegt die Box bereitet
das Repo vor (Konzept übernimmt sie, sie denkt es nicht neu). Danach öffnest du es in Zed.</>
) : (
<>Überarbeitung <span className="font-mono">{neu.id}</span> läuft die Box arbeitet deine
Rückmeldung ein und schreibt die neue Fassung an dieselbe Stelle. Danach hier wieder
Konzept" öffnen.</>
)}
</p>
)
}
return (
<div className="mt-3 space-y-3 border-t border-violet-500/20 pt-2.5">
<div className="space-y-1.5">
<p className="text-[11px] text-muted-foreground">
<b className="text-sky-300/90">Passt etwas nicht?</b> Schreib hin, was anders werden soll
die Box arbeitet es ein und lässt den Rest stehen. Beliebig oft wiederholbar.
</p>
<div className="flex gap-2">
<input
value={hinweis}
onChange={(e) => setHinweis(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && hinweis.trim().length >= 3 && !busy) ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde") }}
placeholder="z. B. „Punkt 3: kein WebSocket, das ist mir zu fragil — nimm Polling“"
maxLength={2000}
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-sky-400/50 focus:outline-none"
/>
<button
onClick={() => ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde")}
disabled={busy || hinweis.trim().length < 3}
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-sky-500/40 bg-sky-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-sky-300 transition-all hover:bg-sky-500/20 cursor-pointer disabled:opacity-50">
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
Nachschärfen
</button>
</div>
</div>
<p className="text-[11px] text-muted-foreground">
<b className="text-violet-300/90">Passt es so?</b> Dann macht die Box daraus ein startklares
Repo Konzept wird übernommen (nicht neu gedacht), dazu Roadmap, AGENTS.md &amp; Co.
Gebaut wird danach von dir in Zed.
</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
<div className="flex rounded-lg border border-border/40 p-0.5">
{(["lxc", "docker"] as const).map((z) => (
<button key={z} onClick={() => setZiel(z)}
title={z === "lxc" ? "Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker) — wird automatisch mitgesichert."
: "Natives Docker: Dockerfile + compose, portabel und pipeline-tauglich."}
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
ziel === z ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
{z === "lxc" ? "LXC" : "Docker"}
</button>
))}
</div>
<span className="text-[11px] text-muted-foreground/70">Aufwand</span>
<div className="flex rounded-lg border border-border/40 p-0.5">
{(["simpel", "gruendlich"] as const).map((t) => (
<button key={t} onClick={() => setTiefe(t)}
title={t === "simpel" ? "Konzept ist schon gehärtet — nur kurz auf die Bau-Sicht nachschärfen (empfohlen)."
: "Nochmal volle Kaskade über die Bau-Sicht: mehrere Rollen, Härtetests. Dauert entsprechend."}
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
tiefe === t ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
{t === "simpel" ? "schlank" : "gründlich"}
</button>
))}
</div>
<button
onClick={() => ruf("/api/ideen/weiterfuehren", { id, ziel, tiefe }, "projekt")}
disabled={busy}
className="ml-auto flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-violet-300 transition-all hover:bg-violet-500/20 cursor-pointer disabled:opacity-50">
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Code2 className="h-3.5 w-3.5" />}
Gefällt mir Projekt daraus machen
</button>
</div>
{fehler && <p className="text-[11px] text-red-400">{fehler}</p>}
</div>
)
}
// Ergebnis einer FERTIGEN Queue-Aufgabe: die Abschluss-Zusammenfassung des Workers
// (kanban_complete). Lazy — wird erst beim Aufklappen geladen, ändert sich nie mehr.
function ErgebnisPanel({ id }: { id: string }) {
const [erg, setErg] = useState<IdeenErgebnis | null>(null)
const [fehler, setFehler] = useState(false)
// Fehler darf nicht kleben: fällt der erste Versuch in ein Deploy-Fenster
// (Zentrale startet gerade neu), muss ein Klick neu laden können (Vorfall 16.07.).
const [versuch, setVersuch] = useState(0)
useEffect(() => {
let weg = false
setFehler(false)
setErg(null)
api<IdeenErgebnis>(`/api/ideen/${id}/ergebnis`)
.then((r) => { if (!weg) setErg(r) })
.catch(() => { if (!weg) setFehler(true) })
return () => { weg = true }
}, [id, versuch])
const text = erg ? (erg.summary || erg.result) : ""
return (
<div className="ml-2 -mt-1">
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/5 p-2.5">
<p className="mb-1 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-emerald-300">
<ClipboardCheck className="h-3 w-3" /> Ergebnis des Workers
</p>
{fehler ? (
<button
type="button"
onClick={() => setVersuch((v) => v + 1)}
className="text-[10px] text-red-400 underline decoration-dotted underline-offset-2 hover:text-red-300"
>
Ergebnis nicht ladbar nochmal versuchen
</button>
) : erg === null ? (
<p className="text-[10px] text-muted-foreground animate-pulse">Wird geladen </p>
) : text ? (
<p className="max-h-64 overflow-y-auto whitespace-pre-wrap text-xs leading-relaxed text-foreground/85">{text}</p>
) : (
<p className="text-[10px] text-muted-foreground italic">
Der Worker hat keine Abschluss-Zusammenfassung hinterlassen (ältere Karte oder Protokollverstoß).
</p>
)}
</div>
</div>
)
}
function StateBadge({ item }: { item: AuftragItem }) {
const st = item.status
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,6 @@ import { ErinnerungenCard } from "./ErinnerungenCard"
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
import { LatencyCard } from "@/components/dashboard/LatencyCard"
import { GovernorCard } from "@/components/dashboard/GovernorCard"
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
const openDrawer = (tab: "maintenance" | "logs") =>
@@ -129,10 +128,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<div className="mt-4">
<LatencyCard />
</div>
{/* Token-Wächter: wie voll ist die laufende Coding-Sitzung, und wie ehrlich zählt er */}
<div className="mt-4">
<GovernorCard />
</div>
</section>
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}