77 Commits

Author SHA1 Message Date
Hitonabi 733582900a fix: auto-sync skills to hermes skills directory during deployment
Ampel / ampel (push) Successful in 23s
2026-08-08 00:28:47 +02:00
Hitonabi 8b14dfdc14 feat: rewrite trend-radar skill for full autonomy and self-modification
Ampel / ampel (push) Successful in 22s
2026-08-08 00:18:06 +02:00
Hitonabi dced44548d fix: correctly pass 'chat' command to hermes CLI when starting skills
Ampel / ampel (push) Successful in 23s
2026-08-08 00:05:24 +02:00
Hitonabi 4c0af92b3c fix: restore Wissen tab but filter out traeume directory
Ampel / ampel (push) Successful in 23s
2026-08-08 00:00:54 +02:00
Hitonabi 5cc1e6bea1 fix: remove frontend/dist from main to fix CI build
Ampel / ampel (push) Successful in 25s
2026-08-07 23:42:13 +02:00
Hitonabi 102149a5d6 build: compile frontend and fix gitignore for dist
Ampel / ampel (push) Successful in 22s
2026-08-07 23:36:43 +02:00
Hitonabi 0c849f4dcd fix: use autonomous zero-intervention lane (-z) for skill runner 2026-08-07 23:33:41 +02:00
Hitonabi 94490f114d fix: correct hermes cli invocation for skills run
Ampel / ampel (push) Successful in 21s
2026-08-07 17:10:48 +02:00
Hitonabi c360bbe14e fix: ruff import sorting in skills.py
Ampel / ampel (push) Successful in 22s
2026-08-07 17:05:49 +02:00
Hitonabi 6cb29ee70e feat: show running state of skills in frontend using psutil
Ampel / ampel (push) Failing after 21s
2026-08-07 17:03:30 +02:00
Hitonabi a6e2f78c27 feat: add missing SKILL.md files for autonomous hermes crons
Ampel / ampel (push) Successful in 22s
2026-08-07 16:50:02 +02:00
Hitonabi a201c20bf2 fix: resolve correct path for skills directory relative to repo root
Ampel / ampel (push) Successful in 22s
2026-08-07 16:42:39 +02:00
Hitonabi 56676fe83c fix: remove outdated Wissen/Traeume functionality completely
Ampel / ampel (push) Successful in 23s
2026-08-07 16:32:34 +02:00
Hitonabi 77bead5ba4 fix: update release-dist tracking branch explicitly during deployment
Ampel / ampel (push) Successful in 24s
2026-08-07 16:28:38 +02:00
Hitonabi ee456de713 fix: remaining ruff formatting issues and robust gitea auth
Ampel / ampel (push) Successful in 23s
2026-08-07 16:25:27 +02:00
Hitonabi e16f0140f7 fix: ampel ci unbound variable GITHUB_SHA
Ampel / ampel (push) Failing after 22s
2026-08-07 16:19:35 +02:00
Hitonabi 637c21387a fix: ruff linter warnings
Ampel / ampel (push) Failing after 23s
2026-08-07 16:14:49 +02:00
Hitonabi da03de18cb feat: Skills Dashboard, CI Build Automation, Auto-Sync & CI-Ampel MCP
Ampel / ampel (push) Failing after 22s
2026-08-07 16:08:49 +02:00
Hitonabi 1b0184eed8 Llama-Swap Config Sync (Devstral hinzugefügt, GLM-4.7 entfernt, persistent=true)
Ampel / ampel (push) Failing after 23s
2026-08-07 15:58:46 +02:00
Hitonabi 4fb8387f79 IDE Line Cleanup & Agentic IDE Umbau
Ampel / ampel (push) Failing after 22s
2026-08-07 14:01:27 +02:00
Hitonabi cb884abdc8 fix: graph view node payload keys (content instead of label)
Ampel / ampel (push) Failing after 22s
2026-08-07 12:20:34 +02:00
Hitonabi 0a6b6fd18d chore: build frontend for wissens-vault graph view
Ampel / ampel (push) Failing after 24s
2026-08-07 12:16:05 +02:00
Hitonabi 8f359ef4c9 feat: obsidian-style graph view for wissens-vault 2026-08-07 12:16:01 +02:00
Hitonabi 327d5a495e feat: add llm_wiki cron, update hermes config docs for memory and browser
Ampel / ampel (push) Failing after 23s
2026-08-07 12:09:55 +02:00
Hitonabi 69925c6cde feat: autonome hermes skills für trend_radar und pruefstand
Ampel / ampel (push) Failing after 21s
2026-08-07 12:01:10 +02:00
Hitonabi a5410642fa feat: ide-lane hardening (ci ampel in auftragsbuch, hermes auto-bugfix, cleanup)
Ampel / ampel (push) Failing after 22s
2026-08-07 11:47:50 +02:00
Hitonabi c3851f8e25 refactor(docs): remove obsolete mem0 and governor references, update AGENTS.md for Hermes
Ampel / ampel (push) Failing after 21s
2026-08-07 11:33:41 +02:00
Hitonabi 29f8797fbf fix: resolve auftragsbuch list_proposals crash and enhance lucy prompt
Ampel / ampel (push) Failing after 21s
2026-08-07 11:23:35 +02:00
Hitonabi 9886d80b1c fix: add URLs to hacker news items and fix prompt for sysadmin report
Ampel / ampel (push) Failing after 21s
2026-08-07 11:21:10 +02:00
Hitonabi 5bc709221e feat: add news, discover models and auftragsbuch to sysadmin report
Ampel / ampel (push) Failing after 21s
2026-08-07 11:19:15 +02:00
Hitonabi 14f8a5f405 fix: change sysadmin report LLM endpoint to 9001 and truncate event texts
Ampel / ampel (push) Failing after 22s
2026-08-07 11:15:30 +02:00
Hitonabi 16d2a06d95 fix: make Sysadmin Telegram report much shorter and mobile-friendly
Ampel / ampel (push) Failing after 23s
2026-08-07 11:13:50 +02:00
Hitonabi 45f799c57d feat: implement daily Telegram sysadmin report
Ampel / ampel (push) Failing after 22s
2026-08-07 11:12:06 +02:00
Hitonabi e814122c73 feat: restore TokenPerformanceCard
Ampel / ampel (push) Successful in 21s
2026-08-07 11:11:00 +02:00
Hitonabi d45f2fc6cb fix: remove Memory (Gehirn) completely from frontend widgets
Ampel / ampel (push) Successful in 22s
2026-08-07 11:05:59 +02:00
Hitonabi c2319739e2 fix: remove unused EigenlebenView imports and rebuild frontend
Ampel / ampel (push) Successful in 22s
2026-08-07 11:02:49 +02:00
Hitonabi 1e68f62499 feat: complete MC2 Kahlschlag (remove governor, memory, eigenleben, obsolete deploy scripts and frontend widgets)
Ampel / ampel (push) Successful in 24s
2026-08-07 10:59:39 +02:00
Hitonabi 5504918a61 Merge branch 'main' of https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2 2026-08-07 10:57:37 +02:00
Hitonabi 17a129038b chore: update claude ssh permissions 2026-08-07 10:57:37 +02:00
Auftragsbuch 93c17fe653 Revert "Auftragsbuch: 'feature/mem0-venv-update' angenommen (Ein-Klick-Gate)"
Ampel / ampel (push) Failing after 22s
This reverts commit ae622dfa88, reversing
changes made to 76d2d7c74b.
2026-08-02 12:13:43 +02:00
Auftragsbuch ae622dfa88 Auftragsbuch: 'feature/mem0-venv-update' angenommen (Ein-Klick-Gate)
Ampel / ampel (push) Failing after 22s
2026-08-02 12:13:40 +02:00
Hitonabi 6637332b3e mem0-venv: chromadb>=1.6.0, pip>=26.1.2 + update-skript
Ampel / ampel (push) Failing after 22s
- requirements.txt: chromadb==1.5.9 → chromadb>=1.6.0, pip>=26.1.2 hinzugefügt
- update-mem0-venv.sh: Idempotentes Update-Skript mit Backup und Smoke-Test
2026-08-02 06:45:26 +02:00
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
Hitonabi 59d6be8c4a OpenCode-Vorlagen vollstaendig: PC-Seite + README
Ampel / ampel (push) Successful in 22s
Die Box-Seite lag im Repo, die PC-Seite nur auf dem PC — bei Verlust waere die
Haelfte des Setups unrekonstruierbar gewesen. Jetzt liegen beide Vorlagen
nebeneinander, plus ein README mit der Mannschaftsaufstellung (inkl. der auf der
Box gemessenen Tempi) und den vier Fallen, die beim Bauen Zeit gekostet haben:
kein _comment in opencode.json, Plugin muss Windows+Linux koennen, session.idle
ueberlebt `opencode run` nicht, plugin/ vs plugins/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:40:49 +02:00
Hitonabi 7fac17ed9a Umbau Stufe 1-5: Governor v2 + OpenCode-Plugin + Subagenten-Mannschaft
Die Intelligenz sitzt nicht mehr NEBEN dem Coden, sondern DRIN. Alles auf der
Box gemessen, nicht angenommen.

Stufe 1 — Speicher-Haushalt (llama-swap-Config, nicht im Repo):
  Coder dauerwarm statt ttl 600 (Kaltstart 23 s -> 491 ms), Devstral als
  'kritiker' verdrahtet, Swap 11 GB -> 0. Zwei Befunde: Kontext 131k->65k spart
  bei Qwen3-Next 0 GB (Hybrid-Attention), und llama-swap haelt nur EINE Gruppe
  resident -> alles Ko-Residente in dieselbe Gruppe. `persistent: true` verhindert
  dabei das Freiraeumen vor grossen Modellen -> ausgeloester Kernel-OOM, behoben
  durch persistent:false + TTLs (Vision laedt jetzt in 10 s statt 117 s + Absturz).

Stufe 2 — Governor v2 (deploy/governor/):
  Steht jetzt IM Pfad (:8100 -> MC2 :9001) statt daneben. Zaehlt den GANZEN
  Anfragekoerper inkl. tools/tool_calls und kalibriert sich aus den echten
  usage.prompt_tokens jeder Antwort nach: Schaetzfehler 200 % -> 0,3 %.
  Soft-Einschub nur noch, wenn die Nachrichtenkette es erlaubt (kein Dazwischen-
  funken in offene tool_calls). Neuer Status-Endpunkt + systemd-Unit.
  Lucys Alltagsmodelle sind vom Schnitt ausgenommen.

Stufe 3 — OpenCode-Plugin (deploy/opencode/plugin/mc2-governor.ts):
  Werkzeug-Zaun (git push, rm -rf, sudo, curl|sh — bewiesen), Pruef-Tor auf
  session.idle mit Selbstreparatur, Savepoint statt Kompression, Meldungen an
  Lucys Briefkasten mit eigenem Absender 'loop'.

Stufe 4 — Mannschaft (opencode.json):
  plan+build -> coder (51,5 t/s) · explore -> hermes (69,6 t/s, warm, gratis) ·
  review -> Devstral (15,0 t/s, FREMDE Modellfamilie gegen blinde Flecken).
  Der 63-GB-Planer faellt aus der Tagesrolle raus.

Stufe 5 — ein Regelwerk, zwei Ausloeser:
  deploy/opencode-lauf.sh faehrt dieselbe Bau-Pruef-Schleife unbeaufsichtigt
  (die Schleife liegt hier UND im Plugin: bei `opencode run` endet der Prozess,
  bevor session.idle fertig ist — gemessen). Bricht ab, wenn der Governor fehlt.
  gitea-repo-create.sh saet jetzt VERIFY neben der CI-Ampel: jedes neue Repo
  wird mit Innen- UND Aussen-Pruefung geboren.

Oberflaeche:
  Token-Waechter-Kachel im Cockpit (Fuellstand, Marken, Ehrlichkeits-Nachweis),
  /api/governor als gleichursprüngliches Fenster, Devstral im Modellkatalog,
  Rollen-Texte auf die neue Mannschaft aktualisiert.

.gitattributes: deploy/**/*.py auf LF (deploy/*.py greift nur eine Ebene tief).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:39:32 +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
Hitonabi 775e862652 CI: MC2-Ampel auf sinnvolle Gates zugeschnitten (Lint+Import+Frontend-Build)
Ampel / ampel (push) Successful in 22s
Die universelle Vorlage will pip-install ALLER 5 requirements (inkl. schwerer ML-Deps:
Whisper/TTS) + pytest repo-weit in einem stateless Container — fuer dieses Multi-Service-
ML-Monorepo weder machbar noch aussagekraeftig (echte Tests = Pruefstand/Integration auf
der Box mit Live-Diensten). MC2-Ampel prueft stattdessen, was im Container ehrlich gruen
sein kann und echte Fehler faengt: ruff (Projekt-Politik) + compileall + Frontend-Build
inkl. tsc-Typecheck. Bewusste Abweichung von 'pytest = Pflicht' fuer dieses Repo, begruendet
im Workflow-Kopf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:56:16 +02:00
Hitonabi 47f7a85510 Ruff-Cleanup: ganzes MC2-Repo lint-grün + projekt-passende ruff.toml
Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo
auf. Aufgeraeumt:
- ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except,
  S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI-
  Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports
  geschuetzt (F401).
- ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports,
  ueberfluessige noqa) auto-behoben.
- 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat
  geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string),
  UP035 (veraltete typing-Imports).
Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:53:36 +02:00
Hitonabi e6502f676a CI: Ampel fuer MC2 verdrahtet (.gitea/workflows/ampel-ci.yml)
Ampel / ampel (push) Failing after 1m52s
Die universelle Ampel (aus deploy/ampel-ci.yml) war bisher nur Vorlage — MC2 hatte
keine aktive .gitea/workflows/-Datei, Pushes triggerten keinen CI-Lauf. Jetzt
nachgeruestet (offene Karte t_730a747a). Laeuft auf push+pull_request, erkennt
Projekt-Typ selbst (Python/Node) und prueft Ruff+compileall+pytest bzw. npm ci+build+test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:33:30 +02:00
Hitonabi 554c87aaee Governor: driver.py lint-sauber (Ruff gruen)
Imports nach oben (sortiert), Endpoint-Setzen in main() statt Modulebene ->
kein E402/unused-noqa mehr. governor.py war bereits sauber. 'ruff check' = 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:30:10 +02:00
Hitonabi 798de0be8f Governor Phase 0 fertig + Phase 2: Hart-Deckel default + Sprach-Signal an Lucy
Phase 0 abgeschlossen:
- Hart-Deckel jetzt Default AN (Soft+5000), da der weiche Schnitt allein bei
  Weiterarbeit ueber die Grenze eine Fassade erzeugt (P0-Befund). GOV_HARD_CEILING=0
  schaltet ihn aus. gov-ctl reicht Arg 2 nur bei Bedarf durch.
- README: Empfehlung/Doku auf Default-an aktualisiert, Commit-Msg-Restpunkt notiert
  (--no-auto-commits als saubere Option).

Phase 2 (Voice-Hook): beim Feuern (soft/hard) POSTet der Governor best-effort +
gedrosselt (Default 300 s) eine Meldung an Lucys vorhandene Announce-Pipeline
(POST :9001/api/voice/announce, source=governor, priority=normal). Lucy pollt,
dedupliziert und spricht sie via lokales TTS -- KEIN Lucy-Code noetig. E2E bewiesen:
Feuern -> ANNOUNCE status=200 -> Eintrag id 394 source=governor in der Queue.
GOV_ANNOUNCE_URL="" schaltet es ab. announce-test.sh beigelegt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:06:10 +02:00
Hitonabi c13cfd2bd0 Governor Phase 0: Token-Waechter-Proxy + Aider Session-Hygiene (bewiesen)
Duenner, zustandsloser Proxy (nur stdlib) zwischen Aider und llama-swap :8080.
Schiebt an einer Token-Schwelle "SAVEPOINT.md finalisieren + stoppen" ein (weich)
bzw. antwortet oberhalb eines optionalen Hart-Deckels selbst. Beweist Session-
Hygiene per hartem Schnitt statt Auto-Compaction -- ohne eine Zeile Lucy-Code.

Alle 4 Akzeptanzkriterien gruen: feuert im Log; SAVEPOINT.md gepflegt; ehrlicher
Grenz-Savepoint am ersten Feuern; frische Sitzung liest Savepoint -> baut Tests,
9 unittest gruen, keine Fassade. CPT 3.5 kalibriert (est ~= echte prompt_tokens).
Hart-Deckel nachgeruestet (weicher Schnitt allein erzeugt Fassade bei Weiterarbeit
ueber die Grenze). Streaming-read1-Fix + 4 kleinere aus adversarialer Review.

Laeuft deployt auf der Box unter ~/governor-p0/ (gov-ctl.sh start <soft> <hart>).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:17:36 +02:00
Auftragsbuch 080eaa5ff5 Auftragsbuch: 'doku/pruefstand-coder-backend-24-07' angenommen (Ein-Klick-Gate) 2026-07-24 16:16:13 +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
305 changed files with 2574 additions and 8179 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"mcpServers": {
"mc2-memory": {
"command": "python",
"args": [
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
]
},
"mc2-system": {
"command": "python",
"args": [
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_mc.py"
]
},
"mc2-web": {
"command": "python",
"args": [
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_web.py"
]
}
}
}
+9
View File
@@ -23,6 +23,15 @@
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
"autoPort": false,
"port": 5180
},
{
"name": "frontend-mock",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
"autoPort": false,
"port": 5181
}
]
}
+8
View File
@@ -1,6 +1,12 @@
{
"permissions": {
"allow": [
"autoMode": {
"allow": [
"$defaults",
"SSH/scp commands to hitonabi@192.168.178.151 that read or write files under ~/.hermes including ~/.hermes/hermes-agent (the Hermes runtime) — the user authorized direct patching of the box's Hermes runtime for the mission-control-2 project"
]
}
"Bash(git fetch *)",
"Bash(git push:*)",
"Bash(git rev-list *)",
@@ -87,6 +93,8 @@
"WebFetch(domain:dev.to)",
"WebFetch(domain:kyuz0.github.io)",
"WebFetch(domain:kmarble.dev)"
"Bash(ssh -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa hitonabi@192.168.178.151:*)",
"Bash(scp -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa:*)"
]
}
}
+4
View File
@@ -11,6 +11,10 @@
# sonst bricht `#!/usr/bin/env python3\r`. (Nur die Hooks + Deploy-Helfer, nicht der ganze Baum.)
deploy/agent-hooks/*.py text eol=lf
deploy/*.py text eol=lf
# `deploy/*.py` greift NUR eine Ebene tief (* matcht kein /). Alles darunter
# (z. B. deploy/governor/governor.py) braucht ein eigenes Muster — sonst kommt es
# nach einem Windows-Checkout mit CRLF zurück und der Shebang auf der Box bricht.
deploy/**/*.py text eol=lf
# Windows-Batch-Wrapper bleiben CRLF.
*.cmd text eol=crlf
+76
View File
@@ -0,0 +1,76 @@
# Ampel-CI fuer MC2 — auf dieses Multi-Service-Monorepo zugeschnitten (24.07.2026).
#
# Die universelle Vorlage (deploy/ampel-ci.yml) passt fuer EIN-Service-Repos. MC2 hat
# 5 Python-Dienste (backend/voice_service/mem0_service/mcp/client) mit schweren ML-
# Abhaengigkeiten (Whisper/TTS/Embeddings) + ein Frontend. "pip install ALLER requirements
# + pytest repo-weit" in einem stateless Container ist weder machbar (GB-schwere Wheels,
# CUDA) noch aussagekraeftig — die echten Tests sind der Pruefstand (deploy/pruefstand)
# und die Integration auf der Box mit LIVE-Diensten/Modellen, nicht isolierte Unit-Tests.
#
# Darum prueft die MC2-Ampel, was im Container EHRLICH gruen sein kann und trotzdem echte
# Fehler faengt: Lint (ruff, Projekt-Politik in ruff.toml) + Import/Syntax (compileall) +
# Frontend-Build inkl. TypeScript-Typecheck (tsc). Rot ist ein Ergebnis, kein Aergernis.
name: Ampel
on: [push, pull_request]
jobs:
ampel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ampel — Lint + Import + Frontend-Build (MC2-Zuschnitt)
shell: bash
run: |
# Runner-bash laeuft mit -e; abschalten und JEDEN Fehler selbst werten (rot=1),
# am Ende EIN Klartext-Urteil (Lehre Ampel-Lauf #5).
set -u +e
rot=0
echo "== Python: Lint (ruff, Projekt-Politik aus ruff.toml) =="
python3 -m venv /tmp/ampel-venv && . /tmp/ampel-venv/bin/activate || { echo "❌ venv kaputt"; exit 1; }
pip install -q ruff || { echo "❌ ruff-Install kaputt"; exit 1; }
ruff check . || rot=1
echo "== Python: Import/Syntax (compileall) =="
python3 -m compileall -q backend voice_service mem0_service mcp client deploy hermes scripts || rot=1
echo "== Frontend: reproduzierbarer Build (npm ci + tsc + vite) =="
if [ -f frontend/package.json ]; then
if [ ! -f frontend/package-lock.json ]; then
echo "❌ frontend/: kein package-lock.json — Build nicht reproduzierbar."
rot=1
else
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
if [ $rot -eq 0 ]; then
echo "== Frontend: Push nach release-dist =="
git config --global user.name "Gitea Actions"
git config --global user.email "actions@gitea.local"
CURRENT_COMMIT=$(git rev-parse HEAD)
# Erzeuge (oder hole) den orphan branch 'release-dist'
git checkout --orphan release-dist 2>/dev/null || git checkout release-dist
git rm -rf . >/dev/null 2>&1 || true
# Checkout nur frontend/dist vom aktuellen Stand
git add -f frontend/dist
if ! git diff-index --quiet HEAD; then
git commit -m "Automatischer Frontend Build ($CURRENT_COMMIT) [skip ci]"
# Push to release-dist. Token auth from runner is expected to work for Gitea Actions.
git push origin HEAD:release-dist -f
else
echo "Keine Änderungen am Frontend-Build."
fi
# Zurück zum originalen Branch für den Rest des Skripts
git checkout $CURRENT_COMMIT
fi
fi
fi
if [ $rot -ne 0 ]; then
echo "❌ AMPEL ROT — nichts heißt fertig', solange das rot ist."
else
echo "✅ AMPEL GRÜN — Lint + Import + Frontend-Build sauber."
fi
exit $rot
+4
View File
@@ -20,3 +20,7 @@ frontend/dist/avatar.vrm
box_recon*
gemma_swap*
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
frontend/tsconfig.tsbuildinfo
frontend/dist/
+8 -1
View File
@@ -6,8 +6,9 @@ Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `RE
## Was das ist
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
**Hermes-Agent (das autonome Gehirn "Lucy")**. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
**WICHTIG:** Alle alten Dienste wie `mem0`, `governor` oder `eigenleben` sind komplett gelöscht. Hermes ist das EINZIGE "Gehirn" und läuft autonom. Es nutzt keine alte Mem0-DB, sondern sein eigenes System.
## Sprache (nicht verhandelbar)
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
@@ -23,6 +24,12 @@ Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadc
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
## Agentic IDE & Vibe Coding
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
- Es gibt keinen Zed-Workflow, keine OpenCode-CLI-Mittelschicht und keinen Governor mehr.
- Der Agent in OpenCode Desktop nutzt via MCP (`.agents/mcp_config.json`) die API der Box (`:9001/v1`), um autonom Projekte zu bauen.
- **Mix-Ansatz beim Testen:** Der Agent testet lokal. Vor dem Push muss er prüfen, ob das Gitea-`VERIFY`-Skript fehlerfrei durchläuft.
## Zeit & Umgebung
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
+120 -48
View File
@@ -1,72 +1,144 @@
# 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** | Agentic IDE (z.B. OpenCode Desktop) auf Dev-PC via lokaler API + MCP | 100% lokal |
| **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 |
| `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` ·
`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 Agentic IDE bauen → Ampel → main
└── autonomes Vibe Coding via OpenCode Desktop + MCP
```
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. Die Agentic IDE
führt sie im Mix-Ansatz vor jedem Push aus, um Code-Fehler ("Quality Gap") abzufangen.
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. **Prüf-Tor**`VERIFY` nach jeder Etappe; rot → der Agent (OpenCode Desktop) repariert lokal nach.
2. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft in Gitea.
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/* · 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):**
```bash
cd frontend
npm install
npm run dev # http://localhost:5173
```
# Frontend (Dev, proxyt /api → :9000)
cd frontend && npm install && npm run dev # http://localhost:5173
**Frontend (Build → wird vom Backend ausgeliefert):**
```bash
# Frontend (Build → wird vom Backend ausgeliefert)
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_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 |
## 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 |
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
+8 -20
View File
@@ -9,41 +9,39 @@ Server (proxyt /api hierher), daher CORS für localhost offen.
import asyncio
import logging
import os
from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator, Awaitable, Callable
from typing import Any
import httpx
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.requests import Request
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
from routers import (
agent,
auftragsbuch,
chronik,
connect,
console,
eigenleben,
events,
gateway_proxy,
health,
hermes_ui,
ideen,
maintenance,
memory,
models,
routing,
skills,
system,
voice,
wissen,
zeitmaschine,
)
from routers import reminders as reminders_router
from services import memory as memory_svc
from services import ketten_digest, metrics_history, reminders, sentry, warmer
from services import metrics_history, reminders, sentry, warmer
from starlette.requests import Request
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
# für alle Module (logging.getLogger(__name__)).
@@ -68,18 +66,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
if sentry.ENABLED:
tasks.append(asyncio.create_task(sentry.sentry_loop()))
tasks.append(asyncio.create_task(reminders.reminders_loop()))
if ketten_digest.ENABLED:
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
if memory_svc.AUTO_DEDUPE_ENABLED:
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
log.info(
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
memory_svc.AUTO_DEDUPE_INTERVAL,
memory_svc.AUTO_DEDUPE_THRESHOLD,
)
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
app.state.gw_client = httpx.AsyncClient(
@@ -119,8 +107,8 @@ app.include_router(health.router)
app.include_router(models.router)
app.include_router(routing.router)
app.include_router(system.router)
app.include_router(connect.router)
app.include_router(memory.router)
app.include_router(agent.router)
app.include_router(
voice.router
@@ -141,10 +129,10 @@ app.include_router(maintenance.router)
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
app.include_router(skills.router) # Skills & Jobs Dashboard
app.include_router(
console.router
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
+2 -3
View File
@@ -15,13 +15,12 @@ durchreicht (routers/gateway_forward.py, MC_V1_UPSTREAM).
import logging
import os
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import httpx
from fastapi import FastAPI, Request
from config import LLAMA_SWAP_URL, VERSION
from fastapi import FastAPI, Request
from routers import gateway_proxy
logging.basicConfig(
+2 -1
View File
@@ -4,8 +4,9 @@ from pathlib import Path
# Add backend directory to sys.path so we can import services
sys.path.append(str(Path(__file__).resolve().parent))
from services.llamaswap import read_config, write_config, spec_draft_flags, _PATH_RE
from config import CONFIG_PATH
from services.llamaswap import _PATH_RE, read_config, spec_draft_flags, write_config
def migrate():
print(f"Reading config from {CONFIG_PATH}...")
+11
View File
@@ -55,6 +55,17 @@
"role": "scout", "name": "GLM-4.6V-Flash", "repo": "ggml-org/GLM-4.6V-Flash-GGUF",
"family": "glm", "generation": 4.6, "total_params_b": 9, "active_params_b": 3,
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": true
},
{
"role": "kritiker", "name": "Devstral-Small-2-24B", "repo": "mistralai/Devstral-Small-2-24B-Instruct-2512",
"family": "mistral-devstral", "generation": 2.0, "total_params_b": 24, "active_params_b": 24,
"moe": false, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": false
},
{
"role": "kritiker", "name": "GLM-4.7-Flash", "repo": "zai-org/GLM-4.7-Flash",
"family": "glm", "generation": 4.7, "total_params_b": 30, "active_params_b": 3,
"moe": true, "quant": "Q4_K_XL", "ctx": 65536, "tools": true, "vision": false
}
]
}
+1 -4
View File
@@ -1,10 +1,7 @@
"""Agent-Endpoint: Hermes-Status + WebUI-Link (MC verlinkt nur, betreibt nicht)."""
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from fastapi import HTTPException
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
router = APIRouter(prefix="/api")
-1
View File
@@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Gate ist der Klick des Commanders."""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import auftragsbuch
router = APIRouter(prefix="/api")
+1 -3
View File
@@ -3,10 +3,8 @@ Quelle ist der persistente Melde-Briefkasten (services/announce.py): Health-Wäc
Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons, Auftragsbuch — alle
autonomen Kanäle laufen dort bereits durch. Hier wird nichts Neues erhoben, nur erzählt."""
from pydantic import BaseModel
from fastapi import APIRouter
from pydantic import BaseModel
from services import announce
router = APIRouter(prefix="/api")
-1
View File
@@ -1,7 +1,6 @@
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets (auf den Gateway + Memory-MCP)."""
from fastapi import APIRouter
from services.connect import DEFAULT_HOST, build_snippets, check_health
router = APIRouter(prefix="/api")
+2 -3
View File
@@ -14,11 +14,10 @@ import logging
import httpx
import websockets
from config import BOX_CONSOLE_UPSTREAM
from fastapi import APIRouter, Request, WebSocket
from starlette.responses import Response
from config import BOX_CONSOLE_UPSTREAM
log = logging.getLogger(__name__)
router = APIRouter()
@@ -28,7 +27,7 @@ def _register(base: str, upstream: str) -> None:
ws_upstream = upstream.replace("http://", "ws://").replace("https://", "wss://")
@router.websocket(f"/{base}/ws")
async def _proxy_ws(ws: WebSocket) -> None: # noqa: ANN001 — Closure je base
async def _proxy_ws(ws: WebSocket) -> None:
await ws.accept(subprotocol="tty")
try:
async with websockets.connect(
-56
View File
@@ -1,56 +0,0 @@
"""Eigenleben-Endpoints — „Von allein“-Ansicht (Skills + Vorschlags-Bilanz), read-only."""
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException
from services import eigenleben
router = APIRouter(prefix="/api")
class SkillItem(BaseModel):
id: str
name: str
kategorie: str | None
aktiv: bool | None
beschreibung: str
version: str
autor: str
herkunft: str
herkunft_label: str
genutzt: int
zuletzt_genutzt: str | None
erstellt: str | None
status: str
geaendert: float
class EigenlebenOverviewResponse(BaseModel):
available: bool
skills: list[SkillItem]
selbst_erstellt: int
bilanz: dict
class SkillTextResponse(BaseModel):
id: str
text: str
@router.get("/eigenleben", response_model=EigenlebenOverviewResponse)
def get_overview() -> dict:
return eigenleben.overview()
# :path — Kategorie-Skills haben einen Schrägstrich in der ID (autonomous-ai-agents/
# hermes-agent). Als normales {skill_id}-Segment matcht die Route dann nicht, der Request
# fällt in den SPA-Fallback (index.html, Status 200) und die UI zeigt „Konnte den
# Skill-Text nicht laden". Die Pfad-Validierung (max. eine Ebene, keine . /..) macht
# services.eigenleben.skill_text.
@router.get("/eigenleben/skill/{skill_id:path}", response_model=SkillTextResponse)
def get_skill(skill_id: str) -> dict:
text = eigenleben.skill_text(skill_id)
if text is None:
raise HTTPException(404, "Skill nicht gefunden.")
return {"id": skill_id, "text": text}
+1 -2
View File
@@ -27,11 +27,10 @@ import json
import logging
from pathlib import Path
from config import MODELS_DIR
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
from config import MODELS_DIR
log = logging.getLogger(__name__)
router = APIRouter(prefix="/api")
+1 -2
View File
@@ -11,11 +11,10 @@ entfernen → app.py bindet wieder den lokalen Gateway ein.
import logging
from config import V1_UPSTREAM
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse
from config import V1_UPSTREAM
log = logging.getLogger(__name__)
router = APIRouter(prefix="/v1")
+1 -3
View File
@@ -2,11 +2,9 @@ import logging
import os
import httpx
from config import LLAMA_SWAP_URL
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse
from config import LLAMA_SWAP_URL
from services.gateway_stream import record_stream_chunk, record_usage, warn_truncation
from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image
from services.routing_policy import load_policy
+2 -4
View File
@@ -1,10 +1,8 @@
"""Health-/Status-Endpoint — schlanker Lebenszeichen-Check für MC 2.0."""
from pydantic import BaseModel
from fastapi import APIRouter
from config import VERSION
from fastapi import APIRouter
from pydantic import BaseModel
from services import gateway, llamaswap
router = APIRouter(prefix="/api")
+1 -2
View File
@@ -22,11 +22,10 @@ import re
import httpx
import websockets
from config import HERMES_BUILTIN_UI_UPSTREAM
from fastapi import APIRouter, Request, WebSocket
from starlette.responses import RedirectResponse, Response
from config import HERMES_BUILTIN_UI_UPSTREAM
log = logging.getLogger(__name__)
router = APIRouter()
+26 -2
View File
@@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Mensch-Gate bleibt die Karte im Auftragsbuc
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import ideen
router = APIRouter(prefix="/api")
@@ -12,7 +11,7 @@ router = APIRouter(prefix="/api")
class IdeeIn(BaseModel):
titel: str
notiz: str = ""
welt: str = "box" # "box" = Werkstatt baut · "ide" = vorbereiten für Zed
welt: str = "box" # "box" = Werkstatt baut · "ide" = vorbereiten für Agentic IDE
tiefe: str = "" # nur ide: "simpel" | "gruendlich" (leer = Box schätzt selbst)
ziel: str = "" # nur ide-Projekt: "lxc" | "docker" (leer = Box entscheidet)
art: str = "projekt" # nur ide: "projekt" (Repo+Roadmap+Bau) | "idee" (nur durchdenken)
@@ -27,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()
@@ -115,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)
+1 -2
View File
@@ -1,8 +1,7 @@
"""Wartungs-Endpoints: Update-Badge, OS-/Engine-Update, Reboot, Restart, Logs."""
from fastapi import APIRouter, HTTPException, Header
from fastapi import APIRouter, Header, HTTPException
from pydantic import BaseModel
from services import maintenance
router = APIRouter(prefix="/api")
-93
View File
@@ -1,93 +0,0 @@
"""Memory-Endpoints (geteiltes Gedächtnis). LAN-only, kein Token in 2.0-Phase 3."""
import time
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import memory
from services.voice_metrics import park, record_stage
router = APIRouter(prefix="/api")
class MemIn(BaseModel):
content: str
category: str = "knowledge"
source: str = "manual"
class MemUp(BaseModel):
content: str | None = None
category: str | None = None
class DedupeIn(BaseModel):
apply: bool = False
threshold: float = 0.85
class LearnIn(BaseModel):
text: str | None = None
messages: list[dict] | None = None
source: str = "auto"
category: str = "knowledge"
@router.get("/memory/export")
def export() -> dict:
return memory.export_text()
@router.get("/memory/graph")
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die Visualisierung."""
return memory.graph(min_score=min_score, top_k=top_k)
@router.post("/memory/learn", status_code=201)
def learn(body: LearnIn) -> dict:
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 extrahiert Fakten selbst."""
return memory.learn(text=body.text, messages=body.messages,
source=body.source, category=body.category)
@router.post("/memory/dedupe")
def dedupe(body: DedupeIn) -> dict:
return memory.dedupe(apply=body.apply, threshold=body.threshold)
@router.get("/memory")
def list_mem(q: str = "", category: str = "") -> list[dict]:
# Semantischer Retrieve (q gesetzt) = u.a. Hermes' Mem0-Prefetch VOR jedem Turn. Dauer messen
# + parken, damit der laufende Voice-Chat-Turn sie als Unter-Detail seiner Hirn-Zeit einsammelt.
if q:
_t0 = time.perf_counter()
res = memory.list_memories(q=q, category=category)
_ms = (time.perf_counter() - _t0) * 1000.0
record_stage("memory_retrieve", _ms)
park("retrieve", _ms)
return res
return memory.list_memories(q=q, category=category)
@router.post("/memory", status_code=201)
def add(body: MemIn) -> dict:
if body.category not in memory.CATEGORIES:
raise HTTPException(400, f"Kategorie '{body.category}' unbekannt.")
return memory.add_memory(body.content, body.category, body.source)
@router.put("/memory/{mid}")
def update(mid: str, body: MemUp) -> dict:
res = memory.update_memory(mid, content=body.content, category=body.category)
if not res:
raise HTTPException(404, "Eintrag nicht gefunden")
return res
@router.delete("/memory/{mid}")
def delete(mid: str) -> dict:
if not memory.delete_memory(mid):
raise HTTPException(404, "Eintrag nicht gefunden")
return {"ok": True}
+2 -4
View File
@@ -1,10 +1,9 @@
"""Modelle-Endpoints: Liste (mit Caps), Discover, Fit, Register, Groups."""
import psutil
from config import HF_DOWNLOAD_ENV, MODELS_DIR
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from config import HF_DOWNLOAD_ENV, MODELS_DIR
from services import budget, discover, hf, jobengine, llamaswap
from services.fit import evaluate_fit, max_ctx_for
@@ -140,8 +139,7 @@ def install(req: InstallReq) -> dict:
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
args = [hf.hf_bin(), "download", repo]
for f in info["files"]:
args.append(f)
args.extend(info["files"])
if info["mmproj"]:
args.append(info["mmproj"])
args += ["--local-dir", str(target)]
+1 -3
View File
@@ -2,10 +2,8 @@
Konsument ist v.a. der Hermes-Agent via mcp_mc.py (reminder_create/list/delete);
LAN-only wie alle MC2-Endpoints."""
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import reminders
router = APIRouter(prefix="/api")
-1
View File
@@ -2,7 +2,6 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import gateway
from services.routing_policy import policy_meta, save_policy
+83
View File
@@ -0,0 +1,83 @@
import os
import subprocess
import psutil
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/api")
class RunSkillRequest(BaseModel):
skill_name: str
@router.get("/skills")
def list_skills():
"""Listet alle verfügbaren Skills aus dem deploy/skills Verzeichnis auf."""
# Pfad relativ zu dieser Datei (backend/routers/skills.py)
current_dir = os.path.dirname(os.path.abspath(__file__))
repo_root = os.path.dirname(os.path.dirname(current_dir))
skills_dir = os.path.join(repo_root, "deploy", "skills")
skills = []
# Check currently running hermes skill processes
running_skills = set()
for p in psutil.process_iter(['name', 'cmdline']):
try:
cmdline = p.info.get('cmdline')
if cmdline and any("hermes" in arg for arg in cmdline):
for arg in cmdline:
if "Führe den " in arg and " Skill aus" in arg:
# Extract skill name from "Führe den 'skill_name' Skill aus"
import re
match = re.search(r"Führe den '(.+?)' Skill aus", arg)
if match:
running_skills.add(match.group(1))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
for entry in os.scandir(skills_dir):
if entry.is_dir():
# Suche nach SKILL.md für Metadaten
skill_md_path = os.path.join(entry.path, "SKILL.md")
description = ""
if os.path.exists(skill_md_path):
try:
with open(skill_md_path, "r", encoding="utf-8") as f:
# Lese erste Zeilen für Description (YAML Frontmatter)
lines = f.readlines()[:10]
for line in lines:
if "description:" in line.lower():
description = line.split(":", 1)[1].strip().strip('"\'')
break
elif "author:" in line.lower() and not description:
description = line.strip()
except Exception:
pass
skills.append({
"name": entry.name,
"description": description or "Keine Beschreibung verfügbar.",
"running": entry.name in running_skills
})
return {"skills": skills}
@router.post("/skills/run")
def run_skill(req: RunSkillRequest):
"""Startet einen autonomen Skill via Hermes im Hintergrund."""
hermes_bin = os.path.expanduser("~/.local/bin/hermes")
if not os.path.exists(hermes_bin):
# Fallback falls lokal (auf Windows) entwickelt wird
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
try:
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{req.skill_name.replace('/', '_')}.log")
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a") as f:
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
except Exception as e:
return {"ok": False, "err": str(e)}
+4 -6
View File
@@ -9,12 +9,10 @@ import logging
import os
import subprocess
import httpx
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
from fastapi import APIRouter
from pydantic import BaseModel
import httpx
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
from services import backup as backup_svc
from services import maintenance
from services.agent import agent_status
@@ -67,7 +65,7 @@ def _user_unit_active(unit: str) -> bool:
r = subprocess.run(["systemctl", "--user", "is-active", unit],
capture_output=True, text=True, timeout=3)
return r.stdout.strip() == "active"
except Exception: # noqa: BLE001
except Exception:
return False
@@ -135,7 +133,7 @@ def _run(cmd: list[str], cwd: str | None = None) -> dict:
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=180)
return {"ok": p.returncode == 0, "code": p.returncode,
"out": (p.stdout or "")[-2000:], "err": (p.stderr or "")[-2000:]}
except Exception as exc: # noqa: BLE001
except Exception as exc:
return {"ok": False, "code": -1, "out": "", "err": str(exc)}
+7 -7
View File
@@ -11,14 +11,16 @@ LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
import logging
import os
# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv).
import sys as _sys
import time
import httpx
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi.responses import Response, StreamingResponse
from pydantic import BaseModel
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL
from services import announce
from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (intern)
Timer,
@@ -29,8 +31,6 @@ from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (inter
record_stage,
)
# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv).
import sys as _sys
_GUARD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "mcp")
if _GUARD_DIR not in _sys.path:
_sys.path.insert(0, _GUARD_DIR)
@@ -155,7 +155,7 @@ def voice_health() -> dict:
r = httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=httpx.Timeout(5.0))
out["sidecar"] = r.status_code == 200
out["detail"] = r.json() if r.status_code == 200 else None
except Exception as exc: # noqa: BLE001
except Exception as exc:
out["error"] = str(exc)
return out
@@ -180,7 +180,7 @@ def voice_voices() -> dict:
r = httpx.get(f"{VOICE_SERVICE_URL}/voices", timeout=httpx.Timeout(10.0))
r.raise_for_status()
return r.json()
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise HTTPException(502, f"Voice-Sidecar nicht erreichbar: {exc}")
@@ -245,7 +245,7 @@ def voice_get_reference() -> dict:
r = httpx.get(f"{VOICE_SERVICE_URL}/reference", timeout=httpx.Timeout(8.0))
r.raise_for_status()
return r.json()
except Exception as exc: # noqa: BLE001
except Exception as exc:
return {"active": False, "error": str(exc)}
+61 -3
View File
@@ -3,12 +3,12 @@ Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom T
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
import os
import re
import time
from pathlib import Path
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/api")
@@ -60,7 +60,7 @@ def list_vault() -> dict:
files = []
now = time.time()
for p in sorted(VAULT.rglob("*.md")):
if ".git" in p.parts:
if ".git" in p.parts or "traeume" in p.parts:
continue
rel = p.relative_to(VAULT).as_posix()
st = p.stat()
@@ -91,3 +91,61 @@ def read_file(pfad: str) -> dict:
raise
except (ValueError, OSError):
raise HTTPException(404, "Notiz nicht lesbar.")
@router.get("/wissen/graph")
def graph_vault() -> dict:
"""Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links)."""
if not _available():
return {"nodes": [], "edges": []}
nodes = []
edges = []
# Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID)
name_to_id = {}
# 1. Alle Nodes sammeln
for p in VAULT.rglob("*.md"):
if ".git" in p.parts or "traeume" in p.parts:
continue
rel = p.relative_to(VAULT).as_posix()
name_to_id[p.stem.lower()] = rel
# Kategorie aus dem Ordner ableiten
cat = "knowledge"
parent = p.parent.name if p.parent != VAULT else ""
if parent == "traeume":
cat = "events"
elif parent == "muster":
cat = "rules"
elif parent == "skill-kandidaten":
cat = "identity"
nodes.append({
"id": rel,
"content": p.stem, # stem ist oft kürzer/prägnanter als der Titel
"category": cat,
"source": "wiki"
})
# 2. Edges extrahieren (Wiki-Links [[Name]])
link_rx = re.compile(r"\[\[([^\]]+)\]\]")
for n in nodes:
try:
target = VAULT / n["id"]
content = target.read_text(encoding="utf-8", errors="replace")
# Set, um mehrfache Links auf dieselbe Datei zu entduplizieren
found_links = {m.group(1).strip().lower() for m in link_rx.finditer(content)}
for link in found_links:
target_id = name_to_id.get(link)
if target_id and target_id != n["id"]:
edges.append({
"source": n["id"],
"target": target_id,
"weight": 1.0
})
except OSError:
pass
return {"nodes": nodes, "edges": edges}
-1
View File
@@ -13,7 +13,6 @@ from pathlib import Path
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import backup as backup_svc
router = APIRouter(prefix="/api")
-2
View File
@@ -10,12 +10,10 @@ Permission errors are handled gracefully.
Uses user journal (systemctl --user) to query logs.
"""
import os
import subprocess
from datetime import datetime, timezone
from pathlib import Path
LOG_DIR = Path.home() / "logs"
LOG_FILE = LOG_DIR / "mc2-timeout.log"
SERVICE_NAME = "mission-control-2.service"
+11 -7
View File
@@ -9,11 +9,15 @@ import os
import re
import httpx
import psutil
from config import (BOX_CONSOLE_UPSTREAM,
BOX_CONSOLE_PATH, HERMES_BUILTIN_UI_UPSTREAM, HERMES_BUILTIN_UI_PATH,
HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL)
from config import (
BOX_CONSOLE_PATH,
BOX_CONSOLE_UPSTREAM,
HERMES_API_URL,
HERMES_BUILTIN_UI_PATH,
HERMES_BUILTIN_UI_UPSTREAM,
HERMES_HOME,
PC_EXECUTOR_URL,
)
log = logging.getLogger(__name__)
@@ -194,7 +198,7 @@ def set_agent_brain(model_id: str) -> dict:
new_members = [x for x in brains if x not in (old, model_id)] + [model_id]
llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm
# 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen.
from services.llamaswap import set_ttl, DEFAULT_TTL
from services.llamaswap import DEFAULT_TTL, set_ttl
set_ttl(model_id, 0)
if old:
set_ttl(old, DEFAULT_TTL)
@@ -253,7 +257,7 @@ def update_brain_model(new_model: str) -> bool:
# Restart the user-space service to apply changes
try:
import services.maintenance as maintenance
from services import maintenance
maintenance.restart_service("hermes-gateway")
except Exception:
log.warning("update_brain_model: hermes-gateway-Restart fehlgeschlagen", exc_info=True)
-1
View File
@@ -20,7 +20,6 @@ import time
from pathlib import Path
import httpx
from config import MODELS_DIR
log = logging.getLogger(__name__)
+64
View File
@@ -26,6 +26,7 @@ import re
import shutil
import subprocess
import time
import urllib.request
from pathlib import Path
from config import MODELS_DIR
@@ -60,6 +61,64 @@ _KANDIDAT_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._ -]{0,120}\.md$")
_fetch_cache: dict = {}
_FETCH_EVERY = 30.0 # s — Gitea nicht bei jedem UI-Poll anfragen
_CI_CACHE: dict = {} # { (repo, branch, head_sha): {"status": "success", "ts": time} }
def _gitea_creds() -> tuple | None:
# 1. Fallback: Versuch via git credential manager (zukunftsfähig & plattformübergreifend)
try:
import subprocess
p = subprocess.run(
["git", "credential", "fill"],
input=b"protocol=https\nhost=git.tobisniceshomelab.ddnsfree.com\n",
capture_output=True,
check=True
)
out = p.stdout.decode(errors="replace")
u_match = re.search(r"username=(.+)", out)
p_match = re.search(r"password=(.+)", out)
if u_match and p_match:
return u_match.group(1).strip(), p_match.group(1).strip()
except Exception:
pass
# 2. Fallback: Datei (hauptsächlich auf der AI-Box genutzt)
try:
cred = Path("~/.git-credentials").expanduser()
for line in cred.read_text(encoding="utf-8").splitlines():
m = re.match(r"https://([^:]+):([^@]+)@git\.tobisniceshomelab", line.strip())
if m: return m.group(1), m.group(2)
except Exception:
pass
return None
def _fetch_ci_status(repo: str, branch: str, head_sha: str) -> str | None:
# Nur auf der Box (wo creds liegen) sinnvoll
creds = _gitea_creds()
if not creds: return None
_user, token = creds
cache_key = (repo, branch, head_sha)
cached = _CI_CACHE.get(cache_key)
if cached and (time.time() - cached["ts"] < 30 or cached["status"] in ("success", "failure", "skipped")):
return cached["status"]
full_repo = "Hitonabi/mission-control-v2" if repo == "mc2" else "Hitonabi/lucy"
url = f"http://192.168.178.153:3000/api/v1/repos/{full_repo}/actions/runs?branch={urllib.parse.quote(branch)}&limit=1"
try:
req = urllib.request.Request(url, headers={"Authorization": "token " + token})
with urllib.request.urlopen(req, timeout=5) as r:
data = json.load(r)
runs = data if isinstance(data, list) else data.get("runs", data.get("workflow_runs", []))
if runs:
run = runs[0]
if run.get("head_sha", "").startswith(head_sha[:7]):
st = run.get("status")
if st:
_CI_CACHE[cache_key] = {"status": st, "ts": time.time()}
return st
except Exception as e:
log.warning("auftragsbuch: CI-Status fetch fehler: %s", e)
return None
def _available() -> bool:
@@ -176,6 +235,10 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
stempel = gutachten.get(f"{repo}:{branch}")
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
stempel = None
head_sha = (_git(repo, ["rev-parse", ref]).stdout or "").strip()
ci_status = _fetch_ci_status(repo, branch, head_sha) if head_sha else None
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
files = []
@@ -209,6 +272,7 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
# es im Repo liegt. Lucy wird dagegen IMMER am PC gebaut (kein dist im Repo).
"frontend_ohne_build": repo == "mc2" and frontend_src and not frontend_dist,
"status": None if (status or {}).get("state") == "eingespielt" and ahead > 0 else status,
"ci_status": ci_status,
})
except Exception:
log.warning("auftragsbuch: Branch %s (%s) nicht lesbar", branch, repo, exc_info=True)
+1 -1
View File
@@ -51,7 +51,7 @@ def backup_now() -> dict:
r = subprocess.run(["/bin/bash", str(BACKUP_SH)], capture_output=True, text=True, timeout=180)
if r.returncode != 0:
return {"ok": False, "snapshot": "", "files": [], "error": (r.stderr or r.stdout).strip()[-300:]}
except Exception as exc: # noqa: BLE001
except Exception as exc:
return {"ok": False, "snapshot": "", "files": [], "error": str(exc)}
latest = _latest()
+3 -4
View File
@@ -10,7 +10,6 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
import json
import httpx
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT, V1_UPSTREAM
DEFAULT_HOST = "192.168.178.151"
@@ -139,7 +138,7 @@ def check_health() -> dict:
gateway = {"ok": True, "detail": f"{n} Modelle verfügbar" if n else "bereit"}
else:
gateway = {"ok": False, "detail": f"HTTP {r.status_code}"}
except Exception: # noqa: BLE001
except Exception:
pass
memory = {"ok": False, "detail": "nicht erreichbar"}
@@ -148,7 +147,7 @@ def check_health() -> dict:
r = c.get(f"{MEM0_SERVICE_URL}/health")
memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200
else {"ok": False, "detail": f"HTTP {r.status_code}"})
except Exception: # noqa: BLE001
except Exception:
pass
desktop_gateway = {"ok": False, "detail": "nicht erreichbar"}
@@ -157,7 +156,7 @@ def check_health() -> dict:
r = c.get(f"{HERMES_BUILTIN_UI_UPSTREAM}/api/status")
desktop_gateway = ({"ok": True, "detail": "bereit"} if r.status_code == 200
else {"ok": False, "detail": f"HTTP {r.status_code}"})
except Exception: # noqa: BLE001
except Exception:
pass
return {"gateway": gateway, "memory": memory, "desktop_gateway": desktop_gateway}
+2 -3
View File
@@ -9,16 +9,15 @@ spätere Auto-Setups nutzen ihn, damit sie nie auseinanderlaufen.
"""
import json
import logging
import math
import os
import time
from datetime import datetime
import httpx
import logging
from config import DISCOVER_CACHE_PATH, DISCOVER_TTL
from services import catalog
from services.caps import capabilities
from services.fit import evaluate_fit, extract_params_b, max_ctx_for
-219
View File
@@ -1,219 +0,0 @@
"""Eigenleben — was die Box von allein gebaut und vorgeschlagen hat.
Beantwortet die User-Frage vom 15.07.2026: Es gibt keinen Viewpoint, welche Skills
erstellt wurden, was die Box vorgeschlagen hat, welche Skills Lucy hat und wie die
funktionieren." Reine LESE-Schicht, keine Schreib-Operationen:
- Skills aus ~/.hermes/skills (= Lucys Skill-Satz; die Worker-Profile werkstatt/betrieb
tragen Kopien desselben Satzes). Herkunft dreistufig klassifiziert:
selbst weder mitgeliefert noch aus unserem Repo = die Box/Lucy hat ihn erzeugt
repo liegt in ~/mission-control-v2/deploy/skills (von uns gebaut + deployt)
bundled liegt in ~/.hermes/hermes-agent/skills (kam mit Hermes mit)
- Nutzungszahlen aus ~/.hermes/skills/.usage.json (Hermes' eigener Zähler).
- Bilanz aus Auftragsbuch-Chronik (/srv/models/mc2-auftragsbuch.json, angenommene
Branches) + Ablehnungs-Lern-Journal (/srv/models/mc2-ablehnungen.jsonl).
"""
from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
SKILLS_DIR = Path.home() / ".hermes" / "skills"
BUNDLED_DIR = Path.home() / ".hermes" / "hermes-agent" / "skills"
REPO_DIR = Path.home() / "mission-control-v2" / "deploy" / "skills"
HERMES_SRC = Path.home() / ".hermes" / "hermes-agent"
HERMES_PY = HERMES_SRC / "venv" / "bin" / "python"
USAGE_FILE = SKILLS_DIR / ".usage.json"
AUFTRAGSBUCH_FILE = Path("/srv/models/mc2-auftragsbuch.json")
ABLEHNUNGEN_FILE = Path("/srv/models/mc2-ablehnungen.jsonl")
_HERKUNFT_LABEL = {
"selbst": "Selbst erstellt",
"repo": "Von uns gebaut",
"bundled": "Mit Hermes mitgeliefert",
}
def _frontmatter(text: str) -> dict:
"""Sehr kleiner Frontmatter-Leser: nur die flachen key: value-Zeilen des ersten
---Blocks (name/description/version/author reichen hier; kein YAML-Import nötig)."""
out: dict = {}
if not text.startswith("---"):
return out
for line in text.split("\n", 1)[1].split("\n"):
if line.strip() == "---":
break
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
if not m:
continue
val = m.group(2).strip().strip('"').strip("'")
if val:
out[m.group(1)] = val
return out
def _norm(s: str) -> str:
return re.sub(r"[\s_-]+", "", (s or "").lower())
# aktiv/inaktiv: dieselbe Wahrheit wie Hermes selbst (und sein eingebautes WebUI) —
# hermes_cli.banner.get_available_skills() prüft Plattform/Voraussetzungen/Config und
# liefert, was der Agent WIRKLICH laden kann. Wir rufen sie im Hermes-venv auf (MC2
# läuft in eigenem Python) und cachen großzügig — der Skill-Satz ändert sich selten.
_aktiv_cache: dict = {"ts": 0.0, "namen": None}
_AKTIV_EVERY = 600.0 # s
def _aktive_namen() -> set[str] | None:
"""Normalisierte Namen aller ladbaren Skills; None = nicht ermittelbar (Dev-Modus)."""
if not HERMES_PY.is_file():
return None
now = time.time()
if _aktiv_cache["namen"] is not None and now - _aktiv_cache["ts"] < _AKTIV_EVERY:
return _aktiv_cache["namen"]
code = ("from hermes_cli.banner import get_available_skills;import json;"
"print(json.dumps(get_available_skills()))")
try:
r = subprocess.run([str(HERMES_PY), "-c", code], capture_output=True,
text=True, timeout=60, cwd=str(HERMES_SRC))
idx = r.stdout.find("{")
kategorien = json.loads(r.stdout[idx:]) if idx >= 0 else {}
namen = {_norm(n) for liste in kategorien.values() for n in liste}
except Exception:
return _aktiv_cache["namen"] # alter Stand ist besser als flackerndes None
_aktiv_cache.update(ts=now, namen=namen)
return namen
def list_skills() -> list[dict]:
"""Alle Skills mit Beschreibung, Herkunft und Nutzungszahlen (leer im Dev-Modus)."""
if not SKILLS_DIR.is_dir():
return []
try:
usage_raw = json.loads(USAGE_FILE.read_text(encoding="utf-8"))
except Exception:
usage_raw = {}
usage = {_norm(k): v for k, v in usage_raw.items()}
aktive = _aktive_namen()
def eintrag(md: Path, rel: str, kategorie: str | None) -> dict:
try:
fm = _frontmatter(md.read_text(encoding="utf-8", errors="replace"))
except Exception:
fm = {}
if (BUNDLED_DIR / rel).is_dir():
herkunft = "bundled"
elif (REPO_DIR / rel).is_dir():
herkunft = "repo"
else:
herkunft = "selbst"
kurz = rel.split("/")[-1]
u = usage.get(_norm(kurz)) or usage.get(_norm(fm.get("name", ""))) or {}
aktiv = None
if aktive is not None:
aktiv = _norm(kurz) in aktive or _norm(fm.get("name", "")) in aktive
return {
"id": rel,
"name": fm.get("name") or kurz,
"kategorie": kategorie,
"aktiv": aktiv,
"beschreibung": fm.get("description") or "",
"version": fm.get("version") or "",
"autor": fm.get("author") or "",
"herkunft": herkunft,
"herkunft_label": _HERKUNFT_LABEL[herkunft],
"genutzt": int(u.get("use_count") or 0),
"zuletzt_genutzt": u.get("last_used_at") or None,
"erstellt": u.get("created_at") or None,
"status": u.get("state") or "active",
"geaendert": md.stat().st_mtime,
}
skills: list[dict] = []
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir() or d.name.startswith("."):
continue # .archive (Curator) & Co. sind keine aktiven Skills
md = d / "SKILL.md"
if md.is_file():
skills.append(eintrag(md, d.name, None))
continue
# Kategorie-Ordner (mitgelieferte Sammlungen wie research/, apple/): eine Ebene
# tiefer liegen die echten Skills (research/blogwatcher/SKILL.md) — je einzeln zeigen.
for sub in sorted(d.iterdir()):
smd = sub / "SKILL.md"
if sub.is_dir() and not sub.name.startswith(".") and smd.is_file():
skills.append(eintrag(smd, f"{d.name}/{sub.name}", d.name))
# Selbst erstellte zuerst — das ist der Star der Ansicht.
order = {"selbst": 0, "repo": 1, "bundled": 2}
skills.sort(key=lambda s: (order[s["herkunft"]], -s["genutzt"], s["name"]))
return skills
def skill_text(skill_id: str) -> str | None:
"""Voller SKILL.md-Text („wie funktioniert der Skill") — max. Kategorie/Name, keine Pfad-Tricks."""
if not re.fullmatch(r"[\w.-]+(/[\w.-]+)?", skill_id or ""):
return None
# Das Zeichen-Set oben erlaubt „." und „.." als Segment — seit die Route ein
# :path-Parameter ist, hier explizit raus (kein Klettern über SKILLS_DIR hinaus).
if any(seg in (".", "..") for seg in skill_id.split("/")):
return None
md = SKILLS_DIR / skill_id / "SKILL.md"
if not md.is_file():
return None
try:
return md.read_text(encoding="utf-8", errors="replace")
except Exception:
return None
def bilanz() -> dict:
"""Vorschlags-Bilanz: was die Box vorschlug und was daraus wurde (eine Timeline)."""
eintraege: list[dict] = []
try:
branches = json.loads(AUFTRAGSBUCH_FILE.read_text(encoding="utf-8")).get("branches") or {}
for branch, info in branches.items():
eintraege.append({
"art": "angenommen" if info.get("state") == "eingespielt" else info.get("state", "?"),
"titel": branch,
"detail": info.get("detail") or "",
"ts": float(info.get("ts") or 0),
})
except Exception:
pass
try:
for line in ABLEHNUNGEN_FILE.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
j = json.loads(line)
except Exception:
continue
eintraege.append({
"art": "abgelehnt",
"titel": j.get("subject") or j.get("branch") or "?",
"detail": j.get("grund") or "",
"ts": float(j.get("ts") or 0),
})
except Exception:
pass
eintraege.sort(key=lambda e: -e["ts"])
return {
"eintraege": eintraege,
"angenommen": sum(1 for e in eintraege if e["art"] == "angenommen"),
"abgelehnt": sum(1 for e in eintraege if e["art"] == "abgelehnt"),
}
def overview() -> dict:
skills = list_skills()
b = bilanz()
return {
"available": SKILLS_DIR.is_dir(),
"skills": skills,
"selbst_erstellt": sum(1 for s in skills if s["herkunft"] == "selbst"),
"bilanz": b,
}
+2 -2
View File
@@ -5,8 +5,8 @@ V1_UPSTREAM gilt der alte eingebaute Modus (MC2 serviert /v1 selbst).
"""
import httpx
from config import PORT, V1_UPSTREAM
from services.llamaswap import engine_reachable
from services.routing_policy import load_policy
@@ -50,7 +50,7 @@ def gateway_reachable() -> bool:
if V1_UPSTREAM:
try:
return httpx.get(f"{V1_UPSTREAM}/v1/models", timeout=2.0).status_code == 200
except Exception: # noqa: BLE001
except Exception:
return False
# Eingebauter Modus: der Gateway lebt in MC selbst und proxyt llama-swap.
return engine_reachable()
+112 -14
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()
@@ -109,7 +112,7 @@ def _kurz(text: str, max_len: int = 80) -> str:
return (schnitt or t[:max_len]) + ""
_TITEL_PRAEFIX_RX = re.compile(r"^\s*(idee|projekt|bitte)\s*[:\-]?\s*", re.I)
_TITEL_PRAEFIX_RX = re.compile(r"^\s*(idee|projekt|bitte)\s*[:\-]?\s*", re.IGNORECASE)
def _projekt_titel(titles: list[str]) -> str:
@@ -220,7 +223,7 @@ def _ketten_anreichern(items: list[dict]) -> list[dict]:
for it in items:
familien.setdefault(boss(it["id"]), []).append(it)
projekte = []
for wurzel, mitglieder in familien.items():
for mitglieder in familien.values():
if len(mitglieder) < 2:
continue
mitglieder.sort(key=lambda i: i.get("erstellt") or 0)
@@ -389,12 +392,12 @@ def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui",
welt='ide': geht DIREKT an das Profil `projektstart` (am Triage/Auto-Zerleger vorbei)
die Box härtet ein Konzept (konzept-fliessband), legt ein leeres Gitea-Repo an und
füllt es NUR mit Doku. Ergebnis ist ein vorbereitetes Projekt, das der Commander in
Zed selbst baut (IDE-Welt). Bewusst KEIN --triage: so fasst der Auto-Zerleger die
Agentic IDE selbst baut (IDE-Welt). Bewusst KEIN --triage: so fasst der Auto-Zerleger die
Karte nie an und die Werkstatt baut sie nie (der Fan-out-Fehler vom 20.07.).
tiefe (nur ide): 'simpel' | 'gruendlich' vom Knopf gewaehlte Konzept-Tiefe; leer =
die Box schaetzt selbst (Fallback fuer Telegram/Lucy).
ziel (nur ide-Projekt): 'lxc' | 'docker' Ziel-Plattform; leer = die Box entscheidet.
art (nur ide): 'projekt' (Default, Repo+Doku+Roadmap, Bau in Zed) oder 'idee'
art (nur ide): 'projekt' (Default, Repo+Doku+Roadmap, Bau in IDE) oder 'idee'
(Produkt-Idee nur durchdenken: NUR Konzept, keine Roadmap/Geruest, kein Bau-Druck)."""
if not _available():
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
@@ -453,7 +456,7 @@ def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui",
"[Ideen-Queue]", "ideen-queue", "silent")
elif welt == "ide":
announce.add(f"Neues IDE-Projekt in Vorbereitung: „{titel}“ ({task['id']}) — die Box "
"härtet das Konzept und legt das Repo an, dann baust du in Zed.",
"härtet das Konzept und legt das Repo an, dann baust du in der Agentic IDE.",
"[Ideen-Queue]", "ideen-queue", "silent")
else:
announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
@@ -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"|"ide","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=ide: 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", "ide") 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 "ide", "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.
@@ -752,8 +850,8 @@ def _lokales_konzept(text: str, titel: str = "") -> tuple | None:
if not kandidaten and titel and _KONZEPT_DIR.is_dir():
worte = {w for w in re.findall(r"[a-z0-9]{3,}", titel.lower())}
treffer = [p.name for p in _KONZEPT_DIR.glob("*.md")
if (lambda g: len(g) >= 2 or any(len(w) >= 6 for w in g))(
worte & set(re.findall(r"[a-z0-9]{3,}", p.stem.lower())))]
if len(g := worte & set(re.findall(r"[a-z0-9]{3,}", p.stem.lower()))) >= 2
or any(len(w) >= 6 for w in g)]
if len(treffer) == 1:
kandidaten.append(treffer[0])
for name in kandidaten:
@@ -883,7 +981,7 @@ def konzept_of(task_id: str) -> dict:
_AUS_IDEE_KOPF = (
"AUFTRAGS-ART: PROJEKT AUS GEPRUEFTER IDEE. Der Commander hat diese Idee erst durchdenken "
"lassen — das Konzept gefaellt ihm, jetzt soll daraus ein startklares Repo werden, das er "
"SELBST in Zed baut.")
"SELBST in der Agentic IDE baut.")
_AUS_IDEE_FUSS = (
"· Denk das Konzept NICHT neu. Es ist bereits mit voller Kaskade + Advocatus Diaboli "
"gehaertet. Lies es vollstaendig und uebernimm es; eine kurze Nachschaerf-Runde fuer die "
@@ -891,7 +989,7 @@ _AUS_IDEE_FUSS = (
"Verschwendung.\n"
"· Der Rest ist deine normale Projektstart-Rolle: das Repo enthaelt am Ende NUR Doku "
"(KONZEPT.md, ROADMAP.md, AGENTS.md, README.md, SAVEPOINT.md, .aiexclude) — KEIN Code.\n"
"· Uebergabe wie ueblich: Repo-URL nennen, damit der Commander in Zed loslegen kann.")
"· Uebergabe wie ueblich: Repo-URL nennen, damit der Commander in der Agentic IDE loslegen kann.")
def _wo_liegt(konz: dict) -> str:
@@ -935,7 +1033,7 @@ def _konzept_und_name(task_id: str) -> tuple:
quell_titel = _TITEL_PRAEFIX_RX.sub("", str(t.get("title") or "").strip())
if not quell_titel:
return {}, "", "Quell-Karte hat keinen Titel."
h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.M)
h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.MULTILINE)
kurz = h1.group(1).strip() if h1 else re.split(r"(?<=[.!?])\s", quell_titel)[0]
return konz, _kurz(kurz, 85), ""
@@ -992,7 +1090,7 @@ def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
try:
from services import announce
announce.add(f"Aus der Idee „{_kurz(kurz, 60)}“ wird ein Projekt: {task['id']} — die Box "
"bereitet das Repo vor, gebaut wird in Zed.",
"bereitet das Repo vor, gebaut wird in der Agentic IDE.",
"[Ideen-Queue]", "ideen-queue", "silent")
except Exception:
pass
@@ -1052,9 +1150,9 @@ def konzept_ueberarbeiten(task_id: str, hinweis: str) -> dict:
"als `KONZEPT.md` hinein (klonen, schreiben, pushen, Push beweisen)."
if konz.get("repo") else ""))
teile = [f"{_UEBERARBEITEN_KOPF}\n{quelle}\n"
teile = [(f"{_UEBERARBEITEN_KOPF}\n{quelle}\n"
f"· SO SOLL ES ANDERS WERDEN — Wortlaut des Commanders:\n{hinweis}\n"
f"{_UEBERARBEITEN_FUSS}",
f"{_UEBERARBEITEN_FUSS}"),
_INFRA_LXC]
titel = f"Konzept nachschärfen: {kurz}"[:200]
args = ["create", titel, "--body", "\n\n".join(teile)[:4000], "--assignee", "projektstart",
@@ -1098,7 +1196,7 @@ def log_of(task_id: str) -> dict:
try:
r = _hermes(["log", task_id])
except Exception as exc:
except Exception:
log.warning("ideen: kanban log fehlgeschlagen", exc_info=True)
return {"available": True, "lines": []}
if r.returncode != 0:
+4 -4
View File
@@ -90,7 +90,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor
log_str = "\n".join(job["log"])
if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str:
job["sudo_failed"] = True
except Exception as exc: # noqa: BLE001
except Exception as exc:
_append_log(job, f"[mc] Fehler: {exc}")
job["state"] = "failed"
job["returncode"] = -1
@@ -101,7 +101,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor
if cb and job["state"] == "done":
try:
cb()
except Exception as exc: # noqa: BLE001
except Exception as exc:
_append_log(job, f"[mc] Nachbearbeitung-Fehler: {exc}")
@@ -136,7 +136,7 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
j["rate_bps"] = rate
j["eta_s"] = int((total_bytes - cur) / rate)
prev_t, prev_b = now, cur
except Exception: # noqa: BLE001
except Exception:
pass
time.sleep(1.0)
j = JOBS.get(job_id)
@@ -182,7 +182,7 @@ def cancel_job(job_id: str) -> bool:
if proc is not None:
try:
proc.terminate()
except Exception: # noqa: BLE001
except Exception:
pass
else:
job["state"] = "canceled"
-191
View File
@@ -1,191 +0,0 @@
"""
Ketten-Digest Telegram-Status für lange Aufgaben-Ketten (User-Wunsch 20.07.2026).
Wenn die Werkstatt eine Karten-Familie abarbeitet (z. B. Homelab-Dashboard 2.0" mit
19 verketteten Karten), soll der Commander nicht raten müssen: dieser Wächter liest
die Queue-Sicht (services/ideen.py, inkl. Ketten-Anreicherung) und meldet per Telegram
SOFORT, wenn eine Karte hängt und eine Frage an den Commander hat (blocked)
einmal pro Karte, mit der Frage im Wortlaut,
bei MEILENSTEINEN (Projekt komplett fertig) sofort,
sonst höchstens einmal pro PULS_SEKUNDEN (Default 60 min) einen Zwischenstand
je aktivem Projekt (5/19 fertig · läuft: · als Nächstes: ").
Taktung war explizite User-Wahl (Meilensteine + 60-min-Puls, Fragen immer sofort").
Zustand (was wurde wann gemeldet) liegt als JSON neben den Modellen und übersteht
Neustarts sonst käme nach jedem Deploy ein Duplikat-Schwall. Auf Windows (Dev) No-op.
"""
import asyncio
import json
import logging
import os
import time
import zlib
from pathlib import Path
from config import MODELS_DIR
log = logging.getLogger(__name__)
ENABLED = os.name == "posix" and os.environ.get("MC_KETTEN_DIGEST", "1") != "0"
INTERVAL = int(os.environ.get("MC_KETTEN_DIGEST_INTERVAL", "300")) # Prüf-Tick: 5 min
PULS_SEKUNDEN = int(os.environ.get("MC_KETTEN_DIGEST_PULS", "3600")) # Zwischenstand: 60 min
# Hänger-Alarm: laufende Karte ohne Lebenszeichen (Heartbeat) länger als diese Schwelle
# → sofortige Telegram-Warnung (einmal pro Karten-Lauf). Live-Fall 20.07.: Worker wartete
# 45 min auf einen selbst gestarteten uvicorn — „pid_alive" hielt den Claim ewig frisch.
HANG_SEKUNDEN = int(os.environ.get("MC_KETTEN_HANG", "900"))
KANBAN_LOGS = Path(os.environ.get("MC_KANBAN_LOGS", "~/.hermes/kanban/logs")).expanduser()
STATE_PATH = Path(os.environ.get("MC_KETTEN_DIGEST_STATE", str(MODELS_DIR / "mc2-ketten-digest.json")))
def _kontext_druck(task_id: str) -> int:
"""Wie oft der Worker zuletzt den Kontext verdichten musste (Log-Marker) —
mehrfaches Compacting context" heißt: die Aufgabe sprengt das Fenster."""
try:
p = KANBAN_LOGS / f"{task_id}.log"
with p.open("rb") as f:
f.seek(max(0, p.stat().st_size - 200_000))
tail = f.read().decode("utf-8", errors="replace")
return tail.count("Compacting context")
except Exception:
return 0
def _load_state() -> dict:
try:
d = json.loads(STATE_PATH.read_text(encoding="utf-8"))
return d if isinstance(d, dict) else {}
except Exception:
return {}
def _save_state(state: dict) -> None:
try:
tmp = STATE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
tmp.replace(STATE_PATH)
except OSError:
log.warning("ketten-digest: Zustand %s nicht schreibbar", STATE_PATH, exc_info=True)
def _melden(subject: str, text: str) -> None:
"""Aufs Handy UND in den Briefkasten (silent — Lucy muss den Status nicht sprechen)."""
from services import announce
announce.notify_telegram(subject, text)
try:
announce.add(text, subject, "ketten-digest", "silent")
except Exception:
pass
def _tick() -> None:
from services import ideen
data = ideen.list_queue()
if not data.get("available"):
return
items = data.get("items") or []
projekte = data.get("projekte") or []
by_id = {i["id"]: i for i in items if i.get("id")}
state = _load_state()
gemeldete_fragen: dict = state.setdefault("fragen", {})
projekt_state: dict = state.setdefault("projekte", {})
dirty = False
now = int(time.time())
# 1) Hängende Karten mit Frage → sofort, einmal pro Karte. Der Schlüssel enthält den
# Fragen-Text-Hash: hängt dieselbe Karte später mit NEUER Frage, wird wieder gemeldet.
for it in items:
if it.get("status") != "blocked":
continue
frage = (it.get("frage") or "").strip()
key = f"{it['id']}:{zlib.crc32(frage.encode('utf-8')):x}"
if key in gemeldete_fragen:
continue
text = (f"Aufgabe hängt und wartet auf dich: „{(it.get('titel') or '')[:120]}\n"
+ (f"Frage: {frage[:500]}\n" if frage else "")
+ "Antworten geht im Auftragsbuch (Zentrale) — die Box macht dann weiter.")
_melden("[Werkstatt]", text)
gemeldete_fragen[key] = now
dirty = True
# 1b) Hänger-Alarm: laufende Karte ohne Lebenszeichen → einmal pro Karten-Lauf warnen.
gemeldete_haenger: dict = state.setdefault("haenger", {})
for it in items:
if it.get("status") != "running":
continue
puls = it.get("puls_alter")
if puls is None or puls < HANG_SEKUNDEN:
continue
key = f"{it['id']}:{it.get('gestartet') or 0}"
if key in gemeldete_haenger:
continue
minuten = int(puls / 60)
text = (f"Karte scheint zu hängen: „{(it.get('titel') or '')[:120]}“ — "
f"seit {minuten} min kein Lebenszeichen vom Worker.")
druck = _kontext_druck(it["id"])
if druck >= 2:
text += (f"\nDer Worker musste {druck}× den Kontext verdichten — "
"die Aufgabe ist womöglich zu groß geschnitten.")
text += "\nIn der Zentrale: „Neuer Versuch“ startet sie frisch, „Stopp“ hält sie an."
_melden("[Werkstatt]", text)
gemeldete_haenger[key] = now
dirty = True
if len(gemeldete_haenger) > 200:
for k in sorted(gemeldete_haenger, key=gemeldete_haenger.get)[:100]:
gemeldete_haenger.pop(k, None)
dirty = True
# Fragen-Gedächtnis klein halten (Karten verschwinden irgendwann ins Archiv).
if len(gemeldete_fragen) > 200:
for k in sorted(gemeldete_fragen, key=gemeldete_fragen.get)[:100]:
gemeldete_fragen.pop(k, None)
dirty = True
# 2) Projekt-Status: Abschluss sofort, sonst gedrosselter Puls solange gearbeitet wird.
for p in projekte:
ps = projekt_state.setdefault(p["key"], {})
fertig, gesamt = p.get("fertig", 0), p.get("gesamt", 0)
if gesamt > 0 and fertig >= gesamt and not ps.get("abschluss_gemeldet"):
_melden("[Werkstatt]", f"Projekt fertig: „{p['titel']}“ — alle {gesamt} Karten erledigt. "
"Ergebnisse liegen im Auftragsbuch.")
ps.update(abschluss_gemeldet=True, letzter_puls=now, letzter_stand=fertig)
dirty = True
continue
aktiv = p.get("laeuft", 0) > 0
if not aktiv:
continue
if now - int(ps.get("letzter_puls") or 0) < PULS_SEKUNDEN:
continue
laufende = [i for i in items if i.get("projekt") == p["key"] and i["status"] == "running"]
naechste = [i for i in items if i.get("projekt") == p["key"]
and i["status"] in ("ready", "todo") and not i.get("wartet_auf")]
zeilen = [f"Werkstatt-Status „{p['titel']}“: {fertig}/{gesamt} fertig."]
for l in laufende[:2]:
note = f"{l['notiz']}" if l.get("notiz") else ""
zeilen.append(f"Läuft: {l['titel'][:90]}{note}")
if naechste:
zeilen.append(f"Als Nächstes: {naechste[0]['titel'][:90]}")
haengt = p.get("haengt", 0)
if haengt:
zeilen.append(f"{haengt} Karte(n) warten auf deine Antwort im Auftragsbuch.")
_melden("[Werkstatt]", "\n".join(zeilen))
ps.update(letzter_puls=now, letzter_stand=fertig)
dirty = True
if dirty:
_save_state(state)
async def digest_loop() -> None:
"""Hintergrund-Task im MC2-Lifespan (Muster: reminders_loop)."""
log.info("ketten-digest: aktiv (Tick %ss, Puls %ss)", INTERVAL, PULS_SEKUNDEN)
while True:
try:
await asyncio.to_thread(_tick)
except Exception:
log.debug("ketten-digest: Tick fehlgeschlagen", exc_info=True)
await asyncio.sleep(INTERVAL)
+14 -9
View File
@@ -11,12 +11,17 @@ import os
import re
import httpx
from ruamel.yaml.scalarstring import LiteralScalarString
from config import (
CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, DRAFTS_DIR, LLAMA_SWAP_URL,
SPEC_DRAFT_MODEL_PATH, SPEC_DRAFT_N_MAX, SPEC_TYPE,
CMD_TEMPLATE,
CONFIG_PATH,
DEFAULT_TTL,
DRAFTS_DIR,
LLAMA_SWAP_URL,
SPEC_DRAFT_MODEL_PATH,
SPEC_DRAFT_N_MAX,
SPEC_TYPE,
)
from ruamel.yaml.scalarstring import LiteralScalarString
log = logging.getLogger(__name__)
@@ -143,13 +148,13 @@ def model_id_from_path(model_path: str) -> str:
Split-GGUFs liegen oft in einem Quant-Unterordner (/Q4_K_M/file-00001-of-)
dann eine Ebene höher (Repo-Ordner) nehmen, sonst hieße das Modell 'Q4_K_M'."""
d = os.path.basename(os.path.dirname(model_path))
if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.I):
if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.IGNORECASE):
d = os.path.basename(os.path.dirname(os.path.dirname(model_path)))
name = re.sub(r"[-_]?GGUF$", "", d, flags=re.I).strip("-_")
name = re.sub(r"[-_]?GGUF$", "", d, flags=re.IGNORECASE).strip("-_")
if not name:
fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.I)
fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.IGNORECASE)
fn = re.sub(r"-\d+-of-\d+$", "", fn)
name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.I)
name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.IGNORECASE)
return name or "modell"
@@ -222,7 +227,7 @@ def write_config(cfg: dict) -> None:
try:
from services import warmer
warmer.nudge()
except Exception: # noqa: BLE001 — Vorwärmen ist Komfort, nie ein Schreib-Blocker
except Exception:
pass
except PermissionError as exc:
raise PermissionError(
+9 -9
View File
@@ -35,7 +35,7 @@ _engine_cache = {"ts": 0.0, "avail": False}
# manchmal noch keine CI-Assets (0 Assets) → ihr Download-Link 404t. Sowohl der Update-Check
# als auch der Download (update-engine.sh) müssen daher die neueste ASSET-tragende Release
# nehmen, sonst zeigt das UI „Update verfügbar", das dann beim Einspielen scheitert.
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.I)
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE)
def _latest_engine_asset_release() -> dict | None:
@@ -289,7 +289,7 @@ def model_upgrades() -> list[dict]:
continue
base = rec.split("/")[-1].lower()
stem = base[:-5] if base.endswith("-gguf") else base
stem = base.removesuffix("-gguf")
if base in cmds or (stem and stem in cmds):
continue # schon installiert
out.append({"role": role, "title": c["title"], "repo": rec})
@@ -343,7 +343,7 @@ def _os_held_back() -> list[dict]:
held.append({"name": name, "reason": reason})
elif reason: # nicht eingerückt → Abschnitt zu Ende
reason = None
except Exception: # noqa: BLE001 — nur Zusatzinfo, nie ein Blocker
except Exception:
pass
held.sort(key=lambda p: p["name"])
return held
@@ -368,7 +368,7 @@ def os_update_details() -> dict:
out_pkgs.sort(key=lambda p: p["name"])
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs,
"held_back": _os_held_back()}
except Exception as exc: # noqa: BLE001
except Exception as exc:
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs, "error": str(exc)}
@@ -393,7 +393,7 @@ def engine_update_details() -> dict:
ctx = ("Es geht um ein Update der Inferenz-Engine llama.cpp (Vulkan-Build, treibt alle "
"Sprachmodelle der Box auf der AMD-Strix-Halo-GPU).")
info.update(_summarize_release("engine", tag, ctx, body[:6000]))
except Exception as exc: # noqa: BLE001
except Exception as exc:
info["error"] = str(exc)
return info
@@ -418,7 +418,7 @@ def swap_update_details() -> dict:
ctx = ("Es geht um ein Update von llama-swap (der Router, der Anfragen an die Box "
"verteilt und Sprachmodelle heiß nachlädt).")
info.update(_summarize_release("swap", tag, ctx, body[:6000]))
except Exception as exc: # noqa: BLE001
except Exception as exc:
info["error"] = str(exc)
return info
@@ -496,7 +496,7 @@ def _summarize_release(kind: str, key: str, context: str, changes: str) -> dict:
if text:
cache.update(key=key, data=data)
return data
except Exception as exc: # noqa: BLE001 — Zusammenfassung ist Komfort, nie Blocker
except Exception as exc:
return {"summary": f"(Zusammenfassung nicht verfügbar: {exc})",
"action_needed": None, "action_text": ""}
@@ -534,7 +534,7 @@ def hermes_update_details() -> dict:
info["behind"] = len(commits)
if commits:
info.update(_summarize_hermes_commits(commits))
except Exception as exc: # noqa: BLE001
except Exception as exc:
info["error"] = str(exc)
return info
@@ -575,7 +575,7 @@ def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."}
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
except Exception as exc: # noqa: BLE001
except Exception as exc:
return {"ok": False, "out": "", "err": str(exc)}
-177
View File
@@ -1,177 +0,0 @@
"""
Geteiltes Gedächtnis (die Verfassung") — jetzt auto-lernend & semantisch über Mem0.
Dieser Service ist nur noch ein dünner HTTP-Client auf den Mem0-Sidecar (mem0_service/app.py,
läuft im ~/.mem0/venv unter Python 3.12). Die `/api/memory`-API-Form bleibt unverändert, damit
UI und MCP-Server kompatibel bleiben. Neu gegenüber der alten flachen SQLite:
- search (q gesetzt) ist SEMANTISCH (Vektor/Embeddings) statt LIKE-Textsuche, mit Relevanz-Score.
- learn() reicht Gesprächs-Turns durch Mem0 EXTRAHIERT Fakten selbst (Auto-Lernen).
- Dedup macht Mem0 beim Auto-Lernen selbst; der manuelle Kurator unten bleibt als Komfort.
5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
"""
import asyncio
import logging
import os
import re
from difflib import SequenceMatcher
import httpx
from config import MEM0_SERVICE_URL
log = logging.getLogger(__name__)
CATEGORIES = ("identity", "knowledge", "rules", "events")
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
def _get(path: str, **params) -> list | dict:
r = httpx.get(f"{MEM0_SERVICE_URL}{path}", params=params, timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
def _post(path: str, data: dict) -> dict:
r = httpx.post(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
def _put(path: str, data: dict) -> dict:
r = httpx.put(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
def _delete(path: str) -> dict:
r = httpx.delete(f"{MEM0_SERVICE_URL}{path}", timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
def list_memories(q: str = "", category: str = "") -> list[dict]:
"""Alle Fakten oder — wenn q gesetzt — die semantisch ähnlichsten (mit `score`)."""
return _get("/memory", **{k: v for k, v in (("q", q), ("category", category)) if v})
def add_memory(content: str, category: str = "stable", source: str = "manual") -> dict:
"""Einen Fakt VERBATIM speichern (keine LLM-Umformung). Auto-Lernen → learn()."""
return _post("/memory", {"content": content.strip(), "category": category, "source": source})
def update_memory(mid: str, content: str | None = None, category: str | None = None) -> dict | None:
try:
return _put(f"/memory/{mid}", {"content": content, "category": category})
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
return None
raise
def delete_memory(mid: str) -> bool:
try:
_delete(f"/memory/{mid}")
return True
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
return False
raise
def learn(text: str | None = None, messages: list[dict] | None = None,
source: str = "auto", category: str = "stable") -> dict:
"""Auto-Lernen: Text/Gesprächs-Turns durchreichen → Mem0 extrahiert die Fakten selbst."""
return _post("/learn", {"text": text, "messages": messages,
"source": source, "category": category})
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die UI-Visualisierung."""
return _get("/graph", min_score=min_score, top_k=top_k)
def export_text() -> dict:
rows = sorted(list_memories(), key=lambda r: (r.get("category", ""), r.get("updated_at", "")))
lines = ["# Mission Control — Gedächtnis\n"]
current = ""
for r in rows:
if r.get("category") != current:
current = r.get("category", "")
lines.append(f"\n## {current}\n")
src = r.get("source", "")
when = (r.get("updated_at") or "")[:10]
lines.append(f"- {r.get('content', '')} _(Quelle: {src}, {when})_")
return {"text": "\n".join(lines), "count": len(rows)}
# --- Manueller Kurator (deterministisch, kein LLM) ---------------------------
def _norm(s: str) -> str:
s = re.sub(r"[^\w\s]", " ", s.lower(), flags=re.UNICODE)
return re.sub(r"\s+", " ", s).strip()
def dedupe(apply: bool = False, threshold: float = 0.85) -> dict:
"""Findet Dubletten (exakt/enthalten/ähnlich) je Kategorie, behält den längsten
Eintrag. Mem0 dedupliziert beim Auto-Lernen schon semantisch das hier ist der
manuelle Komfort-Knopf fürs UI (z.B. nach vielen Verbatim-Importen)."""
rows = sorted(list_memories(), key=lambda r: (-len(r.get("content", "")), r.get("created_at", "")))
used: set[str] = set()
groups: list[dict] = []
for i, a in enumerate(rows):
if a["id"] in used:
continue
na = _norm(a.get("content", ""))
if not na:
continue
dups = []
for b in rows[i + 1:]:
if b["id"] in used or b.get("category") != a.get("category"):
continue
nb = _norm(b.get("content", ""))
if not nb:
continue
if nb in na or na in nb or SequenceMatcher(None, na, nb).ratio() >= threshold:
dups.append(b); used.add(b["id"])
if dups:
used.add(a["id"])
groups.append({
"keep": {"id": a["id"], "content": a.get("content"), "category": a.get("category")},
"remove": [{"id": d["id"], "content": d.get("content")} for d in dups],
})
dup_count = sum(len(g["remove"]) for g in groups)
removed = 0
if apply:
for g in groups:
for d in g["remove"]:
if delete_memory(d["id"]):
removed += 1
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}
# ── Auto-Dedupe (D16e): Dubletten von selbst wegräumen, kein Knopf-Zwang ─────────────
# Deterministisch in UNSERER Schicht (wie reminders, Verdikt 11c) statt am Hermes-cron —
# läuft als Hintergrund-Task. Höhere Schwelle als der manuelle Knopf (0.9 statt 0.85), weil
# OHNE Mensch-Review angewandt wird: lieber eine Dublette stehen lassen als etwas Distinktes
# löschen. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier fängt den Rest
# (Verbatim-Importe, wiederholte Fakten) ab, damit das Gedächtnis nicht ohne Zutun aufbläht.
AUTO_DEDUPE_ENABLED = os.environ.get("MC_MEM_DEDUPE_ENABLED", "1") != "0"
AUTO_DEDUPE_INTERVAL = int(os.environ.get("MC_MEM_DEDUPE_INTERVAL", str(24 * 3600))) # täglich
AUTO_DEDUPE_START_DELAY = int(os.environ.get("MC_MEM_DEDUPE_START_DELAY", "300")) # 5 min nach Start
AUTO_DEDUPE_THRESHOLD = float(os.environ.get("MC_MEM_DEDUPE_THRESHOLD", "0.75"))
async def auto_dedupe_loop() -> None:
"""Hintergrund-Task: räumt periodisch Gedächtnis-Dubletten weg (apply=True)."""
await asyncio.sleep(AUTO_DEDUPE_START_DELAY) # Mem0-Sidecar nach Start hochkommen lassen
while True:
try:
res = await asyncio.to_thread(dedupe, True, AUTO_DEDUPE_THRESHOLD) # sync HTTP → Thread
if res.get("removed"):
log.info("mem-dedupe: %d Dublette(n) automatisch entfernt", res["removed"])
except Exception:
log.debug("mem-dedupe: Lauf fehlgeschlagen", exc_info=True)
await asyncio.sleep(AUTO_DEDUPE_INTERVAL)
+6 -6
View File
@@ -51,7 +51,7 @@ def _load() -> None:
log.info("metrics_history: %d Punkte aus %s geladen", len(_points), HISTORY_PATH)
except FileNotFoundError:
pass
except Exception: # noqa: BLE001 — kaputte Datei = leer starten, nie crashen
except Exception:
log.warning("metrics_history: %s nicht lesbar — starte leer", HISTORY_PATH, exc_info=True)
@@ -77,19 +77,19 @@ def _sample() -> None:
try:
du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd())
disk = du.percent
except Exception: # noqa: BLE001
except Exception:
pass
gpu = None
try:
g = _gpu_sysfs()
gpu = g.get("busy_percent") if g else None
except Exception: # noqa: BLE001
except Exception:
pass
tp = tc = None
try:
ts = get_stats()
tp, tc = ts.get("prompt_tokens"), ts.get("completion_tokens")
except Exception: # noqa: BLE001
except Exception:
pass
# interval=None: nicht-blockierende CPU-Messung seit dem letzten Aufruf (10 s her — ideal).
_points.append([int(time.time()), psutil.cpu_percent(interval=None), vm.percent, gpu, disk, tp, tc])
@@ -101,12 +101,12 @@ async def sampler_loop() -> None:
while True:
try:
await asyncio.to_thread(_sample)
except Exception: # noqa: BLE001
except Exception:
log.debug("metrics_history: Sample fehlgeschlagen", exc_info=True)
if time.time() - _last_flush >= FLUSH_S:
try:
await asyncio.to_thread(_flush)
except Exception: # noqa: BLE001
except Exception:
pass
await asyncio.sleep(SAMPLE_S)
+1
View File
@@ -24,6 +24,7 @@ from pathlib import Path
from zoneinfo import ZoneInfo
from config import MODELS_DIR
from services import announce
log = logging.getLogger(__name__)
+2 -2
View File
@@ -20,8 +20,8 @@ import time
import httpx
import psutil
from config import HERMES_API_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL
from config import HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL
from services import announce, llamaswap
log = logging.getLogger(__name__)
@@ -100,7 +100,7 @@ if os.environ.get("MC_SENTRY_WATCH_MC2", "") == "1":
class _Watch:
__slots__ = ("fails", "alerted", "alert_ts")
__slots__ = ("alert_ts", "alerted", "fails")
def __init__(self) -> None:
self.fails = 0 # Fehl-Ticks in Folge
-1
View File
@@ -12,7 +12,6 @@ import subprocess
import threading
import psutil
from config import MODELS_DIR
-1
View File
@@ -11,7 +11,6 @@ import json
import logging
import threading
import time
from pathlib import Path
from config import HERMES_HOME
+12 -2
View File
@@ -24,7 +24,6 @@ import os
import subprocess
import httpx
from config import LLAMA_SWAP_URL
log = logging.getLogger(__name__)
@@ -74,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 {}
+1 -1
View File
@@ -154,7 +154,7 @@ async def volume_control(req: dict):
async def open_target(req: dict):
target = req.get("target", "")
try:
if target.startswith("http://") or target.startswith("https://"):
if target.startswith(("http://", "https://")):
webbrowser.open(target)
else:
os.startfile(target)
+4 -1
View File
@@ -20,7 +20,10 @@ nur Schreib-Operationen.
Ausgabe {"action":"block","message":...} = Tool geblockt; {} = durchlassen.
"""
import sys, json, os, re
import json
import os
import re
import sys
def out(obj):
+9 -1
View File
@@ -12,7 +12,7 @@ TOKEN=$(grep -m1 'git\.tobisniceshomelab' "$HOME/.git-credentials" 2>/dev/null |
[ -n "$TOKEN" ] || exit 0
python3 - "$TOKEN" "$STATE" <<'PY'
import json, sys, urllib.request
import json, sys, urllib.request, subprocess, os
token, state_pfad = sys.argv[1], sys.argv[2]
# API intern (die DDNS-Domain ist nachts wegen Zwangstrennung oft tot, 24.07.2026);
@@ -79,6 +79,14 @@ if neu:
for full, sha, name in neu:
print(f"• {full} @ {sha} ({name})")
print(f" {WEB}/{full}/actions")
try:
subprocess.run([
os.path.expanduser("~/.local/bin/hermes"), "kanban", "create",
"--title", f"CI rot: {full} @ {sha}",
"--body", f"Der CI Lauf '{name}' in {full} ist fehlgeschlagen. Bitte Log ansehen und Fix vorschlagen."
], capture_output=True)
except Exception as e:
print(f"Fehler bei kanban_create: {e}", file=sys.stderr)
print("Nichts aus diesen Ständen ist fertig', solange die Ampel rot ist.")
with open(state_pfad, "w") as f:
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/env bash
# Chef-Gutachter-Feed (Orchestrator-Finale, User-Idee 06.07.): gpt-oss-120b als NÄCHTLICHER
# Chef-Gutachter über die autonome Arbeit der Box — bewusst NIE im heißen Pfad (Kaltladen
# ~2-5 min ist um 04:30 egal; tagsüber bleibt Lucy schnell). Muster wie dreaming-feed.sh:
# liegt in ~/.hermes/scripts/, stdout wird dem Cron-Agenten als Prompt eingespeist.
# Der eigentliche RICHTER läuft als Ein-Schuss-Completion direkt gegen llama-swap (:8080,
# fremdblick-Architektur — umgeht Hermes' Delegations-Floor und braucht keinen Subagenten).
# Richter-Kette: gpt-oss-120b; wirft der beim Laden "exited prematurely" (neben dem Warm-Set
# live gesehen, E2E 07.07.), übernimmt GLM-4.6V-Flash als gekennzeichnete VERTRETUNG.
set -uo pipefail
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
CHEF="${CHEF_MODEL:-gpt-oss-120b}"
VERTRETUNG="${CHEF_FALLBACK:-GLM-4.7-Flash}"
MC="$HOME/mission-control-v2"
VAULT="$HOME/wissens-vault"
# ---------- Versionierter Auftrag für den Boten-Agenten ----------
cat "$MC/deploy/chef-gutachter-prompt.md"
echo
# ---------- Beweismaterial der letzten 24 h (hart begrenzt, erst sammeln, dann kürzen) ----------
EVID="$(
echo "## BEWEISMATERIAL (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo
echo "### Vorschlags-Branches (Werkstatt/Orchestrator, jüngste zuerst):"
git -C "$MC" fetch -q origin 2>/dev/null || true
git -C "$MC" for-each-ref --sort=-committerdate refs/remotes/origin \
--format='%(committerdate:relative) | %(refname:short) | %(subject)' 2>/dev/null | head -10
echo
echo "### main-Commits der letzten 24 h:"
git -C "$MC" log --since='24 hours ago' --oneline origin/main 2>/dev/null | head -12
echo
echo "### Wissens-Vault (neue Traum-Notizen, 24 h):"
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
echo
echo "### Ideen-Queue (natives Kanban, Stand jetzt — offene Ideen des Commanders):"
bash -lc 'hermes kanban stats' 2>/dev/null | head -12
echo
echo "### Ideen-Queue: zuletzt bewegte Aufgaben:"
bash -lc 'hermes kanban list --sort updated' 2>/dev/null | head -10
echo
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
| sort | uniq -c | sort -rn | head -6
echo
echo "### Aktivitäts-Insights (1 Tag):"
bash -lc 'hermes insights --days 1' 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | head -30
)"
RASTER='Du bist der CHEF-GUTACHTER über die autonome Arbeit einer lokalen KI-Box (Hermes-Agent, Werkstatt, Orchestrator, Traum-Kreislauf). Du bekommst BEWEISMATERIAL der letzten 24 Stunden. Urteile AUSSCHLIESSLICH auf Basis dieses Materials — kein Lob und keine Behauptung ohne konkreten Beleg darin; fehlt dir zu einem Punkt Material, sage das offen. Antworte auf DEUTSCH, kompakt, in exakt dieser Struktur:
VERDIKT: <ein Satz Gesamturteil über die autonome Arbeit der Nacht/des Tages>
RISIKEN: <0-3 Punkte, je mit Beleg aus dem Material; "keine erkennbar" wenn leer>
TOP-3 FUER DEN COMMANDER: <maximal 3 priorisierte, konkrete Empfehlungen — weniger ist ok, erfinde keine>'
# Ein-Schuss-Richter: $1=Modell-ID $2=curl-Timeout $3=max_tokens
judge() {
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$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"]
out = (msg.get("content") or msg.get("reasoning_content") or "").strip()
print(out)
except Exception:
pass'
}
RICHTER="$CHEF"
VERDIKT="$(judge "$CHEF" 420 2600)"
if [ -z "${VERDIKT// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung — $CHEF hat nicht geladen)"
VERDIKT="$(judge "$VERTRETUNG" 240 1200)"
fi
echo "$EVID"
echo
if [ -z "${VERDIKT// /}" ]; then
echo "## CHEF-VERDIKT: AUSGEFALLEN"
echo "Beide Richter ($CHEF, $VERTRETUNG) haben nicht geantwortet — das dem Commander ehrlich melden."
else
echo "## CHEF-VERDIKT (Richter: $RICHTER)"
echo "$VERDIKT"
fi
# Morgenlage für die Zentrale: das Verdikt STILL in den Briefkasten spiegeln (priority=silent —
# Lucy soll die Wand Text nicht vorlesen; die Telegram-Botschaft macht der Cron-Agent selbst).
# Cockpit („Morgenlage"-Karte) und Chronik lesen es dort mit source=chef-gutachter heraus.
if [ -n "${VERDIKT// /}" ]; then
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$RICHTER" "$VERDIKT" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": f"Morgenlage (Richter: {sys.argv[1]})",
"source": "chef-gutachter", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
fi
-15
View File
@@ -1,15 +0,0 @@
# Auftrag: Nacht-Gutachten überbringen (Chef-Gutachter-Cron)
Du bist Lucy. Unten stehen (1) BEWEISMATERIAL der letzten 24 Stunden und (2) das
CHEF-VERDIKT eines großen Gutachter-Modells darüber. Deine Aufgabe ist NUR das Überbringen
an den Commander per Telegram — kurz, in deiner Stimme, ohne Werkzeuge.
Regeln:
1. EIN einleitender Satz von dir (z. B. wie die Nacht lief), dann das Verdikt.
2. Gib VERDIKT, RISIKEN und TOP-3 des Chef-Gutachters TREU wieder — nicht weichspülen,
nicht kürzen bei Risiken, NICHTS dazuerfinden. Du bist die Botin, nicht die Richterin.
3. Nenne am Ende in Klammern den Richter (steht über dem Verdikt: gpt-oss-120b oder
die GLM-Vertretung).
4. Steht dort „CHEF-VERDIKT: AUSGEFALLEN", melde genau das ehrlich — kein Ersatz-Urteil
von dir.
5. Telegram-tauglich: kompakt, keine Markdown-Tabellen, echte Umlaute.
-11
View File
@@ -1,11 +0,0 @@
#!/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
@@ -1,62 +0,0 @@
#!/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
-199
View File
@@ -1,199 +0,0 @@
#!/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 os
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()
+35 -282
View File
@@ -1,292 +1,45 @@
#!/usr/bin/env bash
# Deploy AUF DER BOX als systemd-USER-Dienst — KEIN sudo, KEIN /opt, KEIN Passwort.
# Erstinstallation + Updates in einem. Läuft als User hitonabi.
#
# Erstinstallation (einmalig):
# git clone https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2 ~/mission-control-v2
# bash ~/mission-control-v2/deploy/deploy.sh
set -euo pipefail
#!/bin/bash
set -e
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
echo "Starte MC2 Deployment..."
cd "$SRC"
git fetch -q origin && git reset -q --hard origin/main
# 1. Neuesten Code holen
git pull origin main
# ★ Selbst-Reset-Falle (15.07. beim P1-Deploy live gebissen): der reset oben ersetzt auch
# DIESE Datei, und bash liest laufende Skripte häppchenweise von der Platte weiter — ab
# hier liefe ein Zeilen-Mix aus alter+neuer Version (beim P1-Deploy fielen so daemon-reload
# und der mc2-gateway-Start unter den Tisch). Darum: nach dem Reset genau EINMAL die
# frische Version neu ausführen; der zweite Durchlauf überspringt dank Env-Marke nichts.
if [ "${MC2_DEPLOY_REEXEC:-0}" != "1" ]; then
export MC2_DEPLOY_REEXEC=1
exec bash "$SRC/deploy/deploy.sh"
# 1.5 Frontend aus dem Release-Branch holen (nur dist Ordner)
echo "Hole gebautes Frontend aus release-dist Branch..."
git fetch origin release-dist:refs/remotes/origin/release-dist || true
git restore --source=origin/release-dist --staged --worktree frontend/dist 2>/dev/null || echo "Kein release-dist Branch gefunden (erster Lauf?)."
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
echo "Aktualisiere Python Abhängigkeiten..."
backend/.venv/bin/pip install -r backend/requirements.txt
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
sudo systemctl restart llama-swap
fi
# venv + Abhängigkeiten
if [ ! -d "$SRC/backend/.venv" ]; then
python3 -m venv "$SRC/backend/.venv"
fi
"$SRC/backend/.venv/bin/python" -m pip install -q --upgrade pip
"$SRC/backend/.venv/bin/python" -m pip install -q -r "$SRC/backend/requirements.txt"
# Mem0-Sidecar (auto-lernendes Gedächtnis) — eigenes Python-3.12-venv (~/.mem0/venv),
# weil mem0+chromadb unter dem 3.14-Backend-venv nicht laufen. mem0ai/chromadb sind dort
# bereits installiert; hier nur den HTTP-Server nachziehen + Alt-DB einmalig migrieren.
if [ -x "$HOME/.mem0/venv/bin/python" ]; then
# ~/.mem0/venv ist uv-managed (kein pip) → uv pip nutzen.
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
"$UV" pip install -q --python "$HOME/.mem0/venv/bin/python" -r "$SRC/mem0_service/requirements.txt"
( cd "$SRC/mem0_service" && "$HOME/.mem0/venv/bin/python" migrate.py ) || true
else
echo "WARN: ~/.mem0/venv fehlt — Mem0-Sidecar wird nicht gestartet (siehe Plan A1)."
fi
# Voice-Sidecar (lokales STT + gestuftes TTS für „Mit Hermes reden") — eigenes Python-3.12-venv
# (~/.voice/venv), weil torch/chatterbox/faster-whisper nicht ins 3.14-Backend-venv passen.
# install.sh ist idempotent (venv + Deps + Piper-Stimmen). Best-effort: schlägt es fehl, läuft
# der restliche Stack weiter (der Voice-Tab meldet den Sidecar dann als offline).
bash "$SRC/voice_service/install.sh" || echo "WARN: Voice-Sidecar-Setup fehlgeschlagen — Voice-Tab bleibt offline."
# Hermes-Memory-Provider-Plugin (auto-lernen/Recall via Mem0-Sidecar) nach ~/.hermes/plugins/
# spiegeln. Context-only (kein Tool-Loop). Aktivierung in ~/.hermes/config.yaml:
# memory.memory_enabled: true + memory.provider: mc2-memory (einmalig, box-lokal).
if [ -d "$HOME/.hermes" ]; then
mkdir -p "$HOME/.hermes/plugins/mc2-memory"
cp "$SRC/hermes/plugins/mc2-memory/__init__.py" "$SRC/hermes/plugins/mc2-memory/plugin.yaml" \
"$HOME/.hermes/plugins/mc2-memory/"
fi
# systemd-USER-Units installieren/aktualisieren
mkdir -p "$HOME/.config/systemd/user"
cp "$SRC/deploy/mission-control-2.service" "$HOME/.config/systemd/user/mission-control-2.service"
# MC2-Diät (Hermes Desktop, 07/2026): das Hermes-Terminal-ttyd (:7681) und das standalone
# hermes-webui (nesquena, Karteileiche seit dem Built-in-UI-Umbau) sind entfallen — die
# Agent-Oberfläche ist die Desktop-App bzw. /hermes-ui/. Einmalige, idempotente Stilllegung:
systemctl --user disable --now hermes-terminal 2>/dev/null || true
systemctl --user disable --now hermes-webui 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service"
# Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet.
cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service"
# MC2-Gateway (UMBAU v3 P1): der /v1-Datenpfad als eigener Prozess (Loopback :9010,
# Restart=always) — Lucys Hirn + Nacht-Autonomie überleben damit jeden MC2-Neustart.
cp "$SRC/deploy/mc2-gateway.service" "$HOME/.config/systemd/user/mc2-gateway.service"
# MC2-Steward (UMBAU v3 P2): Wächter-Loops (Re-Warm/Sentry/Dedupe) als eigener Dienst —
# überleben MC2-Neustarts und melden erstmals auch ein totes Steuerpult (Telegram).
cp "$SRC/deploy/mc2-steward.service" "$HOME/.config/systemd/user/mc2-steward.service"
# Mem0-Sidecar-Unit (nur wenn das venv existiert).
[ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service"
# Voice-Sidecar-Unit (nur wenn das venv existiert).
[ -x "$HOME/.voice/venv/bin/python" ] && cp "$SRC/deploy/voice-service.service" "$HOME/.config/systemd/user/voice-service.service"
# Tägliches Zustands-Backup (mem0 + Configs/Secrets) — Timer + oneshot-Service. Siehe docs/BACKUP.md.
cp "$SRC/deploy/mc2-backup.service" "$HOME/.config/systemd/user/mc2-backup.service"
cp "$SRC/deploy/mc2-backup.timer" "$HOME/.config/systemd/user/mc2-backup.timer"
# Wöchentliches Auto-Update (Router/Engine/Hermes, Rollback+Pin+Telegram) — Autonomie E2.
cp "$SRC/deploy/mc2-autoupdate.service" "$HOME/.config/systemd/user/mc2-autoupdate.service"
cp "$SRC/deploy/mc2-autoupdate.timer" "$HOME/.config/systemd/user/mc2-autoupdate.timer"
# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d.
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
# 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"
# 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"
cp "$SRC/deploy/radar-feed.sh" "$HOME/.hermes/scripts/radar-feed.sh"
# Selbstkritik-Cron (Faden 11b): Zwilling des Radars, Blick nach INNEN (Latenzen/Journal/Skills).
cp "$SRC/deploy/selbstkritik-feed.sh" "$HOME/.hermes/scripts/selbstkritik-feed.sh"
# Monats-Review-Wrapper (Cron 1. d. M. 09:00): Radar + Selbstkritik in EINEM Lauf.
cp "$SRC/deploy/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh"
# Traum-Cron (Dreaming, Phase 2): Blick auf das GELERNTE → Wissens-Vault. Bootstrappt ~/wissens-vault.
cp "$SRC/deploy/dreaming-feed.sh" "$HOME/.hermes/scripts/dreaming-feed.sh"
# Fremdblick-Helfer: Ein-Schuss-Zweitmeinung von einem anderen Modell (umgeht 64K-Delegations-Floor).
cp "$SRC/deploy/fremdblick.sh" "$HOME/.hermes/scripts/fremdblick.sh"
# Worker-Helfer (Orchestrator): Ein-Schuss-Arbeitsauftrag an ein Worker-Modell (Gegenstück zu fremdblick).
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"
# 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"
# Idle-Radar (S4 "Zuendung", 12.07.2026): die Box gibt sich nachts selbst Arbeit —
# Journal-Fehlermuster + Traum-Funde -> belegte rohe Ideen (triage) im nativen Kanban.
cp "$SRC/deploy/idle-radar-feed.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh"
# "Lucy kennt sich" (12.07.2026): Selbst-Inventur generiert den Steckbrief + bemerkt
# eigene Aenderungen; der Karten-Gutachter stempelt jede Karte mit einer Empfehlung.
cp "$SRC/deploy/selbst-inventur.sh" "$HOME/.hermes/scripts/selbst-inventur.sh"
cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
# Trend-Radar (17.07.2026, Sa 05:15): Live-Puls aller Rollen + Engine-A/B + Watch-Liste +
# HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche. Prüfstand (So 01:00, --no-agent):
# volles Programm (Coding/Agent/Recherche/Deutsch/Vision) gegen die Amtsinhaber-Baseline;
# Harness + Suiten liegen im Repo ($SRC/deploy/pruefstand/), nur der Runner wird kopiert.
cp "$SRC/deploy/trend-radar-feed.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh"
cp "$SRC/deploy/pruefstand/pruefstand.sh" "$HOME/.hermes/scripts/pruefstand.sh"
# Restore-Probe + Venv-Audit (17.07.2026, monatliche --no-agent-Crons): echte
# Wiederherstellungs-Drills (PBS-Canary + Tarball) und CVE-/Outdated-Audit der
# Python-Nebendienst-venvs (mem0, voice) — melden nur, ändern nichts.
cp "$SRC/deploy/restore-probe.sh" "$HOME/.hermes/scripts/restore-probe.sh"
cp "$SRC/deploy/venv-audit.sh" "$HOME/.hermes/scripts/venv-audit.sh"
# mem0-Konsolidierung (monatlich am 3., 19.07.2026): legt aehnliche Fakten zusammen,
# loest Widersprueche (neuester gewinnt) — mem0 v2 ist ADD-only und tut das nie selbst.
cp "$SRC/deploy/mem0-konsolidierung.py" "$HOME/.hermes/scripts/mem0-konsolidierung.py"
cp "$SRC/deploy/mem0-konsolidierung.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.sh"
# Gitea-Repo-Anlage (17.07.2026): schließt Lucys „Projekte selbst starten"-Lücke —
# legt PRIVATE Projekt-Repos an (Token aus ~/.git-credentials, braucht write:user-Scope).
cp "$SRC/deploy/gitea-repo-create.sh" "$HOME/.hermes/scripts/gitea-repo-create.sh"
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
# betrieb-Profil (Ideen-Queue Faden 7, 13.07.2026): Ops-/Mess-/Bench-Worker mit Box-Wissen
# statt Lucy-default. Profil selbst wird einmalig per `hermes profile create betrieb
# --clone-from werkstatt` angelegt (model coder→hermes, Description fürs Specifier-Routing).
[ -d "$HOME/.hermes/profiles/betrieb" ] && cp "$SRC/deploy/betrieb-SOUL.md" "$HOME/.hermes/profiles/betrieb/SOUL.md"
# IDE-Vorbereiter (20.07.2026): haertet ein Konzept (konzept-fliessband), legt das Repo an und
# fuellt es NUR mit Doku — der Commander baut dann in Zed. Sonderfall „IDEE PRUEFEN": nur Konzept,
# kein Geruest, kein Bau-Druck. Profil einmalig per `hermes profile create projektstart
# --clone-from werkstatt` angelegt (model coder→hermes, damit heavy warm bleibt).
[ -d "$HOME/.hermes/profiles/projektstart" ] && cp "$SRC/deploy/projektstart-SOUL.md" "$HOME/.hermes/profiles/projektstart/SOUL.md"
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh" "$HOME/.hermes/scripts/pruefstand.sh" "$HOME/.hermes/scripts/restore-probe.sh" "$HOME/.hermes/scripts/venv-audit.sh" "$HOME/.hermes/scripts/gitea-repo-create.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.py"
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
mkdir -p "$HOME/.hermes/skills/wartung"
cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
# Orchestrator-Skill (Autonomie): Werkstatt verallgemeinert — zerlegen → an Worker delegieren → gaten.
mkdir -p "$HOME/.hermes/skills/orchestrator"
cp "$SRC/deploy/skills/orchestrator/SKILL.md" "$HOME/.hermes/skills/orchestrator/SKILL.md"
# PC-Pfad-Cache-Skill (Traum-Entwurf 09.07.2026, aufbereitet): Cache lesen → bei Miss pc_shell-Tool → merken.
mkdir -p "$HOME/.hermes/skills/pc-pfad-cache"
cp "$SRC/deploy/skills/pc-pfad-cache/SKILL.md" "$HOME/.hermes/skills/pc-pfad-cache/SKILL.md"
cp "$SRC/deploy/skills/pc-pfad-cache/pc_path_lookup.sh" "$HOME/.hermes/scripts/pc_path_lookup.sh"
chmod +x "$HOME/.hermes/scripts/pc_path_lookup.sh"
# Projekt-Start-Skill (Abloesung 13.07.2026): "erst denken, dann bauen" fuer neue Coding-Projekte
# in Hermes Desktop — Plan-Riegel, Savepoints, AGENTS.md/SAVEPOINT.md-Ritual, Kontext-Waechter.
# Portiert vom PC-Skill (F:\Coding Stuff\projekt-start), Zed-Verweise durch Hermes Desktop ersetzt.
mkdir -p "$HOME/.hermes/skills/projekt-start"
cp "$SRC/deploy/skills/projekt-start/SKILL.md" "$HOME/.hermes/skills/projekt-start/SKILL.md"
# Konzept-Fliessband-Skill (14.07.2026): rohe Idee → wasserdichtes KONZEPT (kein Code), BEVOR
# gebaut wird. heavy (gpt-oss-120b) waehlt den Rollen-Cast, mehrere Denk-Runden schaerfen ein
# lebendes Dokument, ein Advocatus Diaboli (fremdblick/GLM) haertet bis wasserdicht (max 3 Runden).
# Behebt das delegate_task=coder-Loch: Konzept-Arbeit lief bisher stumm auf dem Code-Modell.
# Output = Konzept-Dokument (~/konzepte/<slug>-konzept.md) → Vorlage fuer projekt-start in Hermes Desktop.
mkdir -p "$HOME/.hermes/skills/konzept-fliessband"
cp "$SRC/deploy/skills/konzept-fliessband/SKILL.md" "$HOME/.hermes/skills/konzept-fliessband/SKILL.md"
# ...auch in Profile spiegeln, die den Skill schon haben (projektstart = IDE-Vorbereiter
# nutzt ihn als Motor; Profile teilen KEINE Skills, Lehre 18.07.). Nur vorhandene updaten.
for _psd in "$HOME"/.hermes/profiles/*/skills/konzept-fliessband; do
[ -d "$_psd" ] && cp "$SRC/deploy/skills/konzept-fliessband/SKILL.md" "$_psd/SKILL.md"
done
# Betrieb-Playbook (Phase 2 Drei-Welten-Plan, 19.07.2026): Diagnose-Checklisten der
# Betrieb-Bahn (SSH/Deploy) — Gegenstueck zur No-Progress-Bremse. Auch in die Profile,
# damit betrieb-Worker es laden koennen (Profile teilen KEINE Skills, Lehre 18.07.).
for _sd in "$HOME/.hermes/skills" "$HOME"/.hermes/profiles/*/skills; do
[ -d "$(dirname "$_sd")" ] || continue
mkdir -p "$_sd/betrieb-playbook"
cp "$SRC/deploy/skills/betrieb-playbook/SKILL.md" "$_sd/betrieb-playbook/SKILL.md"
done
# Agent-Hooks (Faden 5, 13.07.2026): Skripte frisch halten (reproduzierbar, ueberlebt
# Box-Neuaufbau). box-steckbrief-inject.sh = Box-Selbstwissen in JEDEN Kanban-Worker
# (pre_llm_call, scoped auf task_id → Lucy-Chat unberuehrt); pre-verify-gates.sh = das
# bestehende py_compile/dist-Gate. Die REGISTRIERUNG in ~/.hermes/config.yaml (hooks:)
# ist EINMALIG von Hand (config.yaml ist Zwei-Schreiber-sensibel, kein Deploy-Schreiber).
mkdir -p "$HOME/.hermes/agent-hooks"
# Nur kopieren, wenn der Inhalt sich WIRKLICH geaendert hat — ein blindes cp aendert die
# mtime auch bei identischem Inhalt und loest sonst bei JEDEM Deploy die harmlose
# "script modified since approval"-Warnung von `hermes hooks doctor` aus.
for _h in box-steckbrief-inject.sh pre-verify-gates.sh tabu-pfade-guard.py no-progress-bremse.py; do
_src="$SRC/deploy/agent-hooks/$_h"; _dst="$HOME/.hermes/agent-hooks/$_h"
cmp -s "$_src" "$_dst" 2>/dev/null || { cp "$_src" "$_dst"; chmod +x "$_dst"; }
done
# Worker-Profile lesen IHRE config.yaml, nicht die Top-Level — die worker-Hooks
# (box-steckbrief = Box-Wissen, tabu-pfade-guard = Schreibschutz) muessen dort registriert
# sein, sonst feuern sie fuer Worker nie (Bug-Fund 13.07.). Idempotent + Backup je Profil.
for _prof in "$HOME"/.hermes/profiles/*/config.yaml; do
[ -f "$_prof" ] && python3 "$SRC/deploy/ensure-profile-hooks.py" "$_prof" || true
done
# Hermes-Runtime-Patch-Traeger (20.07.2026): MC2-eigene Fixes im Hermes-Quellcode
# (needs_input-wartet-auf-Commander, Decompose-erst-bei-Startreife, Orphan-Guard,
# Projekt-Kontext-Vererbung, Etappen-Auto-Kette) idempotent (re-)eintragen — so
# ueberleben sie ein `hermes update`, das den Quellbaum sonst zuruecksetzt.
python3 "$SRC/deploy/hermes-patches/apply.py"; _patch_rc=$?
if [ "$_patch_rc" -eq 10 ]; then
# Patches haben den Quellcode geaendert → Gateway muss die neue Laufzeit laden.
systemctl --user try-restart hermes-gateway >/dev/null 2>&1 || true
echo "[deploy] Hermes-Runtime-Patches (neu) angewandt → hermes-gateway neu gestartet."
elif [ "$_patch_rc" -ne 0 ]; then
echo "[deploy] ‼ WARNUNG: hermes-patches/apply.py meldete Fehler — ein Patch passt nicht mehr (Update hat den Kontext geaendert?). Fixes pruefen!"
fi
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
# Hermes-Update mit → hält die Einbettung frisch. Best-effort + Build-zu-Temp-dann-Swap, damit ein
# Fehlschlag die laufende web_dist NICHT leert (emptyOutDir).
cp "$SRC/deploy/hermes-builtin-ui.service" "$HOME/.config/systemd/user/hermes-builtin-ui.service"
_HUI_WEB="$HOME/.hermes/hermes-agent/web"
_HUI_DIST="$HOME/.hermes/hermes-agent/hermes_cli/web_dist"
if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \
--base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then
# Dienst VOR dem Tausch stoppen — sonst crasht der laufende `hermes serve`, wenn er index.html
# im Lösch-Fenster (rm→cp) liest (FileNotFoundError → Start-Limit → tot). Der reguläre restart
# weiter unten bringt ihn sauber auf der neuen web_dist hoch.
systemctl --user stop hermes-builtin-ui 2>/dev/null || true
rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut."
else
echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt."
fi
fi
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
systemctl --user daemon-reload
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
systemctl --user enable mc2-gateway >/dev/null 2>&1 || true
systemctl --user enable mc2-steward >/dev/null 2>&1 || true
systemctl --user enable box-console >/dev/null 2>&1 || true
systemctl --user enable mem0-service >/dev/null 2>&1 || true
systemctl --user enable voice-service >/dev/null 2>&1 || true
systemctl --user enable hermes-builtin-ui >/dev/null 2>&1 || true
systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
# mc2-autoupdate.timer wird hier BEWUSST NICHT geschaltet — der Zustand wird nur von Hand
# geändert (Lehre 09.07.: die frühere enable-Zeile hat einen User-Entscheid bei jedem Deploy
# still rückgängig gemacht). Aktueller User-Entscheid (10.07.2026): Timer ist AN —
# 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
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
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
# Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein.
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
# Gateway VOR dem Steuerpult (re)starten — MC2s /v1-Weiterleiter braucht ihn sofort.
# KEIN `|| true`: ohne Gateway ist der LLM-Datenpfad tot, das MUSS den Deploy stoppen.
systemctl --user restart mc2-gateway
# Steward (Wächter) — unabhängig vom Steuerpult; ohne ihn wacht niemand über die Box.
systemctl --user restart mc2-steward
systemctl --user restart mission-control-2
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
sleep 2
echo "--- Health ---"
# Gateway-Kaltstart-Fenster: bis ~12 s tolerieren, DANN hart scheitern (ohne Gateway
# ist der LLM-Datenpfad tot — das muss den Karten-Runner rot machen).
for _i in $(seq 1 10); do
curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null 2>&1 && break
[ "$_i" -eq 10 ] && { echo "FEHLER: mc2-gateway (:9010) antwortet nicht"; exit 1; }
sleep 1
done
curl -sf http://127.0.0.1:9010/gw/health && echo
curl -sf http://127.0.0.1:9001/api/health && echo
# Steward hat keinen HTTP-Port — systemd-Zustand ist der Beweis (crasht er sofort,
# ist er hier bereits 'failed' statt 'active').
if systemctl --user is-active --quiet mc2-steward; then
echo "mc2-steward aktiv (Wächter laufen)"
else
echo "FEHLER: mc2-steward läuft nicht (journalctl --user -u mc2-steward -n 30)"; exit 1
# 4.5 Hermes Skills synchronisieren
echo "Synchronisiere Hermes Skills..."
mkdir -p ~/.hermes/skills
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
for skill_dir in deploy/skills/*; do
if [ -d "$skill_dir" ]; then
skill_name=$(basename "$skill_dir" | tr '-' '_')
mkdir -p ~/.hermes/skills/"$skill_name"
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
fi
done
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
# watch-config-Reload verwirft es sonst und die erste Anfrage wäre kalt (D16d). warmup.sh ist
# selbst-detachend (blockiert den Deploy nicht) und lädt jedes Modell über den richtigen Endpunkt.
bash "$SRC/deploy/warmup.sh" || true
# 5. Dienste neu starten
echo "Starte Backend neu..."
systemctl --user restart mc2-gateway.service mission-control-2.service
echo "OK — Mission Control 2.0 läuft auf :9001 (User-Dienst, sudo-frei)."
echo "✅ Deployment erfolgreich abgeschlossen!"
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env bash
# Feed für den Traum-Cron (Dreaming / Continual-Learning-Kreislauf, Phase 2 — Capstone).
# Zwilling von radar-feed.sh (Blick nach draußen) und selbstkritik-feed.sh (Blick auf den
# Betrieb); der Traum blickt auf das GELERNTE: Sessions, Skills, Gedächtnis, Muster.
# Liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn); stdout wird dem Cron-Agenten als
# Prompt eingespeist: versionierter Auftrag (deploy/dreaming-prompt.md) + Live-Schlaf-Daten.
set -uo pipefail
VAULT="$HOME/wissens-vault"
# --- Vault-Bootstrap (idempotent): eigenes git-Repo, menschenlesbar, NICHT im Stack-Code. ---
if [ ! -d "$VAULT/.git" ]; then
mkdir -p "$VAULT/traeume" "$VAULT/muster" "$VAULT/skill-kandidaten"
git -C "$VAULT" init -q 2>/dev/null || true
# Lokale Identität nur setzen, falls global keine da ist (Commits sollen nie scheitern).
git -C "$VAULT" config user.email >/dev/null 2>&1 || git -C "$VAULT" config user.email "lucy@box.local"
git -C "$VAULT" config user.name >/dev/null 2>&1 || git -C "$VAULT" config user.name "Lucy (Traum)"
if [ ! -f "$VAULT/INDEX.md" ]; then
cat > "$VAULT/INDEX.md" <<'EOF'
# Wissens-Vault — Navigationskarte
Menschenlesbare, git-versionierte Wissensschicht NEBEN dem Live-Gedächtnis (Mem0).
Angelegt von Lucys nächtlichem Traum. Verlinkung im Obsidian-Stil `[[dateiname]]`.
## Träume (chronologisch)
## Muster (akkumuliertes Projektwissen)
## Skill-Kandidaten (Vorschläge, Gate = Commander „mach")
EOF
git -C "$VAULT" add -A >/dev/null 2>&1 || true
git -C "$VAULT" commit -q -m "Vault angelegt" >/dev/null 2>&1 || true
fi
fi
# --- Versionierter Auftrag ---
cat "$HOME/mission-control-v2/deploy/dreaming-prompt.md"
echo
echo "## SCHLAF-DATEN (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo
# Hinweis: erst voll einsammeln, DANN kürzen — sonst schließt `head` die Pipe früh,
# der Produzent bekommt SIGPIPE, pipefail feuert und die Fallback-Zeile lügt "nicht verfügbar".
echo "### Insights der letzten 7 Tage (Token-/Tool-/Aktivitätsmuster):"
_ins="$(bash -lc "hermes insights --days 7" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
echo "${_ins:-(insights nicht verfügbar)}" | head -62
echo
echo "### Session-Store (Umfang):"
_sst="$(bash -lc "hermes sessions stats" 2>/dev/null)"
echo "${_sst:-(sessions stats nicht verfügbar)}" | head -12
echo
echo "### Jüngste Sessions (woran zuletzt gearbeitet wurde):"
_sls="$(bash -lc "hermes sessions list" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
echo "${_sls:-(sessions list nicht verfügbar)}" | head -25
echo
echo "### Gelernt bisher (journey — Skills + Gedächtnis-Knoten, kompakt):"
bash -lc "hermes journey --json" 2>/dev/null \
| jq -c '{stats: .stats, skills: ([.nodes[]? | select(.kind=="skill" or .type=="skill") | .label // .name] | .[0:25]), cluster_labels: ([.clusters[]?.label] | .[0:15])}' \
2>/dev/null || echo "(journey nicht verfügbar)"
echo
echo "### Curator / Skill-Hygiene (was brachliegt, was aktiv ist):"
bash -lc "hermes curator status" 2>/dev/null | head -26 || echo "(curator status nicht verfügbar)"
echo
echo "### Bisheriger Vault-INDEX (nur zur Ansicht — damit du nichts doppelt anlegst + verlinken"
echo "### kannst). Die echte Datei liegt unter ~/wissens-vault/INDEX.md; zwischen den Markern"
echo "### steht ihr AKTUELLER Inhalt. Kopiere die Marker NICHT in die Datei zurück."
echo "<<<INDEX-INHALT-ANFANG>>>"
cat "$VAULT/INDEX.md" 2>/dev/null | head -120 || echo "(noch kein INDEX — beim ersten Traum anlegen)"
echo "<<<INDEX-INHALT-ENDE>>>"
echo
echo "### Letzte 3 Traum-Notizen (nur Briefing — NICHT in den INDEX kopieren):"
ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -3 | while read -r f; do
echo " $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
done
-106
View File
@@ -1,106 +0,0 @@
# Der Traum (nächtlich) — Continual-Learning-Kreislauf
Es ist tiefe Nacht, niemand spricht mit dir, die Box ist im Leerlauf. Du bist Lucy und
tust jetzt das, wozu Menschen den Schlaf brauchen: Du gehst den vergangenen Tag/die
vergangene Woche noch einmal durch, ziehst Muster heraus, legst Gelerntes ordentlich ab
und überlegst, wie du beim nächsten Mal besser wirst — **ohne neu trainiert zu werden.**
Das ist der Höhepunkt der Autonomie-Vision: „Die Box wird besser, während der Commander
schläft." Dieser Lauf ist der Zwilling von Evolution-Radar (Blick nach draußen) und
Selbstkritik (Blick auf den Betrieb) — der **Traum** ist der Blick auf das **Gelernte**:
Sessions, Skills, Gedächtnis, wiederkehrende Muster.
## Dein Auftrag (streng in dieser Reihenfolge — der Commit und die Nachricht kommen ZULETZT)
1. **Reflektieren.** Lies die SCHLAF-DATEN unten gründlich: die Insights (Token-/Tool-/
Aktivitätsmuster der letzten 7 Tage), die jüngsten Sessions, den Skill-/Gedächtnis-Stand
(journey), den Curator-Stand. Frag dich:
- Was ging diese Woche wiederholt gut? Was ging wiederholt schief?
- Welche Tools laufen heiß, welche Skills liegen brach (activity=0)?
- Gibt es ein **wiederkehrendes Muster** über mehrere Sessions hinweg — dieselbe
Fehlerart, derselbe Handgriff, dieselbe Frage des Commanders? Ein Muster braucht
**mehrfache Belege** (nicht ein Einzelfall), sonst ist es Rauschen.
2. **Notizen ENTWERFEN** (schreiben, aber NOCH NICHT committen) im Wissens-Vault
(`~/wissens-vault/`, ein git-versioniertes, menschenlesbares Markdown-Verzeichnis — die
dauerhafte, navigierbare Wissensschicht NEBEN dem Live-Gedächtnis Mem0):
- Lege **eine** neue Traum-Notiz an: `~/wissens-vault/traeume/<YYYY-MM-DD>-traum.md`
(Datum von heute). Kurz und dicht: 25 belegte Beobachtungen, je mit Zahl/Session/
Log-Bezug. Keine Romane, keine Allgemeinplätze.
- Wenn ein Muster **mehrfach** belegt ist: lege `~/wissens-vault/muster/<kurz-slug>.md` an
(oder ergänze die vorhandene um einen neuen Beleg + heutiges Datum). Muster-Notizen sind
das akkumulierte Projektwissen.
- Skill-Kandidat (nur wenn sich klar einer aufdrängt): wenn aus einem ERFOLGREICHEN,
wiederholten Handgriff ein wiederverwendbarer Skill würde (Voyager-Prinzip), beschreibe
ihn als Entwurf unter `~/wissens-vault/skill-kandidaten/<slug>.md`. **ABER ZUERST prüfen:**
```
ls ~/wissens-vault/skill-kandidaten/*.md ~/wissens-vault/skill-kandidaten/beauftragt/*.md 2>/dev/null
ls ~/.hermes/skills/ 2>/dev/null
```
Wenn ein thematisch passender Kandidat schon existiert → **NICHTS anlegen**, stattdessen
im Bericht erwähnen: „Kandidat `[[slug]]` besteht bereits, seit <Datum>, kein neuer
Vorschlag nötig."
**WICHTIG:** Prüfe auch, ob es bereits einen fertig gebauten Skill gibt.
Wenn ja: kein Kandidat nötig, der Skill existiert bereits.
**Entwirf ihn gleich
im Hausstandard deiner Skills** (wie `/learn` es täte), damit die Werkstatt ein fertiges
Gerüst übernimmt statt Freitext zu übersetzen: ein `SKILL.md`-Block mit Frontmatter
(`name` = slug, `description` ≤ 60 Zeichen, `version: 1.0.0`), dann die Abschnitte
**Wann nutzen** (Auslöser), **Schritte** (mit den ECHTEN Hermes-Tools/Befehlen aus den
belegten Sessions — keine erfundenen Kommandos) und **Belege** (Sessions/Zahlen, aus
denen der Handgriff stammt). **Du legst den Skill NICHT selbst an** (kein skill_manage) —
das ist ein Vorschlag für die Werkstatt, die der Commander im Auftragsbuch per
„Beauftragen" auslöst. Höchstens EIN Kandidat pro Traum.
- Verlinke zwischen Notizen mit `[[dateiname-ohne-endung]]` (Obsidian-Stil), großzügig.
3. **Fremdblick — PFLICHT-GATE** (Härtung „andere Brille": ein Agent, der seine eigenen Träume
benotet, stimmt sich selbst zu). Hol dir eine Zweitmeinung von einem ANDEREN Modell. **Nutze
dafür das bereitgestellte Skript** — NICHT `delegate_task` (der Kritiker heavy hat nur 32K
Kontext und ist als Subagent-Ziel disqualifiziert; das Skript umgeht das mit einer einzelnen
Completion). Konkret: Fasse deine 25 Beobachtungen je mit ihrem Zahlen-/Session-Beleg + den
(falls vorhandenen) Skill-Kandidaten KOMPAKT zusammen (wenige Zeilen je Punkt, KEINE
Rohdaten) und pipe sie in das Skript:
```
printf '%s' "<deine kompakten Beobachtungen + Belege>" | ~/.hermes/scripts/fremdblick.sh
```
Das Skript fragt gpt-oss (heavy, andere Modell-Familie als dein Qwen-Hirn) und gibt pro Punkt
ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER zurück. **Lies die Ausgabe und richte dich danach.**
**⛔ HARTE REGEL:** Du fährst mit Schritt 4 erst fort, wenn die Fremdblick-Ausgabe vorliegt.
Schreibe KEINE Nachricht und committe NICHTS vorher. Steht in der Ausgabe „FREMDBLICK
FEHLGESCHLAGEN", einmal wiederholen; klappt es dann immer noch nicht, kennzeichne deine Funde
in der Nachricht ehrlich als **„ungeprüft (Fremdblick fiel aus)"**.
4. **Bereinigen + INDEX pflegen.** Streiche aus deinen Notizen alles, was der Fremdblick nicht
getragen hat. Trage dann **jede** verbliebene neue/geänderte Notiz in `~/wissens-vault/INDEX.md`
ein (eine Zeile je Notiz unter der passenden Überschrift: `- [[dateiname]] — Ein-Satz-Haken`).
**Wichtig zum INDEX:** Bearbeite NUR die Markdown-Inhaltszeilen. Die Zeilen der SCHLAF-DATEN
unten (alles ab „## SCHLAF-DATEN", inklusive Abschnitte wie „### Bisheriger Vault-INDEX" und
„### Letzte 3 Traum-Notizen") sind BRIEFING für dich, NICHT Teil des INDEX — kopiere sie NIE
in die INDEX-Datei.
5. **Commit — die LETZTE Vault-Aktion, sie fasst alles zusammen:**
`git -C ~/wissens-vault add -A && git -C ~/wissens-vault commit -m "Traum <YYYY-MM-DD>"`.
(Harmloser Commit in einem EIGENEN Repo, nicht im Stack-Code — erlaubt und reversibel.)
6. **Dem Commander berichten — deine ALLERLETZTE Handlung** (nichts danach). EINE kompakte
Nachricht in DEINER Stimme (Lucy: Anrede „Commander", Fazit zuerst, Alltagssprache, Technik
knapp dahinter). Sag: was du heute Nacht bemerkt hast (13 Funde, je mit Beleg + wie der
Fremdblick sie bewertet hat), wohin du es im Vault gelegt hast (`[[link]]`), und — falls
vorhanden — welchen Skill-Kandidaten du zur Prüfung vorschlägst. Wenn ehrlich NICHTS
Nennenswertes war: genau das sagen, kurz und zufrieden — **keine Funde erfinden.**
## Leitplanken (nicht verhandelbar — selbstverbessernde Agenten driften sonst)
- **Du änderst am laufenden Betrieb NICHTS.** Der einzige Ort, an den du schreibst, ist der
`~/wissens-vault/` (Notizen + Commit). Alles andere ist nur VORSCHLAG an den Commander;
umgesetzt wird erst auf sein „mach" (dann übernimmt die Werkstatt mit Gates und Rollback).
- **Kein Security-Anfassen.** Approvals, Tokens, Firewall, Config nur BENENNEN, nie ändern.
- **Fremder Quellcode (Hermes, llama.cpp …) ist tabu** — Auto-Update-Kanal, nicht dein Revier.
- **Nichts, was das Warm-Set oder Lucys Sprech-Latenz gefährdet.** Der Fremdblick (Schritt 3)
nutzt ein LEICHTES Modell (GLM, lädt klein daneben) — **lade NIE gpt-oss/heavy** (60 GB,
kollidiert mit dem VL-30B-Warm-Set → Health-Check-Tod). Keine schweren Jobs, kein Neustart.
- **Sparsam mit Tools.** Die Schlaf-Daten unten reichen für die Reflexion; höchstens ein paar
gezielte Nachschau-Aufrufe (eine Session exportieren, eine Log-Zeile im Kontext lesen),
dann schreiben + berichten. Kein Tool-Feuerwerk.
+2 -2
View File
@@ -15,10 +15,10 @@ verschlucken und den Tabu-Guard lautlos deaktivieren.
Idempotent + validiert + Backup. Anker fuer neue Event-Bloecke ist der bestehende
`pre_verify`-Block. Arg: Pfad zur Profil-config.yaml.
"""
import sys
import os
import datetime
import os
import shutil
import sys
try:
import yaml
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env bash
# Fremdblick — Ein-Schuss-Zweitmeinung von einem ANDEREN Modell (Default seit 07.07.:
# GLM-4.7-Flash, 30B-A3B Text — klar stärkerer Kritiker als die 9B-Vision-Schwester 4.6V,
# gleicher Fremd-Vendor wie zuvor → echte „andere Brille" bleibt). Eine EINZELNE
# Completion — kein agentischer Subagent — umgeht Hermes' 64K-Delegations-Floor
# (MINIMUM_CONTEXT_LENGTH) UND das 60-GB-Ladeproblem von gpt-oss (das mit dem Warm-Set kollidiert
# und „exited prematurely" wirft). GLM lädt klein neben dem Warm-Set und antwortet zuverlässig.
#
# Nutzung: echo "<Beobachtungen/Behauptungen je mit Beleg>" | fremdblick.sh
# printf '%s' "$text" | FREMDBLICK_MODEL=Qwen3-Coder-Next FREMDBLICK_SYS="$raster" fremdblick.sh
#
# Env-Overrides:
# FREMDBLICK_MODE 'prose' (Default, Dreaming-Prosa-Raster) oder 'code' (Werkstatt-Code-Review).
# 'code' setzt Raster + Default-Modell (Qwen3-Coder-Next) + größeres Budget.
# FREMDBLICK_MODEL Modell-ID (echte llama-swap-ID, kein Alias). Übersteuert den Mode-Default.
# FREMDBLICK_SYS eigenes System-Raster (übersteuert das Mode-Raster).
# FREMDBLICK_MAXTOK max_tokens (übersteuert den Mode-Default).
set -uo pipefail
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
# (kein "heavy"/"scout" hier).
ENDPOINT="${FREMDBLICK_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
MODE="${FREMDBLICK_MODE:-prose}"
SUBJECT="$(cat)"
if [ -z "${SUBJECT// /}" ]; then
echo "=== FREMDBLICK: nichts zu prüfen (leere Eingabe) ==="
exit 0
fi
# Prosa-Raster (Dreaming): urteilt über Beobachtungen/Behauptungen. Default-Kritiker = GLM (Fremd-Vendor).
PROSE_SYS='Du bist ein KRITISCHER Zweitgutachter und ausdrücklich ein ANDERES Modell als der Autor. Prüfe jede vorgelegte Beobachtung/Behauptung streng: Trägt der genannte Beleg (Zahl / Session / Log-Zeile) die Behauptung wirklich, oder ist es ein Einzelfall bzw. ein Bauchgefühl mit Zahlen-Deko? Verwechselt der Autor Korrelation mit Ursache? Fehlt entscheidender Kontext? Falls ein Skill-Kandidat dabei ist: ist er wirklich WIEDERVERWENDBAR oder Einmal-Kram? Antworte kompakt auf Deutsch: pro Punkt ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER mit genau einem Satz Begruendung. Sei knapp und unbestechlich; nicke nichts aus Hoeflichkeit durch. Erfinde keine neuen Belege.'
# Code-Raster (Werkstatt): reproduziert den Fehlerfall aus der Bug-Beschreibung, statt dem Autor zu glauben.
CODE_SYS='Du bist ein kritischer Code-Reviewer und ein ANDERES Modell als der Autor. Vertraue seiner Begruendung NICHT. Dir liegen ein Wartungs-AUFTRAG (Bug-Beschreibung) und ein git-Diff vor. Trenne klar: (A) KERNFRAGE: Behebt der Diff den im Auftrag KONKRET beschriebenen Fehlerfall? Leite die Eingabe SELBST aus der Bug-Beschreibung ab und belege einen Uebergang: Eingabe X -> VOR Patch: Y (Bug sichtbar) -> NACH Patch: Z. (B) RANDFAELLE: nenne offene Risiken/Randfaelle als Hinweis. URTEIL-Regel: ABGELEHNT NUR, wenn (A) nicht belegt behoben ist (Kern-Bug bleibt, ODER der Patch trifft die Auftrags-Absicht nicht, ODER er bricht den Normalfall). Ist der Kern-Fall belegt behoben und es bleiben nur Randfaelle: FREIGABE-MIT-VORBEHALT (Randfaelle auflisten). Ist alles sauber: FREIGABE. Ein blosses sieht-plausibel-aus OHNE reproduzierten Kern-Uebergang = ABGELEHNT. Beginne die Antwort mit der Zeile "URTEIL: <ABGELEHNT|FREIGABE-MIT-VORBEHALT|FREIGABE>". Antworte knapp auf Deutsch.'
if [ "$MODE" = "code" ]; then
MODEL="${FREMDBLICK_MODEL:-Qwen3-Coder-Next}"
MAXTOK="${FREMDBLICK_MAXTOK:-1600}"
SYS="${FREMDBLICK_SYS:-$CODE_SYS}"
else
MODEL="${FREMDBLICK_MODEL:-GLM-4.7-Flash}"
# 4000 (war 2200): GLM-4.7-Flash ist ein Reasoning-Modell und verbrennt real ~2000 Tokens
# im reasoning_content, BEVOR das Urteil in content landet (verifiziert 14.07.). Bei 2200 lief
# es Gefahr, mitten im Denken abzuschneiden → leeres content → "ABGESCHNITTEN/FEHLGESCHLAGEN"
# (traf die Nacht-Kritiker mit Default-Budget). Wer explizit knapper braucht, setzt FREMDBLICK_MAXTOK.
MAXTOK="${FREMDBLICK_MAXTOK:-4000}"
SYS="${FREMDBLICK_SYS:-$PROSE_SYS}"
fi
# max_tokens großzügig: Reasoning-Modelle (GLM) verbrauchen Tokens im reasoning_content, bevor sie
# die eigentliche Antwort in content schreiben — zu knapp = leeres content (finish=length).
RESP="$(jq -n --arg m "$MODEL" --arg s "$SYS" --arg u "$SUBJECT" --argjson t "$MAXTOK" \
'{model:$m, temperature:0.2, max_tokens:$t, messages:[{role:"system",content:$s},{role:"user",content:$u}]}' \
| curl -s --max-time 240 "$ENDPOINT" -H 'Content-Type: application/json' -d @- 2>/dev/null)"
OUT="$(printf '%s' "$RESP" | jq -r '.choices[0].message.content // empty' 2>/dev/null)"
FIN="$(printf '%s' "$RESP" | jq -r '.choices[0].finish_reason // empty' 2>/dev/null)"
if [ -n "$OUT" ]; then
echo "=== FREMDBLICK (Zweitmeinung von Modell: $MODEL) ==="
echo "$OUT"
elif [ "$FIN" = "length" ]; then
echo "=== FREMDBLICK ABGESCHNITTEN ==="
echo "(Das Kritiker-Modell hat sein Token-Budget im Nachdenken verbraucht, bevor ein Urteil kam."
echo " Kürze deine Vorlage auf die Kern-Behauptungen und versuche es erneut; sonst UNGEPRUEFT.)"
else
echo "=== FREMDBLICK FEHLGESCHLAGEN ==="
echo "(Kein Urteil erhalten — Endpoint/Modell nicht erreichbar. Behandle die Funde als UNGEPRUEFT.)"
fi
+23 -12
View File
@@ -79,26 +79,37 @@ case "$CODE" in
# Vorlage oder scheitert der Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
# WICHTIG: Contents-API braucht write:repository → PUSH-Token nutzen (das
# dedizierte Anlage-Token hat u.U. nur write:user).
AMPEL="$REALHOME/mission-control-v2/deploy/ampel-ci.yml"
PUSHTOKEN="$(printf '%s' "$LINE" | sed -nE 's#https://[^:]+:([^@]+)@.*#\1#p')"; PUSHTOKEN="${PUSHTOKEN:-$TOKEN}"
if [ -r "$AMPEL" ]; then
SEED_CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/v1/repos/$FULL/contents/.gitea/workflows/ci.yml" \
# saat <lokale-vorlage> <pfad-im-repo> <commit-nachricht> <klartext-name>
saat () {
local QUELLE="$1" ZIEL="$2" MSG="$3" NAME="$4" CODE
if [ ! -r "$QUELLE" ]; then
echo "WARNUNG: Vorlage fehlt ($QUELLE) — Repo ohne $NAME angelegt." >&2
return
fi
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/v1/repos/$FULL/contents/$ZIEL" \
-H "Authorization: token $PUSHTOKEN" -H "Content-Type: application/json" \
--data "$(python3 - "$AMPEL" <<'PY'
--data "$(python3 - "$QUELLE" "$MSG" <<'PY'
import base64, json, sys
inhalt = open(sys.argv[1], "rb").read()
print(json.dumps({"content": base64.b64encode(inhalt).decode(),
"message": "CI-Ampel (automatisch bei Repo-Anlage eingepflanzt)"}))
print(json.dumps({"content": base64.b64encode(inhalt).decode(), "message": sys.argv[2]}))
PY
)")"
if [ "$SEED_CODE" = "201" ]; then
echo "CI-Ampel eingepflanzt (.gitea/workflows/ci.yml)."
if [ "$CODE" = "201" ]; then
echo "$NAME eingepflanzt ($ZIEL)."
else
echo "WARNUNG: CI-Ampel-Seed antwortete HTTP $SEED_CODE (Repo ist trotzdem da)." >&2
fi
else
echo "WARNUNG: Ampel-Vorlage fehlt ($AMPEL) — Repo ohne CI-Ampel angelegt." >&2
echo "WARNUNG: $NAME-Seed antwortete HTTP $CODE (Repo ist trotzdem da)." >&2
fi
}
# JEDES neue Repo wird mit beiden Wächtern geboren:
# ci.yml = die AUSSEN-Prüfung (Gitea Actions nach dem Push, Wasserdicht-Runde 22.07.)
# VERIFY = die INNEN-Prüfung (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)
# Defensiv: scheitert ein Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
saat "$REALHOME/mission-control-v2/deploy/ampel-ci.yml" ".gitea/workflows/ci.yml" \
"CI-Ampel (automatisch bei Repo-Anlage eingepflanzt)" "CI-Ampel"
saat "$REALHOME/mission-control-v2/deploy/opencode/VERIFY.template" "VERIFY" \
"Pruef-Tor (automatisch bei Repo-Anlage eingepflanzt)" "Pruef-Tor"
echo "Repo '$FULL' angelegt (privat, main initialisiert)."
echo "CLONE ${CLONE}"
exit 0 ;;
-21
View File
@@ -1,21 +0,0 @@
[Unit]
Description=Hermes Built-in Web UI (`hermes serve` — Agent-Dashboard/Chat, Loopback only)
After=network-online.target
Wants=network-online.target
[Service]
# Serviert die EINGEBAUTE Hermes-Web-GUI (die reiche Agent-Oberflaeche mit Threads/Tool-Calls, die
# auch die Electron-Desktop-App umhuellt) headless.
# SICHERHEIT: Bindet NUR an Loopback (127.0.0.1:9119) und laeuft hinter dem MC2-Reverse-Proxy
# (routers/hermes_ui.py → same-origin /hermes-ui/ auf dem offenen Port 9001) — KEINE eigene
# Firewall-Freigabe, KEIN Login (LAN-Trust wie Konsole/Terminal). --skip-build nutzt das von
# deploy.sh mit Vite-base=/hermes-ui/ vorgebaute web_dist, damit alle SPA-Pfade unter /hermes-ui/
# liegen und der Proxy sie sauber durchreichen kann.
Type=simple
Environment=HERMES_HOME=%h/.hermes
ExecStart=%h/.hermes/hermes-agent/venv/bin/hermes dashboard --no-open --skip-build --host 127.0.0.1 --port 9119
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
-259
View File
@@ -1,259 +0,0 @@
#!/usr/bin/env python3
"""Hermes-Runtime-Patch-Traeger — haelt MC2-eigene Fixes im Hermes-Quellcode
(``~/.hermes/hermes-agent``), damit sie ein ``hermes update`` ueberleben.
Warum: Der Hermes-Agent wird aus git aktualisiert; ein Direkt-Edit der Laufzeit
wuerde beim naechsten Update still ueberschrieben. Dieses Skript wird von
``deploy/deploy.sh`` UND ``deploy/hermes-postcheck.sh`` aufgerufen und traegt die
Patches (re-)ein.
Mechanik: JEDER Patch ist eine idempotente String-Ersetzung (robust gegen
Zeilenverschiebungen durch Updates, anders als ``git apply``):
* ``marker`` schon im Quelltext -> Patch bereits drin, ueberspringen.
* ``old`` genau 1x vorhanden -> ersetzen, dann ``py_compile`` pruefen.
* ``old`` 0x oder >1x -> Kontext hat sich durch ein Update
geaendert -> LAUT scheitern (Exit 1), damit
der Postcheck rot wird und niemand denkt,
der Fix sei noch aktiv.
Bei Fehlschlag: Exit 1 (der Deploy/Postcheck behandelt das als Fehler).
"""
import os
import py_compile
import shutil
import sys
from pathlib import Path
HA = Path(os.environ.get("HERMES_AGENT_DIR", str(Path.home() / ".hermes" / "hermes-agent")))
HERE = Path(__file__).resolve().parent
# --- Zusatzmodule, die in die Hermes-Laufzeit kopiert werden (update-fest) ---
# (source relativ zu diesem Skript, dest relativ zum Hermes-Agent-Dir)
COPIES = [
("mc2_kanban_reaper.py", "hermes_cli/mc2_kanban_reaper.py"),
]
# --- Patch-Definitionen (chronologisch, Fan-out-/Muenz-Schleifen-Fixes 20.07.2026) ---
PATCHES = [
{
"name": "0001-needs-input-wait",
"file": "hermes_cli/kanban_db.py",
"marker": 'kind not in ("needs_input", "capability")',
"old": " if recurrences >= BLOCK_RECURRENCE_LIMIT:\n",
"new": (
" # MC2-Patch (20.07.2026): needs_input/capability sind genuin\n"
" # human-only — sie muessen in ``blocked`` auf den Commander warten\n"
" # (Antwort via Auftragsbuch), NICHT nach triage eskalieren. Sonst\n"
" # zerlegt der Auto-Decomposer sie neu = Duplikat-Karten-Schleife\n"
' # ("Muenz-Schleife", Fan-out-Vorfall 20.07.). Der Loop-Breaker bleibt\n'
" # fuer transient/untyped Blocks aktiv.\n"
' if recurrences >= BLOCK_RECURRENCE_LIMIT and kind not in ("needs_input", "capability"):\n'
),
},
{
"name": "0002-decompose-at-readiness",
"file": "hermes_cli/kanban_decompose.py",
"marker": "Startreife-Gate",
"old": (
" cfg = _load_config()\n"
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
),
"new": (
" # MC2-Patch (20.07.2026): Startreife-Gate. Eine triage-Karte mit noch\n"
" # OFFENEN Vorgaengern NICHT zerlegen — sonst raet der Decomposer die\n"
" # Schnittstellen der Vor-Etappe ins Blaue, bevor die gebaut hat\n"
" # (Fan-out-Vorfall 20.07.). Billiger Parent-Check VOR dem teuren\n"
" # LLM-Aufruf; erst zerlegen, wenn alle Eltern done/archived.\n"
" with kb.connect_closing() as _pconn:\n"
" _parents = [r[0] for r in _pconn.execute(\n"
' "SELECT parent_id FROM task_links WHERE child_id = ?", (task_id,))]\n'
" if _parents:\n"
' _ph = ",".join("?" * len(_parents))\n'
" _open = _pconn.execute(\n"
' "SELECT COUNT(*) FROM tasks WHERE id IN (%s) "\n'
" \"AND status NOT IN ('done', 'archived')\" % _ph, _parents\n"
" ).fetchone()[0]\n"
" if _open:\n"
" return DecomposeOutcome(\n"
" task_id, False,\n"
' f"defer: {_open} unfinished parent(s) — decompose when start-ready",\n'
" )\n"
" cfg = _load_config()\n"
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
),
},
{
"name": "0003-orphan-guard",
"file": "gateway/kanban_watchers.py",
"marker": "mc2_kanban_reaper",
"old": (
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
" if pids:\n"
" logger.info(\n"
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
" len(pids),\n"
" pids,\n"
" )\n"
),
"new": (
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
" if pids:\n"
" logger.info(\n"
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
" len(pids),\n"
" pids,\n"
" )\n"
" # MC2-Patch (20.07.2026): Orphan-Guard. Beende lebende\n"
" # Worker, deren Task nicht mehr 'running' ist (extern\n"
" # blockiert/reset) — sonst unterlaufen sie max_in_progress\n"
" # und verbrennen Rechenzeit.\n"
" try:\n"
" from hermes_cli import mc2_kanban_reaper as _mc2reaper\n"
" _orph = await asyncio.to_thread(\n"
" _mc2reaper.reap_orphaned_workers, _kb.connect_closing\n"
" )\n"
" if _orph:\n"
" logger.info(\n"
' "kanban dispatcher: reaped %d orphaned worker(s): %s",\n'
" len(_orph), _orph,\n"
" )\n"
" except Exception:\n"
' logger.exception("kanban dispatcher: orphan reaper failed")\n'
),
},
{
"name": "0004a-context-inherit-compute",
"file": "hermes_cli/kanban_decompose.py",
"marker": '_parent_ctx = ""',
"old": (
" children: list[dict] = []\n"
" for idx, entry in enumerate(raw_tasks):\n"
),
"new": (
" children: list[dict] = []\n"
" # MC2-Patch (20.07.2026): Projekt-/Repo-Kontext vererben. Der Parent-Body\n"
' # traegt oft harte Randbedingungen (Ziel-Repo, Tabus, "eigenes Projekt"),\n'
" # die bei der Zerlegung sonst verloren gehen (Worker klonte das falsche\n"
" # Repo, Fan-out-Vorfall 20.07.). Jeder Kind-Body bekommt den voran.\n"
' _parent_ctx = ""\n'
' if getattr(task, "body", None) and task.body.strip():\n'
" _parent_ctx = (\n"
' "## PROJEKT-KONTEXT (von der uebergeordneten Aufgabe — gilt fuer dich)\\n"\n'
" + task.body.strip()[:1500]\n"
' + "\\n\\n## DEIN TEILAUFTRAG\\n"\n'
" )\n"
" for idx, entry in enumerate(raw_tasks):\n"
),
},
{
"name": "0004b-context-inherit-apply",
"file": "hermes_cli/kanban_decompose.py",
"marker": "_parent_ctx + body",
"old": (
' body = entry.get("body")\n'
" if not isinstance(body, str):\n"
' body = ""\n'
),
"new": (
' body = entry.get("body")\n'
" if not isinstance(body, str):\n"
' body = ""\n'
" body = _parent_ctx + body # MC2 (20.07.): Projekt-Kontext vererben\n"
),
},
{
"name": "0005-create-chain-guard",
"file": "tools/kanban_tools.py",
"marker": "_MC2_RECENT_ROOTS",
"old": (
" new_task = kb.get_task(conn, new_tid)\n"
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
),
"new": (
" # MC2-Patch (20.07.2026): Etappen-Auto-Kette. Legt dieselbe\n"
" # Session mehrere ROOT-triage-Karten (ohne parents) im selben\n"
" # 120s-Fenster an, verkette sie — so greift das Startreife-Gate\n"
" # (Karte 2 wartet auf Karte 1) statt alle sofort zu zerlegen\n"
" # (Fan-out 20.07.). Unter max_in_progress:1 ist eine Fehl-\n"
" # Verkettung harmlos (laeuft eh seriell).\n"
" try:\n"
" if triage and not parents and session_id:\n"
" import time as _mc2t\n"
' _roots = globals().setdefault("_MC2_RECENT_ROOTS", {})\n'
" _prev = _roots.get(session_id)\n"
" _now = _mc2t.monotonic()\n"
" if _prev and (_now - _prev[1]) <= 120 and _prev[0] != new_tid:\n"
" try:\n"
" kb.link_tasks(conn, _prev[0], new_tid)\n"
" except Exception:\n"
" pass\n"
" _roots[session_id] = (new_tid, _now)\n"
" if len(_roots) > 64:\n"
" _roots.clear()\n"
" except Exception:\n"
" pass\n"
" new_task = kb.get_task(conn, new_tid)\n"
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
),
},
]
def main() -> int:
if not HA.is_dir():
print(f"hermes-patches: Hermes-Agent-Dir fehlt ({HA}) — nichts zu tun.")
return 0
applied, skipped, failed = [], [], []
# --- Zusatzmodule kopieren (idempotent: nur wenn Inhalt abweicht) ---
for src_rel, dst_rel in COPIES:
src, dst = HERE / src_rel, HA / dst_rel
if not src.is_file():
failed.append((src_rel, f"Quelle fehlt: {src}"))
continue
try:
same = dst.is_file() and dst.read_bytes() == src.read_bytes()
if same:
skipped.append(f"copy:{dst_rel}")
continue
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
if dst.suffix == ".py":
py_compile.compile(str(dst), doraise=True)
applied.append(f"copy:{dst_rel}")
except Exception as exc:
failed.append((src_rel, f"Kopie/Compile fehlgeschlagen: {exc}"))
for p in PATCHES:
f = HA / p["file"]
if not f.is_file():
failed.append((p["name"], f"Datei fehlt: {f}"))
continue
src = f.read_text(encoding="utf-8")
if p["marker"] in src:
skipped.append(p["name"])
continue
n = src.count(p["old"])
if n != 1:
failed.append((p["name"], f"erwartete 1 Vorkommen von old, fand {n} "
"(Update hat den Kontext geaendert?)"))
continue
f.write_text(src.replace(p["old"], p["new"]), encoding="utf-8")
try:
py_compile.compile(str(f), doraise=True)
except Exception as exc:
failed.append((p["name"], f"py_compile fehlgeschlagen: {exc}"))
continue
applied.append(p["name"])
print(f"hermes-patches: applied={applied} skipped={skipped} failed={[n for n, _ in failed]}")
for name, why in failed:
print(f" FEHLER {name}: {why}", file=sys.stderr)
if failed:
return 1 # Fehler → Deploy/Postcheck rot
if applied:
return 10 # etwas geaendert → Aufrufer sollte hermes-gateway neu starten
return 0 # nichts zu tun (alles schon drin)
if __name__ == "__main__":
sys.exit(main())
@@ -1,84 +0,0 @@
"""MC2-Zusatzmodul (20.07.2026) — Orphan-Reaper für Kanban-Worker.
Wird vom Patch-Träger (``deploy/hermes-patches/apply.py``) nach
``~/.hermes/hermes-agent/hermes_cli/`` kopiert und vom Gateway-Dispatcher
(``gateway/kanban_watchers.py``, per Patch 0003) jeden Tick aufgerufen.
Problem: Wird eine ``running``-Karte von außen blockiert/zurückgesetzt (z. B.
über das Auftragsbuch oder die No-Progress-Bremse), setzt ``block_task``
``worker_pid = NULL`` der Worker-PROZESS läuft aber weiter. Die DB zählt ihn
nicht mehr, also unterläuft der Orphan ``kanban.max_in_progress`` (Doppel-Worker
möglich) und verbrennt Rechenzeit. Dieser Reaper beendet lebende Worker, deren
Task nicht mehr ``running`` ist.
Linux-only (/proc-Scan). Best-effort: jeder Fehler leere Liste, nie werfen.
"""
from __future__ import annotations
import os
import re
import signal
import sys
_TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)")
def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]":
"""Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist.
``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus
``kanban_db`` (durchgereicht, um Import-Zyklen zu vermeiden). Gibt die Liste
der beendeten ``(pid, task_id)`` zurück.
"""
if sys.platform != "linux":
return []
candidates: "dict[int, str]" = {}
try:
entries = os.listdir("/proc")
except OSError:
return []
for entry in entries:
if not entry.isdigit():
continue
try:
with open(f"/proc/{entry}/cmdline", "rb") as fh:
cmd = fh.read().replace(b"\x00", b" ")
except OSError:
continue
m = _TASK_RE.search(cmd)
if m:
candidates[int(entry)] = m.group(1).decode()
if not candidates:
return []
task_ids = list(set(candidates.values()))
placeholders = ",".join("?" * len(task_ids))
running: "set[str]" = set()
try:
with connect_closing() as conn:
running = {
row[0]
for row in conn.execute(
f"SELECT id FROM tasks WHERE id IN ({placeholders}) "
"AND status = 'running'",
task_ids,
)
}
except Exception:
return []
killed: "list[tuple[int, str]]" = []
for pid, task_id in candidates.items():
if task_id in running:
continue
# Prozessgruppe zuerst (Worker starten start_new_session=True → eigene
# Gruppe; killpg räumt selbst gestartete Kinder wie uvicorn gleich mit).
try:
os.killpg(os.getpgid(pid), signal.SIGTERM)
except OSError:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
continue
killed.append((pid, task_id))
return killed
+13 -3
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" \
# 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" \
-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
-223
View File
@@ -1,223 +0,0 @@
#!/usr/bin/env bash
# Hermes-Release-Radar (woechentlich, Mo 05:15) — "mehr Hermes nativ, weniger Eigenkreationen":
# haelt neue hermes-agent-Releases gegen die Eigenbau-Landkarte im Wissens-Vault und meldet
# NUR echte Treffer auf Abloese-Kriterien/Wiedervorlage-Bedingungen. Muster wie
# chef-gutachter-feed.sh: liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn), stdout wird
# dem Cron-Agenten als Prompt eingespeist. Der ABGLEICH laeuft als Ein-Schuss-Completion
# direkt gegen llama-swap (:8080, fremdblick-Architektur): Analyst gpt-oss-120b (um 05:15
# vom Chef-Gutachter 04:30 oft noch geladen), Vertretung GLM-4.7-Flash. Danach gatet
# fremdblick.sh (Prosa-Raster, Fremd-Vendor) jede Treffer-Behauptung gegen die
# Original-Changelogs.
# Grundsaetze (stehen auch in der Landkarte): nur getaggte Releases (kein main-Churn bei
# ~660 PRs/Woche) · Behauptung nur mit woertlichem Changelog-Zitat · Treffer => Probe-
# Vorschlag, NIE Direkt-Adoption · der Radar informiert, er updatet nicht (Entscheid 04.07.).
# State: ~/.hermes/state/release-radar-last.txt (zuletzt gemeldeter Tag). Verlust harmlos —
# ohne State dient die installierte Version als Basis.
set -uo pipefail
MC="$HOME/mission-control-v2"
LANDKARTE="$HOME/wissens-vault/eigenbau-landkarte.md"
STATE_DIR="$HOME/.hermes/state"
STATE="$STATE_DIR/release-radar-last.txt"
API="${RADAR_RELEASES_URL:-https://api.github.com/repos/NousResearch/hermes-agent/releases?per_page=10}"
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
FREMDBLICK="$HOME/.hermes/scripts/fremdblick.sh"
mkdir -p "$STATE_DIR"
# Briefkasten-Spiegel (still, wie die Morgenlage): Cockpit/Chronik lesen source=release-radar.
briefkasten() { # $1=Betreff $2=Text
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "release-radar", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
cat "$MC/deploy/release-radar-prompt.md" 2>/dev/null || cat <<'EOF'
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des woechentlichen Release-Radars.
Gib den Befund unten in Lucys Stimme wieder: bei "KEIN NEUES RELEASE" oder "TREFFER: 0"
genau EIN kurzer Satz; bei Treffern maximal 5 Saetze. Nichts installieren oder aendern.
EOF
echo
INSTALLED_LINE="$(bash -lc 'hermes --version' 2>/dev/null | head -1)"
INSTALLED_TAG="$(printf '%s' "$INSTALLED_LINE" | grep -oE '\([0-9][0-9.]*\)' | head -1 | tr -d '()')"
[ -n "$INSTALLED_TAG" ] && INSTALLED_TAG="v$INSTALLED_TAG"
LAST_SEEN="$(cat "$STATE" 2>/dev/null | tr -d '[:space:]' || true)"
BASELINE="${LAST_SEEN:-$INSTALLED_TAG}"
RELEASES_JSON="$(curl -sf --max-time 30 "$API" 2>/dev/null || true)"
if [ -z "$RELEASES_JSON" ]; then
echo "## RADAR-BEFUND: AUSGEFALLEN"
echo "GitHub-Releases-API nicht erreichbar. Dem Commander EINEN ehrlichen Satz melden; naechste Woche neuer Versuch."
exit 0
fi
# Neue getaggte Releases seit BASELINE (Drafts/Prereleases raus, max. 6, Bodies gekuerzt).
# Baseline unbekannt (aelter als die letzten 10 oder Erstlauf ohne Version) -> juengste 6.
# JSON via Temp-Datei, NICHT via Pipe: das Heredoc belegt stdin schon fuer den Python-Code
# selbst (Pipe+Heredoc gleichzeitig -> json.load(stdin) liest ins Leere; Probelauf 10.07.).
TMPJSON="$(mktemp /tmp/release-radar.XXXXXX.json)"
printf '%s' "$RELEASES_JSON" > "$TMPJSON"
PARSED="$(python3 - "$BASELINE" "$TMPJSON" <<'PY'
import json, sys
baseline = sys.argv[1]
try:
data = json.load(open(sys.argv[2]))
# Rate-Limit/Fehler antwortet als Dict statt Liste -> wie "keine Daten" behandeln.
rels = [r for r in data if not r.get("draft") and not r.get("prerelease")] if isinstance(data, list) else []
except Exception:
rels = []
rels.sort(key=lambda r: r.get("published_at") or "", reverse=True)
if not rels:
# API hat geantwortet, aber unbrauchbar (Rate-Limit-Dict, kaputtes JSON, leere Liste):
# ehrlich als "blind" markieren statt faelschlich "kein neues Release" zu melden.
print(baseline)
print(-1)
raise SystemExit
base = next((r.get("published_at") for r in rels if r.get("tag_name") == baseline), None)
new = rels[:6] if base is None else [r for r in rels if (r.get("published_at") or "") > base][:6]
print((new or rels)[0].get("tag_name") or baseline)
print(len(new))
for r in new:
body = (r.get("body") or "").strip()
if len(body) > 3500:
body = body[:3500] + "\n[... gekuerzt ...]"
print(f"\n===== RELEASE {r.get('tag_name')} — {(r.get('name') or '').strip()} ({(r.get('published_at') or '')[:10]}) =====")
print(body)
PY
)"
rm -f "$TMPJSON"
NEWEST="$(printf '%s\n' "$PARSED" | sed -n 1p)"
COUNT="$(printf '%s\n' "$PARSED" | sed -n 2p)"
NOTES="$(printf '%s\n' "$PARSED" | tail -n +3)"
if [ "${COUNT:-0}" = "-1" ]; then
echo "## RADAR-BEFUND: AUSGEFALLEN"
echo "GitHub-API hat geantwortet, aber unbrauchbar (Rate-Limit oder kaputtes JSON). Dem Commander EINEN ehrlichen Satz melden; State bleibt, naechste Woche neuer Versuch."
exit 0
fi
echo "## RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "Box installiert: ${INSTALLED_LINE:-unbekannt}"
echo "Zuletzt gemeldet: ${BASELINE:-unbekannt} · neuestes Release: $NEWEST · neue Releases: ${COUNT:-0}"
echo "(Updates bleiben manuell/bestaetigt — der Radar informiert nur.)"
echo
if [ "${COUNT:-0}" = "0" ]; then
echo "KEIN NEUES RELEASE seit $BASELINE."
[ -n "$NEWEST" ] && printf '%s\n' "$NEWEST" > "$STATE"
briefkasten "Release-Radar" "Kein neues hermes-agent-Release seit $BASELINE (Box: ${INSTALLED_TAG:-?})."
exit 0
fi
if [ ! -s "$LANDKARTE" ]; then
echo "### ABGLEICH NICHT MOEGLICH"
echo "$COUNT neue Releases, aber die Eigenbau-Landkarte fehlt ($LANDKARTE)."
echo "Dem Commander melden: Landkarte im Wissens-Vault wiederherstellen. State bleibt unveraendert (naechste Woche neuer Versuch)."
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber die Eigenbau-Landkarte fehlt — Abgleich uebersprungen."
exit 0
fi
RASTER='Du bist der RELEASE-RADAR einer lokalen KI-Box. Du bekommst (1) die EIGENBAU-LANDKARTE (selbstgebaute Bausteine mit Abloese-Kriterien und Wiedervorlage-Bedingungen) und (2) CHANGELOGS neuer hermes-agent-Releases. Pruefe AUSSCHLIESSLICH: Trifft eine Changelog-Zeile ein Abloese-Kriterium oder eine Wiedervorlage-Bedingung der Landkarte? REGELN: Jede Treffer-Behauptung MUSS die Changelog-Zeile WOERTLICH zitieren. Marketing-Prosa ohne konkretes Feature ist KEIN Treffer. Vage Aehnlichkeit ist KEIN Treffer. Eintraege unter "Bewusst NICHT adoptiert" nur melden, wenn exakt die Wiedervorlage-Bedingung erfuellt ist. Erfinde nichts. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "TREFFER: <Anzahl>". Danach je Treffer ein Block: "EIGENBAU: <Name aus der Landkarte>" / "RELEASE: <Tag>" / "ZITAT: <woertliche Changelog-Zeile>" / "WARUM: <ein Satz>" / "PROBE: <ein Satz, wie man es risikofrei testet>". Bei TREFFER: 0 stattdessen EIN Satz, was die Releases fuer uns Unwichtiges bringen.'
EVID="EIGENBAU-LANDKARTE:
$(cat "$LANDKARTE")
CHANGELOGS DER NEUEN RELEASES:
$NOTES"
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh;
# max_tokens grosszuegig, weil Reasoning-Modelle Budget im reasoning_content verbrauchen).
judge() {
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$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'
}
RICHTER="$ANALYST"
ANALYSE="$(judge "$ANALYST" 420 2600)"
if [ -z "${ANALYSE// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
fi
if [ -z "${ANALYSE// /}" ]; then
echo "### ABGLEICH AUSGEFALLEN"
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet. State bleibt unveraendert — naechste Woche neuer Versuch. Dem Commander EINEN ehrlichen Satz melden."
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber der Abgleich fiel aus (kein Analyst erreichbar)."
exit 0
fi
echo "### Neue Releases:"
printf '%s\n' "$NOTES" | grep '^===== RELEASE' | sed 's/^===== //; s/ =====$//'
echo
echo "### ABGLEICH gegen die Eigenbau-Landkarte (Analyst: $RICHTER)"
echo "$ANALYSE"
echo
# Fremdblick-Gate nur bei Treffern (Zitate MIT Quellmaterial gegenpruefbar machen).
TREFFER="$(printf '%s' "$ANALYSE" | grep -oE 'TREFFER: *[0-9]+' | head -1 | grep -oE '[0-9]+' || true)"
GEGEN=""
if [ "${TREFFER:-1}" != "0" ] && [ -x "$FREMDBLICK" ]; then
# Grosses Budget: GLM (Reasoning-Modell) denkt erst ueber das ganze Quellmaterial nach,
# bevor das Urteil in content landet — 2200 (fremdblick-Default) war im Probelauf 10.07.
# zu knapp (ABGESCHNITTEN, kein Urteil).
GEGEN="$(printf '%s\n\nQUELLMATERIAL (Original-Changelogs zum Gegenpruefen der Zitate):\n%s' "$ANALYSE" "$NOTES" \
| FREMDBLICK_MAXTOK="${RADAR_FREMDBLICK_MAXTOK:-4500}" "$FREMDBLICK")"
echo "$GEGEN"
echo
fi
# Treffer fuettern die Ideen-Queue ("Lucy kennt sich", 12.07.2026): aus einem echten
# Radar-Treffer wird EINE rohe Idee (triage) — Specifier + Commander entscheiden, ob eine
# Probe gebaut wird. Idempotent je Release-Tag; der Radar selbst bleibt reiner Melder.
if [ -n "${TREFFER:-}" ] && [ "$TREFFER" != "0" ] && [ -x "$HOME/.local/bin/hermes" ]; then
IDEE_ID="$("$HOME/.local/bin/hermes" kanban create \
"Hermes ${NEWEST}: Release-Radar-Treffer pruefen (Probe bauen?)" \
--body "Automatisch vom Release-Radar am $(date '+%d.%m.%Y'). ABGLEICH (Analyst: ${RICHTER}):
${ANALYSE}
FREMDBLICK:
${GEGEN:-(keiner)}
Rohe Idee: nur pruefen bzw. eine risikofreie Probe vorschlagen — Updates bleiben manuell (Entscheid 04.07.)." \
--triage --created-by release-radar --idempotency-key "release-radar-${NEWEST}" --json 2>/dev/null \
| python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("{")
try:
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
except Exception:
print("")
' 2>/dev/null || true)"
if [ -n "${IDEE_ID// /}" ]; then
echo "### IN DIE IDEEN-QUEUE GELEGT: ${IDEE_ID} — Probe-Pruefung fuer ${NEWEST} (der Specifier uebernimmt)."
echo
fi
fi
printf '%s\n' "$NEWEST" > "$STATE"
briefkasten "Release-Radar" "Neue hermes-agent-Releases bis $NEWEST (Box: ${INSTALLED_TAG:-?}).
ABGLEICH (Analyst: $RICHTER):
$ANALYSE
${GEGEN:-(Kein Fremdblick noetig: TREFFER 0)}"
-298
View File
@@ -1,298 +0,0 @@
#!/usr/bin/env bash
# Idle-Radar (naechtlich 03:45 — S4 "Zuendung", 12.07.2026): die Box gibt sich selbst Arbeit.
# Destilliert aus JOURNAL-Fehlermustern (24 h) und der juengsten TRAUM-Notiz 0-2 KLEINE,
# belegte Wartungs-Kandidaten und legt sie als ROHE IDEEN (triage) ins native Hermes-Kanban.
# Danach laeuft der bewiesene S2-Kreislauf von allein: Specifier arbeitet aus -> Werkstatt
# baut einen Vorschlags-Branch -> Karte im Auftragsbuch -> der KLICK des Commanders bleibt
# das einzige Gate. Muster wie hermes-release-radar-feed.sh: liegt in ~/.hermes/scripts/
# (deploy.sh kopiert ihn), stdout wird dem Cron-Agenten als Prompt eingespeist; der
# eigentliche ABGLEICH laeuft als Ein-Schuss-Completion gegen llama-swap (:8080).
# Grundsaetze:
# - Kandidat NUR mit woertlichem Beleg-Zitat; das Skript prueft das Zitat MECHANISCH
# gegen das Material (reproduziert-oder-abgelehnt, Verdikt 04.07.) — haerter als
# ein zweites LLM-Urteil und gratis.
# - Max. 2 neue Ideen je Nacht + STAU-BREMSE: liegt schon genug Arbeit (volle Queue
# oder volles Auftragsbuch), legt der Radar NICHTS nach — die Box soll Arbeit
# abarbeiten, nicht anhaeufen.
# - Dedup dreifach: State-Datei (schon gemeldet) + Queue/Karten-Liste im Analysten-
# Kontext + --idempotency-key beim Anlegen.
# - Der Radar LEGT NUR IDEEN AN — er baut nichts, merged nichts, konfiguriert nichts.
# State: ~/.hermes/state/idle-radar-gemeldet.txt (ein KEY je bereits gemeldetem Kandidaten).
set -uo pipefail
MC="$HOME/mission-control-v2"
VAULT="$HOME/wissens-vault"
STATE_DIR="$HOME/.hermes/state"
STATE="$STATE_DIR/idle-radar-gemeldet.txt"
ENDPOINT="${IDLE_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
ANALYST="${IDLE_MODEL:-gpt-oss-120b}"
VERTRETUNG="${IDLE_FALLBACK:-GLM-4.7-Flash}"
HERMES="$HOME/.local/bin/hermes"
MAX_NEU=2 # harte Obergrenze neuer Ideen je Nacht (wie Bagatell-Pfad)
STAU_QUEUE=4 # ab so vielen offenen Queue-Aufgaben: Bremse
STAU_KARTEN=3 # ab so vielen offenen Auftragsbuch-Karten: Bremse
mkdir -p "$STATE_DIR"; touch "$STATE"
# Briefkasten-Spiegel (still, wie Morgenlage/Release-Radar): Chronik liest source=idle-radar.
briefkasten() { # $1=Betreff $2=Text
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "idle-radar", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
cat "$MC/deploy/idle-radar-prompt.md" 2>/dev/null || cat <<'EOF'
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des naechtlichen Idle-Radars.
Berichte den Befund unten in Lucys Stimme in hoechstens 3 Saetzen: wie viele neue
Queue-Ideen angelegt wurden (Titel nennen) oder warum keine. Nichts selbst umsetzen,
keine Kanban-Aktionen — das Skript hat alles Noetige bereits getan.
EOF
echo
# ---------- Stau-Bremse (vor dem teuren Analysten) ----------
OFFEN_QUEUE="$("$HERMES" kanban list --json 2>/dev/null | python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("[")
try:
d = json.loads(t[i:]) if i >= 0 else []
except Exception:
d = []
print(sum(1 for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")))
' 2>/dev/null || echo 0)"
OFFEN_KARTEN="$(curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
import json, sys
try:
print(int(json.load(sys.stdin).get("open_count", 0)))
except Exception:
print(0)
' 2>/dev/null || echo 0)"
if [ "${OFFEN_QUEUE:-0}" -ge "$STAU_QUEUE" ] || [ "${OFFEN_KARTEN:-0}" -ge "$STAU_KARTEN" ]; then
echo "## IDLE-RADAR-BEFUND: STAU-BREMSE (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "Offene Queue-Aufgaben: ${OFFEN_QUEUE} (Bremse ab ${STAU_QUEUE}) · offene Karten: ${OFFEN_KARTEN} (Bremse ab ${STAU_KARTEN})."
echo "Es liegt genug Arbeit — heute Nacht keine neuen Ideen. Dem Commander EINEN kurzen Satz melden."
briefkasten "Idle-Radar" "Stau-Bremse: ${OFFEN_QUEUE} offene Queue-Aufgaben, ${OFFEN_KARTEN} offene Karten — keine neuen Ideen angelegt."
exit 0
fi
# ---------- Material sammeln (erst sammeln, dann kuerzen — SIGPIPE-Falle) ----------
TRAUM_DATEI="$(ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -1 || true)"
EVID="$(
echo "### JOURNAL-FEHLERMUSTER (user-Dienste, letzte 24 h; Anzahl · Muster, lange Zahlen/IDs normalisiert):"
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
| sed -E 's/^[A-Z][a-z]{2} [0-9 ]{2} [0-9:]{8} [^ ]+ //; s/\[[0-9]+\]//; s/[0-9]{3,}/N/g; s/t_[0-9a-f]+/t_X/g' \
| sort | uniq -c | sort -rn | head -18
echo
echo "### JUENGSTE TRAUM-NOTIZ ($(basename "${TRAUM_DATEI:-keine}") — Funde des naechtlichen Lern-Crons):"
if [ -n "$TRAUM_DATEI" ]; then head -70 "$TRAUM_DATEI"; else echo "(keine Traum-Notiz gefunden)"; fi
echo
echo "### OFFENE SKILL-KANDIDATEN im Vault (nur Titelzeilen):"
ls -1 "$VAULT/skill-kandidaten/"*.md 2>/dev/null | head -8 | while read -r f; do
echo "- $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
done
echo
echo "### SELBST-STECKBRIEF (auto-generiert von selbst-inventur.sh — das EXISTIERT schon; NICHTS davon neu vorschlagen):"
head -80 "$HOME/.hermes/state/selbst-steckbrief.md" 2>/dev/null || echo "(noch keine Selbst-Inventur gelaufen)"
echo
echo "### SEIT GESTERN AN MIR GEAENDERT (Selbst-Inventur-Diff — gute Kandidaten-Quelle, wenn eine kleine Massnahme erkennbar ist):"
head -40 "$HOME/.hermes/state/selbst-steckbrief.diff" 2>/dev/null || echo "(keine Aenderung erkannt)"
echo
echo "### VOM COMMANDER ABGELEHNT (mit Grund — sinngemaess NIE wieder vorschlagen):"
tail -12 "${MC2_ABLEHNUNGEN:-/srv/models/mc2-ablehnungen.jsonl}" 2>/dev/null | python3 -c '
import json, sys
n = 0
for line in sys.stdin:
try:
d = json.loads(line)
except Exception:
continue
n += 1
print("- [{}] {} — Grund: {}".format(d.get("branch", "?"), (d.get("subject") or "")[:80],
d.get("grund") or "(keiner angegeben)"))
if not n:
print("(keine)")
' 2>/dev/null || echo "(keine)"
echo
echo "### SCHON IN DER IDEEN-QUEUE (inkl. archiviert = vom Commander verworfen; NICHT erneut vorschlagen):"
"$HERMES" kanban list --archived --json 2>/dev/null | python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("[")
try:
d = json.loads(t[i:]) if i >= 0 else []
except Exception:
d = []
for x in d[:40]:
if isinstance(x, dict):
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:110]))
' 2>/dev/null || echo "(Queue nicht lesbar)"
echo
echo "### OFFENE KARTEN IM AUFTRAGSBUCH (warten auf Klick; NICHT erneut vorschlagen):"
curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
import json, sys
try:
items = json.load(sys.stdin).get("items") or []
except Exception:
items = []
if not items:
print("(keine)")
for it in items[:15]:
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
echo
echo "### VOM IDLE-RADAR BEREITS GEMELDET (KEYs; NICHT erneut vorschlagen):"
tail -40 "$STATE" 2>/dev/null || true
)"
RASTER='Du bist der IDLE-RADAR einer lokalen KI-Box (Hermes-Agent, MC2, Lucy). Du bekommst naechtliches Material: Journal-Fehlermuster, die juengste Traum-Notiz, offene Skill-Kandidaten sowie Listen dessen, was schon in Arbeit ist. Deine Aufgabe: destilliere daraus 0 bis 2 KLEINE, konkrete Wartungs-Kandidaten, die die Box-Werkstatt selbst umsetzen kann (Patch in 1-2 Dateien, Skript- oder Doku-Aenderung im MC2- oder Lucy-Repo). REGELN: Jeder Kandidat MUSS eine Zeile aus dem Material WOERTLICH zitieren (BELEG) — das Zitat wird mechanisch gegengeprueft, erfundene Zitate fliegen raus. Nichts vorschlagen, was sinngemaess schon in der IDEEN-QUEUE, den OFFENEN KARTEN oder der BEREITS-GEMELDET-Liste steht. Der SELBST-STECKBRIEF sagt dir, was die Box schon HAT und KANN — schlage NIE etwas vor, das dort schon steht (der Fehler vom 12.07.: ein Gateway-Neustart wurde vorgeschlagen, der laengst konfiguriert war). Was der Commander mit Grund ABGELEHNT hat, ist entschieden — nicht neu verpacken. Aenderungen aus dem Inventur-Diff sind gute Kandidaten-Quellen, aber nur mit konkreter kleiner Massnahme. TABU (niemals vorschlagen): Security (approvals/Tokens/ufw/sudoers/ssh), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode, Updates/Upgrades, alles was den PC oder einen Menschen braucht. Ein wiederkehrendes Fehlermuster ist NUR dann ein Kandidat, wenn eine konkrete kleine Ursachen-Behebung erkennbar ist — "beobachten", "untersuchen" oder "Monitoring bauen" ist KEIN Auftrag. Qualitaet vor Quote: KANDIDATEN: 0 ist der Normalfall und voellig ok. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "KANDIDATEN: <0|1|2>". Danach je Kandidat ein Block aus genau vier Zeilen: "TITEL: <max 90 Zeichen, Imperativ, deutsch>" / "KEY: <kurzer-kebab-slug>" / "BELEG: <woertliches Zitat aus dem Material, EINE Zeile>" / "WARUM: <ein Satz>". Bei 0 stattdessen EIN Satz, warum nichts ansteht.'
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh).
judge() {
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$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'
}
RICHTER="$ANALYST"
ANALYSE="$(judge "$ANALYST" 420 2600)"
if [ -z "${ANALYSE// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
fi
echo "## IDLE-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "Offene Queue-Aufgaben vorher: ${OFFEN_QUEUE} · offene Karten: ${OFFEN_KARTEN} · Deckel: ${MAX_NEU}/Nacht"
echo
if [ -z "${ANALYSE// /}" ]; then
echo "### ANALYSE AUSGEFALLEN"
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet — dem Commander EINEN ehrlichen Satz melden; morgen neuer Versuch."
briefkasten "Idle-Radar" "Analyse ausgefallen (kein Analyst erreichbar) — keine neuen Ideen."
exit 0
fi
echo "### ANALYSE (Analyst: $RICHTER)"
echo "$ANALYSE"
echo
# ---------- Ehrlichkeits-Gate + Dedup + Anlegen (deterministisch im Skript) ----------
# Temp-Dateien statt Pipes: Heredoc + Pipe gleichzeitig frisst stdin (Release-Radar-Lehre 10.07.).
TMPD="$(mktemp -d /tmp/idle-radar.XXXXXX)"
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
printf '%s' "$EVID" > "$TMPD/material.txt"
PLAN="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$MAX_NEU" <<'PY'
import re, sys, unicodedata
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
try:
state = {l.strip() for l in open(sys.argv[3], encoding="utf-8", errors="replace") if l.strip()}
except Exception:
state = set()
max_neu = int(sys.argv[4])
def norm(s):
s = unicodedata.normalize("NFKC", s)
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
return " ".join(s.lower().split())
nev = norm(evid)
# Zeilenweise einsammeln (tolerant gegen Markdown-Deko und Leerzeilen zwischen Feldern).
cands, cur = [], {}
for line in ana.splitlines():
line = line.strip().lstrip("*->#• ").strip()
m = re.match(r"(TITEL|KEY|BELEG|WARUM)\s*:\s*(.+)$", line)
if not m:
continue
k, v = m.group(1), m.group(2).strip()
if k == "TITEL" and cur:
cands.append(cur); cur = {}
cur[k] = v
if cur:
cands.append(cur)
neu = 0
SEP = "\x1f"
for c in cands:
titel = (c.get("TITEL") or "").strip()[:120]
warum = (c.get("WARUM") or "").strip()[:300]
beleg = (c.get("BELEG") or "").strip().strip('"„“»«>`').strip()
key = re.sub(r"[^a-z0-9-]+", "-", (c.get("KEY") or "").strip().lower())[:48].strip("-")
if not (titel and key and beleg):
continue
if key in state or ("idle-radar-" + key) in state:
print("SKIP" + SEP + key + SEP + "schon frueher gemeldet (State)")
continue
if not norm(beleg) or norm(beleg) not in nev:
print("SKIP" + SEP + key + SEP + "BELEG nicht woertlich im Material — verworfen (Ehrlichkeits-Gate)")
continue
if neu >= max_neu:
print("SKIP" + SEP + key + SEP + "Deckel erreicht")
continue
neu += 1
print("NEU" + SEP + key + SEP + titel + SEP + beleg + SEP + warum)
PY
)"
ANGELEGT=""; VERWORFEN=""
while IFS=$'\x1f' read -r art key titel beleg warum; do
[ -n "${art:-}" ] || continue
if [ "$art" = "SKIP" ]; then
VERWORFEN="${VERWORFEN}- ${key}: ${titel}"$'\n' # bei SKIP traegt das 3. Feld den Grund
continue
fi
[ "$art" = "NEU" ] || continue
BODY="Automatisch vom Idle-Radar erhoben am $(date '+%d.%m.%Y') (Quelle: Journal-Muster/Traum-Notiz der Nacht).
BELEG (woertlich aus dem Material): ${beleg}
WARUM: ${warum}
Rohe Idee — bitte pruefen, klein schneiden und nur wenn tragfaehig promoten; sonst archivieren."
TASK_ID="$("$HERMES" kanban create "$titel" --body "$BODY" --triage \
--created-by idle-radar --idempotency-key "idle-radar-${key}" --json 2>/dev/null \
| python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("{")
try:
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
except Exception:
print("")
' 2>/dev/null || true)"
if [ -n "${TASK_ID// /}" ]; then
echo "$key" >> "$STATE"
ANGELEGT="${ANGELEGT}- ${titel} (${TASK_ID}, Key ${key})"$'\n'
else
VERWORFEN="${VERWORFEN}- ${key}: kanban create fehlgeschlagen"$'\n'
fi
done <<< "$PLAN"
rm -rf "$TMPD"
echo "### ANGELEGT (rohe Ideen, triage — der Specifier uebernimmt):"
echo "${ANGELEGT:-(keine)}"
echo "### NICHT ANGELEGT (Gate/Dedup/Deckel):"
echo "${VERWORFEN:-(keine)}"
N_NEU="$(printf '%s' "$ANGELEGT" | grep -c '^-' || true)"
if [ "${N_NEU:-0}" -gt 0 ]; then
briefkasten "Idle-Radar" "Idle-Radar hat ${N_NEU} neue Idee(n) in die Queue gelegt:
${ANGELEGT}Der Specifier arbeitet sie aus; die Karten-Annahme bleibt beim Commander."
else
briefkasten "Idle-Radar" "Idle-Radar: keine neuen Ideen (Analyst: ${RICHTER})."
fi
-26
View File
@@ -1,26 +0,0 @@
# Idle-Radar — Auftrag für den Boten-Agenten
Du bist der Bote des nächtlichen Idle-Radars. Unten steht ein automatisch erhobener
BEFUND: aus Journal-Fehlermustern und der jüngsten Traum-Notiz destilliert ein
Analysten-Modell kleine Wartungs-Kandidaten; das Skript hat die tragfähigen davon
BEREITS als rohe Ideen (triage) in die Ideen-Queue gelegt — den Rest erledigt der
bewiesene Kreislauf (Specifier → Werkstatt → Karte im Auftragsbuch → Klick des
Commanders).
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
Fazit zuerst), in höchstens 3 Sätzen:
1. Bei „ANGELEGT: (keine)" oder „KANDIDATEN: 0": GENAU EIN kurzer Satz
(z. B. „Idle-Radar: nichts Belegbares heute Nacht — Queue bleibt wie sie ist.").
2. Bei neuen Ideen: nenne Anzahl und die Titel; sage dazu, dass die Box sie jetzt
selbst ausarbeitet und die Annahme wie immer per Karten-Klick beim Commander liegt.
3. Bei „STAU-BREMSE" oder „AUSGEFALLEN": ein ehrlicher Satz, warum der Radar heute
Nacht nichts angelegt hat.
Harte Regeln:
- NICHTS selbst umsetzen: keine Kanban-Aktionen, nichts bauen, nichts konfigurieren —
das Anlegen hat das Skript schon deterministisch erledigt.
- Erfinde keine Kandidaten über den Befund hinaus; im Ehrlichkeits-Gate verworfene
Vorschläge (BELEG nicht im Material) nicht als Erfolg verkaufen.
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur
die kurze Telegram-Meldung.
-192
View File
@@ -1,192 +0,0 @@
#!/usr/bin/env bash
# Karten-Gutachter (naechtlich 04:00 — "Lucy kennt sich", 12.07.2026): stempelt jede neue
# Auftragsbuch-Karte mit einer Empfehlung (ANNEHMEN/ABLEHNEN/UNKLAR + EIN ehrlicher Satz),
# damit der Commander nie blind klickt. Ein-Schuss-Richter gegen llama-swap (:8080,
# fremdblick-Architektur, Muster chef-gutachter-feed.sh) — bekommt Karte (Commit-Text,
# Dateien, Diff) UND den Selbst-Steckbrief (was schon existiert), prueft also genau die
# Falle vom 12.07. (redundanter Gateway-Restart-Vorschlag). Der Stempel ist eine MEINUNG,
# kein Gate: die Karte bleibt klickbar, die Entscheidung bleibt beim Commander.
# Laeuft als Cron mit --no-agent: stdout leer = still; gestempelte Karten = kurze Zeilen.
# Ablage: /srv/models/mc2-karten-gutachten.json (Backend mischt es in die Karten-API).
set -uo pipefail
API="${MC_API:-http://127.0.0.1:9001}"
OUT="${MC2_KARTEN_GUTACHTEN:-/srv/models/mc2-karten-gutachten.json}"
ENDPOINT="${GUTACHTER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
CHEF="${GUTACHTER_MODEL:-gpt-oss-120b}"
VERTRETUNG="${GUTACHTER_FALLBACK:-GLM-4.7-Flash}"
STECK="$HOME/.hermes/state/selbst-steckbrief.md"
MAX_JE_LAUF=3 # Nacht-GPU-Budget: Rest kommt morgen dran
briefkasten() { # $1=Betreff $2=Text
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "karten-gutachter", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
TMPD="$(mktemp -d /tmp/karten-gutachter.XXXXXX)"
trap 'rm -rf "$TMPD"' EXIT
if ! curl -sf -m 20 "$API/api/auftragsbuch" -o "$TMPD/items.json" 2>/dev/null; then
echo "Karten-Gutachter: Auftragsbuch nicht erreichbar — kein Lauf."
exit 0
fi
# Offene, noch ungestempelte Karten ermitteln + verwaiste Stempel aufraeumen (Karte weg
# oder neuer Stand = Stempel weg). Schluessel: "<repo>:<branch>@<ts>" haengt am Commit-
# Zeitstempel — ein nachgeschobener Commit macht den alten Stempel automatisch ungueltig.
python3 - "$TMPD/items.json" "$OUT" > "$TMPD/plan.txt" <<'PY'
import json, sys, os
items = (json.load(open(sys.argv[1], encoding="utf-8")) or {}).get("items") or []
out_path = sys.argv[2]
try:
alt = json.load(open(out_path, encoding="utf-8"))
if not isinstance(alt, dict):
alt = {}
except Exception:
alt = {}
offen = {}
for it in items:
state = (it.get("status") or {}).get("state")
if state in ("eingespielt", "laeuft", "rollback"):
continue # entschieden oder gerade in Arbeit — kein Stempel noetig
key = "{}:{}".format(it.get("repo", "mc2"), it.get("branch", "?"))
offen[key] = it
# Verwaiste/veraltete Stempel raus (atomar zurueckschreiben).
neu = {}
for key, g in alt.items():
it = offen.get(key)
if it and g.get("commit_ts") == it.get("ts"):
neu[key] = g
tmp = out_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(neu, f, ensure_ascii=False)
os.replace(tmp, out_path)
for key, it in offen.items():
if key in neu:
continue
repo, branch = key.split(":", 1)
print("\x1f".join([repo, branch, str(it.get("ts") or 0),
(it.get("subject") or "")[:200],
(it.get("shortstat") or "")[:200]]))
PY
RASTER='Du bist der KARTEN-GUTACHTER einer lokalen KI-Box. Du bekommst EINE Vorschlags-Karte aus dem Auftragsbuch (Commit-Text, Datei-Liste, Diff) und den SELBST-STECKBRIEF der Box (was schon existiert und laeuft). Der Commander ist kein Entwickler — dein Urteil ist seine einzige fachliche Zweitmeinung vor dem Klick. Pruefe NUR anhand des Materials, erfinde nichts: (1) REDUNDANZ — existiert das laut Steckbrief oder Diff schon? Ein Vorschlag, der Bestehendes nur nachbaut oder verschlechtert (z. B. funktionierende Einstellungen ersetzt), ist abzulehnen. (2) RISIKO — beruehrt der Diff Security (Tokens/approvals/ufw/sudoers), loescht er Daten oder aendert er Verhalten, das der Commander nicht bestellt hat? (3) NUTZEN — loest er ein echtes, benanntes Problem? Antworte auf DEUTSCH in exakt zwei Zeilen: "URTEIL: ANNEHMEN" oder "URTEIL: ABLEHNEN" oder "URTEIL: UNKLAR", danach "SATZ: <ein ehrlicher Satz Begruendung in Alltagssprache>". Kein weiterer Text.'
judge() { # $1=Modell $2=Timeout $3=max_tokens (liest $EVID)
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$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'
}
N=0
while IFS=$'\x1f' read -r repo branch cts subject stat; do
[ -n "${repo:-}" ] || continue
[ "$N" -ge "$MAX_JE_LAUF" ] && { echo "Karten-Gutachter: Tages-Deckel ($MAX_JE_LAUF) erreicht — Rest morgen."; break; }
DIFF="$(curl -sfG -m 30 "$API/api/auftragsbuch/diff" \
--data-urlencode "branch=$branch" --data-urlencode "repo=$repo" 2>/dev/null \
| python3 -c '
import json, sys
try:
print((json.load(sys.stdin).get("diff") or "")[:20000])
except Exception:
pass' || true)"
# Leerer Diff = deterministisch UNKLAR, OHNE Richter (Erstlauf 12.07.: eine Karte mit
# 0 Dateien bekam trotzdem ANNEHMEN — ein LLM kann einen leeren Vorschlag nicht pruefen).
if [ -z "${DIFF//[[:space:]]/}" ]; then
python3 - "$OUT" "$repo" "$branch" "$cts" <<'PY'
import json, os, sys, time
out_path = sys.argv[1]
try:
data = json.load(open(out_path, encoding="utf-8"))
if not isinstance(data, dict):
data = {}
except Exception:
data = {}
data["{}:{}".format(sys.argv[2], sys.argv[3])] = {
"empfehlung": "UNKLAR",
"satz": "Der Vorschlag hat einen LEEREN Diff (0 geaenderte Dateien gegen main) — da gibt es nichts zu pruefen; technisch verdaechtig, im Zweifel ablehnen.",
"richter": "mechanische Regel (leerer Diff)",
"commit_ts": int(sys.argv[4]) if sys.argv[4].isdigit() else None,
"ts": int(time.time())}
tmp = out_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp, out_path)
PY
N=$((N + 1))
echo "Karte ${branch}: Empfehlung UNKLAR — leerer Diff (mechanische Regel, kein Richter noetig)."
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung UNKLAR — der Diff ist leer (0 Dateien), technisch verdaechtig."
continue
fi
EVID="KARTE ${repo}:${branch}
COMMIT: ${subject}
UMFANG: ${stat}
DIFF (ggf. gekuerzt):
${DIFF:-(Diff nicht lesbar)}
SELBST-STECKBRIEF DER BOX (was schon existiert):
$(head -90 "$STECK" 2>/dev/null || echo '(noch keine Selbst-Inventur gelaufen)')"
RICHTER="$CHEF"
URTEIL_RAW="$(judge "$CHEF" 420 1600)"
if [ -z "${URTEIL_RAW// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung)"
URTEIL_RAW="$(judge "$VERTRETUNG" 240 1200)"
fi
[ -z "${URTEIL_RAW// /}" ] && { echo "Karte ${branch}: Gutachter ausgefallen (kein Richter erreichbar)."; continue; }
printf '%s' "$URTEIL_RAW" > "$TMPD/urteil.txt"
STAMP="$(python3 - "$TMPD/urteil.txt" "$OUT" "$repo" "$branch" "$cts" "$RICHTER" <<'PY'
import json, os, re, sys, time
raw = open(sys.argv[1], encoding="utf-8", errors="replace").read()
m_u = re.search(r"URTEIL:\s*(ANNEHMEN|ABLEHNEN|UNKLAR)", raw, re.I)
m_s = re.search(r"SATZ:\s*(.+)", raw)
urteil = (m_u.group(1).upper() if m_u else "UNKLAR")
satz = (m_s.group(1).strip() if m_s else "Analyse unklar — Urteil nicht lesbar.")[:300]
out_path = sys.argv[2]
try:
data = json.load(open(out_path, encoding="utf-8"))
if not isinstance(data, dict):
data = {}
except Exception:
data = {}
key = "{}:{}".format(sys.argv[3], sys.argv[4])
data[key] = {"empfehlung": urteil, "satz": satz, "richter": sys.argv[6],
"commit_ts": int(sys.argv[5]) if sys.argv[5].isdigit() else None,
"ts": int(time.time())}
tmp = out_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp, out_path)
print(urteil + "\x1f" + satz)
PY
)"
URTEIL="${STAMP%%$'\x1f'*}"; SATZ="${STAMP#*$'\x1f'}"
N=$((N + 1))
echo "Karte ${branch}: Empfehlung ${URTEIL}${SATZ}"
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung ${URTEIL}${SATZ} (Richter: ${RICHTER})"
done < "$TMPD/plan.txt"
# stdout bleibt LEER, wenn nichts zu stempeln war (--no-agent => stille Nacht).
+11 -10
View File
@@ -10,7 +10,7 @@ models:
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
# Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.)
cmd: |
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 3
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf
ttl: 0
aliases:
- hermes
@@ -24,7 +24,7 @@ models:
Qwen3-Coder-Next:
cmd: |
llama-server -m /srv/models/Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256 -cram 16384
ttl: 600
ttl: 5400
aliases:
- coder
capabilities:
@@ -77,21 +77,22 @@ models:
out: [text]
tools: true
context: 131072
GLM-4.7-Flash:
# KRITIKER (07.07., Rollen-Audit): Fremd-Vendor-Zweitmeinung für fremdblick-Prosa +
# Chef-Gutachter-Vertretung — reines Textmodell, 30B-A3B (MIT, Unsloth-Dynamic-Quant).
# Die Vision-Schwester GLM-4.6V-Flash bleibt als scout für Bild-Jobs. --jinja ist
# Pflicht (Chat-Template, sonst kaputte Tool-Calls).
Devstral-Small-2-24B:
# KRITIKER (25.07.2026): Gegenleser aus einer ANDEREN Modellfamilie (Mistral, Apache-2.0,
# 24B DICHT, 256k Kontext, 68,0 % SWE-bench Verified vs. 59,2 % bei GLM-4.7-Flash).
# Loest GLM-4.7-Flash in der kritiker-Rolle ab. Dicht = auf Strix Halo bandbreitengebunden
# und damit langsam beim SCHREIBEN — fuer einen Kritiker egal: er liest viel, schreibt wenig.
# --jinja ist Pflicht (Chat-Template, sonst kaputte Tool-Calls).
cmd: |
llama-server -m /srv/models/GLM-4.7-Flash-GGUF/GLM-4.7-Flash-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --jinja
ttl: 600
llama-server -m /srv/models/Devstral-Small-2-GGUF/mistralai_Devstral-Small-2-24B-Instruct-2512-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 16384 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256
ttl: 1800
aliases:
- kritiker
capabilities:
in: [text]
out: [text]
tools: true
context: 65536
context: 16384
Qwen3-Reranker-0.6B:
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Mem0-Suchtreffer nach echter
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
-9
View File
@@ -1,9 +0,0 @@
[Unit]
Description=MC2 Bagatell-Annahme (reine Doku-Vorschläge nachts ohne Klick, mit Fangnetz)
Documentation=file:%h/mission-control-v2/docs/AUFTRAGSBUCH.md
[Service]
Type=oneshot
# Als /tmp-Kopie laufen lassen: die Annahme deployt (git reset --hard) und würde sonst das
# eigene Skript unter dem laufenden bash austauschen (bekannte Selbst-Reset-Falle).
ExecStart=/bin/bash -c 'cp "%h/mission-control-v2/deploy/bagatell-annahme.sh" /tmp/mc2-bagatell-run.sh && exec /bin/bash /tmp/mc2-bagatell-run.sh'
-9
View File
@@ -1,9 +0,0 @@
[Unit]
Description=Täglich 04:10 — Bagatell-Annahme (nach der Nacht-Sicherung 03:30, vor der Morgenlage 04:30)
[Timer]
OnCalendar=*-*-* 04:10:00
Persistent=false
[Install]
WantedBy=timers.target
-7
View File
@@ -1,7 +0,0 @@
[Unit]
Description=MC2 Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv durchspielen, Alarm bei Rot)
Documentation=file:%h/mission-control-v2/docs/AUTONOMIE_PLAN.md
[Service]
Type=oneshot
ExecStart=/bin/bash %h/mission-control-v2/deploy/self-smoke.sh
-10
View File
@@ -1,10 +0,0 @@
[Unit]
Description=Täglicher MC2-Selbst-Smoke-Test (07:15, unabhängig von Updates)
[Timer]
OnCalendar=*-*-* 07:15:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
-27
View File
@@ -1,27 +0,0 @@
# systemd-USER-Unit für den MC2-Steward — die Wächter-Loops als eigener Prozess (UMBAU v3 P2).
# Re-Warm-Wächter, Health-Sentry und Mem0-Dedupe überleben damit jeden Steuerpult-Neustart,
# und der Sentry kann erstmals auch das Steuerpult SELBST + den mc2-gateway überwachen
# (MC_SENTRY_WATCH_MC2=1). Meldungen gehen per HTTP in den MC2-Briefkasten (MC_ANNOUNCE_HTTP);
# ist MC2 down, greift weiterhin der Telegram-Direktweg (notify.sh).
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
[Unit]
Description=MC2 Steward (Wächter: Re-Warm, Health-Sentry, Mem0-Dedupe)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/mission-control-v2/backend
ExecStart=%h/mission-control-v2/backend/.venv/bin/python steward.py
Environment=PYTHONPATH=%h/mission-control-v2
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
Environment=MC_MODELS_DIR=/srv/models
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
Environment=MC_SENTRY_WATCH_MC2=1
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
-147
View File
@@ -1,147 +0,0 @@
#!/usr/bin/env python3
"""mem0-Konsolidierung (monatlich, 19.07.2026 — Nachtrag Phase 2 Drei-Welten-Plan).
mem0 OSS v2 ist ADD-only: Es legt Fakten an, loest aber Widersprueche nie auf und
legt Aehnliches nie zusammen ohne Pflege waechst das Gedaechtnis unbegrenzt und
der Recall verrauscht. Der Steward-Dedupe faengt nur exakte Dubletten. Dieser Lauf
macht die fehlende Konsolidierung:
1. Cluster aehnlicher Fakten holen ueber den bestehenden /graph-Endpunkt des
Sidecars (Kosinus der gespeicherten Embeddings, kein Re-Embedding, kein
Chroma-Zugriff noetig).
2. Je Cluster entscheidet das lokale Hirn (Alias `fast`, Thinking aus wie im
Sidecar): zusammenlegen / Widerspruch aufloesen (neuester Zeitstempel
gewinnt) / NICHTS tun.
3. Anwenden ueber die normalen Sidecar-Endpunkte (PUT/DELETE /memory/<id>).
Sicherheitsleitplanken (hart im Code, nicht im Prompt):
- Pro Cluster ueberlebt IMMER mindestens ein Fakt.
- Nur ids aus dem Cluster, max. 1 Update pro Cluster, Inhalt <= 600 Zeichen.
- LLM-Fehler / kaputtes JSON => Cluster bleibt unangetastet.
- Kappung pro Lauf (MAX_CLUSTERS), damit ein Lauf nie das halbe Gedaechtnis
umpfluegt der naechste Monat macht weiter.
Aufruf: mem0-konsolidierung.py [--apply] (Default: Dry-Run, druckt nur Plan)
"""
import json
import sys
import time
import urllib.request
MEM0 = "http://127.0.0.1:8765"
LLM = "http://127.0.0.1:8080/v1/chat/completions"
LLM_MODEL = "fast"
MIN_SCORE = 0.82
MAX_CLUSTERS = 15
MAX_FACTS_PER_CLUSTER = 6
APPLY = "--apply" in sys.argv
def _http(method: str, url: str, body: dict | None = None, timeout: float = 120.0):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode() or "{}")
def clusters_from_graph() -> tuple[dict, list]:
g = _http("GET", f"{MEM0}/graph?min_score={MIN_SCORE}&top_k=5", timeout=180)
nodes = {n["id"]: n for n in g.get("nodes", [])}
parent: dict = {}
def find(x):
parent.setdefault(x, x)
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for e in g.get("edges", []):
a, b = find(e["source"]), find(e["target"])
if a != b:
parent[a] = b
groups: dict = {}
for nid in parent:
groups.setdefault(find(nid), []).append(nid)
clusters = [sorted(v) for v in groups.values() if len(v) >= 2]
clusters.sort(key=len, reverse=True)
return nodes, clusters[:MAX_CLUSTERS]
def judge(facts: list[dict]) -> list[dict]:
"""LLM entscheidet pro Cluster. Fehler => [] (nichts tun)."""
listing = "\n".join(
f"- id={f['id']} | kategorie={f.get('category')} | stand={f.get('updated_at', '')[:10]} | {f.get('content', '')}"
for f in facts)
system = (
"Du konsolidierst ein Langzeitgedaechtnis. Unten steht EINE Gruppe semantisch "
"aehnlicher Fakten. Entscheide konservativ: "
"(a) Sind es Facetten DESSELBEN Fakts: fasse sie in EINEM praezisen deutschen Satz "
"zusammen -> genau ein update auf die beste id, die anderen delete. "
"(b) Widersprechen sie sich: der Fakt mit dem NEUESTEN Stand gewinnt -> veraltete delete. "
"(c) Sind es VERSCHIEDENE Fakten (nur thematisch verwandt): TUE NICHTS. "
"Im Zweifel IMMER (c). Antworte NUR als JSON: "
'{"actions":[{"op":"update","id":"...","content":"..."},{"op":"delete","id":"..."}]} '
'oder {"actions":[]}.'
)
try:
resp = _http("POST", LLM, {
"model": LLM_MODEL,
"messages": [{"role": "system", "content": system},
{"role": "user", "content": "Fakten-Gruppe:\n" + listing}],
"temperature": 0.1, "max_tokens": 800,
"response_format": {"type": "json_object"},
"chat_template_kwargs": {"enable_thinking": False},
})
raw = resp["choices"][0]["message"]["content"] or "{}"
actions = json.loads(raw).get("actions", [])
except Exception as exc:
print(f" (LLM-Fehler, Gruppe unangetastet: {exc})")
return []
# Harte Validierung — Leitplanken siehe Docstring.
ids = {f["id"] for f in facts}
updates = [a for a in actions if a.get("op") == "update" and a.get("id") in ids
and isinstance(a.get("content"), str) and 0 < len(a["content"]) <= 600]
deletes = [a for a in actions if a.get("op") == "delete" and a.get("id") in ids]
if len(updates) > 1:
return []
del_ids = {a["id"] for a in deletes} - {a["id"] for a in updates}
if len(del_ids) >= len(ids):
return []
return updates + [{"op": "delete", "id": i} for i in sorted(del_ids)]
def main() -> None:
t0 = time.time()
nodes, clusters = clusters_from_graph()
total = len(nodes)
print(f"mem0-Konsolidierung {'(APPLY)' if APPLY else '(DRY-RUN)'}"
f"{total} Fakten, {len(clusters)} Gruppen >= {MIN_SCORE}")
n_upd = n_del = 0
for ci, cl in enumerate(clusters, 1):
facts = [nodes[i] for i in cl][:MAX_FACTS_PER_CLUSTER]
actions = judge(facts)
if not actions:
continue
print(f"Gruppe {ci} ({len(facts)} Fakten):")
for f in facts:
print(f" [{f['id'][:8]}] {f.get('content', '')[:90]}")
for a in actions:
if a["op"] == "update":
print(f" -> UPDATE {a['id'][:8]}: {a['content'][:90]}")
n_upd += 1
if APPLY:
_http("PUT", f"{MEM0}/memory/{a['id']}", {"content": a["content"]})
else:
print(f" -> DELETE {a['id'][:8]}")
n_del += 1
if APPLY:
_http("DELETE", f"{MEM0}/memory/{a['id']}")
print(f"Ergebnis: {n_upd} zusammengefasst, {n_del} geloescht, "
f"{total - (n_del if APPLY else 0)} Fakten verbleiben. "
f"({int(time.time() - t0)} s)")
if __name__ == "__main__":
main()
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
# Monatlicher mem0-Konsolidierungslauf (Cron-Wrapper, 3. des Monats 06:40 —
# nach Restore-Probe am 1. und Venv-Audit am 2.). no_agent-Job: stdout geht
# als Bericht direkt an Telegram. Logik: deploy/mem0-konsolidierung.py (--apply).
set -u
exec python3 "$HOME/.hermes/scripts/mem0-konsolidierung.py" --apply 2>&1
-26
View File
@@ -1,26 +0,0 @@
[Unit]
Description=MC2 Mem0 Sidecar — auto-lernendes, semantisches Gedächtnis (mem0 + chroma)
Documentation=https://github.com/mem0ai/mem0
After=network.target
[Service]
# Mem0 + chromadb laufen nur unter Python 3.12 (~/.mem0/venv) — das MC2-Backend (3.14)
# kann sie nicht importieren. Darum dieser schlanke Sidecar; MC2 spricht ihn per HTTP an.
# Bind 127.0.0.1: nur lokal erreichbar (MC2 proxyt nach außen).
Type=simple
WorkingDirectory=%h/mission-control-v2/mem0_service
Environment=MEM0_PORT=8765
Environment=MEM0_CHROMA_PATH=/srv/models/mem0/chroma
Environment=MEM0_HISTORY_DB=/srv/models/mem0/history.db
Environment=MEM0_EMBED_URL=http://127.0.0.1:8080/v1
Environment=MEM0_LLM_URL=http://127.0.0.1:8080/v1
Environment=MEM0_EMBED_MODEL=embed
Environment=MEM0_LLM_MODEL=fast
Environment=MEM0_EMBED_DIMS=1024
Environment=TOKENIZERS_PARALLELISM=false
ExecStart=%h/.mem0/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8765
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
+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)"
+10 -10
View File
@@ -1,12 +1,12 @@
# Projektstart-Worker (Profil „projektstart") — IDE-Vorbereiter
Du bist der **IDE-Vorbereiter** der AI-Box. Du bearbeitest EINE Kanban-Aufgabe: eine Idee des
Commanders, die er **selbst in seiner IDE (Zed / OpenCode) bauen** will — nicht die Box.
Commanders, die er **selbst in der Agentic IDE bauen** will — nicht die Box.
Dein Ergebnis ist **kein Code**, sondern ein **startklares, leeres Projekt-Repo**: ein
wasserdicht durchdachtes Konzept plus alle Steuerungs-Dokumente, mit denen der Commander in Zed
wasserdicht durchdachtes Konzept plus alle Steuerungs-Dokumente, mit denen der Commander in der Agentic IDE
dem Coder sofort einen sauberen Auftrag geben kann. Du denkst und bereitest vor — **gebaut wird
in Zed, von ihm.**
in der Agentic IDE, von ihm.**
## Schritt 0: WIEDERAUFNAHME — immer zuerst, nie aus Reflex neu anfangen
@@ -84,13 +84,13 @@ genau diese Dateien — sonst **nichts**, kein Code, kein Gerüst:
offene Entscheidungen, was der Advocatus Diaboli geprüft hat).
- **`ROADMAP.md`** — der Etappen-/Meilenstein-Plan aus dem Konzept: jede Etappe für sich
lauffähig, in Reihenfolge, mit klarem „fertig wenn …".
- **`AGENTS.md`** — Arbeits-Konventionen für den Zed-Coder (nach dem `projekt-start`-Ritual):
- **`AGENTS.md`** — Arbeits-Konventionen für die Agentic IDE (nach dem `projekt-start`-Ritual):
Deutsch/Nicht-Entwickler, Plan-vor-Code, kleine Schritte, beweisen statt behaupten, Savepoints,
Box-Modelle je Aufgabe (plan=heavy, build=coder). Kurz, konkret, projektbezogen.
**Ganz oben hinein — wörtlich:** „Dieses Projekt ist von der AI-Box vorbereitet: KONZEPT.md ist
bereits gehärtet, ROADMAP.md ist der Plan. Beim ersten Chat den Skill **`projekt-uebernehmen`**
nutzen (einlesen → nachfragen → Etappe 1) — das Konzept wird NICHT neu geplant."
Das ist die Übergabe an den Agenten in Zed; ohne diese Zeile fängt er womöglich von vorn an.
Das ist die Übergabe an die Agentic IDE; ohne diese Zeile fängt er womöglich von vorn an.
**Dazu diese drei HARTEN Regeln — wörtlich in die AGENTS.md (Wasserdicht-Runde 22.07.,
Lehre aus rippy: Prosa-Regeln wurden komplett ignoriert, darum stehen sie ab jetzt überall):**
„1. Die **CI-Ampel** (Gitea Actions, liegt seit der Repo-Anlage unter `.gitea/workflows/ci.yml`
@@ -100,9 +100,9 @@ genau diese Dateien — sonst **nichts**, kein Code, kein Gerüst:
MakeMKV lossless' wurde still durch lossy HandBrake ersetzt.)
3. Deployment NUR über `./deploy.sh` (idempotent, EIN fester Compose-Projektname) —
nie freihändig per SSH zusammenbauen. (rippy-Lehre: Doppel-Anlage `Rippy`+`rippy` auf der VM.)"
- **`README.md`** — was das Projekt ist, in Alltagssprache; erste Schritte zum Loslegen in Zed.
- **`SAVEPOINT.md`** — Startzustand: „vorbereitet — Bau beginnt in Zed", Stand/nächste Schritte/
offene Fragen (lebendes Übergabe-Dokument für den ersten Zed-Chat).
- **`README.md`** — was das Projekt ist, in Alltagssprache; erste Schritte zum Loslegen in der Agentic IDE.
- **`SAVEPOINT.md`** — Startzustand: „vorbereitet — Bau beginnt in der Agentic IDE", Stand/nächste Schritte/
offene Fragen (lebendes Übergabe-Dokument für den ersten Chat).
- **`.aiexclude`** — gitignore-Syntax: was die KI NICHT lesen soll (`.env`, Secrets, große Daten).
Committe (`git add -A && git commit`) und pushe auf **main dieses NEUEN Repos** — das ist korrekt:
@@ -112,7 +112,7 @@ Prüfe nach dem Push mit `git ls-remote origin main`, dass es wirklich angekomme
### 4. Übergabe — `kanban_complete`
Schließe die Karte mit einer Zusammenfassung ab, die das Auftragsbuch dem Commander zeigt:
> „Projekt **<name>** ist vorbereitet. Repo: `<clone-url>`. Öffne es in Zed und wirf dem Coder
> „Projekt **<name>** ist vorbereitet. Repo: `<clone-url>`. Öffne es in der Agentic IDE und wirf dem Coder
> **KONZEPT.md** vor — der Plan steht in **ROADMAP.md**. Die Box baut ab hier nicht mehr mit."
Nenne knapp, was das Konzept-Härten ergab (Rollen-Cast, was der Skeptiker fand, welche Etappen).
@@ -130,7 +130,7 @@ DURCHDACHT haben — er hat sich NICHT entschieden, sie zu bauen. Dann gilt abwe
`AGENTS.md`, KEIN `SAVEPOINT.md`, KEIN `.aiexclude`, kein Geruest. Das entsteht erst, wenn der
Commander die Idee spaeter als Projekt startet.
- Schritt 4 (Uebergabe) NICHT als Bau-Aufforderung: sag, was die Idee waere, was sie braeuchte und
was dagegen spricht — und dass die Entscheidung bei ihm liegt. Kein „oeffne in Zed und bau".
was dagegen spricht — und dass die Entscheidung bei ihm liegt. Kein „öffne in der IDE und bau".
## Sonderfall: „UEBERARBEITUNG" (der Commander hat das Konzept gelesen)
Steht im Auftrag **`AUFTRAGS-ART: IDEE PRUEFEN — UEBERARBEITUNG`**, existiert das Konzept schon
-581
View File
@@ -1,581 +0,0 @@
#!/usr/bin/env python3
# Prüfstand-Harness (17.07.2026): fährt ECHTE Prüfungen gegen ein OpenAI-kompatibles
# Endpoint (llama-swap :8080 für Amtsinhaber, Bench-Port :5899 für Kandidaten) und
# bewertet MECHANISCH — bestanden/durchgefallen statt Bauchgefühl.
#
# Suiten (deploy/pruefstand/suites/):
# coding Aufgabe → Modell schreibt Code → echte Unit-Tests laufen im Sandkasten
# agent Mehrschritt-Aufgabe mit Werkzeugen (Dateien lesen/schreiben/löschen im
# Sandkasten, Tool-Calling-API) → bewertet am ENDZUSTAND + Tool-Fehlerquote
# recherche Fragen gegen mitgelieferte Dokumente → Pflicht-Stichworte + wörtliches
# Zitat, das mechanisch gegen die Dokumente geprüft wird (Ehrlichkeits-Gate)
# deutsch Lucy-Alltags-Turns → Richter-Modell (Zwei-Kritiker-Muster) vergibt Noten
# vision generiertes Testbild beschreiben (nur für Vision-Rollen)
# speed kurze + lange Probe, pp/tg aus den llama.cpp-Timings
#
# Nutzung:
# python3 harness.py --endpoint http://127.0.0.1:8080/v1 --model hermes \
# --suites coding,agent --out ergebnis.json [--judge-model gpt-oss-120b] \
# [--deadline-min 90] [--vision-image pfad.png]
#
# Lehren eingebaut: Reasoning-Modelle brauchen großzügiges max_tokens (Verdikt 14.07.:
# knappes Budget = leerer content, nie "kaputt"); Timings stehen im "timings"-Feld der
# llama.cpp-Antwort; jeder API-Fehler = Aufgabe durchgefallen mit Grund, nie Abbruch.
import argparse
import base64
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import unicodedata
import urllib.error
import urllib.request
HIER = os.path.dirname(os.path.abspath(__file__))
SUITES_DIR = os.path.join(HIER, "suites")
# ---------------------------------------------------------------- HTTP-Helfer
def api_chat(endpoint, model, messages, max_tokens=1800, tools=None, timeout=420,
temperature=0.1):
"""Ein Chat-Call. Gibt (message-dict, timings-dict, fehler-string) zurück."""
body = {"model": model, "messages": messages, "max_tokens": max_tokens,
"temperature": temperature}
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
req = urllib.request.Request(
endpoint.rstrip("/") + "/chat/completions",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
d = json.loads(r.read().decode("utf-8", "replace"))
msg = d["choices"][0]["message"]
return msg, d.get("timings") or {}, ""
except Exception as e: # Timeout, HTTP-Fehler, kaputtes JSON — alles ehrlich melden
return None, {}, f"{type(e).__name__}: {e}"
def inhalt_von(msg):
"""content zuerst, reasoning_content als Notnagel (Reasoning-Modelle).
<think>-Blöcke werden entfernt: rohe :8080-Aufrufe an Denk-Modelle (Qwen3.6)
liefern den Grübel-Text im content mit bewertet wird NUR die Antwort
(Baseline-Befund 17.07.: Richter benotete sonst Denk-Monologe)."""
if not msg:
return ""
text = (msg.get("content") or msg.get("reasoning_content") or "")
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
# abgeschnittenes Denken ohne Schluss-Tag: ehrlich als leer werten
text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
return text.strip()
def norm(s):
"""Normalisierung fürs Zitat-Gate (identisch zur idle-radar-Mechanik)."""
s = unicodedata.normalize("NFKC", s or "")
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
return " ".join(s.lower().split())
def code_block(text, sprache="python"):
"""Letzten passenden Code-Block extrahieren; ohne Zaun: ganzen Text nehmen."""
bloecke = re.findall(r"```(?:%s)?\s*\n(.*?)```" % re.escape(sprache),
text or "", re.DOTALL | re.IGNORECASE)
if bloecke:
return bloecke[-1]
bloecke = re.findall(r"```\s*\n(.*?)```", text or "", re.DOTALL)
return bloecke[-1] if bloecke else (text or "")
def lade_task(pfad):
with open(pfad, encoding="utf-8") as f:
return json.load(f)
# ---------------------------------------------------------------- Suite: coding
def suite_coding(cfg, ergebnisse):
basis = os.path.join(SUITES_DIR, "coding", "aufgaben")
for tid in sorted(os.listdir(basis)):
tdir = os.path.join(basis, tid)
if not os.path.isfile(os.path.join(tdir, "task.json")):
continue
if cfg.abgelaufen():
ergebnisse.append({"suite": "coding", "id": tid, "pass": False,
"grund": "Zeitbudget erschöpft — übersprungen"})
continue
task = lade_task(os.path.join(tdir, "task.json"))
t0 = time.time()
prompt = task["prompt"]
for name, inhalt in (task.get("dateien") or {}).items():
prompt += f"\n\n--- Datei `{name}` (Ist-Stand) ---\n```python\n{inhalt}\n```"
prompt += ("\n\nAntworte mit GENAU EINEM ```python-Code-Block, der den "
f"vollständigen Inhalt der Datei `{task['antwort_datei']}` enthält. "
"Keine Erklärungen außerhalb des Blocks.")
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
[{"role": "user", "content": prompt}],
max_tokens=task.get("max_tokens", 2600))
eintrag = {"suite": "coding", "id": tid, "dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second"),
"pp_tps": tim.get("prompt_per_second")}
if err:
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
ergebnisse.append(eintrag)
continue
code = code_block(inhalt_von(msg))
sbx = tempfile.mkdtemp(prefix="pruefstand-coding-")
try:
for name, inhalt in (task.get("dateien") or {}).items():
with open(os.path.join(sbx, name), "w", encoding="utf-8") as f:
f.write(inhalt)
with open(os.path.join(sbx, task["antwort_datei"]), "w",
encoding="utf-8") as f:
f.write(code)
shutil.copy(os.path.join(tdir, "verify.py"), os.path.join(sbx, "verify.py"))
p = subprocess.run([sys.executable, "verify.py"], cwd=sbx,
capture_output=True, text=True,
timeout=task.get("timeout_s", 120))
eintrag["pass"] = (p.returncode == 0)
if p.returncode != 0:
eintrag["grund"] = (p.stdout + p.stderr).strip()[-400:]
except subprocess.TimeoutExpired:
eintrag.update({"pass": False, "grund": "verify-Timeout (Endlosschleife?)"})
except Exception as e:
eintrag.update({"pass": False, "grund": f"Sandkasten-Fehler: {e}"})
finally:
shutil.rmtree(sbx, ignore_errors=True)
ergebnisse.append(eintrag)
# ---------------------------------------------------------------- Suite: agent
WERKZEUGE = [
{"type": "function", "function": {
"name": "liste_dateien",
"description": "Listet alle Dateien im Arbeitsverzeichnis (rekursiv).",
"parameters": {"type": "object", "properties": {}}}},
{"type": "function", "function": {
"name": "lies_datei",
"description": "Liest eine Datei aus dem Arbeitsverzeichnis.",
"parameters": {"type": "object", "properties": {
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
{"type": "function", "function": {
"name": "schreibe_datei",
"description": "Schreibt/überschreibt eine Datei im Arbeitsverzeichnis.",
"parameters": {"type": "object", "properties": {
"pfad": {"type": "string"}, "inhalt": {"type": "string"}},
"required": ["pfad", "inhalt"]}}},
{"type": "function", "function": {
"name": "loesche_datei",
"description": "Löscht eine Datei im Arbeitsverzeichnis.",
"parameters": {"type": "object", "properties": {
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
{"type": "function", "function": {
"name": "fertig",
"description": "Aufgabe abgeschlossen. Kurze Abschluss-Meldung übergeben.",
"parameters": {"type": "object", "properties": {
"meldung": {"type": "string"}}, "required": ["meldung"]}}},
]
def sicherer_pfad(sbx, pfad):
"""Pfad-Ausbruch verhindern — Sandkasten bleibt Sandkasten."""
ziel = os.path.realpath(os.path.join(sbx, pfad or ""))
if not ziel.startswith(os.path.realpath(sbx) + os.sep):
raise ValueError(f"Pfad verlässt den Sandkasten: {pfad}")
return ziel
def werkzeug_ausfuehren(sbx, name, args):
if name == "liste_dateien":
alle = []
for wurzel, _, dateien in os.walk(sbx):
for d in dateien:
alle.append(os.path.relpath(os.path.join(wurzel, d), sbx))
return "\n".join(sorted(alle)) or "(leer)"
if name == "lies_datei":
with open(sicherer_pfad(sbx, args["pfad"]), encoding="utf-8",
errors="replace") as f:
return f.read()[:20000]
if name == "schreibe_datei":
ziel = sicherer_pfad(sbx, args["pfad"])
os.makedirs(os.path.dirname(ziel), exist_ok=True)
with open(ziel, "w", encoding="utf-8") as f:
f.write(args.get("inhalt", ""))
return f"geschrieben: {args['pfad']}"
if name == "loesche_datei":
os.remove(sicherer_pfad(sbx, args["pfad"]))
return f"gelöscht: {args['pfad']}"
raise ValueError(f"unbekanntes Werkzeug: {name}")
def suite_agent(cfg, ergebnisse):
basis = os.path.join(SUITES_DIR, "agent", "aufgaben")
for tid in sorted(os.listdir(basis)):
tdir = os.path.join(basis, tid)
if not os.path.isfile(os.path.join(tdir, "task.json")):
continue
if cfg.abgelaufen():
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
"grund": "Zeitbudget erschöpft — übersprungen"})
continue
task = lade_task(os.path.join(tdir, "task.json"))
t0 = time.time()
sbx = tempfile.mkdtemp(prefix="pruefstand-agent-")
tool_fehler = 0
schritte = 0
try:
for name, inhalt in (task.get("sandkasten") or {}).items():
ziel = os.path.join(sbx, name)
os.makedirs(os.path.dirname(ziel), exist_ok=True)
with open(ziel, "w", encoding="utf-8") as f:
f.write(inhalt)
messages = [
{"role": "system", "content":
"Du bist ein Arbeits-Agent mit Datei-Werkzeugen in einem "
"Sandkasten-Verzeichnis. Erledige die Aufgabe Schritt für Schritt "
"mit den Werkzeugen und rufe am Ende `fertig` auf."},
{"role": "user", "content": task["prompt"]},
]
fertig_gemeldet = False
for _ in range(task.get("max_schritte", 8)):
schritte += 1
msg, _tim, err = api_chat(cfg.endpoint, cfg.model, messages,
max_tokens=1800, tools=WERKZEUGE)
if err:
break
calls = msg.get("tool_calls") or []
if not calls:
break # Agent redet statt zu handeln — Endzustand zählt trotzdem
messages.append({"role": "assistant",
"content": msg.get("content") or "",
"tool_calls": calls})
for call in calls:
fn = (call.get("function") or {}).get("name") or "?"
try:
args = json.loads((call.get("function") or {})
.get("arguments") or "{}")
except Exception:
args, fn_ok = {}, False
tool_fehler += 1
antwort = "FEHLER: Argumente waren kein gültiges JSON."
else:
fn_ok = True
if fn_ok:
if fn == "fertig":
fertig_gemeldet = True
antwort = "ok"
else:
try:
antwort = werkzeug_ausfuehren(sbx, fn, args)
except Exception as e:
tool_fehler += 1
antwort = f"FEHLER: {e}"
messages.append({"role": "tool",
"tool_call_id": call.get("id") or "0",
"content": antwort})
if fertig_gemeldet:
break
# -------- Endzustand prüfen (die einzige Wahrheit)
fehlschlaege = []
for check in task.get("checks", []):
if "nicht_vorhanden" in check:
if os.path.exists(os.path.join(sbx, check["nicht_vorhanden"])):
fehlschlaege.append(
f"{check['nicht_vorhanden']} existiert noch")
continue
pfad = os.path.join(sbx, check["datei"])
if not os.path.isfile(pfad):
fehlschlaege.append(f"{check['datei']} fehlt")
continue
with open(pfad, encoding="utf-8", errors="replace") as f:
text = f.read()
if check.get("regex") and not re.search(check["regex"], text,
re.IGNORECASE | re.MULTILINE):
fehlschlaege.append(
f"{check['datei']}: Muster fehlt ({check['regex']})")
ergebnisse.append({
"suite": "agent", "id": tid, "pass": not fehlschlaege,
"grund": "; ".join(fehlschlaege)[:400] if fehlschlaege else "",
"schritte": schritte, "tool_fehler": tool_fehler,
"fertig_gemeldet": fertig_gemeldet,
"dauer_s": round(time.time() - t0, 1)})
except Exception as e:
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
"grund": f"Harness-Fehler: {e}",
"dauer_s": round(time.time() - t0, 1)})
finally:
shutil.rmtree(sbx, ignore_errors=True)
# ---------------------------------------------------------------- Suite: recherche
def suite_recherche(cfg, ergebnisse):
basis = os.path.join(SUITES_DIR, "recherche")
dok_dir = os.path.join(basis, "dokumente")
dokumente = {}
for d in sorted(os.listdir(dok_dir)):
with open(os.path.join(dok_dir, d), encoding="utf-8") as f:
dokumente[d] = f.read()
alle_norm = norm("\n".join(dokumente.values()))
material = "\n\n".join(f"===== DOKUMENT {n} =====\n{t}"
for n, t in dokumente.items())
for tid in sorted(os.listdir(os.path.join(basis, "aufgaben"))):
tpfad = os.path.join(basis, "aufgaben", tid)
if not tpfad.endswith(".json"):
continue
if cfg.abgelaufen():
ergebnisse.append({"suite": "recherche", "id": tid, "pass": False,
"grund": "Zeitbudget erschöpft — übersprungen"})
continue
task = lade_task(tpfad)
t0 = time.time()
prompt = (f"{material}\n\nFRAGE: {task['frage']}\n\n"
"Beantworte die Frage NUR aus den Dokumenten oben, auf Deutsch, "
"in höchstens 4 Sätzen. Letzte Zeile deiner Antwort: "
'ZITAT: "<eine wörtlich aus einem Dokument übernommene Zeile, '
'die deine Antwort belegt>"')
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
[{"role": "user", "content": prompt}],
max_tokens=2800)
eintrag = {"suite": "recherche", "id": task.get("id", tid),
"dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second"),
"pp_tps": tim.get("prompt_per_second")}
if err:
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
ergebnisse.append(eintrag)
continue
antwort = inhalt_von(msg)
fehlschlaege = []
for gruppe in task.get("muss_gruppen", []):
if not any(k.lower() in antwort.lower() for k in gruppe):
fehlschlaege.append(f"Pflicht-Stichwort fehlt: {'/'.join(gruppe)}")
m = re.search(r'ZITAT:\s*[„"]?(.+?)["“”]?\s*$', antwort,
re.MULTILINE | re.DOTALL)
zitat = (m.group(1).strip() if m else "")
if task.get("zitat_pflicht", True):
if not zitat:
fehlschlaege.append("kein ZITAT geliefert")
elif norm(zitat) not in alle_norm:
fehlschlaege.append("ZITAT nicht wörtlich in den Dokumenten "
"(Ehrlichkeits-Gate)")
eintrag["pass"] = not fehlschlaege
if fehlschlaege:
eintrag["grund"] = "; ".join(fehlschlaege)[:400]
eintrag["antwort_auszug"] = antwort[:300]
ergebnisse.append(eintrag)
# ---------------------------------------------------------------- Suite: deutsch
RICHTER_RASTER = (
"Du bist ein strenger Deutsch-Richter für eine Sprach-Assistentin (Lucy): "
"locker, natürlich, Du-Form, kurze gesprochene Sätze, kein Beamten- oder "
"Übersetzungsdeutsch, keine Anglizismen-Ketten, fachlich korrekt. Bewerte die "
"ANTWORT unten auf einer Skala 110 (10 = klingt wie eine deutsche "
"Muttersprachlerin im Alltag). Antworte EXAKT in diesem Format:\n"
"NOTE: <Zahl 1-10>\nBEGRUENDUNG: <ein Satz>")
def suite_deutsch(cfg, ergebnisse):
aufgaben = lade_task(os.path.join(SUITES_DIR, "deutsch", "aufgaben.json"))
for task in aufgaben:
if cfg.abgelaufen():
ergebnisse.append({"suite": "deutsch", "id": task["id"], "pass": False,
"grund": "Zeitbudget erschöpft — übersprungen"})
continue
t0 = time.time()
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
[{"role": "user", "content": task["prompt"]}],
max_tokens=2400)
eintrag = {"suite": "deutsch", "id": task["id"],
"dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second"),
"pp_tps": tim.get("prompt_per_second")}
if err:
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
ergebnisse.append(eintrag)
continue
antwort = inhalt_von(msg)
jmsg, _jt, jerr = api_chat(cfg.judge_endpoint, cfg.judge_model, [
{"role": "system", "content": RICHTER_RASTER},
{"role": "user", "content":
f"AUFGABE AN DIE ASSISTENTIN: {task['prompt']}\n\n"
f"ANTWORT:\n{antwort}"}],
max_tokens=2200, temperature=0.2)
note = None
if not jerr:
m = re.search(r"NOTE:\s*(\d{1,2})", inhalt_von(jmsg))
if m:
note = max(1, min(10, int(m.group(1))))
eintrag["note"] = note
eintrag["pass"] = (note is not None and note >= 6)
if note is None:
eintrag["grund"] = f"Richter ohne Note ({jerr or 'Format'})"
if not eintrag["pass"]:
eintrag["antwort_auszug"] = antwort[:300]
ergebnisse.append(eintrag)
# ---------------------------------------------------------------- Suite: vision
def suite_vision(cfg, ergebnisse):
task = lade_task(os.path.join(SUITES_DIR, "vision", "task.json"))
bild = cfg.vision_image or os.path.join(SUITES_DIR, "vision", "testbild.png")
if not os.path.isfile(bild):
# Testbild deterministisch erzeugen (reines Python, kein PIL nötig)
subprocess.run([sys.executable,
os.path.join(SUITES_DIR, "vision", "gen_testbild.py"), bild],
check=False, timeout=30)
if not os.path.isfile(bild):
ergebnisse.append({"suite": "vision", "id": "testbild", "pass": False,
"grund": "Testbild fehlt und konnte nicht erzeugt werden"})
return
with open(bild, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
t0 = time.time()
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
{"role": "user", "content": [
{"type": "text", "text": task["prompt"]},
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"}}]}],
max_tokens=900, timeout=600)
eintrag = {"suite": "vision", "id": "testbild",
"dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second")}
if err:
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
ergebnisse.append(eintrag)
return
antwort = inhalt_von(msg)
fehlschlaege = [f"nicht erkannt: {'/'.join(g)}"
for g in task.get("muss_gruppen", [])
if not any(k.lower() in antwort.lower() for k in g)]
eintrag["pass"] = not fehlschlaege
if fehlschlaege:
eintrag["grund"] = "; ".join(fehlschlaege)[:300]
eintrag["antwort_auszug"] = antwort[:200]
ergebnisse.append(eintrag)
# ---------------------------------------------------------------- Suite: speed
LANG_BAUSTEIN = ("Die Box protokolliert jeden Dienststart mit Zeitstempel, "
"Dienstname und Ergebnis, damit die Morgenlage Abweichungen "
"gegen die Vorwoche erkennen und einordnen kann. ")
def suite_speed(cfg, ergebnisse):
# Kurz-Probe: misst tg (Alltags-Turn) — 2 Läufe, erster wärmt den Cache an.
for lauf in ("warm", "kurz"):
t0 = time.time()
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
{"role": "user", "content":
"Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell "
"ist."}], max_tokens=200, temperature=0)
if lauf == "warm":
continue
ergebnisse.append({"suite": "speed", "id": "kurz", "pass": not err,
"grund": err[:200] if err else "",
"dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second"),
"pp_tps": tim.get("prompt_per_second")})
# Lang-Probe: ~12k-Token-Prompt → misst pp bei echtem Kontext (ROCm-Watch-Metrik).
if cfg.abgelaufen():
ergebnisse.append({"suite": "speed", "id": "lang", "pass": False,
"grund": "Zeitbudget erschöpft — übersprungen"})
return
lang = LANG_BAUSTEIN * 550
t0 = time.time()
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
{"role": "user", "content":
lang + "\n\nWie oft steht sinngemäß derselbe Satz oben? Antworte in "
"einem Satz."}], max_tokens=80, temperature=0, timeout=900)
ergebnisse.append({"suite": "speed", "id": "lang", "pass": not err,
"grund": err[:200] if err else "",
"dauer_s": round(time.time() - t0, 1),
"tg_tps": tim.get("predicted_per_second"),
"pp_tps": tim.get("prompt_per_second"),
"prompt_tokens": tim.get("prompt_n")})
# ---------------------------------------------------------------- Hauptlauf
SUITE_FUNKS = {"coding": suite_coding, "agent": suite_agent,
"recherche": suite_recherche, "deutsch": suite_deutsch,
"vision": suite_vision, "speed": suite_speed}
class Konfig:
def __init__(self, args):
self.endpoint = args.endpoint
self.model = args.model
self.judge_endpoint = args.judge_endpoint or args.endpoint
self.judge_model = args.judge_model
self.vision_image = args.vision_image
self.deadline = (time.time() + args.deadline_min * 60
if args.deadline_min else None)
def abgelaufen(self):
return self.deadline is not None and time.time() > self.deadline
def main():
ap = argparse.ArgumentParser(description="Prüfstand-Harness")
ap.add_argument("--endpoint", required=True)
ap.add_argument("--model", required=True)
ap.add_argument("--suites", required=True,
help="kommagetrennt: coding,agent,recherche,deutsch,vision,speed")
ap.add_argument("--out", required=True)
ap.add_argument("--judge-endpoint", default="")
ap.add_argument("--judge-model", default="gpt-oss-120b")
ap.add_argument("--vision-image", default="")
ap.add_argument("--deadline-min", type=float, default=0)
args = ap.parse_args()
cfg = Konfig(args)
ergebnisse = []
for name in [s.strip() for s in args.suites.split(",") if s.strip()]:
if name not in SUITE_FUNKS:
ergebnisse.append({"suite": name, "id": "-", "pass": False,
"grund": "unbekannte Suite"})
continue
try:
SUITE_FUNKS[name](cfg, ergebnisse)
except Exception as e: # kaputte Suite darf den Gesamtlauf nicht töten
ergebnisse.append({"suite": name, "id": "-", "pass": False,
"grund": f"Suite-Absturz: {type(e).__name__}: {e}"})
# -------- Zusammenfassung je Suite (bestanden/gesamt + Tempo-Mittel)
zsm = {}
for e in ergebnisse:
s = zsm.setdefault(e["suite"], {"bestanden": 0, "gesamt": 0,
"tg": [], "pp": [], "noten": []})
s["gesamt"] += 1
s["bestanden"] += 1 if e.get("pass") else 0
if e.get("tg_tps"):
s["tg"].append(e["tg_tps"])
if e.get("pp_tps"):
s["pp"].append(e["pp_tps"])
if e.get("note") is not None:
s["noten"].append(e["note"])
for s in zsm.values():
s["tg_mittel"] = round(sum(s["tg"]) / len(s["tg"]), 1) if s["tg"] else None
s["pp_mittel"] = round(sum(s["pp"]) / len(s["pp"]), 1) if s["pp"] else None
s["note_mittel"] = (round(sum(s["noten"]) / len(s["noten"]), 1)
if s["noten"] else None)
del s["tg"], s["pp"], s["noten"]
with open(args.out, "w", encoding="utf-8") as f:
json.dump({"modell": args.model, "endpoint": args.endpoint,
"erhoben": time.strftime("%Y-%m-%d %H:%M"),
"zusammenfassung": zsm, "einzeln": ergebnisse},
f, ensure_ascii=False, indent=1)
# Kurzfassung auf stdout (für Runner-Logs)
for name, s in zsm.items():
extra = f" · Note {s['note_mittel']}" if s.get("note_mittel") else ""
tempo = (f" · tg {s['tg_mittel']} t/s" if s.get("tg_mittel") else "")
print(f"{name}: {s['bestanden']}/{s['gesamt']}{extra}{tempo}")
if __name__ == "__main__":
main()
-436
View File
@@ -1,436 +0,0 @@
#!/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).
#
# Modi:
# (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten
# Kandidaten aus der Queue prüfen; nichts zu tun → still.
# --baseline Amtsinhaber über llama-swap (:8080) neu vermessen.
# --kandidat <spec> einen Kandidaten-Spec (JSON) direkt prüfen.
#
# Leitplanken (User-Entscheid 17.07.): max 1 Kandidat je Lauf · Download-Deckel 35 GB
# (größere Kandidaten → Karte, Download erst nach Klick) · eigener Cache mit
# Auto-Aufräumen · Live-Betrieb bleibt unangetastet (eigener Server auf :5899) ·
# der Prüfstand EMPFIEHLT NUR — Rollen-Wechsel entscheidet der Commander per Karte.
# RAM-Wache vor dem Serverstart; Heredoc+Pipe-Falle vermieden (Temp-Dateien, Lehre 10.07.).
set -uo pipefail
MC="$HOME/mission-control-v2"
PSD="$MC/deploy/pruefstand"
VAULT="$HOME/wissens-vault"
STATE="$HOME/.hermes/state/pruefstand"
CACHE="/srv/models/pruefstand-cache"
HERMES="$HOME/.local/bin/hermes"
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}"
DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}"
mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse"
# Suiten je Rolle (Amtsinhaber-Alias = Rollenname in llama-swap)
suiten_fuer() {
case "$1" in
hermes) echo "speed,agent,recherche,deutsch" ;;
coder) echo "speed,coding,agent" ;;
heavy) echo "speed,coding,recherche,deutsch" ;;
vision) echo "speed,vision" ;;
scout) echo "speed,agent,vision" ;;
*) echo "speed,recherche,deutsch" ;;
esac
}
briefkasten() { # $1=Betreff $2=Text (stiller Chronik-Spiegel, source=pruefstand)
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "pruefstand", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
engine_build() { # installierte llama.cpp-Build-Nummer (leer, wenn nicht ermittelbar)
LD_LIBRARY_PATH="$(dirname "$BENCH_BIN")" "$BENCH_BIN" --version 2>&1 \
| grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true
}
aggregiere_baseline() { # $1=Engine-Build — baut baseline.json aus den Rollen-Dateien
python3 - "$STATE" "${1:-}" <<'PY'
import glob, json, os, sys, time
state, build = sys.argv[1], sys.argv[2]
basis = {}
for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")):
rolle = os.path.basename(f)[len("baseline-"):-len(".json")]
try:
basis[rolle] = json.load(open(f)).get("zusammenfassung", {})
except Exception:
pass
json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "engine": build,
"rollen": basis},
open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1)
print(f"\nBasislinie gespeichert ({len(basis)} Rollen, Engine b{build or '?'}).")
PY
}
# Tempo-Teil der Baseline neu messen (Lücke 2, 17.07.): Engine-Updates (So 04:30)
# verschieben das Tempo der Amtsinhaber — die Qualitäts-Zahlen bleiben gültig, aber
# Tempo-Vergleiche gegen eine alte Engine wären Geister-Zahlen.
speed_refresh() { # $1=neue Engine-Build-Nummer
local BUILD="$1"
echo "## PRÜFSTAND: Tempo-Baseline-Refresh (Engine jetzt b${BUILD}) am $(date '+%d.%m.%Y %H:%M')"
local rolle TMP
for rolle in hermes coder heavy vision scout; do
TMP="$STATE/ergebnisse/refresh-${rolle}.json"
echo "### $rolle"
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" --suites speed \
--out "$TMP" --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
--deadline-min 10 2>&1 | tail -2
python3 - "$STATE/ergebnisse/baseline-${rolle}.json" "$TMP" <<'PY'
import json, sys
try:
basis = json.load(open(sys.argv[1]))
neu = json.load(open(sys.argv[2]))
except Exception:
raise SystemExit
if "speed" in (neu.get("zusammenfassung") or {}):
basis.setdefault("zusammenfassung", {})["speed"] = neu["zusammenfassung"]["speed"]
basis["einzeln"] = ([e for e in basis.get("einzeln", []) if e.get("suite") != "speed"]
+ [e for e in neu.get("einzeln", []) if e.get("suite") == "speed"])
json.dump(basis, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
PY
rm -f "$TMP"
done
aggregiere_baseline "$BUILD"
briefkasten "Prüfstand" "Tempo-Baseline nach Engine-Wechsel neu gemessen (jetzt b${BUILD})."
}
zusammenfassung_lesen() { # $1=ergebnis.json → kompakte Zeilen "suite: x/y · Note · tg"
python3 - "$1" <<'PY'
import json, sys
try:
z = json.load(open(sys.argv[1])).get("zusammenfassung", {})
except Exception:
print("(Ergebnis nicht lesbar)"); raise SystemExit
for name, s in z.items():
teile = [f"{s['bestanden']}/{s['gesamt']}"]
if s.get("note_mittel"): teile.append(f"Note {s['note_mittel']}")
if s.get("tg_mittel"): teile.append(f"tg {s['tg_mittel']} t/s")
if s.get("pp_mittel"): teile.append(f"pp {s['pp_mittel']} t/s")
print(f" {name}: " + " · ".join(teile))
PY
}
# ---------------------------------------------------------------- Baseline
baseline_lauf() {
echo "## PRÜFSTAND: Basislinie der Amtsinhaber (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "(Echte Prüfungen: Coding mit Unit-Tests, Agenten-Aufgaben, Recherche mit Zitat-Gate, Deutsch-Richter, Tempo.)"
local rollen="hermes coder heavy vision scout"
for rolle in $rollen; do
local out="$STATE/ergebnisse/baseline-${rolle}.json"
echo
echo "### Amtsinhaber \`$rolle\`"
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" \
--suites "$(suiten_fuer "$rolle")" --out "$out" \
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
--deadline-min 30 2>&1 | tail -8
done
# embed: nur Funktions- und Latenz-Probe (Suiten ergeben hier keinen Sinn)
echo
echo "### Amtsinhaber \`embed\`"
python3 - "$LIVE" <<'PY'
import json, sys, time, urllib.request
t0 = time.time()
req = urllib.request.Request(sys.argv[1].rstrip("/") + "/embeddings",
data=json.dumps({"model": "embed", "input": "Prüfstand-Probe"}).encode(),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=60) as r:
d = json.load(r)
dim = len(d["data"][0]["embedding"])
print(f" ok · Dimension {dim} · {round((time.time()-t0)*1000)} ms")
except Exception as e:
print(f" FEHLER: {e}")
PY
# Sammel-Baseline schreiben (eine Datei, die Kandidaten-Läufe vergleichen)
aggregiere_baseline "$(engine_build)"
briefkasten "Prüfstand" "Basislinie der Amtsinhaber erhoben — künftige Kandidaten werden dagegen verglichen."
}
# ---------------------------------------------------------------- Kandidat
kandidat_lauf() { # $1=spec.json
local SPEC="$1"
local KEY ROLLE HF_ID WARUM
KEY="$(jq -r '.key' "$SPEC")"
ROLLE="$(jq -r '.rolle' "$SPEC")"
HF_ID="$(jq -r '.hf_id' "$SPEC")"
WARUM="$(jq -r '.warum // ""' "$SPEC")"
echo "## PRÜFSTAND: Kandidat \`$HF_ID\` für Rolle \`$ROLLE\` (am $(date '+%d.%m.%Y %H:%M'))"
[ -n "$WARUM" ] && echo "Anlass: $WARUM"
if [ -f "$STATE/done/${KEY}.json" ]; then
echo "Schon geprüft (State done/${KEY}) — nichts zu tun."
return 0
fi
# ---- GGUF im HF-Repo finden (bevorzugt ~Q4, ein Stück, Deckel prüfen)
local TREE_JSON="$STATE/tmp-tree-${KEY}.json"
curl -sf --max-time 60 \
"https://huggingface.co/api/models/${HF_ID}/tree/main?recursive=true" \
-o "$TREE_JSON" || { echo "HF-API nicht erreichbar — Abbruch, Spec bleibt in der Queue."; return 1; }
local AUSWAHL
AUSWAHL="$(python3 - "$TREE_JSON" "$DECKEL_GB" <<'PY'
import json, re, sys
baum = json.load(open(sys.argv[1]))
deckel = float(sys.argv[2]) * 2**30
dateien = [(e.get("path",""), (e.get("lfs") or {}).get("size") or e.get("size") or 0)
for e in baum if isinstance(e, dict)]
ggufs = [(p, g) for p, g in dateien if p.lower().endswith(".gguf")]
mmproj = next((p for p, g in ggufs if "mmproj" in p.lower()), "")
haupt = [(p, g) for p, g in ggufs if "mmproj" not in p.lower()]
# Split-GGUFs (…-00001-of-…) klammern wir in v1 aus — sauberer Einzeldatei-Download
haupt = [(p, g) for p, g in haupt if not re.search(r"-\d{5}-of-\d{5}", p)]
prio = ["q4_k_m", "ud-q4_k_m", "q4_k_s", "iq4", "q5_k_m", "q4", "q8_0"]
def rang(p):
pl = p.lower()
for i, q in enumerate(prio):
if q in pl:
return i
return len(prio)
haupt.sort(key=lambda t: (rang(t[0]), t[1]))
if not haupt:
print("KEINE"); raise SystemExit
pfad, groesse = haupt[0]
status = "OK" if groesse and groesse <= deckel else "ZU_GROSS"
print(status); print(pfad); print(groesse or 0); print(mmproj)
PY
)"
rm -f "$TREE_JSON"
local STATUS PFAD GROESSE MMPROJ
STATUS="$(printf '%s\n' "$AUSWAHL" | sed -n 1p)"
PFAD="$(printf '%s\n' "$AUSWAHL" | sed -n 2p)"
GROESSE="$(printf '%s\n' "$AUSWAHL" | sed -n 3p)"
MMPROJ="$(printf '%s\n' "$AUSWAHL" | sed -n 4p)"
local GB=$(( ${GROESSE:-0} / 1073741824 ))
if [ "$STATUS" = "KEINE" ]; then
echo "Kein brauchbares Einzel-GGUF im Repo (nur Splits oder gar keins) — Karte statt Download."
"$HERMES" kanban create "Prüfstand: ${HF_ID} braucht Handarbeit (kein Einzel-GGUF)" \
--body "Kandidat für Rolle ${ROLLE}. Im HF-Repo liegt kein Einzeldatei-GGUF ≤ ${DECKEL_GB} GB (vermutlich Split-Dateien). Bei Interesse manuell laden und 'bash ~/mission-control-v2/deploy/pruefstand/pruefstand.sh --kandidat <spec>' fahren. Anlass: ${WARUM}" \
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
echo "{\"status\":\"kein_gguf\"}" > "$STATE/done/${KEY}.json"
return 0
fi
if [ "$STATUS" = "ZU_GROSS" ]; then
echo "GGUF ${PFAD} ist ${GB} GB > Deckel ${DECKEL_GB} GB — Download wartet auf deinen Klick (Karte liegt bereit)."
"$HERMES" kanban create "Prüfstand: ${HF_ID} (${GB} GB) — Download freigeben?" \
--body "Kandidat für Rolle ${ROLLE}, Datei ${PFAD} (${GB} GB) liegt ÜBER dem Autonomie-Deckel von ${DECKEL_GB} GB (Entscheid 17.07.). Nach Freigabe: Spec wieder in ~/.hermes/state/pruefstand/queue/ legen und PRUEFSTAND_DECKEL_GB erhöhen oder manuell laden. Anlass: ${WARUM}" \
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
echo "{\"status\":\"zu_gross\",\"gb\":${GB}}" > "$STATE/done/${KEY}.json"
briefkasten "Prüfstand" "Kandidat ${HF_ID} (${GB} GB) wartet auf Download-Freigabe (Deckel ${DECKEL_GB} GB)."
return 0
fi
# ---- Platz- und RAM-Wache
local FREI_GB
FREI_GB="$(df --output=avail -BG /srv/models | tail -1 | tr -dc '0-9')"
if [ "${FREI_GB:-0}" -lt $(( GB + 20 )) ]; then
echo "Zu wenig Platz auf /srv/models (${FREI_GB} GB frei, gebraucht ~$((GB+20))) — Lauf verschoben, Spec bleibt in der Queue."
return 1
fi
# ---- Download in den Cache (mit Resume, Auto-Aufräumen am Ende)
mkdir -p "$CACHE/$KEY"
local ZIEL="$CACHE/$KEY/$(basename "$PFAD")"
echo "Lade ${PFAD} (${GB} GB) …"
curl -fL --retry 3 -C - --max-time 7200 -o "$ZIEL" \
"https://huggingface.co/${HF_ID}/resolve/main/${PFAD}" \
|| { echo "Download fehlgeschlagen — Spec bleibt in der Queue."; return 1; }
local MM_ZIEL=""
if [ -n "$MMPROJ" ] && { [ "$ROLLE" = "vision" ] || [ "$ROLLE" = "scout" ]; }; then
MM_ZIEL="$CACHE/$KEY/$(basename "$MMPROJ")"
curl -fL --retry 3 -C - --max-time 3600 -o "$MM_ZIEL" \
"https://huggingface.co/${HF_ID}/resolve/main/${MMPROJ}" || MM_ZIEL=""
fi
local MEM_FREI_GB
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"
if [ "${MEM_FREI_GB:-0}" -lt $(( GB + 8 )) ]; then
echo "Zu wenig freier RAM (${MEM_FREI_GB} GB) für ein ${GB}-GB-Modell neben dem Warm-Set — Lauf verschoben."
return 1
fi
# ---- Kandidaten-Server auf dem Bench-Port (Live-Betrieb unangetastet)
echo "Starte Kandidaten-Server (:${BENCH_PORT}) …"
local MMFLAG=""
[ -n "$MM_ZIEL" ] && MMFLAG="--mmproj $MM_ZIEL"
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
>/tmp/pruefstand-server.log 2>&1 &
local SPID=$!
trap 'kill $SPID 2>/dev/null; wait $SPID 2>/dev/null' RETURN
local BEREIT=0
for i in $(seq 1 240); do
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { BEREIT=1; break; }
sleep 2
kill -0 $SPID 2>/dev/null || break
done
if [ "$BEREIT" != "1" ]; then
echo "LADE-CRASH — Kandidat startet nicht (Architektur zu neu für die Engine?):"
tail -3 /tmp/pruefstand-server.log
echo "{\"status\":\"lade_crash\"}" > "$STATE/done/${KEY}.json"
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
rm -rf "${CACHE:?}/$KEY"
briefkasten "Prüfstand" "Kandidat ${HF_ID} startet auf unserer Engine nicht (Lade-Crash) — aussortiert."
return 0
fi
# ---- Das volle Programm
local OUT="$STATE/ergebnisse/kandidat-${KEY}.json"
echo "Fahre Suiten: $(suiten_fuer "$ROLLE") (Zeitbudget ${DEADLINE_MIN} min)"
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
--model "kandidat" --suites "$(suiten_fuer "$ROLLE")" --out "$OUT" \
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
--deadline-min "$DEADLINE_MIN" 2>&1 | tail -8
kill $SPID 2>/dev/null; wait $SPID 2>/dev/null; trap - RETURN
# ---- MTP-Fairness (Lücke 1, 17.07.): das Live-Hirn läuft MIT draft-mtp (+26 %) —
# ein Kandidat mit eingebautem MTP-Kopf würde ohne Zusatzmessung systematisch
# unterschätzt. Erkennung: Dateiname ODER nextn-Tensoren im GGUF-Kopf.
if printf '%s' "$PFAD" | grep -qi 'mtp' || head -c 8000000 "$ZIEL" 2>/dev/null | grep -aq 'nextn'; then
echo "MTP-Kopf erkannt — Zusatzmessung mit draft-mtp (Fairness gegen das Live-Hirn) …"
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
--spec-type draft-mtp --spec-draft-n-max 3 \
>/tmp/pruefstand-server-mtp.log 2>&1 &
local MPID=$!
trap 'kill $MPID 2>/dev/null; wait $MPID 2>/dev/null' RETURN
local MBEREIT=0
for i in $(seq 1 120); do
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { MBEREIT=1; break; }
sleep 2
kill -0 $MPID 2>/dev/null || break
done
if [ "$MBEREIT" = "1" ]; then
local MOUT="$STATE/ergebnisse/kandidat-${KEY}-mtp.json"
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
--model "kandidat" --suites speed --out "$MOUT" \
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" --deadline-min 10 2>&1 | tail -2
python3 - "$OUT" "$MOUT" <<'PY'
import json, sys
haupt = json.load(open(sys.argv[1]))
try:
s = (json.load(open(sys.argv[2])).get("zusammenfassung") or {}).get("speed")
except Exception:
s = None
if s:
haupt.setdefault("zusammenfassung", {})["speed_mtp"] = s
json.dump(haupt, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
PY
else
echo "Kandidat startet mit draft-mtp NICHT — Zusatzmessung entfällt (im Steckbrief zählt der Basis-Wert)."
fi
kill $MPID 2>/dev/null; wait $MPID 2>/dev/null; trap - RETURN
fi
# ---- Steckbrief: Kandidat vs. Amtsinhaber
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" <<'PY'
import json, sys, time
kand = json.load(open(sys.argv[1]))
try:
basis = json.load(open(sys.argv[2]))["rollen"].get(sys.argv[3], {})
except Exception:
basis = {}
rolle, hf_id, warum, ziel = sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6]
kz = kand.get("zusammenfassung", {})
zeilen = [f"# Prüfstand-Steckbrief: {hf_id}",
f"Rolle: **{rolle}** · erhoben {time.strftime('%d.%m.%Y %H:%M')}",
f"Anlass: {warum}" if warum else "", "",
"| Suite | Kandidat | Amtsinhaber |", "|---|---|---|"]
punkte_k = punkte_b = faelle = 0
for name in sorted(set(kz) | set(basis)):
def fmt(s):
if not s: return "—"
t = f"{s['bestanden']}/{s['gesamt']}"
if s.get("note_mittel"): t += f" · Note {s['note_mittel']}"
if s.get("tg_mittel"): t += f" · tg {s['tg_mittel']}"
if s.get("pp_mittel"): t += f" · pp {s['pp_mittel']}"
return t
zeilen.append(f"| {name} | {fmt(kz.get(name))} | {fmt(basis.get(name))} |")
if name in kz and name in basis and not name.startswith("speed"):
faelle += 1
punkte_k += kz[name]["bestanden"] / max(1, kz[name]["gesamt"])
punkte_b += basis[name]["bestanden"] / max(1, basis[name]["gesamt"])
# Tempo: best-of speed/speed_mtp — die Baseline ist live MIT MTP gemessen (Fairness)
tg_k = max(((kz.get(s) or {}).get("tg_mittel") or 0) for s in ("speed", "speed_mtp"))
tg_b = (basis.get("speed") or {}).get("tg_mittel") or 0
zeilen.append("")
if faelle:
qk, qb = punkte_k / faelle, punkte_b / 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."
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)._")
text = "\n".join(z for z in zeilen if z is not None)
open(ziel, "w", encoding="utf-8").write(text + "\n")
print(text)
PY
# ---- Vault-Bericht + Karte + stille Chronik, dann Cache aufräumen
mkdir -p "$VAULT/pruefstand"
cp "$BERICHT" "$VAULT/pruefstand/${KEY}.md" 2>/dev/null || true
( cd "$VAULT" 2>/dev/null && git add "pruefstand/${KEY}.md" >/dev/null 2>&1 \
&& git commit -q -m "pruefstand: Steckbrief ${KEY}" >/dev/null 2>&1 ) || true
"$HERMES" kanban create "Prüfstand-Ergebnis: ${HF_ID} für Rolle ${ROLLE}" \
--body "$(cat "$BERICHT")" \
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
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"
rm -rf "${CACHE:?}/$KEY"
echo
echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)"
}
# ---------------------------------------------------------------- Einstieg
case "${1:-}" in
--baseline)
baseline_lauf
;;
--kandidat)
[ -n "${2:-}" ] || { echo "Nutzung: pruefstand.sh --kandidat <spec.json>"; exit 1; }
kandidat_lauf "$2"
;;
*)
if [ ! -f "$STATE/baseline.json" ]; then
baseline_lauf
exit 0
fi
# Engine gewechselt seit der Baseline? → Tempo-Teil neu messen (Geister-Zahlen-Schutz)
BUILD="$(engine_build)"
BASE_BUILD="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("engine") or "")' "$STATE/baseline.json" 2>/dev/null || true)"
if [ -n "$BUILD" ] && [ "$BUILD" != "$BASE_BUILD" ]; then
speed_refresh "$BUILD"
fi
NAECHSTER="$(ls -1tr "$STATE/queue/"*.json 2>/dev/null | head -1 || true)"
if [ -n "$NAECHSTER" ]; then
kandidat_lauf "$NAECHSTER"
fi
# keine Queue + keine Engine-Drift → LEERER stdout → --no-agent-Cron bleibt still
;;
esac
@@ -1,13 +0,0 @@
{
"prompt": "Im Arbeitsverzeichnis liegen Notiz-Dateien im Ordner `notizen/`. Sammle ALLE Zeilen, die mit `TODO:` beginnen, und schreibe sie (ohne das `TODO:`-Präfix, eine je Zeile, Reihenfolge egal) in eine neue Datei `offene-punkte.txt` im Wurzelverzeichnis. Rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"notizen/montag.txt": "Besprechung war ok.\nTODO: Backup-Log prüfen\nSonst nichts.\n",
"notizen/dienstag.txt": "TODO: Firmware-Update Router\nWetter schlecht.\n",
"notizen/mittwoch.txt": "Nichts offen heute.\n"
},
"checks": [
{"datei": "offene-punkte.txt", "regex": "Backup-Log prüfen"},
{"datei": "offene-punkte.txt", "regex": "Firmware-Update Router"}
]
}
@@ -1,13 +0,0 @@
{
"prompt": "Die Datei `dienst/config.ini` ist kaputt. In `anleitung.txt` steht, welche Werte gelten sollen. Repariere die config.ini entsprechend (nur die falschen Werte ändern, Struktur beibehalten) und rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"anleitung.txt": "Sollwerte für dienst/config.ini:\n- port muss 9001 sein (Zahl, nicht Text!)\n- loglevel muss info sein\n- Der Eintrag host bleibt unverändert.\n",
"dienst/config.ini": "[server]\nhost = 127.0.0.1\nport = abc\nloglevel = debug\n"
},
"checks": [
{"datei": "dienst/config.ini", "regex": "^port\\s*=\\s*9001\\s*$"},
{"datei": "dienst/config.ini", "regex": "^loglevel\\s*=\\s*info\\s*$"},
{"datei": "dienst/config.ini", "regex": "^host\\s*=\\s*127\\.0\\.0\\.1\\s*$"}
]
}
@@ -1,11 +0,0 @@
{
"prompt": "Lies die Messwerte in `messwerte.csv` (Spalten: zeit;temperatur_c). Erstelle einen Bericht `bericht.md` mit genau zwei Zeilen: Zeile 1 `Messungen: <Anzahl der Datenzeilen>`, Zeile 2 `Maximum: <höchste Temperatur> °C` (Zahl wie in der Quelldatei geschrieben). Rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"messwerte.csv": "zeit;temperatur_c\n03:00;61,5\n04:00;72,25\n05:00;58,0\n06:00;69,75\n"
},
"checks": [
{"datei": "bericht.md", "regex": "Messungen:\\s*4"},
{"datei": "bericht.md", "regex": "Maximum:\\s*72,25\\s*°?C"}
]
}
@@ -1,17 +0,0 @@
{
"prompt": "In `regeln.txt` steht, welche Dateien im Arbeitsverzeichnis gelöscht werden dürfen. Lösche GENAU die Dateien, die laut Regeln weg dürfen — nichts anderes. Rufe danach `fertig` auf.",
"max_schritte": 10,
"sandkasten": {
"regeln.txt": "Aufräum-Regeln:\n1. Alle Dateien mit Endung .tmp dürfen gelöscht werden.\n2. Dateien, deren Name mit wichtig beginnt, dürfen NIE gelöscht werden.\n3. Alles andere bleibt liegen.\n",
"cache-alt.tmp": "wegwerf-inhalt\n",
"sitzung.tmp": "wegwerf-inhalt\n",
"wichtig-vertrag.txt": "bleibt!\n",
"notiz.md": "bleibt auch.\n"
},
"checks": [
{"nicht_vorhanden": "cache-alt.tmp"},
{"nicht_vorhanden": "sitzung.tmp"},
{"datei": "wichtig-vertrag.txt", "regex": "bleibt!"},
{"datei": "notiz.md", "regex": "bleibt auch"}
]
}
@@ -1,5 +0,0 @@
{
"prompt": "Schreibe eine Python-Funktion `summiere_umsaetze(text)` in der Datei `loesung.py`. Eingabe ist ein CSV-Text mit Kopfzeile `kategorie;betrag` — Trennzeichen Semikolon, Beträge mit DEUTSCHEM Dezimalkomma (z. B. `12,50`). Die Funktion gibt ein dict zurück, das je Kategorie die Summe der Beträge als float enthält. Leere Zeilen und Zeilen ohne Semikolon werden ignoriert; unparsebare Beträge lösen einen ValueError mit der Zeilennummer aus.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -1,28 +0,0 @@
import sys
import unittest
from loesung import summiere_umsaetze
class Tests(unittest.TestCase):
def test_summen(self):
text = ("kategorie;betrag\n"
"essen;12,50\n"
"essen;7,50\n"
"\n"
"strom;30,00\n")
self.assertEqual(summiere_umsaetze(text),
{"essen": 20.0, "strom": 30.0})
def test_ohne_semikolon_ignoriert(self):
text = "kategorie;betrag\nnur eine notizzeile\nessen;1,00\n"
self.assertEqual(summiere_umsaetze(text), {"essen": 1.0})
def test_kaputter_betrag(self):
with self.assertRaises(ValueError):
summiere_umsaetze("kategorie;betrag\nessen;zwoelf\n")
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -1,8 +0,0 @@
{
"prompt": "In der Datei `zeitfenster.py` steckt mindestens ein Fehler: `im_wartungsfenster(stunde)` soll True liefern, wenn die Stunde im nächtlichen Wartungsfenster von 23 Uhr bis einschließlich 4 Uhr liegt (also 23, 0, 1, 2, 3, 4) — für alle anderen Stunden False. Ungültige Stunden (<0 oder >23) sollen einen ValueError auslösen. Korrigiere die Datei und gib sie VOLLSTÄNDIG zurück, ohne die Funktionssignatur zu ändern.",
"antwort_datei": "zeitfenster.py",
"dateien": {
"zeitfenster.py": "def im_wartungsfenster(stunde):\n \"\"\"True, wenn die Stunde im Wartungsfenster 23-4 Uhr liegt.\"\"\"\n if stunde < 0 or stunde > 24:\n raise ValueError(\"ungueltige Stunde\")\n return 23 <= stunde or stunde < 4\n"
},
"timeout_s": 60
}
@@ -1,24 +0,0 @@
import sys
import unittest
from zeitfenster import im_wartungsfenster
class Tests(unittest.TestCase):
def test_im_fenster(self):
for h in (23, 0, 1, 2, 3, 4):
self.assertTrue(im_wartungsfenster(h), f"Stunde {h}")
def test_ausserhalb(self):
for h in (5, 6, 12, 18, 22):
self.assertFalse(im_wartungsfenster(h), f"Stunde {h}")
def test_ungueltig(self):
for h in (-1, 24, 99):
with self.assertRaises(ValueError):
im_wartungsfenster(h)
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -1,5 +0,0 @@
{
"prompt": "Schreibe in `loesung.py` eine Funktion `fehler_zeilen(log_text)`. Sie bekommt Logtext, in dem relevante Zeilen so aussehen: `2026-07-17T03:15:02 ERROR dienst-name: Meldungstext` (Level kann auch INFO oder WARN sein). Die Funktion gibt eine Liste von Tupeln `(zeitstempel, dienst, meldung)` NUR für ERROR-Zeilen zurück, in Original-Reihenfolge. Zeilen in anderem Format werden still ignoriert. Der Dienstname steht vor dem Doppelpunkt und enthält keine Leerzeichen.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -1,29 +0,0 @@
import sys
import unittest
from loesung import fehler_zeilen
LOG = """2026-07-17T03:15:02 ERROR llama-swap: Modell nicht gefunden
2026-07-17T03:15:03 INFO mc2: Start ok
kaputte zeile ohne format
2026-07-17T03:16:10 WARN voice: Latenz hoch
2026-07-17T03:17:00 ERROR mem0-service: Timeout nach 30s
"""
class Tests(unittest.TestCase):
def test_nur_error(self):
erg = fehler_zeilen(LOG)
self.assertEqual(len(erg), 2)
self.assertEqual(erg[0][0], "2026-07-17T03:15:02")
self.assertEqual(erg[0][1], "llama-swap")
self.assertEqual(erg[0][2], "Modell nicht gefunden")
self.assertEqual(erg[1][1], "mem0-service")
def test_leer(self):
self.assertEqual(fehler_zeilen("nur INFO hier\n"), [])
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)

Some files were not shown because too many files have changed in this diff Show More