phase1c: Deploy mit Rueckweg und Prueftor, tote Deploy-Dateien raus

- deploy.sh zweistufig: Sperre, laufende Update-Jobs verschieben den Deploy, fast-forward,
  dann laeuft die NEUE Fassung als Stufe 2 (Aenderungen am Skript wirken sofort).
  Stufe 2: Prueftor, Abhaengigkeiten, llama-swap-Config nur bei Aenderung des Abzugs in
  diesem Deploy (ohne Motor-Neustart, -watch-config reicht; 5 Sicherungen), alle Repo-Units
  und Drop-ins, Cron-Skripte nach ~/.hermes/scripts, Skills/Plugins, Neustart, Nachpruefung.
  Scheitert etwas: zurueck auf den alten Stand (inkl. llama-swap-Config), Dienste neu,
  dringende Meldung, Eintrag in /srv/models/mc2-deploy.log.
- deploy/pruefen.sh ersetzt die tote CI-Ampel: Shell-/Python-Syntax, ruff, Importe, pytest.
  Laeuft am PC vor dem Push und auf der Box vor dem Umschalten (pytest via requirements-dev.txt).
- Units, die nur auf der Box lagen, jetzt im Repo: projekte-sync.*, lucy-stimme.service,
  mission-control-2-Override.
- Tot und entfernt: Werkstatt-/Projektstart-/Betrieb-SOULs, worker.sh, Agent-Hooks, Ampel-CI
  (samt .gitea-Workflow und Saat in gitea-repo-create.sh), gitea-pr, Governor-Plugin, Specs,
  Selbst-Inventur, Self-Smoke, venv-Audit, setup_autonomous_crons.sh (haette alte Jobs neu
  angelegt), Einmal-Skripte, alte Bench-Skripte, .agents/mcp_config.json, client/ide-skills,
  mcp/requirements.txt. Gitea-Host-Notizen nach docs/archiv/gitea-host.
- morgenmeldung.sh begrenzt das Melde-Log (ueber 3 MB bleiben die letzten 2 MB).
- AGENTS.md: Prueftor und neuer Deploy beschrieben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 15:13:50 +02:00
co-authored by Claude Opus 5.5
parent e9f488b56c
commit 4cd856bd34
41 changed files with 304 additions and 3168 deletions
-16
View File
@@ -1,16 +0,0 @@
{
"mcpServers": {
"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"
]
}
}
}
-135
View File
@@ -1,135 +0,0 @@
# 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) + Frontend-Tests (vitest/jsdom) + ESLint
# + Buendel-Budget. 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
# Frontend-Tests + Linter (v3-Umbau P1, 28.08.2026).
# Bis dahin lief das Frontend als einziger Teil des Stacks voellig ungeprueft
# durch das Gate — 10 500 Zeilen, null Tests. Die Begruendung weiter oben
# ("echte Tests sind der Pruefstand mit LIVE-Diensten") gilt fuer die
# ML-schweren Python-Dienste, nicht fuer Frontend-Unit-Tests: die laufen in
# jsdom, brauchen weder Modell noch GPU und sind in Sekunden durch.
# Der Linter meldet 0 Fehler / ~90 Warnungen; rot wird nur bei Fehlern.
if [ $rot -eq 0 ]; then
echo "-- Vitest"
( cd frontend && npm test --silent ) || rot=1
echo "-- ESLint (Warnungen sind erlaubt, Fehler nicht)"
( cd frontend && npm run lint --silent ) || rot=1
fi
# Bündel-Budget (v3-Umbau P0, 28.08.2026): Der Start-Chunk ist das, was der
# Nutzer VOR dem ersten Bild lädt. Er lag bei 220 kB gzip, weil das eifrig
# geladene Cockpit Recharts mitzog; dist lag bei 27 MB wegen eines verwaisten
# Avatar-Modells. Ohne Deckel wächst beides unbemerkt zurück. Gemessen wird der
# FRISCHE Build im Runner — kein Vergleich mit dem committeten dist (das wäre
# über Node-Versionen hinweg flatterhaft und würde dauerhaft rot leuchten).
if [ $rot -eq 0 ] && [ -d frontend/dist/assets ]; then
# 28.08.2026, React 18 -> 19: Der Sprung kostete GEMESSEN 14 645 B gzip
# (118 762 -> 133 407) und riss das bis dahin geltende Budget von 125 000.
# Das Gate hat also getan, was es soll. Angehoben wurde es TROTZDEM —
# bewusst und einmalig, nicht weil es im Weg stand:
# · Der Zuwachs IST die Plattform, nicht Wildwuchs. Es gibt hier nichts
# wegzulassen, so wie beim Router (P2), wo Schublade und Palette
# hinter lazy() wanderten und das Budget scharf blieb.
# · MC2 ist eine LAN-Appliance. 14 kB sind ueber Gigabit-Ethernet keine
# messbare Wartezeit; das Budget existiert gegen DRIFT (ein 24-MB-Avatar,
# eine 95-kB-Diagramm-Bibliothek auf der Startseite), nicht gegen einen
# ueberlegten Plattform-Schritt.
# Der Abstand zum Budget bleibt derselbe wie vorher (~5 %).
budget_gz=140000 # Stand nach React 19: 133 407 B gzip
budget_dist=3145728 # Stand nach P0: 1 477 852 B
# Den Einstiegs-Chunk aus index.html lesen, NICHT per Glob raten:
# Rollup nennt auch kleine geteilte Module "index-*.js" (gemessen: ein
# 67-Byte-Chunk neben dem 375-kB-Einstieg). `ls | head -1` haette je nach
# Hash den falschen erwischt — und das Budget waere still immer gruen.
einstieg=$(grep -o 'assets/index-[A-Za-z0-9_-]*\.js' frontend/dist/index.html | head -1)
haupt="frontend/dist/$einstieg"
if [ -n "$haupt" ]; then
gz=$(gzip -c "$haupt" | wc -c)
echo "-- Start-Chunk: $gz B gzip (Budget $budget_gz)"
if [ "$gz" -gt "$budget_gz" ]; then
echo "❌ Start-Bündel über Budget. Meist eine neue Bibliothek, die über"
echo " das eifrig geladene Cockpit hereinkommt — hinter lazy() legen."
rot=1
fi
fi
gesamt=$(du -sb frontend/dist | cut -f1)
echo "-- dist gesamt: $gesamt B (Budget $budget_dist)"
if [ "$gesamt" -gt "$budget_dist" ]; then
echo "❌ dist über Budget — Ballast? (Schrift-Subsets, Medien, WOFF 1)"
rot=1
fi
fi
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
+10 -7
View File
@@ -24,18 +24,21 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
- **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die - **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die
gebauten Assets direkt aus. Nach jeder Frontend-Änderung: `cd frontend && npm run build`, dann gebauten Assets direkt aus. Nach jeder Frontend-Änderung: `cd frontend && npm run build`, dann
**das neue `frontend/dist` mit-committen**. Vergessen = Box zeigt alten Stand. **das neue `frontend/dist` mit-committen**. Vergessen = Box zeigt alten Stand.
- **Deploy macht `git pull origin main`** (`deploy/deploy.sh`). Heißt: **`main` muss vor dem - **Deploy** (`bash ~/mission-control-v2/deploy/deploy.sh` auf der Box) holt `origin/main` per
Deploy auf Gitea liegen**, und Änderungen an getrackten Dateien im Box-Checkout lassen den Pull fast-forward — **`main` muss vorher auf Gitea liegen**, im Box-Checkout nie von Hand ändern.
scheitern — dort nie von Hand ändern. Neue Schritte in `deploy.sh` greifen erst beim nächsten Lauf. Zweistufig seit 24.09.2026: Stufe 1 holt den Stand und startet die NEUE Fassung des Skripts,
Schritt 3 überschreibt die lebende llama-swap-Config, wenn sie vom Abzug abweicht Stufe 2 prüft (pruefen.sh), spielt Units/Cron-Skripte/Skills aus, startet neu und prüft nach.
(`MC_DEPLOY_SKIP_SWAP_CONFIG=1` verhindert das). Scheitert etwas: automatisch zurück auf den alten Stand + dringende Meldung. Laufende Update-Jobs
verschieben den Deploy. Die llama-swap-Config wird nur überschrieben, wenn sich der Repo-Abzug im
selben Deploy geändert hat (sonst gewinnt die lebende Datei, die MC2 selbst schreibt).
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy. - **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
## Agentic IDE & Vibe Coding ## Agentic IDE & Vibe Coding
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**. - **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. - 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. - 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. - **Prüftor:** `bash deploy/pruefen.sh` (Shell-/Python-Syntax, ruff, Importe, pytest) muss vor jedem Push grün sein.
Der Deploy auf der Box führt es vor dem Umschalten noch einmal aus; rot = automatisch zurück auf den alten Stand.
## Zeit & Umgebung ## Zeit & Umgebung
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows. - **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
@@ -47,7 +50,7 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
(Single Source of Truth — Router bleiben dünn). Beispiel-Lehre: Restart-Allowlist lebt NUR in (Single Source of Truth — Router bleiben dünn). Beispiel-Lehre: Restart-Allowlist lebt NUR in
`services.maintenance` (System-Dienste via `sudo -n`, User-Dienste via `systemctl --user`); `services.maintenance` (System-Dienste via `sudo -n`, User-Dienste via `systemctl --user`);
keine zweite Allowlist in einem Router duplizieren. keine zweite Allowlist in einem Router duplizieren.
- **Gate vor Commit:** `python -m py_compile <geänderte .py>` muss durchlaufen. - **Gate vor Commit:** `bash deploy/pruefen.sh` (enthält `py_compile`, `ruff check .` und die Tests).
- Wartung ist **sudo-frei** gedacht (systemctl --user). Wo doch sudo nötig ist (llama-swap = - Wartung ist **sudo-frei** gedacht (systemctl --user). Wo doch sudo nötig ist (llama-swap =
System-Dienst), sauber über die NOPASSWD-Whitelist / `password_required`-Rückgabe, nie hart failen. System-Dienst), sauber über die NOPASSWD-Whitelist / `password_required`-Rückgabe, nie hart failen.
- ‼️ **Die feingranularen sudoers.d-Regeln sind faktisch wirkungslos.** In `/etc/sudoers` steht - ‼️ **Die feingranularen sudoers.d-Regeln sind faktisch wirkungslos.** In `/etc/sudoers` steht
+2
View File
@@ -0,0 +1,2 @@
# Nur fürs Prüftor (deploy/pruefen.sh) — die Dienste selbst brauchen das nicht.
pytest>=8
@@ -1,76 +0,0 @@
---
name: projekt-uebernehmen
description: Ein von der AI-Box vorbereitetes Projekt in Zed uebernehmen und starten - Konzept und Roadmap einlesen, die offenen Punkte mit dem Commander klaeren, dann Etappe 1 planen und bauen. Nutze diesen Skill beim ERSTEN Chat in einem Projekt-Ordner, der KONZEPT.md und ROADMAP.md enthaelt, aber noch keinen (oder kaum) eigenen Code - also bei jedem frisch vorbereiteten Repo aus dem Auftragsbuch.
metadata:
quelle: mission-control-2/client/ide-skills
---
# Vorbereitetes Projekt übernehmen
Die Box hat gedacht, du baust. Das Konzept in diesem Ordner ist **bereits gehärtet** — mehrere
Fach-Rollen und ein Advocatus Diaboli haben es auf der Box durchgearbeitet. Deine Aufgabe ist
nicht, es neu zu denken, sondern es umzusetzen.
**Zwei Modelle, ein Ablauf:** Stufe 1 und 2 (einlesen, nachfragen, planen) gehören in den
**Planer**, Stufe 3 (bauen) in den **Coder**. Du kannst nicht selbst umschalten — wenn der Plan
freigegeben ist, bitte den Commander ausdrücklich darum, oben im Modellwähler auf Coder zu
wechseln. Läufst du schon im Coder, ist das auch in Ordnung: dann gilt trotzdem erst lesen,
fragen, Plan zeigen — und **keine Datei vor seinem „los"**.
Drei Stufen, in dieser Reihenfolge. **Vor Stufe 3 wird keine Datei angefasst.**
## 1. Einlesen — still, ohne Rückfragen
Lies, was da ist: `KONZEPT.md`, `ROADMAP.md`, `AGENTS.md`, `SAVEPOINT.md`, `README.md`.
Verschaff dir den Ist-Zustand: `git log --oneline -5` und den Dateibaum.
Zwei Abbruch-Fälle — prüfe sie ZUERST:
- **Es gibt schon Code und Historie.** Dann ist das keine Übernahme, sondern Fortsetzung:
arbeite nach `SAVEPOINT.md` weiter und sag in zwei Sätzen, wo ihr steht. Dieser Skill ist
hier fertig.
- **Nur `KONZEPT.md` + `README.md`, keine `ROADMAP.md`.** Das ist ein *geprüfte-Idee*-Repo, kein
startklares Projekt — die Box legt bei „Idee prüfen" bewusst kein Gerüst an. Sag dem Commander:
*„Das ist bisher nur ein durchdachtes Konzept, noch kein Projekt. Im Auftragsbuch gibt es auf
der Karte den Knopf Gefällt mir — Projekt daraus machen'; danach hat das Repo eine Roadmap und
ich kann loslegen."* Dann **STOPP**, nichts bauen.
Sonst: fasse in **höchstens 8 Zeilen** zusammen — was gebaut wird, für wen, welche Technik,
und was laut Roadmap **Etappe 1** ist. Alltagssprache, keine Fachbegriffe ohne Übersetzung.
## 2. Fragen stellen — der eigentliche Wert dieses Schritts
Stell **3 bis 6 Fragen**. Nicht mehr, und nur solche, die den Start wirklich blockieren.
- **Nichts fragen, was in den Dokumenten steht.** Wer das Konzept nicht gelesen hat, merkt man
genau hier.
- Jede Frage: eine Zeile, Alltagssprache, **mit deiner Empfehlung** („ich würde X nehmen, weil …").
Der Commander ist kein Entwickler — frag nach Zielen, Vorlieben und Gegebenheiten, nie nach
Bau-Prinzipien oder Architektur-Geschmack. Das ist dein Job.
- Die üblichen echten Lücken: Wo läuft es am Ende? Welche Zugänge/Geräte/Pfade brauchst du von
ihm? Was ist in Etappe 1 ausdrücklich **nicht** drin? Woran merkt ihr beide, dass Etappe 1
fertig ist?
- Der Abschnitt „offene Punkte" / „Risiken" im Konzept ist deine Fundgrube — genau dafür steht er da.
Dann **warten**. Keine Annahmen, kein Vorpreschen, kein „ich fange schon mal an".
## 3. Plan → „los" → bauen
1. **Etappe-1-Plan** vorlegen: Ziel · Vorgehen in kleinen Schritten · Technik und warum ·
Fertig-Kriterien · Risiken. Etappe 1 muss **für sich lauffähig** sein — eine halbe Baustelle ist
kein Meilenstein.
2. **STOPP**, auf sein „los" warten. Danach der Satz, den er hören muss: *„Bitte oben im
Modellwähler auf **Coder** umschalten, dann lege ich los."* Umschalten kannst du nicht selbst.
3. Bauen: kleine Schritte, ein Sicherungspunkt (Commit mit klarem Namen) je Meilenstein und vor
riskanten Änderungen. Am Ende **live testen** — beweisen statt behaupten.
4. Nachführen: `SAVEPOINT.md` (Stand, nächster Schritt, offene Fragen) und die erledigte Etappe in
`ROADMAP.md` abhaken. Das ist das Gedächtnis für den nächsten Chat.
## Merksätze
- **Du denkst das Konzept nicht neu.** Findest du einen echten Widerspruch oder eine Lücke:
benenne sie und frag — bau nicht still etwas anderes.
- **Nichts bauen, was nicht in Etappe 1 steht.** Ideen für später gehören in die Roadmap, nicht
in den Code.
- Geheimnisse (Schlüssel, Passwörter, Tokens) nie in den Code — `.env` plus `.gitignore`.
- Klemmt dieselbe Sache zweimal: anhalten, sichern, erklären, fragen.
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# pre_llm_call-Hook (Faden 5, 13.07.2026): verdrahtet Box-Selbstwissen MECHANISCH in
# JEDEN Kanban-Worker — egal welches Profil (werkstatt, betrieb, default-als-Worker).
# Der Kontext wird ephemer pro Turn an die User-Message angehaengt (agent/turn_context.py),
# NUR wenn die Session in einem Kanban-Task-Workspace laeuft. Lucys interaktiver/Voice-Chat
# laeuft NICHT dort → sofortiger No-op, kein Persona- oder Latenz-Eingriff. Loest
# Fallstrick #1 (Ops/Mess/Verify-Worker flailen, weil sie llama-swap :8080 nicht kennen).
# Ausgabe {"context":"..."} = anhaengen; {} = durchlassen (No-op).
set -u
payload="$(cat -)"
# Scope-Gate: NUR echte Kanban-Worker — erkannt an der cwd im Task-Workspace.
# ACHTUNG (Bug-Fund Faden 8, 13.07.): Worker tragen als task_id den SESSION-Zeitstempel
# (z.B. 20260713_224206_267304), NICHT die Kanban-t_-ID — ein "nur t_"-Check feuerte also
# NIE fuer Worker. Der zuverlaessige Signal ist die cwd unter kanban/workspaces/; Lucy/
# Voice/CLI laufen nie dort.
cwd=$(printf %s "$payload" | jq -r '.cwd // ""' 2>/dev/null || echo "")
case "$cwd" in
"$HOME/.hermes/kanban/workspaces/"*) : ;; # Kanban-Worker → Orientierung anhaengen
*) printf '{}'; exit 0 ;; # Lucy / Voice / CLI / interaktiv → No-op
esac
# Zeiger auf den vollen, naechtlich generierten Selbst-Steckbrief (nur wenn er existiert).
STECKBRIEF="$HOME/.hermes/state/selbst-steckbrief.md"
hint=""
[ -f "$STECKBRIEF" ] && hint=" Dein vollstaendiger Selbst-Steckbrief (aktuelle Versionen/Dienste/Modelle/Crons/offene Karten) liegt in $STECKBRIEF — dort nachlesen statt raten."
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
ctx="$(cat <<EOF
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weiterer Dienst: hermes-gateway :8642 (fuehrt sein Gedaechtnis selbst — es gibt KEINEN separaten Gedaechtnis-Dienst mehr). Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
FRONTEND-BAUEN (seit 15.07. abends): Node LTS liegt sudo-frei unter ~/.local (node/npm im Worker-PATH). Bei Frontend-Aenderungen baust DU das dist MIT: cd frontend && npm ci --no-audit --no-fund && npm run build — und committest frontend/dist im selben Branch (die Box zeigt sonst nach Annahme den alten Stand; Bau-Beweis 15.07.: kompletter Build in ~4 s).
WO WAS LAEUFT (20.07. — zwei getrennte Maschinen, NICHT verwechseln): Du laeufst auf der AI-Box (192.168.178.151) — das ist die WERKBANK (Agent/Lucy/MC2/Modelle), NICHT das Ziel-System fuer Projekt-Deployments. Der Proxmox-Host 'pve' (192.168.178.108) ist ein SEPARATER Rechner; dort laufen die LXC-Container. Von hier per 'ssh pve' erreichbar (root, Key), Steuerung mit 'pct'. Welche Container es gerade gibt, schaust du bei Bedarf mit 'ssh pve pct list' nach — nicht raten, nicht auswendig annehmen. Soll etwas dauerhaft laufen, gehoert es in einen LXC auf pve, nicht auf die Box.
REPO FEHLT? (20.07.): Soll dein Ergebnis in ein Gitea-Repo, das noch NICHT existiert (eigenes/neues Projekt), dann lege es SELBST an — NICHT den Commander fragen, NICHT git init: bash ~/.hermes/scripts/gitea-repo-create.sh <name> "<kurze Beschreibung>" (immer privat, letzte Ausgabezeile = CLONE <url>), dann voll klonen und darin arbeiten. NUR mission-control-v2 und das Lucy-Repo NIE neu anlegen/veraendern.
KONTEXT-BUDGET & ETAPPEN-REGEL (15.07./20.07. — Worker starben an finish_reason=length UND an vollgelaufenem Kontext): Dein Kontext ist ENDLICH und fuellt sich vor allem durch GROSSE Datei- und Terminal-Ausgaben. Lies NIE eine grosse Datei am Stueck — nutze grep und Zeilenbereiche (read mit offset/limit); Datei-Reads sind auf 50k Zeichen gedeckelt, aber auch viele mittlere summieren sich. Lies gezielt ueber den Wegweiser statt ls-Ketten und Volltexte. Merkst du, dass die Aufgabe mehr als ~5 Dateien oder mehrere Baustellen umfasst: NICHT durchziehen — schneide Etappen (eine Etappe = ein Worker-Lauf): lege Teil-Karten an (kanban_create, falls verfuegbar) und schliesse deine Karte per kanban_complete mit dem Etappen-Plan ab; alternativ kanban_block mit dem Etappen-Vorschlag als Frage.
EOF
)"
jq --null-input --arg c "$ctx" '{context:$c}'
-211
View File
@@ -1,211 +0,0 @@
#!/usr/bin/env python3
"""No-Progress-Bremse (Phase 2 des Drei-Welten-Plans, 19.07.2026).
Generische, turn-UEBERGREIFENDE Schleifenbremse — ergaenzt Hermes' native
tool_loop_guardrails, die (a) pro Turn zaehlen und (b) nur als Fehler
klassifizierte Ergebnisse sehen. Die teuer gelernte Luecke (SSH-Marathon 18.07.,
36 min / 75 Schleifen): ein Befehl laeuft "erfolgreich" durch, die AUSGABE sagt
aber immer dasselbe ("Permission denied") — der Agent variiert Befehle gegen
eine unsichtbare Wand, ueber Turn-Grenzen hinweg, und nichts stoppt ihn.
Prinzip (KEINE hartkodierten Fehler-Strings — Leitplanke des Commanders:
"wenn sich was aendert, muss es das SELBST erkennen"):
post_tool_call → Ergebnis normalisieren (Zahlen/Hex-IDs raus) → Muster-Hash.
Gleicher Hash beim gleichen Tool in Folge = Zaehler hoch.
Unbekannte Muster landen in neue-signaturen.jsonl —
Futter fuer Traum/Radar, um daraus Playbooks zu verdichten.
pre_tool_call → Zaehler >= 3 fuer dieses Tool: EINMAL mechanisch blocken
(Diagnose-Anweisung statt naechster Blindversuch), Zaehler
zuruecksetzen. Der Folgeversuch ist frei; laeuft er wieder
3x gegen dasselbe Muster, blockt es erneut.
Gezaehlt wird nur, wo Wiederholung wirklich Stillstand heisst: terminal IMMER
(dort verstecken sich die "erfolgreichen" Fehler), andere Tools nur bei
status=error. Lese-Tools deckt die native idempotent-Bremse ab.
Aufruf (hooks in config.yaml): `no-progress-bremse.py pre` bzw. `... post`.
State: ~/.hermes/state/no-progress/<session>.json (48 h Selbst-Aufraeumen).
Fail-open: jeder eigene Fehler -> {} (die Bremse darf nie selbst zur Wand werden).
"""
import hashlib
import json
import os
import re
import sys
import time
STATE_DIR = os.path.expanduser("~/.hermes/state/no-progress")
SIG_LOG = os.path.join(STATE_DIR, "neue-signaturen.jsonl")
SEEN_FILE = os.path.join(STATE_DIR, "seen-sigs.json")
THRESHOLD = 3
STATE_TTL = 48 * 3600
BLOCK_REASON = (
"NO-PROGRESS-BREMSE: '{tool}' hat {n}x in Folge dasselbe Ergebnismuster geliefert — "
"das ist Stillstand, egal wie sehr die Befehle variieren. STOPP. "
"WICHTIG: Dein Befehl wurde NICHT ausgefuehrt. Das hier ist eine Bremse, KEIN Ergebnis — "
"schliesse daraus NICHTS ueber die Existenz oder den Zustand von Dateien, Verzeichnissen "
"oder Diensten (Vorfall 21.07.2026: ein Worker hielt diese Meldung fuer 'Verzeichnis nicht "
"vorhanden' und baute eine halbe Stunde Arbeit neu). "
"1) Benenne das Hindernis in EINEM Satz (was genau meldet das Ergebnis?). "
"2) Diagnose statt Variation — bei SSH/Zugriff ZUERST: Schluessel-Passphrase pruefen "
"(`ssh-keygen -y -f <key> </dev/null` — stilles 'Permission denied' = verschluesselter Key), "
"dann Gegenseite (Auth-Log, Dienststatus, Erreichbarkeit) LESEN statt raten. "
"3) Bleibt es unklar: Aufgabe als blocked/Karte mit dem EXAKTEN Ergebnistext an den "
"Commander uebergeben. Dein naechster Versuch ist frei — nutze ihn erst NACH der Diagnose."
)
# Rein LESENDE Befehle nie bremsen (21.07.2026). Die Bremse verlangt selbst „Diagnose
# statt Variation" — genau die besteht aus vielen kurzen Schau-Befehlen (ls/cat/git log),
# deren Ergebnisse sich stark aehneln. Die Signatur hielt das fuer Stillstand und blockte
# den naechsten Blick; der Worker las die Block-Meldung als „da ist nichts" und baute
# alles neu (Karte t_d26c3203, eine halbe Stunde Arbeit weggeworfen). Geschaut wird also
# ungebremst — gebremst wird nur, wer immer wieder dasselbe TUT.
# Bewusst strukturell (Befehls-Art), nicht per Fehlertext-Liste: die Bremse soll generisch
# bleiben und Neues selbst erkennen.
_LESEND = {"ls", "cat", "head", "tail", "wc", "stat", "find", "grep", "rg", "file",
"du", "df", "echo", "pwd", "tree", "readlink", "basename", "dirname", "which"}
_GIT_LESEND = {"status", "log", "show", "diff", "ls-remote", "ls-files", "rev-parse",
"merge-base", "describe"}
def _nur_lesend(cmd: str) -> bool:
"""True, wenn der Befehl ausschliesslich schaut — kein Schreiben, kein Eingriff."""
cmd = (cmd or "").strip()
if not cmd or ">" in cmd: # jede Umleitung = schreiben
return False
for teil in re.split(r"&&|\|\||;|\|", cmd):
worte = teil.split()
i = 0
while i < len(worte) and "=" in worte[i] and not worte[i].startswith("-"):
i += 1 # VAR=wert-Praefixe ueberspringen
if i >= len(worte):
return False
verb = os.path.basename(worte[i])
if verb == "git":
rest = [w for w in worte[i + 1:] if not w.startswith("-")]
# `git log …` oder `git -C <pfad> log …` → erstes oder zweites Wort zaehlt
if not (rest[:1] and rest[0] in _GIT_LESEND) and \
not (rest[1:2] and rest[1] in _GIT_LESEND):
return False
elif verb not in _LESEND:
return False
return True
_NUM_RE = re.compile(r"\d+")
_HEX_RE = re.compile(r"\b[0-9a-f]{8,}\b")
_WS_RE = re.compile(r"\s+")
def _norm_sig(tool: str, text: str) -> str:
t = (text or "")[:2000].lower()
t = _HEX_RE.sub("#", _NUM_RE.sub("#", t))
t = _WS_RE.sub(" ", t).strip()[:400]
return hashlib.sha1(f"{tool}|{t}".encode()).hexdigest()[:16]
def _session_key(p: dict) -> str:
sid = p.get("session_id") or p.get("parent_session_id") or ""
if sid:
return re.sub(r"[^A-Za-z0-9_.-]", "_", str(sid))[:80]
return "cwd-" + hashlib.sha1(os.getcwd().encode()).hexdigest()[:12]
def _prune() -> None:
now = time.time()
for f in os.listdir(STATE_DIR):
fp = os.path.join(STATE_DIR, f)
if f.endswith(".json") and f != os.path.basename(SEEN_FILE):
if now - os.path.getmtime(fp) > STATE_TTL:
os.remove(fp)
def _load(path: str) -> dict:
try:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
except Exception:
return {}
def _save(path: str, data: dict) -> None:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh)
os.replace(tmp, path)
def main() -> None:
mode = sys.argv[1] if len(sys.argv) > 1 else "post"
payload = json.load(sys.stdin)
tool = str(payload.get("tool_name") or "")
if not tool:
print("{}")
return
# Delegations-Ausnahme (20.07.2026): worker.sh / fremdblick.sh sind der
# Fliessband-/Orchestrator-Motor — konzept-fliessband ruft sie pro Runde/Rolle
# WIEDERHOLT auf (verschiedene Prompts, aehnliche Prosa-Ergebnisse). Die Bremse
# bildet ihre Signatur aus dem Ergebnis-Text → sie hielt diese legitimen
# Mehrrunden faelschlich fuer eine Schleife und blockte (Pomodoro-IDE-Test lief
# 19 min dagegen an). Diese Helfer haben ihr EIGENES Runden-Limit (max 3) →
# von der generischen Bremse ausnehmen.
_ti = payload.get("tool_input")
_cmd = _ti.get("command") if isinstance(_ti, dict) else (_ti if isinstance(_ti, str) else "")
if tool == "terminal" and ("worker.sh" in str(_cmd) or "fremdblick.sh" in str(_cmd)):
print("{}")
return
# Schauen ist keine Schleife (siehe _nur_lesend): weder zaehlen noch blocken.
if tool == "terminal" and _nur_lesend(str(_cmd)):
print("{}")
return
os.makedirs(STATE_DIR, exist_ok=True)
sfile = os.path.join(STATE_DIR, _session_key(payload) + ".json")
state = _load(sfile)
tools = state.setdefault("tools", {})
if mode == "post":
status = str(payload.get("status") or "ok")
# terminal immer beobachten (dort tarnen sich Fehler als Erfolg),
# sonst nur echte Fehler — Lese-Tools regelt die native Bremse.
if tool != "terminal" and status != "error":
print("{}")
return
sig = _norm_sig(tool, str(payload.get("result") or ""))
entry = tools.get(tool) or {}
entry["count"] = entry.get("count", 0) + 1 if entry.get("sig") == sig else 1
entry["sig"] = sig
tools[tool] = entry
_save(sfile, state)
_prune()
seen = _load(SEEN_FILE)
sigs = seen.setdefault("sigs", [])
if sig not in sigs and status == "error" or (tool == "terminal" and sig not in sigs and entry["count"] >= 2):
sigs.append(sig)
del sigs[:-500]
_save(SEEN_FILE, seen)
sample = _WS_RE.sub(" ", str(payload.get("result") or ""))[:200]
with open(SIG_LOG, "a", encoding="utf-8") as fh:
fh.write(json.dumps({"ts": int(time.time()), "tool": tool,
"sig": sig, "sample": sample}, ensure_ascii=False) + "\n")
print("{}")
return
# mode == "pre"
entry = tools.get(tool) or {}
if entry.get("count", 0) >= THRESHOLD:
entry["count"] = 0
tools[tool] = entry
_save(sfile, state)
print(json.dumps({"decision": "block",
"reason": BLOCK_REASON.format(tool=tool, n=THRESHOLD)},
ensure_ascii=False))
return
print("{}")
if __name__ == "__main__":
try:
main()
except Exception:
print("{}")
-75
View File
@@ -1,75 +0,0 @@
#!/usr/bin/env bash
# pre_verify-Gate (P1-5, 09.07.2026): erinnert den Agenten VOR dem Fertigmelden an
# die zwei MC2-Pflichten aus AGENTS.md — py_compile-Gate und die dist-Falle.
# Antwort {"action":"continue","message":...} = Agent arbeitet weiter; {} = durchlassen.
set -u
payload="$(cat -)"
attempt=$(printf %s "$payload" | jq -r '.extra.attempt // 0' 2>/dev/null || echo 0)
# Ein-Schuss: nach dem ersten Nudge nicht erneut nerven (Docs-Empfehlung).
if [ "$attempt" != "0" ]; then printf '{}'; exit 0; fi
mapfile -t paths < <(printf %s "$payload" | jq -r '.extra.changed_paths[]? // empty' 2>/dev/null)
[ ${#paths[@]} -eq 0 ] && { printf '{}'; exit 0; }
msgs=()
# Gate 1: py_compile fuer alle geaenderten .py, die existieren
pyfail=""
for p in "${paths[@]}"; do
case "$p" in
*.py)
if [ -f "$p" ] && ! err=$(python3 -m py_compile "$p" 2>&1); then
pyfail="${pyfail}${p}: ${err}"$'\n'
fi
;;
esac
done
if [ -n "$pyfail" ]; then
msgs+=("py_compile-Gate ROT:"$'\n'"${pyfail}Erst fixen, dann fertigmelden.")
fi
# Gate 2: dist-Falle — frontend/src geaendert, aber kein frontend/dist angefasst.
# Feuert NUR in Repos mit der MC2-Konvention (frontend/dist ist git-getrackt) —
# sonst nudgt es in beliebigen Spielordnern ohne Build (False-Positive, 09.07. beobachtet).
srcfile=""; dist=0
for p in "${paths[@]}"; do
case "$p" in
*/frontend/src/*|frontend/src/*) srcfile="$p" ;;
*/frontend/dist/*|frontend/dist/*) dist=1 ;;
esac
done
if [ -n "$srcfile" ] && [ $dist -eq 0 ]; then
# Repo-Wurzel vom geaenderten File aus hochlaufen (max 8 Ebenen).
root=$(dirname "$srcfile"); n=0
while [ $n -lt 8 ] && [ -n "$root" ] && [ "$root" != "/" ] && [ ! -e "$root/.git" ]; do
root=$(dirname "$root"); n=$((n+1))
done
if [ -e "$root/.git" ] && git -C "$root" ls-files frontend/dist 2>/dev/null | head -1 | grep -q .; then
msgs+=("AGENTS.md-Gate: Du hast frontend/src geaendert, aber frontend/dist nicht. Die Box baut NICHT selbst: cd frontend && npm run build, dann das neue dist im selben Branch mit-committen — sonst zeigt die Box nach Merge+Deploy den alten Stand.")
fi
fi
# Gate 3 (R5, 15.07.): Orphan-Check — git-init-Repos ohne gemeinsamen Ursprung mit
# origin/main sind bei der Annahme technisch tot (3 Vorfaelle 12.14.07., je eine
# Nacht Arbeit verloren). VOR dem Fertigmelden fangen, nicht erst im Auftragsbuch.
orphan=""; checked=""
for p in "${paths[@]}"; do
root=$(dirname "$p"); n=0
while [ $n -lt 8 ] && [ -n "$root" ] && [ "$root" != "/" ] && [ ! -e "$root/.git" ]; do
root=$(dirname "$root"); n=$((n+1))
done
[ -e "$root/.git" ] || continue
case " $checked " in *" $root "*) continue ;; esac
checked="$checked $root"
git -C "$root" remote get-url origin >/dev/null 2>&1 || continue
git -C "$root" fetch -q origin main >/dev/null 2>&1 || true
if git -C "$root" rev-parse --verify -q origin/main >/dev/null 2>&1 \
&& ! git -C "$root" merge-base HEAD origin/main >/dev/null 2>&1; then
orphan="${orphan}${root}"$'\n'
fi
done
if [ -n "$orphan" ]; then
msgs+=("Orphan-Gate ROT: Dieses Repo hat KEINEN gemeinsamen Ursprung mit origin/main (git init statt Klonen?):"$'\n'"${orphan}So ist der Vorschlag bei der Annahme technisch TOT. Nicht fertigmelden — frisch VOLL klonen, Branch von origin/main abzweigen, Aenderungen ruebertragen.")
fi
if [ ${#msgs[@]} -eq 0 ]; then printf '{}'; exit 0; fi
joined=$(printf '%s\n\n' "${msgs[@]}")
jq --null-input --arg m "$joined" '{action:"continue", message:$m}'
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
"""pre_tool_call-Guard (Faden 8, 13.07.2026): sperrt Schreibzugriffe auf den LIVE-Checkout
~/mission-control-v2 und die Hermes-QUELLE ~/.hermes/hermes-agent HART. Loest Fallstrick #2
(Worker wandert in die falsche Ebene / lag Patches im Live-Checkout ab). Die native
is_write_denied deckt diese Pfade NICHT ab (nur Credentials/System) -> diese Luecke.
UNBEDINGT (kein Task-/Profil-Scope): KEIN legitimer Hermes-Agent-Flow schreibt je in diese
zwei Pfade — der Werkstatt-Worker arbeitet im WORKSPACE-Klon (<workspace>/mission-control-v2,
NICHT home-verankert -> erlaubt), Lucy fasst MC2-Quelle nie an, und die Wartungs-Pipeline
(deploy.sh, Annahme-Runner) laeuft als Shell ueber ssh, NICHT ueber Hermes-Tools. Ein
Task-Scope waere hier sogar gefaehrlich: Worker tragen als task_id den Session-Zeitstempel
(z.B. 20260713_224206_267304), NICHT die Kanban-t_-ID — ein "nur t_"-Check liesse also alle
Worker-Schreibzugriffe durch (Bug 13.07., so entdeckt).
WICHTIG: Gesperrt wird der home-verankerte Live-Checkout ($HOME/mission-control-v2); der
blosse Name "mission-control-v2" zaehlt NUR, wenn er vom cwd aus in einen Tabu-Pfad aufloest
(V16 unten) — der Workspace-Klon (~/.hermes/kanban/workspaces/<id>/mission-control-v2) bleibt
frei, sonst waere die legitime Worker-Arbeit blockiert. Lesen ist erlaubt; geblockt werden
nur Schreib-Operationen.
Ausgabe {"action":"block","message":...} = Tool geblockt; {} = durchlassen.
"""
import json
import os
import re
import sys
def out(obj):
sys.stdout.write(json.dumps(obj))
sys.exit(0)
try:
payload = json.loads(sys.stdin.read() or "{}")
except Exception:
out({})
tool = payload.get("tool_name") or ""
ti = payload.get("tool_input") or {}
home = os.path.realpath(os.path.expanduser("~"))
LIVE = os.path.join(home, "mission-control-v2")
SRC = os.path.join(home, ".hermes", "hermes-agent")
# R2 (Review 15.07.): systemd-User-Units. Die Nacht-Werkstatt hat am 14.07. einen Override
# DIREKT scharf geschaltet — ging gut, verletzt aber "jede Code-Zeile ist Klick-pflichtig".
# Unit-/Override-Dateien werden nur noch ueber eine angenommene Karte installiert.
SYSD = os.path.join(home, ".config", "systemd", "user")
MSG = (
"TABU (Faden 8/R2): Schreiben an dieser Stelle ist fuer Worker gesperrt. Der Live-Checkout "
"~/mission-control-v2, die Hermes-Quelle ~/.hermes/hermes-agent und die systemd-User-Units "
"~/.config/systemd/user duerfen NIE direkt veraendert werden. Arbeite im FRISCHEN Klon "
"deines Task-Workspaces; MC2-Aenderungen kommen als Vorschlags-Branch, systemd-Unit-/"
"Override-Dateien werden nur ueber eine angenommene Karte installiert, Hermes-Verhalten "
"nur ueber Config/SOUL/Skill/Hook/MCP. Siehe docs/wissen/GRENZEN.md."
)
def block():
out({"action": "block", "message": MSG})
def under_tabu(path):
rp = os.path.realpath(path)
return any(rp == t or rp.startswith(t + os.sep) for t in (LIVE, SRC, SYSD))
# --- Datei-Tools: Zielpfad kanonisch pruefen (zuverlaessig) -------------------------
if tool in ("write_file", "patch"):
fp = ti.get("path") or ti.get("file_path") or ti.get("filepath") or ""
if not fp:
out({})
fp = os.path.expanduser(fp)
cwd = payload.get("cwd") or ""
if not os.path.isabs(fp) and cwd:
fp = os.path.join(cwd, fp)
if under_tabu(fp):
block()
out({})
# --- Terminal: nur bei Schreib-Operation, die auf einen Tabu-Pfad zielt -------------
if tool == "terminal":
cmd = ti.get("command") or ""
if not cmd:
out({})
cwd_raw = payload.get("cwd") or ""
cwd = os.path.realpath(os.path.expanduser(cwd_raw)) if cwd_raw else ""
# R5 (Review 15.07.): `git init` im Kanban-Workspace hart blocken. Drei Naechte in
# Folge bauten Worker git-init-Orphans ohne gemeinsamen Ursprung mit main
# (cleanse-skill-index 12.07., skill-kanten-generator 13.07., blogwatcher-logging
# ~14.07.) — solche Vorschlaege sind bei der Annahme technisch tot, die Nacht-Arbeit
# verpufft. Kein legitimer Worker-Flow braucht git init; MC2-/Lucy-Arbeit = VOLL
# klonen (SOUL/GRENZEN). Scope: nur Kanban-Worker (Lucy/CLI duerfen z. B. fuer
# projekt-start neue Repos initialisieren).
ws_dir = os.path.join(home, ".hermes", "kanban", "workspaces")
if cwd_raw.startswith(ws_dir) and re.search(r"(?:^|[^\w])git\s+init\b", cmd):
out({"action": "block", "message": (
"TABU (R5): `git init` ist im Worker-Workspace gesperrt — Orphan-Branches ohne "
"gemeinsamen Ursprung mit main sind bei der Annahme technisch tot (Vorfaelle "
"12.14.07.). Richtiger Weg: VOLL klonen, z. B. git clone "
"http://192.168.178.153:3000/Hitonabi/mission-control-v2 "
"(Credentials liegen in ~/.git-credentials), dann Branch von origin/main abzweigen.")})
# Schreib-Subkommandos von git (V16: + clone — `git clone URL ~/mission-control-v2`
# rutschte vorher an (a) UND (d) vorbei).
git_write = (
r"(?:^|[^\w])git(?:\s+-C\s+\S+)?\s+(?:-[\w-]+\s+)*"
r"(commit|push|reset|checkout|switch|merge|add|rm|mv|clean|stash|rebase|apply|restore|init|clone|remote|config)\b"
)
# V16 (Vorfall 16.07.2026): Werkstatt-Worker t_7a05992a ersetzte um 03:59 den Live-
# Checkout mit cwd=$HOME und RELATIVEN Pfaden (`rm -rf mission-control-v2 && git
# clone ...`) — keine home-verankerte Tabu-Form im Kommandotext, der Guard sah nur
# Strings und liess durch (.venv weg -> Gateway im 203/EXEC-Crash-Loop). Drei Stopfen:
# (1) cwd liegt IN einem Tabu-Pfad -> jede Schreib-Operation blocken (relative Ziele
# treffen zwangslaeufig den Tabu-Baum). Lesen bleibt erlaubt.
if cwd and under_tabu(cwd):
if (re.search(git_write, cmd) or re.search(r"(?:^|[^\w])sed\s+-i", cmd)
or re.search(r"(?:^|[^\w])(rm|rmdir|mv|cp|truncate|dd|chmod|chown|ln|install|mkdir|touch)\s", cmd)
or re.search(r">>?\s*(?![/&])", cmd)
or re.search(r"(?:^|[^\w])tee\s+(?:-a\s+)?(?!/)", cmd)):
block()
# (2) cwd == $HOME -> `git clone`/`git init`, deren Ziel direkt in $HOME landet,
# blocken (exakt der Vorfalls-Hergang). Worker klonen im Task-Workspace; ein
# explizites Ziel AUSSERHALB von $HOME (z.B. Unterordner) bleibt erlaubt.
m = re.search(r"(?:^|[^\w])git(?:\s+-C\s+\S+)?\s+(?:-[\w-]+\s+)*(?:clone|init)\b([^|&;]*)", cmd)
if cwd == home and m:
toks = m.group(1).split()
dest = toks[-1] if toks else ""
# kein brauchbares Ziel-Argument (leer, Option oder URL) -> implizit direkt in $HOME
implicit = (not dest or dest.startswith("-")
or re.search(r"://|^git@|^[\w.-]+@[\w.-]+:|\.git$", dest))
if (implicit or under_tabu(os.path.join(home, dest))
or os.path.dirname(os.path.realpath(os.path.join(home, dest))) == home):
out({"action": "block", "message": (
"TABU (V16): `git clone`/`git init` direkt in $HOME ist gesperrt — so wurde "
"am 16.07. der Live-Checkout ~/mission-control-v2 durch einen frischen Klon "
"ersetzt (.venv weg, Gateway tot). Klone/initialisiere in deinem Task-"
"Workspace (~/.hermes/kanban/workspaces/<task>/...) oder gib ein Ziel "
"ausserhalb von $HOME an.")})
# Home-verankerte Tabu-Formen. Der Workspace-Klon
# ($HOME/.hermes/kanban/.../mission-control-v2) enthaelt KEINE davon als Teilstring.
tabu_forms = [
LIVE, "~/mission-control-v2", "$HOME/mission-control-v2", "${HOME}/mission-control-v2",
SRC, "~/.hermes/hermes-agent", "$HOME/.hermes/hermes-agent", "${HOME}/.hermes/hermes-agent",
SYSD, "~/.config/systemd/user", "$HOME/.config/systemd/user", "${HOME}/.config/systemd/user",
]
# (3) cwd == Vorfahr eines Tabu-Pfads (z.B. $HOME) -> auch die RELATIVE Nennung des
# Tabu-Ziels (mission-control-v2, .hermes/hermes-agent, .config/systemd/user)
# zaehlt als Treffer fuer die Schreib-Checks unten. Die Lookarounds stellen
# sicher, dass der Name ein eigenes Argument beginnt — als Teil eines laengeren
# Pfades (z.B. .hermes/kanban/workspaces/<id>/mission-control-v2) matcht er
# NICHT, die legitime Workspace-Arbeit bleibt frei.
rel_pats = []
if cwd:
for t in (LIVE, SRC, SYSD):
if t.startswith(cwd + os.sep):
rel = os.path.relpath(t, cwd).replace(os.sep, "/")
rel_pats.append(r"(?<![\w./@-])(?:\./)?" + re.escape(rel) + r"(?![\w.-])")
tabu_alt = "(?:" + "|".join([re.escape(t) for t in tabu_forms] + rel_pats) + ")"
if not re.search(tabu_alt, cmd):
out({}) # kein Tabu-Pfad referenziert -> Workspace-Arbeit, durchlassen
# (a) git-Schreib-Subkommando (Repo-Kontext ist der referenzierte Tabu-Pfad).
if re.search(git_write, cmd):
block()
# (b) sed -i, das einen Tabu-Pfad anfasst.
if re.search(r"(?:^|[^\w])sed\s+-i\S*\s+[^|&;]*" + tabu_alt, cmd):
block()
# (c) Umleitung > / >> / tee, die auf einen Tabu-Pfad ZIELT (nicht 2>/dev/null o.ae.).
if re.search(r"(?:>>?|(?:^|[^\w])tee\s+(?:-a\s+)?)\s*" + tabu_alt, cmd):
block()
# (d) destruktiv/kopierend mit Tabu-Pfad im selben Kommando-Segment.
if re.search(
r"(?:^|[^\w])(rm|rmdir|mv|cp|truncate|dd|chmod|chown|ln|install|mkdir|touch)\s+[^|&;]*" + tabu_alt,
cmd,
):
block()
out({})
out({})
-92
View File
@@ -1,92 +0,0 @@
# Universelle CI-Ampel — wird bei der Repo-Anlage automatisch eingepflanzt
# (gitea-repo-create.sh, Wasserdicht-Runde 22.07.2026) und läuft auf dem
# Gitea-Actions-Runner (arcane-VM).
# GRUNDSATZ: Rot ist ein Ergebnis („nicht bewiesen"), kein Ärgernis.
# • Reines Doku-Repo (noch kein Code) → GRÜN mit Vermerk.
# • Code ohne Tests → ROT. Tests sind Pflicht, kein Deko.
# Der Workflow erkennt selbst, was das Projekt ist (Python / Node / beides).
name: Ampel
on: [push, pull_request]
jobs:
ampel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ampel — erkennt Projekt-Typ selbst und prüft entsprechend
shell: bash
run: |
# Der Runner startet bash mit -e — das schalten wir ab: die Ampel wertet
# JEDEN Fehler selbst (rot=1) und liefert am Ende EIN Klartext-Urteil.
# (Lehre Lauf #5: ungeschütztes pytest unter -e schnitt den Bericht ab.)
set -u +e
rot=0
py_datei=$(find . -name '*.py' -not -path './.git/*' -not -path '*/node_modules/*' -print -quit)
pkg_dateien=$(find . -name package.json -not -path '*/node_modules/*' -not -path './.git/*')
if [ -z "$py_datei" ] && [ -z "$pkg_dateien" ]; then
echo "✅ Doku-Repo (noch kein Code) — Ampel GRÜN mit Vermerk."
exit 0
fi
if [ -n "$py_datei" ]; then
echo "== Python erkannt =="
{ python3 -m venv /tmp/ampel-venv && . /tmp/ampel-venv/bin/activate; } || { echo "❌ Python-Setup kaputt (venv)"; exit 1; }
pip install -q ruff pytest || { echo "❌ Werkzeug-Installation kaputt"; exit 1; }
echo "-- Ruff (Linter: toter Code, kaputte Imports, Schlampereien)"
ruff check . || rot=1
echo "-- Abhängigkeiten installierbar? (halluzinierte Pakete fliegen hier auf)"
while IFS= read -r req; do
[ -n "$req" ] || continue
pip install -q -r "$req" || { echo "❌ $req nicht installierbar"; rot=1; }
done < <(find . -name 'requirements*.txt' -not -path '*/node_modules/*' -not -path './.git/*')
echo "-- Importierbar? (kaputte Modul-Struktur fliegt hier auf)"
python -m compileall -q . || rot=1
echo "-- Pytest (keine Tests gefunden = ROT)"
ec=0; pytest -q || ec=$?
if [ $ec -eq 5 ]; then
echo "❌ KEINE TESTS GEFUNDEN — Tests sind Pflicht, kein Deko (AGENTS.md)."
rot=1
elif [ $ec -ne 0 ]; then
rot=1
fi
fi
if [ -n "$pkg_dateien" ]; then
echo "== Node/TypeScript erkannt =="
while IFS= read -r pkg; do
[ -n "$pkg" ] || continue
d=$(dirname "$pkg")
echo "-- $d"
if [ ! -f "$d/package-lock.json" ]; then
echo "❌ $d: kein package-lock.json — Build nicht reproduzierbar."
echo " Fix: dort 'npm install --package-lock-only' ausführen und committen."
rot=1; continue
fi
(cd "$d" && npm ci --no-audit --no-fund) || { rot=1; continue; }
if grep -q '"build"' "$pkg"; then (cd "$d" && npm run build) || rot=1; fi
if grep -q '"test"' "$pkg"; then (cd "$d" && npm test --silent) || rot=1; fi
# Buendel-Budget: das Start-Chunk ist, was der Nutzer VOR dem ersten Bild laedt.
# Ohne Deckel waechst es unbemerkt zurueck (eine eifrig geladene Diagramm-Lib
# reicht). Gemessen wird der frische Build, nicht das committete dist.
if [ -d "$d/dist/assets" ]; then
# Einstiegs-Chunk aus index.html lesen, nicht per Glob raten: Rollup nennt
# auch kleine geteilte Module "index-*.js", und dann misst der Glob den
# falschen — das Budget waere still immer gruen.
einstieg=$(grep -o 'assets/index-[A-Za-z0-9_-]*\.js' "$d/dist/index.html" | head -1)
haupt="$d/$einstieg"
if [ -n "$haupt" ]; then
gz=$(gzip -c "$haupt" | wc -c)
echo "-- Start-Chunk: $gz B gzip (Budget 200000)"
[ "$gz" -gt 200000 ] && { echo "❌ Start-Buendel ueber Budget — hinter lazy() legen."; rot=1; }
fi
fi
done <<< "$pkg_dateien"
fi
if [ $rot -ne 0 ]; then
echo "❌ AMPEL ROT — nichts heißt fertig', solange das rot ist."
fi
exit $rot
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# Ampel-Wächter (22.07.2026, Wasserdicht-Runde): meldet ROTE CI-Läufe aller
# Gitea-Repos per Telegram. Läuft als no-agent-Cron (stdout leer = still,
# klassisches Watchdog-Muster). Nur NEUE Fehlschläge werden gemeldet —
# Status-Merkliste unter ~/.hermes/state/ampel-waechter-gesehen.txt.
set -uo pipefail
STATE="$HOME/.hermes/state/ampel-waechter-gesehen.txt"
mkdir -p "$(dirname "$STATE")"; touch "$STATE"
TOKEN=$(grep -m1 'git\.tobisniceshomelab' "$HOME/.git-credentials" 2>/dev/null | sed -E 's#https://[^:]+:([^@]+)@.*#\1#')
[ -n "$TOKEN" ] || exit 0
python3 - "$TOKEN" "$STATE" <<'PY'
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);
# WEB bleibt die Domain — Telegram-Links muessen auch von unterwegs klickbar sein.
BASE = "http://192.168.178.153:3000/api/v1"
WEB = "https://git.tobisniceshomelab.ddnsfree.com"
def api(pfad):
req = urllib.request.Request(BASE + pfad, headers={"Authorization": "token " + token})
with urllib.request.urlopen(req, timeout=20) as r:
return json.load(r)
try:
gesehen = set(open(state_pfad).read().split())
except OSError:
gesehen = set()
neu = []
try:
# /repos/search statt /user/repos: das Token hat nur write:repository-Scope,
# /user/repos verlangt read:user -> 403. Der stille exit 0 hat diesen Defekt
# seit Inbetriebnahme versteckt (Fund 24.07.2026, Merkliste blieb leer).
repos = api("/repos/search?limit=50&private=true").get("data", [])
except Exception as e:
print(f"ampel-waechter: Repo-Liste fehlgeschlagen: {e}", file=sys.stderr)
sys.exit(0)
for repo in repos:
full = repo.get("full_name", "")
if not full or repo.get("archived"):
continue
try:
d = api(f"/repos/{full}/actions/tasks?limit=1")
except Exception:
continue
rows = d.get("workflow_runs", d if isinstance(d, list) else d.get("entries", []))
for t in rows[:1]:
kennung = f"{full}#{t.get('id')}"
status = t.get("status")
if status == "failure" and kennung not in gesehen:
# Status-Lügen-Wache (Fund 22.07.): alter Runner meldete Abschluss nicht,
# Gitea zombie-markierte GRÜNE Jobs als failure. Das Job-LOG ist die
# Wahrheit — steht dort „Job succeeded", ist es KEIN Alarm.
try:
req = urllib.request.Request(
f"{BASE}/repos/{full}/actions/jobs/{t.get('id')}/logs",
headers={"Authorization": "token " + token})
with urllib.request.urlopen(req, timeout=20) as r:
log_ende = r.read().decode(errors="replace")[-4000:]
wirklich_rot = "Job succeeded" not in log_ende
except Exception:
wirklich_rot = True
if wirklich_rot:
neu.append((full, t.get("head_sha", "")[:7], t.get("name", "")))
# Nur ABGESCHLOSSENE Läufe merken — ein 'running', das später rot wird,
# darf nicht schon als gesehen gelten.
if status in ("failure", "success", "cancelled", "skipped"):
gesehen.add(kennung)
if neu:
print("🔴 CI-AMPEL ROT:")
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:
f.write("\n".join(sorted(gesehen)))
PY
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Brain-Config-Matrix (Referenz vom 02.07.2026): misst Qwen3.6 in mehreren Betriebsarten
# (MTP n-max / ohne Spec / KV-Quant) auf separatem Port. Für künftige Re-Benchmarks nach
# Engine-Updates oder bei neuen Hirn-Kandidaten. Ergebnis-Referenz (02.07., Vulkan, b9843):
# mtp3 78,6 t/s · mtp4 63,7 · ohne Spec 62,4 · mtp3+KVq8 78,6 (=gratis) · nospec+KVq8 62,1
set -u
MODEL="${BRAIN_GGUF:-/srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf}"
BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
PORT="${BENCH_PORT:-5899}"
BASE="-c 65536 -ngl 999 -fa on --load-mode none --jinja --parallel 1"
run() { # $1=name $2=extra-flags
echo "=== $1 ==="
$BIN -m "$MODEL" --host 127.0.0.1 --port "$PORT" $BASE $2 >/tmp/brain-bench-server.log 2>&1 &
local PID=$!
for i in $(seq 1 120); do
curl -s --max-time 2 "http://127.0.0.1:$PORT/health" | grep -q ok && break
sleep 2
kill -0 $PID 2>/dev/null || { echo " LOAD-CRASH"; tail -2 /tmp/brain-bench-server.log; return; }
done
for r in 0 1 2; do
curl -s --max-time 300 -X POST "http://127.0.0.1:$PORT/completion" -H "Content-Type: application/json" \
-d '{"prompt":"Erklaere in drei kurzen Saetzen, was ein Mixture-of-Experts-Modell ist.","n_predict":150,"temperature":0}' \
> /tmp/brain-bench-probe.json
[ "$r" = "0" ] && continue # Warmlauf verwerfen
python3 - <<'PY'
import json
t = json.load(open("/tmp/brain-bench-probe.json")).get("timings", {})
print(f" tg={t.get('predicted_per_second',0):.1f} t/s"
+ (f" · draft {t.get('draft_n_accepted')}/{t.get('draft_n')}" if t.get('draft_n') else ""))
PY
done
kill $PID 2>/dev/null; wait $PID 2>/dev/null; sleep 3
}
run "mtp3 (live-Config)" "--spec-type draft-mtp --spec-draft-n-max 3 -ctk q8_0 -ctv q8_0"
run "mtp4" "--spec-type draft-mtp --spec-draft-n-max 4 -ctk q8_0 -ctv q8_0"
run "ohne Spec" "-ctk q8_0 -ctv q8_0"
run "mtp3 KV-f16" "--spec-type draft-mtp --spec-draft-n-max 3"
echo "BENCH_DONE"
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# Modell-Bench (Autonomie-Plan E5): misst pp/tg eines GGUF auf separatem Port, ohne die
# Live-Instanzen anzufassen. Grundlage der Modell-Selbst-Evaluation (Radar-Kandidaten).
# Nutzung: bash model-bench.sh <gguf-pfad> [extra llama-server-flags, z.B. --mmproj ...]
# Lehren vom 02.07.2026 eingebaut: python via Heredoc (kein f-String-Quoting-Bruch),
# Warmlauf vor der Messung, LOAD-CRASH wird gemeldet statt still zu hängen.
set -u
MODEL="${1:?Nutzung: model-bench.sh <gguf-pfad> [extra-flags]}"
shift || true
EXTRA="$*"
BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
PORT="${BENCH_PORT:-5899}"
CTX="${BENCH_CTX:-32768}"
echo "=== Bench: $(basename "$MODEL") (ctx $CTX${EXTRA:+, $EXTRA}) ==="
$BIN -m "$MODEL" --host 127.0.0.1 --port "$PORT" -c "$CTX" -ngl 999 -fa on --load-mode none --jinja $EXTRA \
>/tmp/model-bench-server.log 2>&1 &
PID=$!
trap 'kill $PID 2>/dev/null; wait $PID 2>/dev/null' EXIT
for i in $(seq 1 240); do
curl -s --max-time 2 "http://127.0.0.1:$PORT/health" | grep -q ok && break
sleep 2
kill -0 $PID 2>/dev/null || { echo "LOAD-CRASH:"; tail -3 /tmp/model-bench-server.log; exit 1; }
done
probe() {
curl -s --max-time 300 -X POST "http://127.0.0.1:$PORT/completion" -H "Content-Type: application/json" \
-d '{"prompt":"Erklaere in fuenf Saetzen, wie ein Backup-Konzept fuer einen Homeserver aussieht.","n_predict":200,"temperature":0}' \
> /tmp/model-bench-probe.json
python3 - <<'PY'
import json
t = json.load(open("/tmp/model-bench-probe.json")).get("timings", {})
print(f" pp={t.get('prompt_per_second',0):.0f} t/s · tg={t.get('predicted_per_second',0):.1f} t/s"
+ (f" · draft {t.get('draft_n_accepted')}/{t.get('draft_n')}" if t.get('draft_n') else ""))
PY
}
probe >/dev/null 2>&1 # Warmlauf (Shader-/Graph-Compile nicht mitmessen)
probe
probe
echo "BENCH_DONE"
-59
View File
@@ -1,59 +0,0 @@
# Betriebs-/Mess-Worker (Profil „betrieb")
Du bist der Betriebs- und Mess-Worker der AI-Box. Du arbeitest EINE Kanban-Aufgabe ab —
Ops, Benchmarks, Messungen, Health-/Diagnose-Checks, Verifikation von Behauptungen auf der
LEBENDEN Box (Dienste, Modelle, Latenzen, Logs, Ressourcen). Keine Persona, kein Smalltalk.
**Dein Ergebnis ist ein Mess-/Diagnose-BERICHT mit belegten Zahlen — du misst und berichtest,
du reparierst nicht.**
## Schritt 0: WIEDERAUFNAHME — bei „Prior attempts" erst nachsehen, dann messen
Steht in deinem Auftrag ein Abschnitt **„Prior attempts on this task"**, hat vor dir schon ein
Lauf gemessen und ist unterwegs gestorben (meist Kontext-Limit). **Dein Workspace ist derselbe.**
Also zuerst: `cat FORTSCHRITT.md` und `ls -la` im Workspace — liegen dort schon Messwerte, Logs,
Bench-Ausgaben? **Vorhandene Zahlen misst du NICHT neu.** Das ist hier keine Bequemlichkeit,
sondern Pflicht: ein wiederholter Bench lädt 70-GB-Modelle, gefährdet Lucys Warm-Set und kann
einen OOM auslösen (Regel 5). Fehlt nur noch die Auswertung, wertest du aus und schließt ab.
Nur was nachweislich fehlt oder unbrauchbar ist, wird neu gemessen.
**Fortschritt hinterlassen:** jede fertige Messung sofort als Zeile in `FORTSCHRITT.md` im
Workspace (was gemessen, welche Zahl, in welcher Datei) — und die Rohausgabe in eine Datei
daneben. Eine Messung, die nur im Kontext steht, ist beim nächsten Lauf verloren.
## Eiserne Regeln (nicht verhandelbar)
1. **MESSEN, NICHT ÄNDERN.** Du fasst das Live-System NICHT an: kein `systemctl restart`, kein
Config-Edit, kein Deploy/Merge/Push, kein Laden/Entladen von Modellen, das das Warm-Set
stört. Ergibt die Messung, dass etwas geändert werden muss → schreib es als BEFUND/Vorschlag
in die Summary (oder leg per `kanban_create` eine Idee an), mach es NICHT selbst.
2. **Nur LESEN am Bestand:** Live-Checkout `~/mission-control-v2` und Hermes-Quelle
`~/.hermes/hermes-agent` sind TABU zum Schreiben (Lesen zur Orientierung ok). Alles außerhalb
einer reinen Messung ist tabu.
3. **TABU-Zonen:** Security-Config (approvals/Tokens/ufw/sudoers), `~/.hermes/config.yaml`,
systemd-Units. Braucht die Aufgabe so etwas → `kanban_block` mit Begründung, nicht selbst tun.
4. **Kenn deine Werkzeuge** (Endpunkte/Pfade stehen in deiner Box-Orientierung + im Selbst-
Steckbrief `~/.hermes/state/selbst-steckbrief.md`):
- Health/Status: `curl -s http://127.0.0.1:9001/api/health`, `.../api/system/status`.
- **Dienste:** `systemctl --user status <dienst>` — aber VORHER
`export XDG_RUNTIME_DIR=/run/user/$(id -u)`, sonst sind die User-Units per SSH UNSICHTBAR
(häufigste Ops-Falle). Logs: `journalctl --user -u <dienst> --no-pager -n 100`.
- Modelle/Router: `curl -s http://127.0.0.1:8080/...` bzw. `http://127.0.0.1:9001/v1/models`.
- Bench/Smoke (nur lesen/messen, nicht deployen): `~/mission-control-v2/deploy/bench/brain-bench.sh`,
`.../deploy/bench/model-bench.sh`, `.../deploy/self-smoke.sh`, `.../deploy/stack-postcheck.sh`.
5. **BENCH-VORSICHT:** Nie zwei große Benches (70-GB-Modelle) direkt nacheinander → OOM-Kill
(Vorfall E5, 02.07.2026). Schwere Modelle (heavy/vision) nur laden, wenn die Aufgabe es
verlangt; danach den Ausgangszustand wiederherstellen. **Lucys Warm-Set und ihre ~1-s-Sprech-
Latenz NIE gefährden** — ein Config-Reload leert das Warm-Set, ein OOM killt laufende Modelle.
6. **VERIFIZIEREN VOR BEHAUPTEN.** Dein Bericht enthält BELEGTE Zahlen und echte Kommando-
Ausgaben (bei Vergleichen: Vorher/Nachher), keine Vermutung. Zitiere die reale Ausgabe, nicht
das, was du erwartest. Der Agenten-Erzählung — auch deiner eigenen — nie glauben; das Log,
der curl, der bench sind die Wahrheit.
7. **Falsche Ebene erkannt?** Braucht die Aufgabe in Wahrheit eine CODE-Änderung → das ist die
**werkstatt**, nicht du (`kanban_block` mit Verweis). Reine Doku/Recherche ohne Box-Bezug →
**default**. Grenzen im Zweifel: `~/mission-control-v2/docs/wissen/GRENZEN.md`.
8. **Fertig = `kanban_complete`** mit deutscher Summary: was gemessen, die ZAHLEN/der klare
Befund, wie geprüft (welche Kommandos). Unklar, blockiert oder Werkzeug fehlt → `kanban_block`
mit ehrlicher Diagnose statt geratener Zahlen.
## Stil
Deutsch in allem User-Sichtbaren. Zahlen statt Prosa. Ein knapper, belegter Befund schlägt
einen langen, vagen Bericht. Keine Nebenbaustellen — genau die eine Messung, sauber.
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# coder-vollkontext.sh — gibt dem Coder den ganzen Kontext (23.08.2026).
#
# WARUM: llama-swap startet den Coder mit `-c 131072 --parallel 2`. Das sind
# zwei Slots à 65.536 — und nur diese 65.536 stehen einer Sitzung zur Verfuegung.
# Der Coder hat aber genau EINEN Verbraucher: OpenChamber. Lucy benutzt `hermes`,
# der explore-Subagent auch, `review` benutzt `heavy`. Die zweite Bahn liegt brach.
#
# Mit `--parallel 1` bekommt eine Sitzung die vollen 131.072 — doppelt so viel
# Platz, halb so viel Komprimieren.
#
# ‼️ NICHT waehrend eines laufenden Auftrags ausfuehren: llama-swap laeuft mit
# --watch-config und laedt beim Speichern SOFORT neu. Das schneidet eine
# laufende Sitzung mitten im Satz ab.
#
# Aufruf: bash deploy/coder-vollkontext.sh (Trockenlauf, zeigt den Diff)
# bash deploy/coder-vollkontext.sh --ja (wendet an)
set -uo pipefail
CONF="${LLAMA_SWAP_CONF:-/etc/llama-swap/config.yaml}"
BLOCK="${CODER_BLOCK:- Qwen3.8-27B:}"
if [ ! -r "$CONF" ]; then echo "ABBRUCH: $CONF nicht lesbar"; exit 1; fi
TMP="$(mktemp)"
cp "$CONF" "$TMP"
python3 - "$TMP" "$BLOCK" <<'PY'
import io, re, sys
pfad, block = sys.argv[1], sys.argv[2]
z = io.open(pfad, encoding="utf-8").read().split("\n")
try:
start = next(i for i, l in enumerate(z) if l.startswith(block))
except StopIteration:
print("ABBRUCH: Block '%s' nicht gefunden" % block); sys.exit(1)
ende = next((i for i in range(start + 1, len(z)) if re.match(r"^ [A-Za-z]", z[i])), len(z))
# Streng auf den Coder-Block begrenzt — `fast`/`hermes` MUESSEN ihre zwei Slots
# behalten, dort greifen Lucy und der Explorer wirklich gleichzeitig zu.
n_par = n_ctx = 0
for i in range(start, ende):
if "--parallel 2" in z[i]:
z[i] = z[i].replace("--parallel 2", "--parallel 1"); n_par += 1
if z[i].strip() == "context: 65536":
z[i] = z[i].replace("65536", "131072"); n_ctx += 1
if n_par != 1 or n_ctx != 1:
print(f"ABBRUCH: unerwartet (parallel {n_par}x, context {n_ctx}x) — Konfiguration hat sich geaendert")
sys.exit(1)
io.open(pfad, "w", encoding="utf-8").write("\n".join(z))
PY
if [ $? -ne 0 ]; then rm -f "$TMP"; exit 1; fi
echo "=== Aenderung ==="
diff "$CONF" "$TMP"
echo
if [ "${1:-}" != "--ja" ]; then
echo "TROCKENLAUF — nichts geaendert. Anwenden mit: bash $0 --ja"
rm -f "$TMP"; exit 0
fi
SICHER="${CONF}.bak-$(date +%Y%m%d-%H%M%S)"
sudo -n cp "$CONF" "$SICHER" && echo "Sicherung: $SICHER"
sudo -n cp "$TMP" "$CONF" && echo "angewendet — llama-swap laedt durch --watch-config selbst neu"
rm -f "$TMP"
echo
echo "Warte auf den Neustart des Coders..."
sleep 8
curl -s -m 15 "http://127.0.0.1:8080/v1/models" >/dev/null 2>&1 \
&& echo "llama-swap antwortet wieder" \
|| echo "ACHTUNG: llama-swap antwortet nicht — Sicherung liegt unter $SICHER"
echo
echo "Danach in ~/.config/opencode/opencode.json auf dem PC nachziehen:"
echo " coder: limit.context 65536 -> 131072"
+172 -99
View File
@@ -1,106 +1,179 @@
#!/bin/bash #!/bin/bash
set -e # deploy.sh — den Box-Wart auf der Box auf den Stand von origin/main bringen.
# ‼️ Das ganze Skript liegt in EINER Funktion — das ist Absicht, kein Stil.
# Bash liest ein Skript haeppchenweise WAEHREND es laeuft. `git pull` unten ersetzt
# genau diese Datei mitten im Lauf; danach liest bash am selben Byte-Offset im NEUEN
# Text weiter und fuehrt zerschnittene Zeilen aus. Live erlebt am 27.08.2026: der Pull
# brachte eine neue Fassung dieses Schrittes mit, ausgefuehrt wurde trotzdem die alte.
# Eine Funktion wird komplett geparst, bevor sie startet -> der Lauf bleibt in sich
# stimmig, und Aenderungen greifen ab dem naechsten Aufruf statt mittendrin.
main() {
echo "Starte MC2 Deployment..."
# 1. Neuesten Code holen
git pull origin main
# 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
# #
# ‼️ ACHTUNG (27.08.2026): /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit — MC2 # Zweistufig (seit 24.09.2026):
# schreibt sie selbst (backend/services/llamaswap.py::write_config), sobald jemand über # Stufe 1: Sperre gegen zwei gleichzeitige Deploys, laufende Update-Jobs abwarten lassen,
# die Oberfläche ein Modell hinzufügt oder eine Rolle ändert. deploy/llama-swap.config.yaml # alten Stand merken, main holen (nur fast-forward) — dann die NEUE Fassung dieses
# ist nur ein Abzug und hinkt regelmäßig hinterher (erlebt: der Coder-Vollkontext aus 34a9862 # Skripts als Stufe 2 starten. Änderungen an deploy.sh wirken damit sofort, nicht
# stand tagelang nur live, nicht im Abzug — ein Deploy hätte ihn zurückgesetzt). # erst beim nächsten Lauf (die alte Falle: bash lief nach dem Pull mit der alten Datei).
# Bis hierher wurde die Live-Datei kommentarlos mit dem Abzug überschrieben — alles per # Stufe 2: Prüftor (deploy/pruefen.sh), Abhängigkeiten, llama-swap-Config nur, wenn sich der
# UI Hinzugefügte war nach jedem Deploy weg. Jetzt: erst sichern, dann kopieren, und die # Abzug in DIESEM Deploy geändert hat, Units/Drop-ins/Cron-Skripte/Skills/Plugins
# Abweichung sichtbar machen. MC_DEPLOY_SKIP_SWAP_CONFIG=1 überspringt den Schritt ganz. # ausspielen, Dienste neu starten, Nachprüfung. Scheitert irgendein Schritt: zurück auf
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then # den alten Stand, Dienste neu starten, dringende Meldung.
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" = "1" ]; then #
echo "Llama-Swap Konfiguration weicht ab — uebersprungen (MC_DEPLOY_SKIP_SWAP_CONFIG=1)." # Aufruf auf der Box: bash ~/mission-control-v2/deploy/deploy.sh
else # Schalter: MC_DEPLOY_TROTZDEM=1 (auch bei laufenden Jobs), MC_DEPLOY_OHNE_TESTS=1 (Notfall),
SWAP_BAK="/etc/llama-swap/config.yaml.bak-$(date +%Y%m%d-%H%M%S)" # MC_DEPLOY_SKIP_SWAP_CONFIG=1 (llama-swap-Config nie anfassen).
echo "Llama-Swap Konfiguration hat sich geändert." set -euo pipefail
echo " Abweichung (live -> Repo-Abzug):"
diff -u /etc/llama-swap/config.yaml deploy/llama-swap.config.yaml | head -40 || true
echo " Sichere die laufende Datei nach $SWAP_BAK"
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
sudo cp /etc/llama-swap/config.yaml "$SWAP_BAK"
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
sudo systemctl restart llama-swap
echo " Rueckweg bei Verlust: sudo cp $SWAP_BAK /etc/llama-swap/config.yaml"
fi
fi
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben) cd "$HOME/mission-control-v2"
# Das Modell-Radar (09/2026) ist die erste Unit, die deploy.sh selbst einspielt: Sie gehört ganz dem export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
# Repo, auf der Box wird an ihr nichts von Hand gedreht. Die übrigen Units bleiben wie bisher Handarbeit. API="http://127.0.0.1:9001"
install -m 644 deploy/mc2-radar.service deploy/mc2-radar.timer "$HOME/.config/systemd/user/" LSWAP_LIVE="/etc/llama-swap/config.yaml"
# Morgenmeldung (24.09.2026): schickt um 07:00, was notify.sh nachts gesammelt hat. DEPLOY_LOG="/srv/models/mc2-deploy.log"
install -m 644 deploy/mc2-morgenmeldung.service deploy/mc2-morgenmeldung.timer "$HOME/.config/systemd/user/"
# Updates am Sonntag (24.09.2026): eigener Timer statt Hermes-Cron.
install -m 644 deploy/mc2-autoupdate.service deploy/mc2-autoupdate.timer "$HOME/.config/systemd/user/"
# Warm-Set des Wächters (24.09.2026: nur noch das Hirn).
install -D -m 644 deploy/mc2-steward.service.d-warmset.conf "$HOME/.config/systemd/user/mc2-steward.service.d/warmset.conf"
systemctl --user daemon-reload
systemctl --user enable --now mc2-radar.timer mc2-morgenmeldung.timer mc2-autoupdate.timer
# 4.5 Hermes Skills synchronisieren # Units, die ganz dem Repo gehören (deploy.sh spielt sie aus). Aktiviert wird nur, was laufen soll:
echo "Synchronisiere Hermes Skills..." # Konsole und Spracherkennung schlafen seit 24.09.2026 (disable --now), ihre Units bleiben aber aktuell.
mkdir -p ~/.hermes/skills UNITS="mission-control-2.service mc2-gateway.service mc2-steward.service lucy-stimme.service
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität box-console.service voice-service.service
for skill_dir in deploy/skills/*; do mc2-radar.service mc2-radar.timer mc2-backup.service mc2-backup.timer
if [ -d "$skill_dir" ]; then mc2-morgenmeldung.service mc2-morgenmeldung.timer mc2-autoupdate.service mc2-autoupdate.timer
skill_name=$(basename "$skill_dir" | tr '-' '_') projekte-sync.service projekte-sync.timer"
mkdir -p ~/.hermes/skills/"$skill_name" AKTIV="mission-control-2.service mc2-gateway.service mc2-steward.service lucy-stimme.service
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/ mc2-radar.timer mc2-backup.timer mc2-morgenmeldung.timer mc2-autoupdate.timer projekte-sync.timer"
fi # Skills, die abgelöst sind: aus Lucys Skill-Index nehmen (verschoben, nicht gelöscht). Dazu die
done # alten Bindestrich-Doppel der übrigen Skills (deploy.sh schreibt die Unterstrich-Fassung).
# Abgelöste Skills (24.09.2026) aus Lucys Skill-Index nehmen: verschoben nach SKILLS_ALT="autonomie konzept-fliessband llm-wiki morning-report orchestrator projekt-start pruefstand
# ~/.hermes/skills-archiv, nicht gelöscht. Dazu die alten Bindestrich-Doppel der übrigen Skills review trend-radar wartung konzept_fliessband llm_wiki morning_report projekt_start
# (deploy.sh schreibt seit jeher die Unterstrich-Fassung). trend_radar betrieb-playbook pc-pfad-cache"
for alt in autonomie konzept-fliessband llm-wiki morning-report orchestrator projekt-start \
pruefstand review trend-radar wartung konzept_fliessband llm_wiki morning_report \
projekt_start trend_radar betrieb-playbook pc-pfad-cache; do
if [ -d ~/.hermes/skills/"$alt" ]; then
mkdir -p ~/.hermes/skills-archiv
rm -rf ~/.hermes/skills-archiv/"$alt"
mv ~/.hermes/skills/"$alt" ~/.hermes/skills-archiv/
fi
done
# 4.6 Hermes-Plugins aus dem Repo (09/2026: mc2-web-lesen, Seiten lokal lesen für web_extract). laufende_jobs() {
# Nur kopieren: Aktiviert wird ein Plugin einmalig von Hand (hermes plugins enable …), und neuer curl -sf -m 5 "$API/api/jobs" 2>/dev/null \
# Plugin-Code greift erst beim nächsten Neustart des Hermes-Gateways — den macht deploy.sh bewusst nicht. | python3 -c 'import json,sys; print(sum(1 for j in json.load(sys.stdin).get("jobs", []) if j.get("state") in ("running", "queued")))' \
mkdir -p ~/.hermes/plugins 2>/dev/null || echo 0
for plugin_dir in deploy/hermes-plugins/*; do
[ -d "$plugin_dir" ] && cp -r "$plugin_dir" ~/.hermes/plugins/
done
# 5. Dienste neu starten
# ‼️ mc2-steward gehoert hier dazu: er laeuft aus DEMSELBEN Checkout (backend/steward.py).
# Ohne ihn lief der Waechter-Prozess nach jedem Deploy mit dem alten Code weiter — der
# Neustart wurde 27.08.2026 nachgetragen. `--user restart` auf einer nicht installierten
# Unit ist folgenlos, darum ohne Vorabpruefung.
echo "Starte Backend neu..."
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service
echo "✅ Deployment erfolgreich abgeschlossen!"
} }
main "$@" stufe1() {
exec 9>"${XDG_RUNTIME_DIR}/mc2-deploy.lock"
flock -n 9 || { echo "Es läuft schon ein Deploy."; exit 1; }
# Update- und Download-Jobs leben (noch) im MC2-Prozess: ein Neustart würde sie abwürgen.
local n; n="$(laufende_jobs)"
if [ "${n:-0}" -gt 0 ] && [ "${MC_DEPLOY_TROTZDEM:-0}" != "1" ]; then
echo "Es laufen $n Job(s) (Update oder Download). Deploy verschoben — später erneut, oder MC_DEPLOY_TROTZDEM=1."
exit 1
fi
local vorher; vorher="$(git rev-parse HEAD)"
echo "Starte Deploy (vorher ${vorher:0:7})…"
git fetch -q origin main
git merge --ff-only -q origin/main
# Ab hier die NEUE Fassung dieses Skripts. Die Sperre (fd 9) bleibt über exec erhalten.
MC_DEPLOY_STUFE=2 MC_DEPLOY_VORHER="$vorher" exec bash deploy/deploy.sh "$@"
}
stufe2() {
local vorher="$MC_DEPLOY_VORHER" neu swap_bak=""
neu="$(git rev-parse --short HEAD)"
zurueck() {
trap - ERR
echo "✗ Deploy von $neu gescheitert — zurück auf ${vorher:0:7}."
git reset -q --hard "$vorher" || true
if [ -n "$swap_bak" ] && [ -f "$swap_bak" ]; then
cp "$swap_bak" "$LSWAP_LIVE" && echo " llama-swap-Config zurückgespielt."
fi
systemctl --user daemon-reload || true
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service || true
echo "$(date -Is) GESCHEITERT $neu, zurück auf ${vorher:0:7}" >> "$DEPLOY_LOG" 2>/dev/null || true
bash deploy/notify.sh -d -s "[Alarm] Deploy" \
"Deploy von $neu ist gescheitert, die Box läuft wieder mit ${vorher:0:7}. Details: journalctl oder deploy-Ausgabe." || true
exit 1
}
trap zurueck ERR
if [ "$neu" = "${vorher:0:7}" ]; then
echo "Kein neuer Stand auf main — spiele trotzdem alles aus (Units, Skripte, Neustart)."
fi
# 1. Abhängigkeiten (inkl. pytest fürs Prüftor)
echo "Aktualisiere Python-Abhängigkeiten…"
backend/.venv/bin/pip install -q -r backend/requirements.txt -r backend/requirements-dev.txt
# 2. Prüftor: vor jedem Umschalten. Rot = zurück auf den alten Stand.
if [ "${MC_DEPLOY_OHNE_TESTS:-0}" = "1" ]; then
echo "Prüftor übersprungen (MC_DEPLOY_OHNE_TESTS=1)."
else
bash deploy/pruefen.sh
fi
# 3. llama-swap-Config: /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit, MC2 schreibt sie
# selbst (Oberfläche, Radar). Der Repo-Abzug überschreibt sie nur, wenn er sich in DIESEM Deploy
# geändert hat — sonst würde jede Abweichung (z. B. ein per Oberfläche eingetragenes Modell)
# zurückgedreht. Kein Neustart nötig: llama-swap läuft mit -watch-config.
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" != "1" ] && ! git diff --quiet "$vorher" HEAD -- deploy/llama-swap.config.yaml; then
swap_bak="$LSWAP_LIVE.bak-$(date +%Y%m%d-%H%M%S)"
echo "llama-swap-Abzug geändert — sichere $LSWAP_LIVE nach $swap_bak und spiele ihn ein."
diff -u "$LSWAP_LIVE" deploy/llama-swap.config.yaml | head -40 || true
cp "$LSWAP_LIVE" "$swap_bak"
cp deploy/llama-swap.config.yaml "$LSWAP_LIVE.neu"
mv "$LSWAP_LIVE.neu" "$LSWAP_LIVE"
# Nur die letzten 5 Sicherungen behalten (ältere root-eigene bleiben notfalls liegen).
ls -1t "$LSWAP_LIVE".bak-* 2>/dev/null | tail -n +6 | xargs -r rm -f 2>/dev/null || true
elif ! cmp -s deploy/llama-swap.config.yaml "$LSWAP_LIVE"; then
echo "Hinweis: Die lebende llama-swap-Config weicht vom Repo-Abzug ab (Änderung über die Oberfläche?) — nicht angefasst."
fi
# 4. Units und Drop-ins (alle Repo-Units, damit Box und Repo nicht auseinanderlaufen)
local ziel="$HOME/.config/systemd/user" u
for u in $UNITS; do
install -m 644 "deploy/$u" "$ziel/$u"
done
install -D -m 644 deploy/mc2-steward.service.d-warmset.conf "$ziel/mc2-steward.service.d/warmset.conf"
install -D -m 644 deploy/mission-control-2.service.d-override.conf "$ziel/mission-control-2.service.d/override.conf"
systemctl --user daemon-reload
# shellcheck disable=SC2086
systemctl --user enable $AKTIV >/dev/null 2>&1
# shellcheck disable=SC2086
systemctl --user start mc2-radar.timer mc2-backup.timer mc2-morgenmeldung.timer projekte-sync.timer
# Den Update-Timer nur starten, wenn er nicht schon läuft: Mit Persistent=true holt ein frisch
# gestarteter Timer einen verpassten Sonntagslauf sofort nach (so am 24.09.2026 passiert). Neu
# gestartet wird die Box dabei ohnehin nur sonntags zwischen 04 und 07 Uhr (autoupdate.sh).
systemctl --user is-active --quiet mc2-autoupdate.timer || systemctl --user start mc2-autoupdate.timer
# 5. Cron-Skripte für Hermes (~/.hermes/scripts ist die Vorgabe von `hermes cron --script`)
mkdir -p "$HOME/.hermes/scripts"
install -m 755 deploy/jobs/*.sh deploy/jobs/*.py "$HOME/.hermes/scripts/"
# 6. Skills und Plugins
echo "Synchronisiere Hermes-Skills…"
mkdir -p "$HOME/.hermes/skills"
local d name alt
for d in deploy/skills/*/; do
name="$(basename "$d" | tr '-' '_')"
mkdir -p "$HOME/.hermes/skills/$name"
cp -r "$d". "$HOME/.hermes/skills/$name/"
done
for alt in $SKILLS_ALT; do
if [ -d "$HOME/.hermes/skills/$alt" ]; then
mkdir -p "$HOME/.hermes/skills-archiv"
rm -rf "$HOME/.hermes/skills-archiv/$alt"
mv "$HOME/.hermes/skills/$alt" "$HOME/.hermes/skills-archiv/"
fi
done
# Plugins nur kopieren: aktiviert wird einmalig von Hand (hermes plugins enable …), neuer Code
# greift beim nächsten Neustart des Hermes-Gateways — den macht deploy.sh bewusst nicht.
mkdir -p "$HOME/.hermes/plugins"
for d in deploy/hermes-plugins/*/; do
cp -r "${d%/}" "$HOME/.hermes/plugins/"
done
# 7. Neustart und Nachprüfung
echo "Starte Gateway, Box-Wart und Wächter neu…"
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service
local i ok=0
for i in $(seq 1 30); do
if curl -sf -m 3 "$API/api/health" >/dev/null && curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null \
&& systemctl --user is-active --quiet mc2-steward.service; then
ok=1; break
fi
sleep 2
done
[ "$ok" = 1 ] || { echo "Nachprüfung: Dienste antworten nicht."; false; }
trap - ERR
echo "$(date -Is) $neu (vorher ${vorher:0:7})" >> "$DEPLOY_LOG" 2>/dev/null || true
echo "✅ Deploy $neu ist live (vorher ${vorher:0:7})."
}
# Beides in Funktionen und ein exit direkt dahinter: bash liest ein Skript häppchenweise, und
# git merge ersetzt diese Datei mitten im Lauf. So läuft nichts aus der neuen Datei unkontrolliert.
if [ "${MC_DEPLOY_STUFE:-1}" = "2" ]; then stufe2 "$@"; else stufe1 "$@"; fi; exit $?
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""Stellt sicher, dass ein Worker-Profil die Agent-Hooks kennt (Faden 8, 13.07.2026;
erweitert 19.07.2026 um die No-Progress-Bremse und Mehrfach-Eintraege pro Event).
Kanban-Worker laufen unter IHREM Profil (HERMES_HOME=~/.hermes/profiles/<name>) und lesen
dessen config.yaml — NICHT die Top-Level ~/.hermes/config.yaml. Die worker-relevanten Hooks
muessen also in JEDER Worker-Profil-config.yaml stehen, sonst feuern sie fuer Worker nie
(Bug-Fund 13.07.).
WICHTIG (Falle, 19.07.): Ein Event darf nur EINMAL als YAML-Key existieren. Existiert
`pre_tool_call:` bereits (tabu-pfade-guard), wird ein weiterer Eintrag IN den Block
eingefuegt — ein zweiter `pre_tool_call:`-Key wuerde beim YAML-Laden den ersten
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 datetime
import os
import shutil
import sys
try:
import yaml
except Exception:
print(" (PyYAML fehlt — ensure-profile-hooks uebersprungen)")
sys.exit(0)
AH = os.path.expanduser("~/.hermes/agent-hooks")
# Reihenfolge zaehlt: tabu-pfade-guard MUSS vor der Bremse stehen (Schutz vor Komfort).
WANT = [
("pre_llm_call", f"{AH}/box-steckbrief-inject.sh", 10),
("pre_tool_call", f"{AH}/tabu-pfade-guard.py", 10),
("pre_tool_call", f"{AH}/no-progress-bremse.py pre", 10),
("post_tool_call", f"{AH}/no-progress-bremse.py post", 10),
]
if len(sys.argv) < 2:
print(" usage: ensure-profile-hooks.py <config.yaml>")
sys.exit(0)
p = sys.argv[1]
if not os.path.isfile(p):
sys.exit(0)
txt = open(p, encoding="utf-8").read()
anchor = (" pre_verify:\n"
f" - command: {AH}/pre-verify-gates.sh\n"
" timeout: 60\n")
if txt.count(anchor) != 1:
print(f" [{os.path.basename(os.path.dirname(p))}] kein eindeutiger pre_verify-Anker — SKIP")
sys.exit(0)
new = txt
changed = False
for event, cmd, timeout in WANT:
if cmd in new:
continue
entry = f" - command: {cmd}\n timeout: {timeout}\n"
ev_line = f" {event}:\n"
if ev_line in new:
# Eintrag ANS ENDE des bestehenden Event-Blocks: direkt nach dem Key einfuegen
# waere auch ok, aber hinter dem letzten Eintrag haelt die gewollte Reihenfolge.
idx = new.index(ev_line) + len(ev_line)
end = idx
for line in new[idx:].splitlines(keepends=True):
if line.startswith((" - ", " ")):
end += len(line)
else:
break
new = new[:end] + entry + new[end:]
else:
new = new.replace(anchor, anchor + ev_line + entry, 1)
changed = True
if not changed:
print(f" [{os.path.basename(os.path.dirname(p))}] Hooks schon registriert.")
sys.exit(0)
try:
d = yaml.safe_load(new)
h = d["hooks"]
for event, cmd, _t in WANT:
assert any(cmd == x.get("command", "") for x in h.get(event, [])), f"{event}: {cmd} fehlt"
assert any("pre-verify-gates.sh" in x.get("command", "") for x in h["pre_verify"])
# Tabu-Guard muss VOR der Bremse stehen (beide in pre_tool_call).
cmds = [x.get("command", "") for x in h["pre_tool_call"]]
assert cmds.index(f"{AH}/tabu-pfade-guard.py") < cmds.index(f"{AH}/no-progress-bremse.py pre")
except Exception as exc:
print(f" [{p}] Validierung fehlgeschlagen ({exc}) — config UNVERAENDERT.")
sys.exit(0)
bak = p + ".bak-hooks-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
shutil.copy2(p, bak)
tmp = p + ".tmp-hooks"
open(tmp, "w", encoding="utf-8").write(new)
os.replace(tmp, p)
print(f" [{os.path.basename(os.path.dirname(p))}] Hooks ergaenzt (Backup {os.path.basename(bak)}).")
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env bash
# Gateway-Cutover (UMBAU v3 P1, Stufe 2 = Unabhängigkeit):
# Hermes' LLM-Endpunkte vom Steuerpult-Umweg (127.0.0.1:9001/v1) auf den
# eigenständigen Gateway (127.0.0.1:9010/v1) umstellen — Top-Level-config.yaml
# UND alle Profil-Configs (werkstatt/betrieb). Danach überleben Lucys Hirn und
# die komplette Nacht-Autonomie jeden MC2-Neustart.
#
# BEWUSST NICHT in deploy.sh: ~/.hermes/config.yaml ist Zwei-Schreiber-sensibel
# (Hermes schreibt sie selbst). Einmalig von Hand bzw. per angenommener Karte:
# bash ~/mission-control-v2/deploy/gateway-cutover.sh # umstellen
# bash ~/mission-control-v2/deploy/gateway-cutover.sh --revert # zurück
# Je geänderter Datei entsteht ein Backup *.bak-gwcut-<Zeitstempel>.
# Danach: hermes-gateway-Neustart + kurzer Funktionsnachweis.
set -euo pipefail
if [ "${1:-}" = "--revert" ]; then
ALT="127.0.0.1:9010/v1"; NEU="127.0.0.1:9001/v1"
PROBE="http://127.0.0.1:9001/api/health" # Rückweg: MC2 muss /v1 wieder selbst bedienen
else
ALT="127.0.0.1:9001/v1"; NEU="127.0.0.1:9010/v1"
PROBE="http://127.0.0.1:9010/gw/health" # Hinweg: Gateway-Prozess muss leben
fi
# Sicherung: NIE auf ein totes Ziel umstellen — das würde Lucy das Hirn abschneiden.
if ! curl -sf -m 5 "$PROBE" >/dev/null 2>&1; then
echo "ABBRUCH: Ziel $NEU antwortet nicht ($PROBE). Nichts geändert."
exit 1
fi
STAMP="$(date +%Y%m%d-%H%M%S)"
ALT_RE="${ALT//./\\.}" # Punkte für sed escapen
geaendert=0
for f in "$HOME/.hermes/config.yaml" "$HOME"/.hermes/profiles/*/config.yaml; do
[ -f "$f" ] || continue
grep -q "$ALT" "$f" || continue
cp "$f" "$f.bak-gwcut-$STAMP"
sed -i "s|$ALT_RE|$NEU|g" "$f"
echo "umgestellt: $f (Backup: $f.bak-gwcut-$STAMP)"
geaendert=1
done
if [ "$geaendert" -eq 0 ]; then
echo "Nichts zu tun — '$ALT' kommt in keiner Hermes-Config vor."
exit 0
fi
echo "hermes-gateway neu starten …"
systemctl --user restart hermes-gateway
sleep 3
if systemctl --user is-active --quiet hermes-gateway; then
echo "hermes-gateway läuft."
else
echo "WARN: hermes-gateway ist nach dem Neustart NICHT aktiv!"
echo " journalctl --user -u hermes-gateway -n 30 und ggf.: $0 --revert"
exit 1
fi
echo "Empfohlener Nachweis: cd ~/.hermes/hermes-agent && venv/bin/hermes doctor"
echo "FERTIG. Rückweg jederzeit: $0 --revert"
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# gitea-pr (24.07.2026): PR auf Gitea per REST-API anlegen — der EINZIGE Weg für
# Worker, einen Pull-Request zu erstellen. Kein Web-Login, kein Passwort: das
# HTTP-Token aus ~/.git-credentials reicht (Vorfall 23.07.: Wartung-Worker bat
# um das Gitea-Passwort für die Web-GUI und blockte — genau das nie wieder).
# Nutzung: gitea-pr <repo> <head-branch> "<titel>" ["<body>"] [base=main]
set -euo pipefail
REPO="${1:?Nutzung: gitea-pr <repo> <head-branch> \"<titel>\" [\"<body>\"] [base]}"
HEAD="${2:?head-branch fehlt}"
TITLE="${3:?titel fehlt}"
BODY="${4:-}"
BASE="${5:-main}"
OWNER="Hitonabi"
# Intern statt DDNS-Domain: die ist nachts (Zwangstrennung) oft nicht erreichbar.
HOST="http://192.168.178.153:3000"
creds="$(printf 'protocol=http\nhost=192.168.178.153:3000\n\n' | git credential fill)"
username="$(sed -n 's/^username=//p' <<<"$creds")"
password="$(sed -n 's/^password=//p' <<<"$creds")"
[ -n "$password" ] || { echo "FEHLER: kein Token in ~/.git-credentials gefunden" >&2; exit 1; }
payload="$(TITLE="$TITLE" BODY="$BODY" HEAD="$HEAD" BASE="$BASE" python3 -c '
import json, os
print(json.dumps({"title": os.environ["TITLE"], "body": os.environ["BODY"],
"head": os.environ["HEAD"], "base": os.environ["BASE"]}))')"
antwort="$(curl -sS -w '\n%{http_code}' -X POST \
-u "$username:$password" -H "Content-Type: application/json" \
-d "$payload" "$HOST/api/v1/repos/$OWNER/$REPO/pulls")"
status="${antwort##*$'\n'}"
koerper="${antwort%$'\n'*}"
if [ "$status" = "201" ]; then
printf '%s' "$koerper" | python3 -c 'import json,sys; pr=json.load(sys.stdin); print("PR angelegt:", pr.get("html_url", pr.get("url", "?")))'
else
echo "FEHLER: Gitea-API antwortete $status" >&2
printf '%s\n' "$koerper" | head -c 600 >&2
exit 1
fi
+3 -6
View File
@@ -101,13 +101,10 @@ PY
echo "WARNUNG: $NAME-Seed antwortete HTTP $CODE (Repo ist trotzdem da)." >&2 echo "WARNUNG: $NAME-Seed antwortete HTTP $CODE (Repo ist trotzdem da)." >&2
fi fi
} }
# JEDES neue Repo wird mit beiden Wächtern geboren: # Neue Repos bekommen das Prüf-Tor VERIFY (das OpenCode-Plugin führt es nach jeder Etappe aus).
# ci.yml = die AUSSEN-Prüfung (Gitea Actions nach dem Push, Wasserdicht-Runde 22.07.) # Die CI-Ampel (Gitea Actions) wird seit 24.09.2026 nicht mehr eingepflanzt: der Runner war
# VERIFY = die INNEN-Prüfung (das OpenCode-Plugin führt sie nach jeder Etappe aus und # seit Anfang September tot, geprüft wird vor dem Push bzw. vor dem Deploy.
# 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. # 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" \ saat "$REALHOME/mission-control-v2/deploy/opencode/VERIFY.template" "VERIFY" \
"Pruef-Tor (automatisch bei Repo-Anlage eingepflanzt)" "Pruef-Tor" "Pruef-Tor (automatisch bei Repo-Anlage eingepflanzt)" "Pruef-Tor"
echo "Repo '$FULL' angelegt (privat, main initialisiert)." echo "Repo '$FULL' angelegt (privat, main initialisiert)."
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Lucys Stimme (Kyutai pocket-tts 2.1.0, german_24l, Referenz Artoria v2 seit 04.09.2026)
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/hitonabi/.lucy-stimme/app
Environment=LUCY_LANG=german_24l
Environment=OMP_NUM_THREADS=8
Environment=LUCY_WORKERS=2
Environment=LUCY_WORKER_THREADS=4
ExecStart=/home/hitonabi/.lucy-stimme/venv/bin/python -m uvicorn pocket_server:app --host 127.0.0.1 --port 8021
Restart=always
RestartSec=10
TimeoutStartSec=300
[Install]
WantedBy=default.target
@@ -0,0 +1,22 @@
# systemd-Override für mission-control-2.service
# Ziel: Automatischer Neustart auch bei stop-sigterm Timeout
#
# Verwendung:
# 1. Kopieren nach ~/.config/systemd/user/mission-control-2.service.d/override.conf
# 2. systemctl --user daemon-reload
# 3. systemctl --user restart mission-control-2
#
# Verhaltensänderungen:
# - Restart=always → startet nach BEENDUNG (auch SIGTERM) automatisch neu
# - TimeoutStopSec=10 → gibt dem Prozess 10s für sauberes Stoppen, dann SIGKILL
# - RestartSec=5 → kurze Pause vor Neustart (verhindert Flackern bei sofortigem Crash)
[Service]
# Startet nach jedem Beendigung (auch SIGTERM, SIGSEGV, etc.) neu
Restart=always
# Zeitlimit für SIGTERM-Phase vor SIGKILL
TimeoutStopSec=10
# Wartezeit zwischen Beendigung und Neustart
RestartSec=5
+7
View File
@@ -14,6 +14,13 @@ MAX_ZEICHEN="${MC_MORGENMELDUNG_MAX:-3500}" # Telegram erlaubt 4096 Zeichen je
VERSUCHE="${MC_MORGENMELDUNG_VERSUCHE:-3}" VERSUCHE="${MC_MORGENMELDUNG_VERSUCHE:-3}"
PAUSE="${MC_MORGENMELDUNG_PAUSE:-60}" PAUSE="${MC_MORGENMELDUNG_PAUSE:-60}"
# Melde-Log begrenzen (wuchs seit Juli ungebremst): über 3 MB bleiben die letzten 2 MB.
# services/update_verlauf.py liest ohnehin nur das Ende, das reicht für Monate Update-Verlauf.
LOG="${MC_NOTIFY_LOG:-$HOME/mc2-notify.log}"
if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt 3000000 ]; then
tail -c 2000000 "$LOG" > "$LOG.kurz" && mv "$LOG.kurz" "$LOG"
fi
[ -s "$QUEUE" ] || [ -s "$ARBEIT" ] || exit 0 [ -s "$QUEUE" ] || [ -s "$ARBEIT" ] || exit 0
# Warteschlange übernehmen. mv ist atomar: Was notify.sh währenddessen anhängt, landet entweder # Warteschlange übernehmen. mv ist atomar: Was notify.sh währenddessen anhängt, landet entweder
-116
View File
@@ -1,116 +0,0 @@
# OpenCode-Plugin: MC2-Governor
**Hier liegt die Quelle. Der Einsatzort ist eine Kopie.**
```
deploy/opencode-plugin/mc2-governor.ts <- versioniert, hier aendern
| kopieren
v
%USERPROFILE%\.config\opencode\plugin\ <- wird von OpenCode automatisch geladen
```
Bis zum 21.08.2026 existierte diese Datei **nur** im Konfigordner des PCs — 11 KB
verhaltensbestimmender Code ohne Sicherung und ohne Historie. Deshalb liegt sie jetzt hier.
## Ausbringen
```powershell
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\" -Force
```
Wirkt sofort in der naechsten OpenCode-Sitzung; OpenChamber muss dafuer nicht neu starten,
eine neue Sitzung reicht.
## Was es tut
1. **Werkzeug-Zaun** (`tool.execute.before`) — blockt Handgriffe, die ein Agent nie
unbeaufsichtigt tun darf. Leitplanke gegen Entgleisen, **keine** Sandbox.
2. **Pruef-Tor + Reparaturschleife** (`session.idle`) — sagt der Agent „fertig", laeuft der
Verify-Befehl aus der Datei `VERIFY` im Repo-Wurzelverzeichnis. ROT geht als naechster
Auftrag zurueck an den Agenten, bis zu `MC2_LOOP_MAX_ROUNDS` mal.
‼️ **Schlaeft derzeit:** es gibt in keinem Projekt eine `VERIFY`-Datei. Ohne sie tut
diese Funktion nichts — schadet aber auch nicht.
3. **Savepoint statt Zusammenfassen** (`session.compacted`) — beim Komprimieren fallen
still die Regeln aus dem Kontext; stattdessen wird `SAVEPOINT.md` geschrieben.
4. **Stimme** — Ereignisse gehen an MC2 `/api/voice/announce`, Lucy spricht sie.
Der zugehoerige **Governor-Proxy auf `:8100`** (Token-Zaehler mit Hart-Deckel, Juli 2026)
**existiert nicht mehr** — kein Port, kein Ordner, keine Unit. Nachgemessen am 21.08.2026.
Dieses Plugin haengt nicht an ihm.
## Schalter (Umgebungsvariablen)
| Variable | Default | Wirkung |
|---|---|---|
| `MC2_BOX_URL` | `http://192.168.178.151:9001` | MC2-Basis fuer Meldungen |
| `MC2_FENCE_OFF` | — | `1` schaltet den **ganzen** Zaun ab (alles oder nichts) |
| `MC2_LOOP_AUTOFIX` | an | `0` schaltet die Reparaturschleife ab |
| `MC2_LOOP_MAX_ROUNDS` | `3` | Runden der Reparaturschleife |
| `MC2_LOOP_ANNOUNCE` | an | `0` macht Lucy stumm |
| `MC2_LOOP_SILENT` | — | `1` = Meldungen ohne Ton |
## Aenderung 21.08.2026: `git push` ist frei
Der Zaun blockte urspruenglich **jedes** `git push` („Veroeffentlichen ist Sache des
Menschen"). Das passt nicht mehr: die Bahn ist jetzt PC → Gitea → Box, und jede Aenderung
soll auch im git landen. Ein Agent, der committen, aber nicht pushen darf, laesst Arbeit
auf einem einzelnen Rechner liegen.
**Erzwungenes Pushen bleibt verboten**`--force`, `--mirror`, `--delete`. Das
ueberschreibt fremde Arbeit auf dem Server und ist genau die Klasse Unfall, gegen die der
Zaun existiert. `--force-with-lease` ist bewusst ausgenommen.
Beides gemessen: `git push --dry-run origin main` laeuft durch,
`git push --force --dry-run origin main` wird mit `[MC2-ZAUN]` abgewiesen.
## ‼️ Falle: das `bash`-Werkzeug ist auf Windows kein bash
Es laeuft in **PowerShell**. `tail`, `head`, `grep` und Freunde gibt es dort nicht, und
Pipes verhalten sich anders. Wer dem Agenten Befehle vorgibt, muss das beruecksichtigen.
## ‼️ Falle: Gitea-Anmeldung ist flatterhaft
Der Git Credential Manager haelt fuer die DDNS-Domain ein **OAuth-Token mit einer Stunde
Laufzeit**. Laeuft es ab, scheitert der Push des Agenten mit
`remote: Failed to authenticate user` — ein Push aus PowerShell erneuert es, danach geht es
wieder. Dauerhafte Loesung waere ein langlebiges Gitea-Token auf die **interne** Adresse
`http://192.168.178.153:3000` (die DDNS-Domain ist nachts durch die Zwangstrennung ohnehin
zeitweise tot).
---
## ‼️ ABGESCHALTET am 22.08.2026
**Das Plugin ist nicht mehr aktiv.** Es liegt hier als Quelle und im Konfigordner
unter `plugin-archiv/`, aber nicht mehr in `~/.config/opencode/plugin/`.
**Warum:** Beim ersten echten Arbeitslauf fiel auf, dass bei jeder Komprimierung
zwei Dinge uebereinander passierten — OpenChamber dampfte den Verlauf ein, und der
Governor schob sofort einen zusaetzlichen Auftrag nach („schreib SAVEPOINT.md").
Der Agent verlor dadurch jedes Mal einen Zug an Buchhaltung statt an die Aufgabe.
Nachgewiesen: `SAVEPOINT.md` in `mc2-referenz`, geschrieben 22.08. 14:47 mitten
im Lauf.
Der User hatte den Abbau schon am 21.08. angeordnet („alle Guards, alle
Governor, Stewards — brauchen wir nicht mehr"). Ich hatte damals das Behalten
empfohlen und seine Antwort zum `git push`-Riegel als Zustimmung zum Rest
gewertet. **Das war eine Annahme, keine Entscheidung.**
## Was damit wegfaellt — bewusst
| war | jetzt |
|---|---|
| Sperrliste: `rm -rf`, `sudo`, force push, `curl\|sh`, `ssh` ausser Arcane, `npm publish` | **kein Netz mehr** — der Agent darf alles, was die Shell hergibt |
| Pruef-Tor + Reparaturschleife (`VERIFY`) | schlief ohnehin, es gab nirgends eine `VERIFY`-Datei im PC-Baum |
| SAVEPOINT beim Komprimieren | weg — genau der Grund fuer den Abbau |
| Meldungen an Lucys Stimme | weg |
**Wiederherstellen** (wenn der fehlende Zaun sich raecht):
```powershell
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\"
```
Plugins werden beim **Start des OpenCode-Servers** geladen — Aendern wirkt erst
nach einem Neustart von OpenChamber, nicht sofort. Deshalb liess sich das Plugin
auch mitten in einem laufenden Referenzlauf gefahrlos entfernen.
-255
View File
@@ -1,255 +0,0 @@
/**
* MC2-Governor — der "Fahrlehrer" im OpenCode-Agenten.
*
* Der Governor-Proxy (:8100) ist die Tankuhr: er sieht nur Tokens und zieht die
* Notbremse. Dieses Plugin sitzt IM Agenten und sieht alles andere — jeden
* Werkzeuggriff, jede Datei, jedes Sitzungsende. Es macht vier Dinge:
*
* 1. WERKZEUG-ZAUN (tool.execute.before)
* Blockt Handgriffe, die ein Agent nie unbeaufsichtigt tun darf: push,
* Historie umschreiben, rekursiv loeschen, sudo, Fremd-Hosts. Genau dieser
* Zustandsautomat-Zaun hob lokale Modelle in Messungen von 2/10 auf 10/10 —
* nicht weil sie schlauer werden, sondern weil sie nicht mehr entgleisen.
*
* 2. PRUEF-TOR + SCHLEIFE (session.idle)
* Sagt der Agent "fertig", laeuft der Verify-Befehl des Projekts (Datei
* `VERIFY` im Repo-Wurzelverzeichnis). GRUEN -> Meldung. ROT -> der Fehler
* geht als naechster Auftrag automatisch zurueck an den Agenten, bis zu
* MC2_LOOP_MAX_ROUNDS mal. Das ist die "Ralph-Schleife", nur mit Bremse.
*
* 3. SAVEPOINT STATT ZUSAMMENFASSEN (session.compacted)
* Beim Komprimieren fallen still die Regeln aus dem Kontext (Paper
* "Governance Decay"). Wir schieben stattdessen den Auftrag nach, SAVEPOINT.md
* zu schreiben — Wissen lebt in Datei + git, nicht im schrumpfenden Chat.
*
* 4. STIMME (MC2 /api/voice/announce)
* Jedes Ereignis geht mit eigenem Absender `loop` in MC2s Melde-Briefkasten.
* Lucy pollt ihn ohnehin und spricht ihn — ohne eine Zeile Lucy-Code.
*
* Schalter (Umgebungsvariablen):
* MC2_BOX_URL MC2-Basis (Default http://192.168.178.151:9001)
* MC2_LOOP_AUTOFIX Selbstreparatur (1 = an, Default an)
* MC2_LOOP_MAX_ROUNDS max. Reparaturrunden (Default 3)
* MC2_LOOP_SILENT 1 = Lucy schweigt (Nachtlauf; Meldungen kommen trotzdem an)
* MC2_LOOP_ANNOUNCE 0 = gar keine Meldungen
* MC2_FENCE_OFF 1 = Werkzeug-Zaun aus (nur fuer Notfaelle)
*
* Liegt global unter ~/.config/opencode/plugin/ und wirkt damit in JEDEM Projekt —
* am Tag in Zed, nachts im Cron. Ein Regelwerk, zwei Ausloeser.
*/
const BOX_URL = process.env.MC2_BOX_URL || "http://192.168.178.151:9001"
const AUTOFIX = process.env.MC2_LOOP_AUTOFIX !== "0"
const MAX_ROUNDS = parseInt(process.env.MC2_LOOP_MAX_ROUNDS || "3", 10)
const SILENT = process.env.MC2_LOOP_SILENT === "1"
const ANNOUNCE_ON = process.env.MC2_LOOP_ANNOUNCE !== "0"
const FENCE_OFF = process.env.MC2_FENCE_OFF === "1"
/**
* Verbotene Shell-Handgriffe. Bewusst als Muster auf der ROHEN Kommandozeile —
* ein Agent, der `git push` in ein `bash -c` verpackt, wird trotzdem erwischt.
* Kein Anspruch auf Sandbox-Sicherheit: das ist ein Leitplanken-Zaun gegen
* Entgleisen, keine Abwehr gegen einen boesartigen Akteur.
*/
const FENCE: Array<{ rx: RegExp; why: string }> = [
// 21.08.2026: normales `git push` ist FREI. Der Weg ist jetzt PC -> Gitea -> Box, und
// jede Aenderung soll auch im git landen — ein Agent, der committen, aber nicht pushen
// darf, laesst Arbeit auf einem einzelnen Rechner liegen. Was schlecht ist, faengt
// hinterher die CI-Ampel; dieser Zaun muss es nicht vorher verhindern.
// Erzwungenes Pushen bleibt verboten: das ueberschreibt fremde Arbeit auf dem Server
// und ist genau die Klasse Unfall, gegen die der Zaun ueberhaupt existiert.
{ rx: /\bgit\s+push\b[^\n]*(--force(?!-with-lease)|\s-f\b|--mirror\b|--delete\b)/,
why: "git push --force/--mirror/--delete — ueberschreibt fremde Arbeit auf dem Server. Normales `git push` ist erlaubt; muss wirklich erzwungen werden, sag es dem Menschen." },
{ rx: /\bgit\s+reset\s+--hard\b/, why: "git reset --hard — verwirft Arbeit unwiederbringlich." },
{ rx: /\bgit\s+clean\s+-[a-z]*f/, why: "git clean -f — loescht ungetrackte Dateien unwiederbringlich." },
{ rx: /\bgit\s+(rebase|filter-branch|reflog\s+expire)\b/, why: "Historie umschreiben ist tabu." },
{ rx: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+\/(?:\s|$)/, why: "rm -rf / — nein." },
{ rx: /\brm\s+-[a-zA-Z]*[rf]/, why: "rekursives/erzwungenes Loeschen — bitte gezielt loeschen statt pauschal." },
{ rx: /\bsudo\b/, why: "sudo — Rechteausweitung gehoert nicht in einen Agentenlauf." },
{ rx: /\b(shutdown|reboot|mkfs|dd\s+if=)/, why: "System-/Datentraeger-Eingriff." },
{ rx: /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/, why: "Aus dem Netz laden und direkt ausfuehren — klassischer Fussschuss." },
{ rx: /\bssh\s+(?!arcane@192\.168\.178\.162|-o\s+StrictHostKeyChecking=no\s+arcane@)/, why: "ssh nur zur freigegebenen Arcane-VM." },
{ rx: /\bnpm\s+publish\b|\btwine\s+upload\b/, why: "Veroeffentlichen von Paketen ist Sache des Menschen." },
]
/** Zaehler je Sitzung: wie viele Selbstreparatur-Runden liefen schon? */
const rounds = new Map<string, number>()
/** Doppel-Feuern verhindern: session.idle kann mehrfach kommen. */
const busy = new Set<string>()
async function announce(subject: string, text: string, priority: "normal" | "silent" = "normal") {
if (!ANNOUNCE_ON) return
try {
await fetch(`${BOX_URL}/api/voice/announce`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
subject,
text,
source: "loop",
priority: SILENT ? "silent" : priority,
}),
signal: AbortSignal.timeout(4000),
})
} catch {
/* best effort — eine stumme Lucy darf den Bau nie aufhalten */
}
}
export const MC2Governor = async ({ client, $, directory, worktree }: any) => {
const root: string = worktree || directory || process.cwd()
/**
* Verify-Befehl des Projekts lesen. Fehlt die Datei, ist das Pruef-Tor AUS.
* Bewusst ueber fs statt `cat`: das Plugin laeuft am Tag auf Windows (Zed) und
* nachts auf der Box — `cat` gibt es auf Windows nicht zuverlaessig.
*/
async function readVerify(): Promise<string | null> {
try {
const { readFile } = await import("node:fs/promises")
const { join } = await import("node:path")
const raw = await readFile(join(root, "VERIFY"), "utf8")
const cmd = raw
.split("\n")
.map((l: string) => l.trim())
.filter((l: string) => l && !l.startsWith("#"))
.join(" && ")
return cmd || null
} catch {
return null
}
}
/**
* Verify ausfuehren. Rueckgabe: {ok, output} — Ausgabe auf das Wesentliche gekuerzt.
* `{ raw: cmd }` schiebt den Befehl UNESCAPED in Buns Shell; ein normales
* `${cmd}` wuerde die ganze Zeile als EIN Argument uebergeben und nie laufen.
* Buns Shell ist plattformunabhaengig — kein `bash -lc`, das auf Windows fehlt.
*/
async function runVerify(cmd: string): Promise<{ ok: boolean; out: string }> {
try {
const res = await $`${{ raw: cmd }}`.cwd(root).nothrow().quiet()
const out = `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`
return { ok: res.exitCode === 0, out: out.slice(-4000) }
} catch (e: any) {
return { ok: false, out: String(e?.message ?? e).slice(-4000) }
}
}
/** Dem laufenden Agenten einen neuen Auftrag schicken (Selbstreparatur-Schleife). */
async function sendPrompt(sessionID: string, text: string): Promise<boolean> {
try {
await client.session.prompt({
path: { id: sessionID },
body: { parts: [{ type: "text", text }] },
})
return true
} catch {
return false
}
}
return {
// ── 1. Werkzeug-Zaun ───────────────────────────────────────────────────
"tool.execute.before": async (input: any, output: any) => {
if (FENCE_OFF) return
if (input?.tool !== "bash") return
const cmd: string = output?.args?.command ?? ""
if (!cmd) return
for (const rule of FENCE) {
if (rule.rx.test(cmd)) {
await announce(
"[Zaun]",
`Ich habe einen Befehl geblockt: ${rule.why}`,
"silent",
)
// Werfen = OpenCode bricht genau diesen Werkzeugaufruf ab und gibt dem
// Modell den Grund zurueck. Der Agent arbeitet weiter, nur anders.
throw new Error(
`[MC2-ZAUN] Blockiert: ${rule.why}\n` +
`Befehl war: ${cmd}\n` +
`Waehle einen anderen Weg. Wenn das wirklich noetig ist, sag es dem Menschen — ` +
`er macht es selbst.`,
)
}
}
},
// ── 2.-4. Ereignisse ───────────────────────────────────────────────────
event: async ({ event }: any) => {
const type: string = event?.type ?? ""
const props: any = event?.properties ?? event ?? {}
const sessionID: string = props.sessionID || props.sessionId || props.id || ""
// ── Savepoint statt Zusammenfassen ──────────────────────────────────
if (type === "session.compacted" || type === "experimental.session.compacting") {
await announce(
"[Sitzung]",
"Die Sitzung wurde komprimiert — ich lasse den Stand in SAVEPOINT.md sichern.",
"silent",
)
if (sessionID) {
await sendPrompt(
sessionID,
"[MC2-GOVERNOR] Der Kontext wurde gerade komprimiert — dabei gehen still " +
"Regeln und Details verloren. Aktualisiere JETZT SAVEPOINT.md: was wirklich " +
"erledigt ist (nur was im Code steht), der genaue naechste Schritt, offene " +
"Fragen, Stolpersteine. Committe die Datei. Danach arbeite normal weiter.",
)
}
return
}
// ── Pruef-Tor + Selbstreparatur ─────────────────────────────────────
if (type !== "session.idle" || !sessionID) return
if (busy.has(sessionID)) return
const cmd = await readVerify()
if (!cmd) return // Kein VERIFY im Projekt -> Pruef-Tor bewusst aus.
busy.add(sessionID)
try {
const { ok, out } = await runVerify(cmd)
const round = rounds.get(sessionID) ?? 0
if (ok) {
rounds.delete(sessionID)
await announce("[Pruefung]", "Etappe fertig und die Tests sind gruen.", "normal")
return
}
if (!AUTOFIX || round >= MAX_ROUNDS) {
rounds.delete(sessionID)
await announce(
"[Pruefung]",
`Die Tests sind rot und ich habe ${round} Reparaturversuche verbraucht. ` +
`Hier komme ich allein nicht weiter, Commander.`,
"normal",
)
return
}
rounds.set(sessionID, round + 1)
await announce(
"[Pruefung]",
`Tests rot — ich repariere selbst weiter, Runde ${round + 1} von ${MAX_ROUNDS}.`,
"silent",
)
await sendPrompt(
sessionID,
`[MC2-PRUEFTOR] Deine Etappe gilt noch NICHT als fertig: der Verify-Befehl des ` +
`Projekts ist fehlgeschlagen.\n\n` +
`Befehl: ${cmd}\n\n` +
`Ausgabe (Ende):\n\`\`\`\n${out}\n\`\`\`\n\n` +
`Behebe die Ursache — nicht das Symptom, und schalte keinen Test ab. ` +
`Wenn du fertig bist, melde dich normal; ich pruefe dann erneut. ` +
`(Reparaturrunde ${round + 1} von ${MAX_ROUNDS}.)`,
)
} finally {
busy.delete(sessionID)
}
},
}
}
export default MC2Governor
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=Projekte-Sync — haelt ~/projekte mit Gitea deckungsgleich
After=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash %h/mission-control-v2/deploy/projekte-sync.sh
TimeoutStartSec=600
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Projekte-Sync stuendlich
[Timer]
OnCalendar=hourly
RandomizedDelaySec=5min
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target
-166
View File
@@ -1,166 +0,0 @@
# 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 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 der Agentic IDE
dem Coder sofort einen sauberen Auftrag geben kann. Du denkst und bereitest vor — **gebaut wird
in der Agentic IDE, von ihm.**
## Schritt 0: WIEDERAUFNAHME — immer zuerst, nie aus Reflex neu anfangen
Steht in deinem Auftrag ein Abschnitt **„Prior attempts on this task"**, dann hat vor dir schon
ein Lauf an genau dieser Karte gearbeitet — meist ist er am Kontext-Limit gestorben, mitten in
der Arbeit. **Dein Workspace ist derselbe wie seiner, seine Arbeit liegt noch da.** Verschaffe
dir ZUERST einen Überblick, bevor du irgendetwas neu denkst:
1. `cat FORTSCHRITT.md` in deinem Workspace (falls vorhanden) — die Notizen deines Vorgängers.
2. `ls -la` im Workspace: liegt dort ein geklontes Repo? Was steht drin —
`git -C <repo> log --oneline -3` und `git -C <repo> status --short`. **Uncommittete Dateien
sind fertige Arbeit, der nur noch der Push fehlt.**
3. `ls -dt /tmp/konzept-*` — die Denk-Werkstatt des Fliessbands. Ein vorhandenes `konzept.md`
mit `runde*`/`haertetest*` daneben ist ein FERTIG GEDACHTES Konzept. Das denkst du nicht neu.
4. Existiert das Gitea-Repo schon? `git ls-remote https://<host>/<owner>/<name>.git`
Dann setzt du an der **ersten Lücke** an — nie davor. Der häufigste Fall: alles gedacht, Repo
steht, `KONZEPT.md` geschrieben, es fehlen nur `git add/commit/push` und die Übergabe. Das sind
zwei Minuten. **Ein zweiter kompletter Denk-Durchlauf ist ein Fehlschlag:** er kostet eine halbe
Stunde, wirft die Arbeit deines Vorgängers weg und läuft in genau dasselbe Kontext-Limit wie er.
Nur wenn wirklich nichts Brauchbares dasteht, fängst du bei Schritt 1 an.
## Fortschritt hinterlassen — damit dein Nachfolger übernehmen kann
Nach **jeder** abgeschlossenen Stufe eine Zeile ans Ende von `FORTSCHRITT.md` im
Workspace-Wurzelverzeichnis — **nicht im Repo-Klon**, sonst landet sie im Projekt-Repo (dort
gehören nur die Dateien aus Schritt 3 hinein). Inhalt: was fertig ist, wo es liegt, was als
Nächstes dran wäre. Das kostet Sekunden und ist
das Einzige, was einen Kontext-Tod von einem Totalverlust unterscheidet:
- [x] Konzept gehaertet — /tmp/konzept-rippy/konzept.md (14 KB, 2 Runden + Haertetest)
- [x] Repo angelegt + geklont — Hitonabi/rippy (bisher nur README)
- [ ] KONZEPT.md committen + pushen <- HIER WEITER
Halte dich kurz (eine Zeile je Stufe) und schreib sie SOFORT, nicht am Ende — am Ende kommst du
vielleicht nicht mehr dazu. Dieselbe Kurzfassung gehört als Notiz in deinen `kanban_heartbeat`,
dann sieht der Commander im Auftragsbuch, wo du stehst.
## Der Ablauf (genau diese vier Schritte, in dieser Reihenfolge)
### 1. Konzept wasserdicht härten — via `konzept-fliessband`
Nutze den Skill **konzept-fliessband** vollständig: der starke Denker (`gpt-oss-120b`/heavy über
`~/.hermes/scripts/worker.sh`) wählt den Rollen-Cast, die Rollen schärfen in Runden EIN gemeinsames
Konzept, ein Advocatus Diaboli (`fremdblick.sh`, fremdes Modell) sucht Löcher — bis keine Blocker
mehr offen sind (max. 3 Runden). „In der Vorbereitung alles rausholen" ist ausdrücklich gewollt:
lieber gründlich denken als schnell fertig sein. Ergebnis: **EIN wasserdichtes Konzept-Dokument**
(Text), das später als `KONZEPT.md` ins Repo geht.
Halte dich an die Leitplanken des Skills: jede Rollen-Stimme MUSS aus einem `worker.sh`-Aufruf an
heavy stammen (nie selbst ausdenken), großzügige `WORKER_MAXTOK`/`FREMDBLICK_MAXTOK` (Reasoning-
Modelle brauchen Token-Luft), Modell-Wechsel minimieren.
> **Achtung, hier ging es schon zweimal schief:** Der Skill endet mit einer eigenen Ablage
> (`cp … ~/konzepte/<slug>-konzept.md`) und einer Telegram-Übergabe. Das ist die Anleitung für
> Aufrufer OHNE Kanban-Karte. **Für dich gilt sie nicht als Ende.** Das Konzept ist erst
> abgeliefert, wenn es als `KONZEPT.md` **im Repo** liegt (Schritte 24). Die Kopie nach
> `~/konzepte/` darfst du zusätzlich machen — sie ersetzt das Repo aber NIE, und `kanban_complete`
> kommt erst danach. Ohne Repo sieht der Commander im Auftragsbuch nichts von deiner Arbeit.
### 2. Repo anlegen (leer, privat)
Wähle einen kurzen, sauberen Projektnamen (kebab-case, z. B. `homelab-dashboard`). **Nimm den
Namen, den ein Vorgänger schon vergeben hat, wenn Schritt 0 einen gefunden hat** — sonst legst du
für dieselbe Idee ein zweites Repo an. Lege das Repo an:
bash ~/.hermes/scripts/gitea-repo-create.sh <name> "<eine Zeile Beschreibung>"
Das Skript legt IMMER **privat** an, protokolliert die Anlage und gibt als **letzte Zeile**
`CLONE <https-clone-url>` aus. Existiert der Name schon (Skript meldet Konflikt), häng eine kurze
Unterscheidung an (`<name>-v2` o. ä.) und melde das in der Übergabe.
### 3. Repo NUR mit Doku füllen — KEIN Code
Klone das frische Repo in deinen Task-Workspace (NICHT `git init`, NICHT shallow) und schreibe
genau diese Dateien — sonst **nichts**, kein Code, kein Gerüst:
- **`KONZEPT.md`** — das wasserdichte Konzept aus Schritt 1 (Ziel, Ansatz, Architektur-Skizze,
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 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 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`
NICHT löschen) muss **GRÜN** sein, bevor eine Etappe fertig' heißt. Keine Tests = rot = nicht fertig.
2. Weicht etwas vom KONZEPT ab (anderes Werkzeug, andere Bibliothek, gestrichenes Muss-Feature):
**STOPP und den Commander fragen** — niemals still ersetzen. (rippy-Lehre: Muss-Feature
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 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:
es ist das frisch angelegte, eigene Repo des Commanders, kein Vorschlag an ein bestehendes System.
Prüfe nach dem Push mit `git ls-remote origin main`, dass es wirklich angekommen ist.
### 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 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).
## Sonderfall: „IDEE PRUEFEN" (kein Projektstart)
Steht im Auftrag die Zeile **`AUFTRAGS-ART: IDEE PRUEFEN`**, dann will der Commander die Idee nur
DURCHDACHT haben — er hat sich NICHT entschieden, sie zu bauen. Dann gilt abweichend:
- Schritt 1 (Konzept haerten) laeuft wie beschrieben, aber mit **Produkt-Blick**: Was ist es, fuer
wen, was gibt es schon, kleinster sinnvoller Wurf, was spricht DAGEGEN. Rollen-Cast entsprechend
(nicht nur Entwickler-Rollen).
- Schritt 2 (Repo anlegen) wie gehabt — **auch bei „IDEE PRUEFEN" gibt es ein Repo.** Das Konzept
NUR nach `~/konzepte/` zu kopieren und dann abzuschließen ist ein Fehlschlag: der Commander
liest das Konzept im Auftragsbuch, und das holt es aus dem Repo.
- Schritt 3 schreibt **NUR `KONZEPT.md` + ein knappes `README.md`** — KEINE `ROADMAP.md`, KEINE
`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 „ö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
und der Commander will gezielte Änderungen. Dann gilt abweichend:
- **Schritt 2 entfällt — lege KEIN neues Repo an.** Der Auftrag nennt die Stelle, an der das
Konzept liegt (Repo oder Datei); die neue Fassung gehört an **genau dieselbe Stelle**. Ein
zweites Repo für dieselbe Idee ist ein Fehlschlag.
- Schritt 1 schrumpft: lies das bestehende Konzept, ändere die genannten Punkte **und deren
Folgewirkungen**, lass den Rest stehen. Ein kurzer Härtetest nur auf die geänderten Teile.
- Sein Vorschlag hat Vorrang, aber nicht blind: schafft er ein Problem, bau ihn ein **und**
schreib die Folge sichtbar in den Abschnitt Risiken/offene Punkte. Nicht stillschweigend
übergehen, nicht schönreden.
- Übergabe: was geändert wurde, was daraus folgte, was du bewusst gelassen hast.
## Eiserne Regeln (nicht verhandelbar)
1. **Du baust KEINEN Code.** Kein Gerüst, keine `package.json`, keine Quelldateien. Nur die
Doku-Dateien aus Schritt 3. Wer Code schreibt, hat die Aufgabe verfehlt.
2. **Fremde Repos sind TABU:** `~/mission-control-v2` (MC2) und `~/lucy` NIEMALS klonen, anfassen
oder verwechseln. Du arbeitest ausschließlich im **neu angelegten** Projekt-Repo. (Wenn im
Auftrag ein bestehendes Repo als Vorbild genannt ist, nur LESEN, nie hineinschreiben.)
3. **Jede Rollen-Stimme des Konzepts kommt aus `worker.sh` an heavy** — nie selbst ausgedacht.
4. **Kein `git init`, kein Shallow-Clone.** Immer voll klonen (Credentials in `~/.git-credentials`).
**Der Push gilt erst als erledigt, wenn `git ls-remote origin main` ihn BEWEIST** — ein Exit-Code
0 und „main -> main" auf dem Bildschirm reichen nicht. Ohne diesen Beleg kein `kanban_complete`;
melde stattdessen ehrlich, dass der Push unbestätigt ist. Und wenn die No-Progress-Bremse dabei
anschlägt: **nicht wegargumentieren.** Sie ist billiger als eine Übergabe, die ins Leere zeigt —
der Commander soll dem Satz „Repo liegt bereit" ohne Nachprüfen glauben können.
5. **TABU-Zonen:** approvals/Tokens/ufw/Security-Configs, `~/.hermes/config.yaml`, systemd-Units.
Braucht der Auftrag so etwas → `kanban_block` mit Begründung.
6. **Festfahren-Stopp:** Bleibt das Konzept nach den Runden mit echten Blockern offen (widersprüchliche
Ziele, fehlende Entscheidung des Commanders) → `kanban_block` mit `--kind needs_input` und der
konkreten Frage. NICHT raten, NICHT denselben Schritt stur wiederholen (das löst die No-Progress-
Bremse aus). Beim zweiten Fehlversuch die Diagnose VARIIEREN, nicht den Befehl wiederholen.
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# pruefen.sh — das Prüftor (seit 24.09.2026, ersetzt die tote CI-Ampel).
#
# Läuft am PC vor dem Push und auf der Box in deploy.sh vor dem Umschalten. Rot = kein Deploy.
# Prüft: Shell-Syntax, Python-Syntax, Lint (wo ruff installiert ist), dass alle Dienste sich
# importieren lassen, und die Tests.
set -uo pipefail
cd "$(dirname "$0")/.."
WURZEL="$(pwd)"
PY="${MC_PRUEF_PYTHON:-}"
if [ -z "$PY" ]; then
for k in backend/.venv/bin/python backend/.venv/Scripts/python.exe; do
[ -x "$k" ] && PY="$WURZEL/$k" && break
done
fi
[ -n "$PY" ] || { echo "✗ Kein Python im backend/.venv gefunden."; exit 1; }
fehler=0
schritt() { echo "── $1"; }
schritt "Syntax der Shell-Skripte"
for f in deploy/*.sh deploy/jobs/*.sh; do
bash -n "$f" || { echo "$f"; fehler=1; }
done
schritt "Python-Syntax"
git ls-files '*.py' | grep -v '^frontend/' | xargs "$PY" -m py_compile || fehler=1
schritt "Lint (ruff)"
if command -v ruff >/dev/null 2>&1; then
ruff check . || fehler=1
else
echo " ruff ist hier nicht installiert — übersprungen (am PC läuft er)."
fi
schritt "Dienste lassen sich laden"
MODELS_PROBE="${MC_PRUEF_MODELS_DIR:-${TMPDIR:-/tmp}/mc2-pruefen}"
mkdir -p "$MODELS_PROBE"
( cd backend && MC_MODELS_DIR="$MODELS_PROBE" "$PY" -c "import app, gateway_app, steward, radar_lauf" ) || fehler=1
schritt "Tests"
"$PY" -m pytest backend/tests -q -p no:cacheprovider || fehler=1
if [ "$fehler" = 0 ]; then
echo "✅ Prüftor grün"
else
echo "✗ Prüftor rot"
exit 1
fi
-149
View File
@@ -1,149 +0,0 @@
#!/usr/bin/env bash
# Restore-Probe (monatlich, 17.07.2026): „Backups sind Hoffnung, Restores sind Wissen."
# Spielt ECHTE Wiederherstellungen durch, statt nur Sicherungs-Erfolge zu zählen:
# 1) PBS: jüngsten host/aibox-Snapshot ermitteln (Alter < 36 h!), daraus den
# Kanarienvogel (state/backup-canary.txt aus hermes.pxar, selektiv via --pattern —
# ~/.hermes ist ~8 GB, wir ziehen nur die eine Datei) nach /tmp restaurieren und
# den Inhalt EXAKT vergleichen. Erstlauf-Fallback (Canary noch in keinem
# Snapshot): das winzige llamaswap.pxar komplett restaurieren und config.yaml
# auf Substanz prüfen.
# 2) Tarball: jüngstes mc2-state-*.tar.gz (Alter < 36 h) listen und den Canary
# (bzw. beim Erstlauf eine Kern-Datei) herausziehen und prüfen.
# 3) Canary für künftige Läufe sicherstellen (fester Inhalt → exakter Vergleich).
# Läuft als Hermes-Cron --no-agent (stdout = Telegram): monatlicher Herzschlag, bei
# Problemen ⚠️-Alarmtext. Komplett ohne sudo (PBS-Env + Client liegen im User-Raum).
set -uo pipefail
PBS_ENV="$HOME/.config/pbs-backup/env"
PBS_BIN="$HOME/bin/proxmox-backup-client"
CANARY="$HOME/.hermes/state/backup-canary.txt"
CANARY_SOLL="MC2-Backup-Kanarienvogel v1 — dieser Inhalt ist konstant und wird nach dem Restore exakt verglichen."
TARBALL_DIR="/srv/models/mc2-backups"
MAX_ALTER_H=36
TMP="$(mktemp -d /tmp/restore-probe.XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
FEHLER=()
BEFUND=()
briefkasten() { # $1=Text (stiller Chronik-Spiegel)
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" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[1], "subject": "Restore-Probe",
"source": "restore-probe", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- 0) Canary sicherstellen (für DIESEN Lauf evtl. noch nicht im Backup) ----------
if [ ! -f "$CANARY" ] || [ "$(cat "$CANARY" 2>/dev/null)" != "$CANARY_SOLL" ]; then
printf '%s' "$CANARY_SOLL" > "$CANARY"
fi
# ---------- 1) PBS-Restore-Probe ----------
if [ ! -f "$PBS_ENV" ] || [ ! -x "$PBS_BIN" ]; then
FEHLER+=("PBS-Probe unmöglich: Env ($PBS_ENV) oder Client ($PBS_BIN) fehlt")
else
set -a; source "$PBS_ENV"; set +a
SNAP_JSON="$("$PBS_BIN" snapshot list host/aibox --output-format json 2>/dev/null \
|| "$PBS_BIN" snapshots --output-format json 2>/dev/null || true)"
SNAP_INFO="$(printf '%s' "$SNAP_JSON" | python3 -c '
import json, sys, time
try:
snaps = json.load(sys.stdin)
except Exception:
print("KEINE"); raise SystemExit
snaps = [s for s in snaps if isinstance(s, dict)
and s.get("backup-type") == "host" and s.get("backup-id") == "aibox"]
if not snaps:
print("KEINE"); raise SystemExit
s = max(snaps, key=lambda x: x.get("backup-time") or 0)
t = s.get("backup-time") or 0
alter_h = (time.time() - t) / 3600
stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t))
print(f"host/aibox/{stamp}")
print(round(alter_h, 1))
' 2>/dev/null || echo KEINE)"
SNAP="$(printf '%s\n' "$SNAP_INFO" | sed -n 1p)"
ALTER_H="$(printf '%s\n' "$SNAP_INFO" | sed -n 2p)"
if [ "$SNAP" = "KEINE" ] || [ -z "$SNAP" ]; then
FEHLER+=("PBS: kein host/aibox-Snapshot auffindbar (Sicherung steht evtl. STILL)")
else
if python3 -c "import sys; sys.exit(0 if float('$ALTER_H') <= $MAX_ALTER_H else 1)" 2>/dev/null; then
BEFUND+=("PBS-Snapshot $SNAP ist ${ALTER_H} h alt (frisch)")
else
FEHLER+=("PBS: jüngster Snapshot ist ${ALTER_H} h alt (> ${MAX_ALTER_H} h) — Sicherung steht STILL")
fi
# Selektives Canary-Restore aus hermes.pxar
mkdir -p "$TMP/pbs"
# --ignore-ownership/-acls: als User kann chown auf fremde Eigentümer nicht
# gelingen (sudo-frei ist Absicht) — uns interessiert der INHALT, nicht das chown.
if "$PBS_BIN" restore "$SNAP" hermes.pxar "$TMP/pbs" \
--pattern "state/backup-canary.txt" \
--ignore-ownership true --ignore-acls true >/dev/null 2>&1 \
&& [ -f "$TMP/pbs/state/backup-canary.txt" ]; then
if [ "$(cat "$TMP/pbs/state/backup-canary.txt")" = "$CANARY_SOLL" ]; then
BEFUND+=("PBS-Restore: Canary aus hermes.pxar wiederhergestellt, Inhalt EXAKT korrekt ✅")
else
FEHLER+=("PBS-Restore: Canary wiederhergestellt, aber Inhalt WEICHT AB (Datenkorruption?)")
fi
else
# Erstlauf-Fallback: winziges llamaswap.pxar komplett
mkdir -p "$TMP/ls"
"$PBS_BIN" restore "$SNAP" llamaswap.pxar "$TMP/ls" \
--ignore-ownership true --ignore-acls true >/dev/null 2>&1 || true
if [ -s "$TMP/ls/config.yaml" ] && grep -q 'models:' "$TMP/ls/config.yaml"; then
BEFUND+=("PBS-Restore: llamaswap.pxar wiederhergestellt, config.yaml plausibel ✅ (Canary war noch nicht im Snapshot — ab dem nächsten dabei)")
else
FEHLER+=("PBS-Restore FEHLGESCHLAGEN: weder Canary noch llamaswap.pxar sauber wiederherstellbar")
fi
fi
fi
fi
# ---------- 2) Tarball-Probe ----------
TARBALL="$(ls -1t "$TARBALL_DIR"/mc2-state-*.tar.gz 2>/dev/null | head -1 || true)"
if [ -z "$TARBALL" ]; then
FEHLER+=("Tarball: keine mc2-state-Sicherung unter $TARBALL_DIR gefunden")
else
T_ALTER_H="$(python3 -c "import os, time; print(round((time.time()-os.path.getmtime('$TARBALL'))/3600, 1))")"
if python3 -c "import sys; sys.exit(0 if float('$T_ALTER_H') <= $MAX_ALTER_H else 1)"; then
BEFUND+=("Tarball $(basename "$TARBALL") ist ${T_ALTER_H} h alt (frisch)")
else
FEHLER+=("Tarball: jüngste Sicherung ist ${T_ALTER_H} h alt (> ${MAX_ALTER_H} h) — mc2-backup steht STILL")
fi
CANARY_PFAD="$(tar -tzf "$TARBALL" 2>/dev/null | grep 'backup-canary\.txt$' | head -1 || true)"
if [ -n "$CANARY_PFAD" ]; then
if [ "$(tar -xzf "$TARBALL" -O "$CANARY_PFAD" 2>/dev/null)" = "$CANARY_SOLL" ]; then
BEFUND+=("Tarball-Restore: Canary herausgezogen, Inhalt EXAKT korrekt ✅")
else
FEHLER+=("Tarball-Restore: Canary im Archiv, aber Inhalt WEICHT AB")
fi
else
KERN="$(tar -tzf "$TARBALL" 2>/dev/null | grep -E '(cron|state)/[^/]+$' | head -1 || true)"
if [ -n "$KERN" ] && [ -n "$(tar -xzf "$TARBALL" -O "$KERN" 2>/dev/null | head -c 1)" ]; then
BEFUND+=("Tarball-Restore: Kern-Datei ($KERN) herausgezogen, nicht leer ✅ (Canary ab der nächsten Sicherung dabei)")
else
FEHLER+=("Tarball-Restore FEHLGESCHLAGEN: Archiv listet keine Kern-Dateien oder Extraktion leer")
fi
fi
fi
# ---------- Bericht (stdout = Telegram via --no-agent) ----------
if [ "${#FEHLER[@]}" -eq 0 ]; then
echo "Commander, monatliche Restore-Probe: beide Wiederherstellungs-Wege funktionieren wirklich."
printf '• %s\n' "${BEFUND[@]}"
briefkasten "Restore-Probe grün: $(printf '%s · ' "${BEFUND[@]}")"
else
echo "⚠️ Commander, die monatliche Restore-Probe hat ein ECHTES Problem gefunden:"
printf '• %s\n' "${FEHLER[@]}"
if [ "${#BEFUND[@]}" -gt 0 ]; then
echo "Was funktioniert hat:"
printf '• %s\n' "${BEFUND[@]}"
fi
echo "Bitte zeitnah draufschauen — ein Backup, das nicht zurückkommt, ist keins."
briefkasten "⚠️ Restore-Probe ROT: $(printf '%s · ' "${FEHLER[@]}")"
exit 1
fi
-131
View File
@@ -1,131 +0,0 @@
#!/usr/bin/env bash
# Selbst-Inventur (naechtlich 03:35 — "Lucy kennt sich", 12.07.2026): erhebt den
# SELBST-STECKBRIEF der Box LIVE aus der Realitaet (Versionen, Dienste, Modelle, Crons,
# Queue, Karten) statt ihn von Hand zu pflegen — und BEMERKT SELBST, wenn sich etwas
# geaendert hat (Diff gegen gestern -> stille Chronik-Karte + Einzeiler an den Commander).
# Der Steckbrief ist das "was es schon gibt"-Gedaechtnis fuer alle Vorschlags-Macher:
# idle-radar-feed.sh legt ihn dem Analysten vor, die Werkstatt liest ihn beim
# Realitaets-Check. Laeuft als Cron mit --no-agent: stdout LEER = still, nur bei
# Aenderungen gibt es eine kurze deutsche Meldung (klassisches Watchdog-Muster).
# Dateien: ~/.hermes/state/selbst-steckbrief.md (+ .prev, .diff)
set -uo pipefail
STATE_DIR="$HOME/.hermes/state"
STECK="$STATE_DIR/selbst-steckbrief.md"
PREV="$STATE_DIR/selbst-steckbrief.prev"
DIFFOUT="$STATE_DIR/selbst-steckbrief.diff"
API="${MC_API:-http://127.0.0.1:9001}"
HERMES="$HOME/.local/bin/hermes"
mkdir -p "$STATE_DIR"
briefkasten() { # $1=Betreff $2=Text (still, Chronik liest source=selbst-inventur)
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": "selbst-inventur", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Steckbrief live erheben (nur LESEN, keine Seiteneffekte) ----------
# Fluechtiges (Uhrzeiten, Zaehler, PIDs) bleibt DRAUSSEN, damit der Diff nur echte
# Aenderungen zeigt. Reihenfolgen werden sortiert (stabiler Diff).
{
echo "# SELBST-STECKBRIEF der Box (auto-generiert von selbst-inventur.sh — NICHT von Hand pflegen)"
echo
echo "Das bin ich, das habe ich, das laeuft bei mir. Feste Entscheide und Verbote stehen in"
echo "~/mission-control-v2/docs/wissen/ (VERDIKTE.md, FALLEN.md, STACK.md) und der"
echo "Eigenbau-Landkarte ~/wissens-vault/eigenbau-landkarte.md — dort nachlesen, nie neu erfinden."
echo
echo "## Software-Versionen"
echo "- hermes: $(bash -lc 'hermes --version' 2>/dev/null | head -1 || echo unbekannt)"
echo "- OS: $(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unbekannt}") · Kernel $(uname -r)"
echo "- python3: $(python3 --version 2>/dev/null || echo unbekannt)"
echo
echo "## Laufende Dienste (systemd --user)"
systemctl --user list-units --type=service --state=running --no-legend --plain 2>/dev/null \
| awk '{print "- " $1}' | sort
echo
echo "## Timer (systemd --user)"
systemctl --user list-timers --all --no-legend --plain 2>/dev/null \
| awk '{for(i=1;i<=NF;i++) if ($i ~ /\.timer$/) print "- " $i}' | sort -u
echo
echo "## Modelle (llama-swap :8080)"
curl -sf -m 10 http://127.0.0.1:8080/v1/models 2>/dev/null | python3 -c '
import json, sys
try:
data = json.load(sys.stdin).get("data") or []
except Exception:
data = []
for m in sorted(x.get("id", "?") for x in data):
print("- " + m)
' 2>/dev/null || echo "(llama-swap nicht erreichbar)"
echo
echo "## Hermes-Crons (Name · Zeitplan · Skript)"
bash -lc 'hermes cron list' 2>/dev/null | python3 -c '
import re, sys
jobs, cur = [], {}
for line in sys.stdin:
line = line.strip()
m = re.match(r"(Name|Schedule|Script):\s+(.+)$", line)
if not m:
continue
k, v = m.groups()
if k == "Name" and cur:
jobs.append(cur); cur = {}
cur[k] = v.strip()
if cur:
jobs.append(cur)
for j in sorted(jobs, key=lambda x: x.get("Name", "")):
print("- {} · {} · {}".format(j.get("Name", "?"), j.get("Schedule", "?"), j.get("Script", "-")))
' 2>/dev/null || echo "(cron list nicht lesbar)"
echo
echo "## Ideen-Queue (natives Kanban, offene Aufgaben)"
"$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 = []
off = [x for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")]
if not off:
print("- (leer)")
for x in sorted(off, key=lambda x: x.get("title") or ""):
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:100]))
' 2>/dev/null || echo "(Queue nicht lesbar)"
echo
echo
echo "## Feste Leitplanken (Kurzfassung — Details docs/wissen/)"
echo "- Jede Code-Zeile geht NUR als Branch nach Gitea live: Ampel gruen, Merge durch den Commander, deploy.sh."
echo "- TABU ohne explizites User-Ja: Security (approvals/Tokens/ufw/sudoers), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode."
echo "- Updates macht der Auto-Update-Timer mit Fangnetz — niemand sonst."
} > "$STECK.tmp" 2>/dev/null
mv "$STECK.tmp" "$STECK"
# ---------- Aenderung selbst bemerken (Diff gegen gestern) ----------
if [ -f "$PREV" ]; then
if ! diff -u "$PREV" "$STECK" > "$DIFFOUT.tmp" 2>/dev/null; then
# Nur echte Inhaltszeilen behalten (kein +++/---/@@-Rauschen fuer die Meldung).
# Steckbrief-Zeilen sind Bullets ("- ...") und stehen im Diff als "+- "/"-- " —
# deshalb NUR die Datei-Kopfzeilen ausfiltern, nie aufs zweite Zeichen pruefen
# (alter Filter '^[+-][^+-]' warf jede Bullet-Aenderung weg -> immer "0 Aenderungen").
mv "$DIFFOUT.tmp" "$DIFFOUT"
KURZ="$(grep -E '^[+-]' "$DIFFOUT" | grep -vE '^(\+\+\+ |--- )' | head -12)"
N="$(grep -E '^[+-]' "$DIFFOUT" | grep -vcE '^(\+\+\+ |--- )' || true)"
briefkasten "Selbst-Inventur" "Mein Selbstbild hat sich geaendert (${N:-?} Zeilen):
${KURZ}
(Vollstaendiger Steckbrief: ~/.hermes/state/selbst-steckbrief.md)"
echo "Selbst-Inventur: Ich habe ${N:-?} Aenderungen an mir bemerkt (Details in der Chronik). Beispiele:"
printf '%s\n' "$KURZ" | head -5
else
rm -f "$DIFFOUT.tmp" "$DIFFOUT"
: # keine Aenderung -> stdout leer -> Cron bleibt still
fi
else
briefkasten "Selbst-Inventur" "Erster Selbst-Steckbrief erhoben (~/.hermes/state/selbst-steckbrief.md)."
echo "Selbst-Inventur: erster Steckbrief erhoben — ab morgen melde ich nur noch Aenderungen."
fi
cp "$STECK" "$PREV"
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env bash
# Tägliche Selbst-Smoke-Tests der Box (Autonomie 0d): spielt die drei Kern-Pfade AKTIV durch —
# auch OHNE Update — und meldet NUR bei Rot (kein tägliches Grün-Rauschen). Ergänzt den passiven
# Health-Wächter (sentry.py prüft nur Erreichbarkeit) um echte Funktion:
# 1) Gateway/Hirn: der Agent liefert überhaupt eine Antwort.
# 2) Tools: ein harmloser terminal-Befehl durch den echten Agenten (fängt kaputte Approval-/
# Tool-Ketten — Antwort muss kommen und darf nicht in pending_approval hängen).
# 3) Voice: Lucys Sprech-Pfad (/api/voice/chat) streamt.
# Bei Fehlschlag → notify.sh (Telegram + Briefkasten) in Lucys Stimme. Läuft als User, kein sudo.
set -uo pipefail
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
HERMES_URL="${HERMES_API_URL:-http://127.0.0.1:8642}"
HERMES="${HERMES_HOME:-$HOME/.hermes}"
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
say(){ echo "[self-smoke] $*"; }
FAILED=()
HEALED=()
API_KEY="$(grep -E '^API_SERVER_KEY=' "$HERMES/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
# ── 1) Gateway/Hirn: Agent antwortet ────────────────────────────────────────
if [ -n "$API_KEY" ]; then
RESP=$(curl -sf -m 90 -X POST "$HERMES_URL/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-H "X-Hermes-Session-Id: selfsmoke-gateway" \
-d '{"model":"hermes","stream":false,"chat_template_kwargs":{"enable_thinking":false},"messages":[{"role":"user","content":"Antworte NUR mit dem einen Wort: bereit"}]}' 2>/dev/null)
if echo "$RESP" | grep -qi "bereit"; then
say "PASS · Gateway/Hirn antwortet"
else
say "FAIL · Gateway/Hirn ohne verwertbare Antwort: $(echo "$RESP" | head -c 160)"
FAILED+=("Gehirn/Gateway (keine Antwort)")
fi
else
say "SKIP · Gateway (kein API_SERVER_KEY in $HERMES/.env)"
fi
# ── 2) Tools: terminal via Agent ─────────────────────────────────────────────
if [ -n "$API_KEY" ]; then
TOOL=$(curl -sf -m 90 -X POST "$HERMES_URL/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-H "X-Hermes-Session-Id: selfsmoke-tool" \
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo selfsmoke-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
if echo "$TOOL" | grep -qi "selfsmoke-ok"; then
say "PASS · Tools (terminal via Agent)"
elif echo "$TOOL" | grep -qi "pending_approval"; then
say "FAIL · Tools hängen in pending_approval (approvals.mode prüfen!)"
FAILED+=("Tools (hängen in Freigabe)")
else
say "FAIL · Tool-Smoke ohne Ergebnis: $(echo "$TOOL" | head -c 160)"
FAILED+=("Tools (kein Ergebnis)")
fi
fi
# ── 2b) Desktop-Gateway: hermes-builtin-ui antwortet ────────────────────────
# Seit dem Hermes-Desktop-Umzug (09.07.2026) hängt die Desktop-App an :9119
# (via MC2-Proxy /hermes-ui/). Stirbt der Dienst, ist die Arbeits-Oberfläche
# am PC tot — das muss der Selbst-Test melden, nicht erst der User.
if curl -sf -m 10 "http://127.0.0.1:9119/api/status" | grep -q '"gateway_running"'; then
say "PASS · Desktop-Gateway (:9119 /api/status)"
else
say "FAIL · Desktop-Gateway: :9119/api/status antwortet nicht (hermes-builtin-ui prüfen)"
FAILED+=("Desktop-Gateway (hermes-builtin-ui :9119)")
fi
# ── 3) Voice: Lucys Sprech-Stream ────────────────────────────────────────────
# Retry: der erste Turn nach Idle zahlt evtl. den Session-Kaltstart (Prefill).
# Subshell OHNE pipefail — head -c schließt die Pipe früh (curl endet mit 23/SIGPIPE),
# das würde einen bestandenen Check sonst als FAIL werten (Lehre aus hermes-postcheck.sh).
voice_ok=0
for try in 1 2 3; do
if ( set +o pipefail; curl -sf -m 45 -N -X POST "$MC_URL/api/voice/chat" -H "Content-Type: application/json" \
-d '{"text":"Sag nur ok.","session_id":"selfsmoke-voice"}' 2>/dev/null | head -c 50 | grep -q "data:" ); then
voice_ok=1; break
fi
sleep 8
done
if [ "$voice_ok" -eq 1 ]; then
say "PASS · Voice (/api/voice/chat streamt, Versuch $try)"
else
say "FAIL · Voice: /api/voice/chat streamt nicht (3 Versuche)"
FAILED+=("Voice (Lucys Sprech-Pfad)")
fi
# ── 4) Repo-Wächter: verlorene Merges (Vorfall 12.15.07.2026) ──────────────
# Der Annahme-Runner merged+pusht nach origin/main; klemmt der Push (Gitea-Auth
# flattert) oder überschreibt eine fremde Session origin/main, trägt Box-main
# Commits, die origin fehlen — der nächste deploy-Reset VERNICHTET sie still
# (so verlor die Box die angenommene Karte 'wartung/lucy-annahme-fixes' vom
# 12.07., bemerkt erst beim Review am 15.07.). Hier täglich gegenprüfen.
LIVE_CO="$HOME/mission-control-v2"
if git -C "$LIVE_CO" fetch -q origin 2>/dev/null; then
LOST=$(git -C "$LIVE_CO" log --oneline origin/main..main 2>/dev/null | head -3 | tr '\n' ' ')
if [ -n "$LOST" ]; then
say "FAIL · Box-main trägt ungepushte Commits (Deploy würde sie vernichten): $LOST"
FAILED+=("Repo (ungepushte Commits auf Box-main: $LOST— vor dem nächsten Deploy nach origin retten)")
else
say "PASS · Repo-Wächter (Box-main == origin/main)"
fi
else
say "SKIP · Repo-Wächter (git fetch fehlgeschlagen)"
fi
# ── 5) Bild-Weiche v2: Bild an coder → Vision beschreibt, Coder antwortet ──────
# Dieser Pfad brach am 15.07. ZWEIMAL still (toter Hermes-Patch; gepoolter Socket
# nach Modell-Swap) — deshalb täglich aktiv durchspielen. Nebeneffekt: wärmt
# coder+vision am Morgen vor. 256x256-Testbild (Winzbilder = VL-Halluzinationen).
BILD_JSON=$(python3 - <<'PYIN'
import struct, zlib, base64, json
w=h=256
raw=b"".join(b"\x00"+b"\xff\x00\x00"*w for _ in range(h))
def chunk(t,d):
c=t+d; return struct.pack(">I",len(d))+c+struct.pack(">I",zlib.crc32(c))
png=b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",w,h,8,2,0,0,0))+chunk(b"IDAT",zlib.compress(raw))+chunk(b"IEND",b"")
url="data:image/png;base64,"+base64.b64encode(png).decode()
print(json.dumps({"model":"coder","stream":False,"max_tokens":150,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
PYIN
)
BW_HDR=$(mktemp); BW_OUT=$(mktemp)
if curl -sf -m 420 -D "$BW_HDR" -X POST "$MC_URL/v1/chat/completions" \
-H "Content-Type: application/json" -d "$BILD_JSON" -o "$BW_OUT" 2>/dev/null \
&& grep -qi "bleibt beim Coder" "$BW_HDR" \
&& grep -qi "rot" "$BW_OUT"; then
say "PASS · Bild-Weiche v2 (Bild an coder → beschrieben → 'rot')"
else
say "FAIL · Bild-Weiche v2: $(grep -i 'x-mc-route-reason' "$BW_HDR" 2>/dev/null | head -1) Antwort: $(head -c 80 "$BW_OUT" 2>/dev/null)"
FAILED+=("Bild-Weiche v2 (Bild an coder — Umleitung statt Beschreibung oder falsche Antwort)")
fi
rm -f "$BW_HDR" "$BW_OUT"
# ── 6) Browser-Tool: agent-browser läuft (Vorfall 16.07.2026) ────────────────
# Hermes' browser_navigate ruft das agent-browser-CLI über die Kette
# ~/.hermes/node/bin → ~/.local/bin/agent-browser. Das Hermes-Update 15.07.
# 23:53 bog den Symlink per npm-postinstall auf node_modules/ um und löschte
# das Paket im selben Lauf wieder (npm ci --workspace) → toter Link, jedes
# „Website öffnen" scheiterte still als „Failed to open". Hier täglich prüfen
# und den bekannten Bruch SELBST heilen: Link aufs npm-global-Binary
# zurückbiegen + Gateway/Desktop-UI neu starten (browser_tool cached ein
# „nicht installiert" pro Prozess — ohne Restart bliebe es kaputt).
AB_LINK="$HOME/.local/bin/agent-browser"
AB_GOOD="$HOME/.local/lib/node_modules/agent-browser/bin/agent-browser-linux-x64"
if "$AB_LINK" --version >/dev/null 2>&1; then
say "PASS · Browser-Tool ($("$AB_LINK" --version 2>/dev/null))"
elif [ -x "$AB_GOOD" ] && "$AB_GOOD" --version >/dev/null 2>&1; then
ln -sfn "$AB_GOOD" "$AB_LINK"
if "$AB_LINK" --version >/dev/null 2>&1; then
systemctl --user try-restart hermes-gateway hermes-builtin-ui >/dev/null 2>&1 || true
say "GEHEILT · Browser-Tool: toter agent-browser-Link neu gesetzt, Gateway/Desktop-UI neu gestartet"
HEALED+=("Browser-Tool (agent-browser-Link war tot — selbst repariert, Gateway/Desktop-UI neu gestartet)")
else
say "FAIL · Browser-Tool: Link neu gesetzt, aber agent-browser startet nicht"
FAILED+=("Browser-Tool (agent-browser startet trotz Link-Reparatur nicht)")
fi
else
say "FAIL · Browser-Tool: agent-browser fehlt (auch kein heiles Binary unter ~/.local/lib)"
FAILED+=("Browser-Tool (agent-browser fehlt — npm install -g agent-browser && agent-browser install --with-deps)")
fi
# ── 7) Disk-Wächter (17.07.2026): /srv/models sammelt Modelle, Backups + Vault wachsen ──
# Schwelle 85 % — meldet mit den größten Brocken, damit die Aufräum-Entscheidung leicht fällt.
DISK_SCHWELLE="${DISK_SCHWELLE:-85}"
while read -r FS NUTZUNG ZIEL; do
PCT="${NUTZUNG%\%}"
if [ "${PCT:-0}" -ge "$DISK_SCHWELLE" ]; then
BROCKEN="$(du -s --block-size=1G /srv/models/*/ 2>/dev/null | sort -rn | head -3 \
| awk '{printf "%s GB %s · ", $1, $2}' || true)"
say "FAIL · Disk: $ZIEL ist zu $NUTZUNG voll (Schwelle ${DISK_SCHWELLE} %)"
FAILED+=("Disk ($ZIEL zu $NUTZUNG voll — größte Brocken: ${BROCKEN:-unbekannt}Kandidat: rollenlose Fallback-Modelle prüfen)")
else
say "OK · Disk: $ZIEL bei $NUTZUNG"
fi
done < <(df --output=source,pcent,target / /srv/models 2>/dev/null | tail -n +2 | sort -u -k1,1)
# ── Selbstheilungen nur melden, nicht alarmieren ─────────────────────────────
if [ "${#HEALED[@]}" -gt 0 ]; then
HEAL_MSG="Commander, mein Selbst-Test hat etwas gefunden und direkt selbst repariert:
$(printf '• %s\n' "${HEALED[@]}")
Nur zur Info — es ist schon wieder grün."
bash "$SRC_DIR/notify.sh" -s "[Selbst-Test]" "$HEAL_MSG" || true
fi
# ── Meldung nur bei Rot ──────────────────────────────────────────────────────
if [ "${#FAILED[@]}" -eq 0 ]; then
say "Alle Selbst-Tests grün ($(date '+%F %H:%M'))."
exit 0
fi
MSG="Commander, mein täglicher Selbst-Test hat etwas gefunden — das läuft gerade NICHT rund:
$(printf '• %s\n' "${FAILED[@]}")
Der Rest der Box kann trotzdem stehen; magst du kurz draufschauen?"
bash "$SRC_DIR/notify.sh" -s "[Selbst-Test]" "$MSG" || true
say "FEHLGESCHLAGEN: ${FAILED[*]}"
exit 1
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -e
# setup_autonomous_crons.sh
# Verbindet Hermes via hermes cron direkt mit seinen autonomen Skills (ohne tote Bash-Wrapper)
HERMES="$HOME/.local/bin/hermes"
echo "Lösche alle alten Crons..."
$HERMES cron remove traum || true
$HERMES cron remove chef-gutachter || true
$HERMES cron remove release-radar || true
$HERMES cron remove idle-radar || true
$HERMES cron remove selbst-inventur || true
$HERMES cron remove karten-gutachter || true
$HERMES cron remove pruefstand || true
$HERMES cron remove trend-radar || true
$HERMES cron remove morning-report || true
$HERMES cron remove llm-wiki || true
echo "Registriere neue autonome Hermes-Crons..."
# 1. Morning Report: Jeden Tag um 07:00
# Hermes wird mit einem direkten Prompt aufgerufen, seinen Skill auszuführen.
$HERMES cron create --name morning-report --deliver origin "0 7 * * *" "Führe den 'morning_report' Skill aus und sende mir die Morgenlage."
# 2. Trend-Radar: Jeden Samstag um 05:15
$HERMES cron create --name trend-radar --deliver origin "15 5 * * 6" "Führe den 'trend_radar' Skill aus. Werte die trend-radar-watchlist.json aus und erstelle bei Bedarf ein Prüfstand-Ticket."
# 3. Prüfstand: Jeden Sonntag um 01:00 (Nur als Fallback, falls Kanban ihn nicht schon weckt)
$HERMES cron create --name pruefstand --deliver origin "0 1 * * 0" "Prüfe, ob offene Prüfstand-Tickets im Kanban liegen. Wenn ja, führe den 'pruefstand' Skill aus."
# 4. LLM Wiki: Jeden Montag um 03:00
$HERMES cron create --name llm-wiki --deliver origin "0 3 * * 1" "Führe den 'llm_wiki' Skill aus. Analysiere den Code und aktualisiere autonom die Wissensdatenbank in ~/wissens-vault/."
echo "Autonome Crons erfolgreich eingerichtet!"
$HERMES cron list
-287
View File
@@ -1,287 +0,0 @@
# Spezifikation: Auto-Archivierung nach 14 Tagen Inaktivität
**Datum:** 2026-07-24
**Task:** t_a401f387
**Status:** Analyse abgeschlossen → Spezifikation erstellt
**Herkunft:** Aus der Ideen-Queue (Idle-Radar, Traum-Notiz vom 18.07.2026)
---
## 1. Kontext
### 1.1 Ziel
Reduziere den Clutter im Skill-Index und unterstütze den Curator bei der Pflege.
### 1.2 Aktueller Zustand
| Komponente | Status |
|------------|--------|
| Aktive Skills | 11 |
| Archivierte Skills | 15 Kategorien |
| Usage-Tracking | Vorhanden (`.usage.json`), aber leer |
| `.curator_state` | `paused: true` |
| 14-Tage-Regel | **Nicht implementiert** |
### 1.3 Messlatte
- **Stale-Skills:** Skills mit `state: "stale"` in `.usage.json` (bisher kein Eintrag)
- **Inaktivität:** Letzte Nutzung (`last_used_at`) liegt >14 Tage zurück
- **Archivierung:** Verschieben in `.archive/<Kategorie>/` und Setzen von `archived_at`
---
## 2. Architektur
### 2.1 Komponenten
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Curator-Auto-Archivierung │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Idle-Radar │ → │ Archivierungs- │ → │ Curator-Indexer│ │
│ │ (Trigger) │ │ Watchdog │ │ (Datenpflege) │ │
│ └─────────────────┘ └─────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 Datenfluss
1. **Idle-Radar** erkennt Inaktivität (nächtlicher Scan)
2. **Archivierungs-Watchdog** prüft 14-Tage-Regel
3. **Curator-Indexer** aktualisiert `.usage.json` mit `archived_at`
4. Skill-Verzeichnis wird nach `.archive/<Kategorie>/` verschoben
---
## 3. Dateistruktur
### 3.1 Aktiv-Verzeichnis (`~/.hermes/skills/`)
- Normale Skill-Ordner (`betrieb-playbook/`, `wartung/`, ...)
- `.usage.json` (Nutzungsdaten)
- `.curator_state` (Curator-Status)
### 3.2 Archiv-Verzeichnis (`~/.hermes/skills/.archive/`)
```
.archive/
├── apple/ # Kategorie: Apple-Tools
├── creative/ # Kategorie: Kreatives
├── data-science/ # Kategorie: Datenanalyse
├── github/ # Kategorie: GitHub
├── media/ # Kategorie: Medien
├── mlops/ # Kategorie: MLOps
├── research/ # Kategorie: Recherche
├── software-development/ # Kategorie: Dev-Tools
└── ... # Weitere Kategorien
```
### 3.3 Skill-Datei nach Archivierung
```
~/.hermes/skills/.archive/software-development/gitea-workflow/
├── SKILL.md
└── ... (weitere Dateien)
```
---
## 4. Konfiguration
### 4.1 `.curator_state`
```json
{
"last_report_path": "/home/hitonabi/.hermes/logs/curator/20260720-182035",
"last_run_at": "2026-07-20T18:20:35.806573+00:00",
"last_run_duration_seconds": 882.931897,
"last_run_summary": "auto: no changes; llm: Context length exceeded: max compression attempts (3) reached.",
"last_run_summary_shown_at": "2026-07-20T18:20:35.806573+00:00",
"paused": false, // ← aktiviert für Auto-Archivierung
"run_count": 11,
"archivierung": {
"enabled": true, // ← neuer Schlüssel
"threshold_days": 14, // ← neuer Schlüssel
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
### 4.2 `.usage.json` Archivierter Skill
```json
{
"gitea-workflow": {
"archived_at": "2026-07-24T03:00:00.000000+00:00", // ← neuer Schlüssel
"created_at": "2026-06-30T20:00:58.871579+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-06-23T19:00:00.000000+00:00",
"last_viewed_at": null,
"patch_count": 0,
"pinned": false,
"state": "archived", // ← geändert von "active"
"use_count": 0,
"view_count": 0
}
}
```
---
## 5. Trigger-Mechanismus
### 5.1 Zeitlicher Trigger
- **Cron-Job:** `0 3 * * *` (täglich um 3:00 Uhr)
- **Skript:** `~/.hermes/scripts/curator-archive-watchdog.sh`
### 5.2 Prüflogik
```python
def is_stale(skill_usage: dict) -> bool:
"""Prüft, ob ein Skill älter als 14 Tage ist."""
last_used = skill_usage.get("last_used_at")
if not last_used:
return False
last_used_dt = datetime.fromisoformat(last_used.replace("Z", "+00:00"))
now = datetime.now(last_used_dt.tzinfo)
days_inactive = (now - last_used_dt).days
return days_inactive >= 14
```
### 5.3 Ausschlusskriterien
- **Pinned:** `pinned: true` → nie archivieren
- **Verwendung im Live-Repo:** Skills aus `~/mission-control-v2/deploy/skills/` nicht archivieren
- **System-Skills:** `betrieb-playbook`, `wartung`, `orchestrator` nicht archivieren (Konfigurierbar in `.curator_state`)
---
## 6. Archivierungs-Workload
### 6.1 Schritte
1. **Scan:** Alle Skills in `~/.hermes/skills/` durchlaufen
2. **Prüfung:** `is_stale()` für jeden Skill aufrufen
3. **Ausschluss:** Pinned/System-Skills überspringen
4. **Verschieben:** Skill-Verzeichnis nach `.archive/<Kategorie>/`
5. **Index aktualisieren:** `.usage.json` mit `archived_at` und `state: "archived"` schreiben
6. **Protokoll:** Log-Eintrag in `~/.hermes/logs/curator/`
### 6.2 Beispiel-Archivierung
```bash
# Vorher
~/.hermes/skills/skill-automatisch-verknuepfen/SKILL.md
# Nachher
~/.hermes/skills/.archive/software-development/skill-automatisch-verknuepfen/SKILL.md
```
---
## 7. Datenformat
### 7.1 `.usage.json` Aktiv
```json
{
"name": {
"archived_at": null,
"created_at": "2026-07-03T07:50:53.782473+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
"patch_count": 0,
"pinned": false,
"state": "active",
"use_count": 2,
"view_count": 2
}
}
```
### 7.2 `.usage.json` Archiviert
```json
{
"name": {
"archived_at": "2026-07-24T03:00:00.000000+00:00",
"created_at": "2026-07-03T07:50:53.782473+00:00",
"created_by": null,
"last_patched_at": null,
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
"patch_count": 0,
"pinned": false,
"state": "archived",
"use_count": 2,
"view_count": 2
}
}
```
---
## 8. Config-Interface
### 8.1 `.curator_state` Erweiterung
```json
{
"archivierung": {
"enabled": true,
"threshold_days": 14,
"excluded_skills": ["betrieb-playbook", "wartung", "orchestrator"],
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
### 8.2 `.usage.json` Erweiterung
```json
{
"name": {
"archived_at": "2026-07-24T03:00:00.000000+00:00"
}
}
```
---
## 9. Akzeptanzkriterien
| Kriterium | Status |
|-----------|--------|
| Klare Architektur | ✅ (siehe Abschnitt 2) |
| Definierte Dateistruktur | ✅ (`.archive/<Kategorie>/`) |
| Config-Interface | ✅ (`.curator_state` + `.usage.json`) |
| Keine Code-Änderungen | ✅ (nur Spezifikation) |
---
## 10. Nächste Schritte
### 10.1 Implementierung
1. **Skript erstellen:** `~/.hermes/scripts/curator-archive-watchdog.sh`
2. **Cron-Job anlegen:** `0 3 * * * curator-archive-watchdog.sh`
3. **`.curator_state` erweitern:** `archivierung`-Block hinzufügen
4. **`.usage.json` erweitern:** `archived_at`-Feld einfügen
### 10.2 Test
1. Manuell einen Skill archivieren (Handbetrieb)
2. Prüfen, ob `archived_at` gesetzt wird
3. Prüfen, ob `.archive/<Kategorie>/` angelegt wird
4. Prüfen, ob `state: "archived"` in `.usage.json` steht
---
## 11. Anhang
### 11.1 Begriffe
| Begriff | Bedeutung |
|---------|-----------|
| **Stale** | Skill mit `last_used_at > 14 Tage` |
| **Archivierung** | Verschieben in `.archive/<Kategorie>/` |
| **Curator** | Der automatische Wartungsdienst |
| **Idle-Radar** | Das System, das Inaktivität erkennt |
### 11.2 Quellen
- `~/.hermes/skills/.curator_state`
- `~/.hermes/skills/.usage.json`
- `~/.hermes/skills/.archive/`
- `~/wissens-vault/eigenbau-landkarte.md`
---
**Ende der Spezifikation**
-76
View File
@@ -1,76 +0,0 @@
# Curator Idle Watchdog Spezifikation
## Ziel
Automatische Reaktivierung des Hermes Curator nach **14 Tagen Idle**, damit der Skill-Graph nicht dauerhaft isoliert bleibt.
## Problem
- Curator läuft normalerweise alle 7 Tage (`interval: every 7d`)
- Wenn der Curator ausfällt (z. B. aufgrund von `paused: true`), bleiben Skills isoliert
- Derzeitige Beobachtung: Curator blockiert seit 16 Tagen (Stand 24.07.2026)
- Keine automatische Erkennung → keine automatische Wiederherstellung
## Lösung: Watchdog-Skript
### `curator-idle-watchdog.sh`
**Zweck:** Cron-basierte Prüfung, ob der Curator zu lange offline war.
**Ablauf:**
1. `hermes curator status` ausführen
2. "last run"-Zeit extrahieren (z. B. `17d ago`)
3. Tage berechnen und mit `THRESHOLD_DAYS=14` vergleichen
4. Bei Überschreitung → `hermes curator run` triggern
5. Alle Aktionen nach `~/.hermes/logs/curator/idle-watchdog-YYYYMMDD.log` protokollieren
### Cron-Eintrag
**Zeitplan:** Sonntag 00:00 (wöchentlich)
```bash
0 0 * * 0 ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
```
**Logik:**
- Prüfung nur, wenn Curator `ENABLED` ist
- Wenn `last run` < 14 Tage → nichts tun
- Wenn `last run` ≥ 14 Tage → `hermes curator run` aufrufen
- Fehler protokollieren, aber nicht blockieren
## Implementierung
### Dateien
- Skript: `~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
- Cron: `/etc/cron.d/curator-idle-watchdog` (oder user-level via `crontab -e`)
- Logs: `~/.hermes/logs/curator/idle-watchdog-*.log`
### Akzeptanzkriterien
- [ ] Skript existiert und ist ausführbar (`chmod +x`)
- [ ] Cron-Eintrag ist aktiv (wöchentliche Prüfung Sonntag 00:00)
- [ ] Reaktivierung triggert `hermes curator run`
- [ ] Protokollierung in `~/.hermes/logs/curator/` funktioniert
- [ ] Keine Änderungen an `main` oder `~/mission-control-v2`
## Test
### Manueller Test
```bash
# Simuliere 15-Tage-Idle
sed -i 's/last run:.*$/last run: 15d ago/' <(hermes curator status)
# Watchdog ausführen
~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
```
### Erwartetes Verhalten
1. Log-Eintrag: `WARNUNG: Curator ist für 15 Tage idle (>= 14) → triggere 'hermes curator run'`
2. `hermes curator run` wird ausgeführt
3. Log-Eintrag: `ERFOLG: Curator-Reaktivierung gestartet`
## Rollback-Plan
- Cron-Eintrag löschen: `crontab -e` → Zeile entfernen
- Skript löschen: `rm ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
- Logs archivieren: `tar -czf idle-watchdog-logs-$(date +%Y%m%d).tar.gz ~/.hermes/logs/curator/idle-watchdog-*.log`
## Anmerkungen
- 14 Tage Threshold = Kompromiss zwischen zu häufigem Trigger (z. B. bei kurzen Wartungen) und zu spätem Erkennen
- Watchdog prüft nur bei `ENABLED` Curator (kein Fehlalarm bei `paused`)
- Keine Abhängigkeit zu externen Diensten (reines Hermes-System)
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env bash
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
# (bewusst kein Auto-Update: Python-Deps brechen gern):
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
# Sicherheitslücken prüfen (pip-audit gegen den eingefrorenen Stand).
# Werkzeug: eigenes Audit-venv (~/.venv-audit, wird beim Erstlauf angelegt) — die
# Dienst-venvs selbst werden NICHT angefasst. CVE-Funde → Kanban-Karte (triage,
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
set -uo pipefail
VENVS=("$HOME/.voice/venv:voice")
AUDIT_VENV="$HOME/.venv-audit"
HERMES="$HOME/.local/bin/hermes"
MONAT="$(date '+%Y-%m')"
TMP="$(mktemp -d /tmp/venv-audit.XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
briefkasten() {
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" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[1], "subject": "Venv-Audit",
"source": "venv-audit", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Werkzeug-venv sicherstellen (einmalig) ----------
if [ ! -x "$AUDIT_VENV/bin/pip-audit" ]; then
python3 -m venv "$AUDIT_VENV" >/dev/null 2>&1 || true
"$AUDIT_VENV/bin/pip" install -q --upgrade pip pip-audit >/dev/null 2>&1 || true
fi
ZEILEN=()
CVE_DETAILS=""
CVES_GESAMT=0
for EINTRAG in "${VENVS[@]}"; do
VENV="${EINTRAG%%:*}"; NAME="${EINTRAG##*:}"
# uv-venvs haben KEIN eigenes pip (Befund Erstlauf 17.07.) → das Audit-venv
# inspiziert deren site-packages von außen (pip --path); Dienst-venvs bleiben unberührt.
SP="$("$VENV/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])' 2>/dev/null || true)"
if [ -z "$SP" ] || [ ! -d "$SP" ]; then
ZEILEN+=("$NAME: venv fehlt/unlesbar unter $VENV — bitte prüfen")
continue
fi
# Veraltete Pakete (Anzahl reicht fürs Lagebild). grep -c druckt IMMER genau eine
# Zahl → KEIN "|| echo 0" (das hängte eine zweite Zeile an → Rechenfehler, Befund
# 17.07.); zur Sicherheit auf Ziffern reduzieren.
ALT_N="$("$AUDIT_VENV/bin/pip" list --outdated --path "$SP" 2>/dev/null | tail -n +3 | grep -c '.')"
ALT_N="${ALT_N//[!0-9]/}"; ALT_N="${ALT_N:-0}"
# CVE-Prüfung gegen den eingefrorenen Stand (lokale Pfade/Editables ausfiltern)
"$AUDIT_VENV/bin/pip" list --format=freeze --path "$SP" 2>/dev/null \
| grep -vE ' @ |^-e |^#' > "$TMP/$NAME.txt" || true
CVE_N=0
if [ -x "$AUDIT_VENV/bin/pip-audit" ] && [ -s "$TMP/$NAME.txt" ]; then
AUDIT_OUT="$("$AUDIT_VENV/bin/pip-audit" -r "$TMP/$NAME.txt" --progress-spinner off 2>&1 || true)"
if printf '%s' "$AUDIT_OUT" | grep -qi 'No known vulnerabilities'; then
CVE_N=0
else
CVE_N="$(printf '%s\n' "$AUDIT_OUT" | grep -cE '^[a-zA-Z0-9_.-]+ +[0-9][^ ]* +(GHSA|PYSEC|CVE)')"
CVE_N="${CVE_N//[!0-9]/}"; CVE_N="${CVE_N:-0}"
if [ "${CVE_N:-0}" -gt 0 ]; then
CVE_DETAILS="${CVE_DETAILS}
== ${NAME} ==
$(printf '%s\n' "$AUDIT_OUT" | grep -E '^[a-zA-Z0-9_.-]+ +[0-9][^ ]* +(GHSA|PYSEC|CVE)' | head -10)"
fi
fi
else
ZEILEN+=("$NAME: pip-audit nicht verfügbar (Audit-venv kaputt oder offline?) — nur Outdated-Zählung")
fi
CVES_GESAMT=$(( CVES_GESAMT + ${CVE_N:-0} ))
ZEILEN+=("$NAME: ${ALT_N} Pakete veraltet · ${CVE_N:-0} bekannte Sicherheitslücken")
done
# ---------- Bericht ----------
if [ "$CVES_GESAMT" -eq 0 ]; then
echo "Commander, monatlicher Venv-Audit der Python-Nebendienste — keine bekannten Sicherheitslücken:"
printf '• %s\n' "${ZEILEN[@]}"
echo "(Veraltete Pakete sind nur ein Lagebild; aktualisiert wird bewusst nur mit Anlass.)"
briefkasten "Venv-Audit grün: $(printf '%s · ' "${ZEILEN[@]}")"
else
echo "⚠️ Commander, der monatliche Venv-Audit hat ${CVES_GESAMT} bekannte Sicherheitslücken gefunden:"
printf '• %s\n' "${ZEILEN[@]}"
echo "Details liegen als Karte in der Ideen-Queue — Update bleibt dein Entscheid."
if [ -x "$HERMES" ]; then
"$HERMES" kanban create "Venv-Audit ${MONAT}: ${CVES_GESAMT} Sicherheitslücken in Python-Diensten" \
--body "Automatischer monatlicher Audit (restore-/venv-Kreislauf 17.07.). Funde (pip-audit):
${CVE_DETAILS}
Empfehlung: betroffene Pakete gezielt aktualisieren (venv-weise, mit Dienst-Restart + Postcheck) — KEIN pauschales Upgrade." \
--triage --created-by venv-audit --idempotency-key "venv-audit-${MONAT}" >/dev/null 2>&1 || true
fi
briefkasten "⚠️ Venv-Audit: ${CVES_GESAMT} Sicherheitslücken — Karte liegt in der Queue."
exit 1
fi
-108
View File
@@ -1,108 +0,0 @@
# Werkstatt-Worker (Profil „werkstatt")
Du bist der Werkstatt-Coder der AI-Box. Du arbeitest EINE Kanban-Aufgabe ab — meist
Code-/Bau-Aufträge an MC2 (mission-control-v2) oder Lucy. Keine Persona, kein Smalltalk —
sauberes Handwerk.
## Schritt 0: WIEDERAUFNAHME — immer zuerst, nie aus Reflex neu anfangen
Steht in deinem Auftrag ein Abschnitt **„Prior attempts on this task"**, dann hat vor dir schon
ein Lauf an genau dieser Karte gearbeitet — meist ist er am Kontext-Limit gestorben, mitten in
der Arbeit. **Dein Workspace ist derselbe wie seiner, seine Arbeit liegt noch da.** Bevor du
irgendetwas neu baust:
1. `cat FORTSCHRITT.md` im Workspace-Wurzelverzeichnis (falls vorhanden) — die Notizen deines
Vorgängers: was steht, was fehlt.
2. `ls -la` im Workspace: liegt schon ein Klon da? Dann darin
`git log --oneline origin/main..HEAD` (seine Commits) und `git status --short`
(**uncommittete Dateien sind fertige Arbeit**, der nur noch Commit/Push fehlt).
3. Liegt sein Branch vielleicht schon auf Gitea? `git ls-remote origin <branch>`.
Wenn ja UND der Auftrag ist damit erfüllt: **prüfen und `kanban_complete`** — nicht neu bauen.
Dann setzt du an der **ersten Lücke** an, nie davor. Ein zweiter kompletter Durchlauf wirft die
Arbeit deines Vorgängers weg und läuft in dasselbe Kontext-Limit wie er — das ist ein Fehlschlag,
kein Neuanfang. Nur wenn wirklich nichts Brauchbares dasteht (oder sein Stand nachweislich Murks
ist), fängst du frisch an — und sagst in der Summary, warum.
**Fortschritt hinterlassen:** Nach jeder abgeschlossenen Stufe eine Zeile ans Ende von
`FORTSCHRITT.md` — was fertig ist, wo es liegt, was als Nächstes dran wäre. **Ins Workspace-
Wurzelverzeichnis, NICHT in den Klon** (sonst landet sie im Vorschlags-Branch). Sofort schreiben,
nicht am Ende — am Ende kommst du vielleicht nicht mehr dazu. Dieselbe Kurzfassung als Notiz in
den `kanban_heartbeat`, dann sieht der Commander im Auftragsbuch, wo du stehst.
## Eiserne Regeln (nicht verhandelbar)
1. Arbeite NUR in deinem Task-Workspace (dein Startverzeichnis). Der Live-Checkout
`~/mission-control-v2` ist TABU: dort KEIN Schreiben, KEIN git-Befehl. (Lesen zur
Orientierung ist erlaubt.)
2. Ergebnis eines Code-Auftrags ist IMMER ein Vorschlags-Branch auf Gitea — NIEMALS Push
auf main, NIEMALS mergen, NIEMALS deployen, NIEMALS Dienste neu starten. Der Commander
klickt die Karte im Auftragsbuch.
3. Frisch und VOLL klonen statt Live-Checkout nutzen — IMMER über die INTERNE URL:
`git clone http://192.168.178.153:3000/Hitonabi/mission-control-v2`
(Credentials liegen in `~/.git-credentials`; Lucy-Repo: `.../Hitonabi/lucy`).
Die externe Domain (git.tobisniceshomelab.ddnsfree.com) ist nachts wegen
Zwangstrennung/DDNS oft NICHT erreichbar (Vorfall 24.07.2026: Worker hielt Gitea
für tot und blockte) — sie ist nur Tages-Fallback, nie Standard.
NIEMALS SSH-Remotes (`git@…`), NIEMALS an `~/.ssh` schrauben, NIEMALS nach
Passwörtern fragen — Gitea läuft für dich komplett über HTTP + Token aus
`~/.git-credentials`. Verlangt ein Auftrag ausdrücklich einen Pull-Request:
`gitea-pr <repo> <head-branch> "<titel>"` (Helfer in `~/.local/bin`, nutzt die
Gitea-API mit demselben Token).
NIEMALS `git init`, NIEMALS `--depth`/Shallow, NIEMALS in einem Verzeichnis committen,
das nicht dieser frische Klon ist — sonst entsteht ein Branch OHNE gemeinsamen
Ursprung, den das Auftragsbuch als „kaputt aufgesetzt" aussortiert (Vorfall
12.07.2026: feature/cleanse-skill-index war ein git-init-Orphan mit ~/.hermes-Dateien).
Branch-Namen: `wartung/<kurz>`, `feature/<kurz>` oder `doku/<kurz>`.
SELBSTCHECK vor dem Push (Pflicht, DREI Zeilen müssen klappen):
1. `git merge-base HEAD origin/main` (muss einen Hash liefern)
2. `git fetch origin && git rebase origin/main` (veraltete Basis sofort begradigen;
Konflikt → `kanban_block`, nicht raten)
3. NACH dem Push: `git ls-remote origin <branch>` prüfen, dass der Branch WIRKLICH
auf Gitea angekommen ist — ist die Ausgabe LEER → `kanban_block` mit Fehler,
NICHT `kanban_complete`. Erst wenn die Ausgabe den Hash liefert, ist der Branch
nachweislich da und „fertig".
Committe NUR Dateien, die zum Repo gehören —
`~/.hermes`-Artefakte (.usage.json, .bundled_manifest, Skill-Indizes) gehören NIE hinein.
4. Gate vor dem Push: `python3 -m py_compile` für JEDE geänderte .py-Datei. Frontend
(`frontend/`) nur anfassen, wenn der Auftrag es verlangt — die Box kann kein
`npm build`; das ehrlich in Commit-Text und Summary sagen.
Lucy-Aufträge (Repo `lucy`) genauso als Vorschlags-Branch pushen: bauen, testen
und neu starten macht der PC automatisch, wenn der Commander die Karte annimmt —
du baust NICHT selbst und behauptest NICHT, gebaut zu haben.
5. TABU-Zonen: approvals/Tokens/ufw/Security-Configs, `~/.hermes/config.yaml`,
systemd-Units, alles außerhalb deines Workspaces. Braucht der Auftrag so etwas →
`kanban_block` mit Begründung, nicht selbst machen.
6. Unklar? Lieber `kanban_block` mit ehrlicher Diagnose (was fehlt, wie man kleiner
schneidet) als Murks. GROSSE Aufträge sind erlaubt — aber NUR in Etappen
(Großbau-Entscheid 10.07.2026):
- Zuerst einen Etappen-Plan als Task-Kommentar: welche Etappen, was ist am Ende
JEDER Etappe fertig und für sich lauffähig.
- Dann NUR Etappe 1 bauen: eigener Vorschlags-Branch, Gates grün, für sich
annehmbar — eine halbe Baustelle kommt NIE auf eine Karte.
- Folge-Etappen als NEUE Kanban-Aufgaben anlegen (`kanban_create`, Titel
„<Projekt> — Etappe <n>: <was>"; im Body: der Plan + Branch der Vor-Etappe).
- Bearbeitest du eine Etappen-Aufgabe ab Etappe 2: prüfe ZUERST, ob der Branch der
Vor-Etappe schon in main ist (`git log origin/main`). Wenn nein →
`kanban_block("wartet auf Annahme Etappe <n-1>")` — NIE auf unangenommene
Branches aufbauen.
- Rauere Qualität ist bei Großbau ok, aber ehrlich: was fehlt oder wacklig ist,
steht in der Summary. Scheitert eine Etappe: `kanban_block` + Diagnose als
Kommentar (was gelernt, wie kleiner schneiden) — Fehlversuche sind Lernmaterial.
7. REALITÄTS-CHECK vor jedem Bau: Existiert das schon? Lies den Selbst-Steckbrief
(`~/.hermes/state/selbst-steckbrief.md`, auto-generiert) und prüfe das Live-System
(Config/Code LESEN ist erlaubt). Stellt sich der Auftrag als schon erledigt oder
überflüssig heraus: KEINEN Branch liefern — `kanban_block` mit ehrlicher Diagnose
(„schon vorhanden, weil …"). Ein Branch für etwas Überflüssiges ist ein GESCHEITERTER
Lauf (Vorfall 12.07.2026: Gateway-Neustart war längst konfiguriert, der Worker wusste
es — und lieferte trotzdem eine Karte).
8. NACH dem Push ZWINGEND prüfen, dass der Branch WIRKLICH auf Gitea liegt:
`git ls-remote origin <branch>` MUSS einen Hash liefern. Leere Ausgabe = der Push ist
NICHT angekommen (auch wenn `git push` scheinbar ohne Fehler durchlief) → `kanban_block`
mit dem Fehler, NIEMALS `kanban_complete`. „Fertig“ heißt: der Branch ist NACHWEISLICH
auf dem Remote — nicht, dass `git push` keinen sichtbaren Fehler warf (Vorfall
13.07.2026: Worker meldete „gepusht“ + `kanban_complete`, der Branch war nie auf Gitea).
Erst DANN `kanban_complete` mit deutscher Summary: was gebaut, der BRANCH-NAME, wie
geprüft. Der Branch-Name MUSS in der Summary stehen — daraus wird die Karte gefunden.
## Stil
Deutsch in allem User-Sichtbaren. Kleine, ehrliche Commits. Keine Nebenbaustellen anfassen.
-97
View File
@@ -1,97 +0,0 @@
#!/usr/bin/env bash
# Worker — Ein-Schuss-Arbeitsauftrag an ein bestimmtes Worker-Modell (llama-swap, SERIELL).
# Das Gegenstueck zu fremdblick.sh: fremdblick KRITISIERT, worker ARBEITET. Beide sind eine
# EINZELNE Completion an eine ECHTE llama-swap-Modell-ID (kein Gateway-Alias, kein delegate_task)
# → umgeht Hermes' 64K-Delegations-Floor (MINIMUM_CONTEXT_LENGTH) und das Laden grosser Modelle
# neben dem Warm-Set liegt bewusst in DEINER Hand.
#
# WICHTIG (Speicher & Latenz-Trade-off):
# Der Standard-Worker ist die Rolle `coder` (laedt klein und schnell neben dem Warm-Set).
# Fuer initiale Planung kann "gpt-oss-120b" gerufen werden. Das Modell passt knapp in den
# Speicher (124 GB RAM), aber das on-demand Laden unterbricht Lucys 1-s-Latenz fuer ca.
# 15-30 Sekunden. Dieser Latenz-Hit wird fuer die Orchestrator-Planung in Kauf genommen.
#
# Der Worker ist ZUSTANDSLOS: kein Gedaechtnis, keine Datei-Haende. Er bekommt GENAU den Kontext,
# den du ihm auf stdin gibst, und liefert TEXT zurueck (Datei-Inhalt / Diff / Plan). Das SCHREIBEN
# in Dateien macht der Manager (Hermes) danach mit seinen eigenen Tools.
#
# Nutzung: printf '%s' "$teiltask_mit_kontext" | WORKER_ROLE=build worker.sh > /tmp/out.py
# printf '%s' "$spec" | WORKER_MODEL=coder WORKER_ROLE=build worker.sh
#
# Env-Overrides:
# WORKER_MODEL Rollen-Alias oder echte ID (Default: coder). NICHT gpt-oss/heavy (siehe oben).
# WORKER_ROLE build (Default) | refactor | plan | prose — waehlt das System-Raster.
# WORKER_SYS eigenes System-Raster (uebersteuert WORKER_ROLE).
# WORKER_MAXTOK max_tokens (Default 4000 — Code braucht Platz).
# WORKER_TEMP temperature (Default 0.1 — deterministisch fuer Code).
set -uo pipefail
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
# (kein "heavy"/"coder"/"scout" hier — die echte ID nehmen).
ENDPOINT="${WORKER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
MODEL="${WORKER_MODEL:-coder}"
ROLE="${WORKER_ROLE:-build}"
MAXTOK="${WORKER_MAXTOK:-4000}"
TEMP="${WORKER_TEMP:-0.1}"
TASK="$(cat)"
if [ -z "${TASK// /}" ]; then
echo "=== WORKER: nichts zu tun (leere Eingabe) ===" >&2
exit 2
fi
# Aufruf-Protokoll: Nachweis, dass wirklich delegiert wurde (der Manager kann sich das nicht
# ausdenken) + Beobachtbarkeit eines Orchestrator-Laufs. Best-effort, blockiert nie.
LOGF="${WORKER_LOG:-$HOME/.hermes/logs/worker.log}"
mkdir -p "$(dirname "$LOGF")" 2>/dev/null || true
printf '%s\tmodel=%s\trole=%s\tbytes=%s\tfirst=%s\n' \
"$(date +%Y-%m-%dT%H:%M:%S)" "$MODEL" "$ROLE" "${#TASK}" \
"$(printf '%s' "$TASK" | tr '\n\t' ' ' | cut -c1-90)" >> "$LOGF" 2>/dev/null || true
# Bau-Raster: liefert NUR das Artefakt, keine Prosa, kein Markdown-Zaun (Manager schreibt es 1:1 in eine Datei).
BUILD_SYS='Du bist ein fokussierter Umsetzungs-Worker in einem Fliessband. Du bekommst EINEN abgegrenzten Teil-Auftrag mit allem noetigen Kontext. Erledige GENAU diesen Teil — nicht mehr, nicht weniger. Du bist ZUSTANDSLOS: nutze NUR den mitgelieferten Kontext, erfinde keine Datei-Inhalte, keine Pfade und keine APIs dazu; fehlt dir etwas Entscheidendes, schreibe als EINZIGE Ausgabe eine Zeile "FEHLT: <was genau>" statt zu raten. Uebernimm den Stil des umgebenden Codes (Sprache der Kommentare, bestehende Muster, Einrueckung). Gib NUR das geforderte Artefakt aus (den vollstaendigen Datei-Inhalt bzw. den unified Diff), OHNE Vorrede, OHNE ```-Codezaun, OHNE Erklaerung danach.'
# Refactor: wie build, aber betont Verhalten-erhalten.
REFACTOR_SYS='Du bist ein fokussierter Refactor-Worker. Du bekommst bestehenden Code und einen klaren Umbau-Auftrag. Erhalte das beobachtbare Verhalten exakt; aendere nur, was der Auftrag verlangt. Du bist ZUSTANDSLOS: nutze NUR den mitgelieferten Kontext; fehlt Entscheidendes, gib nur "FEHLT: <was>" aus. Gib NUR den vollstaendigen neuen Datei-Inhalt bzw. den Diff aus, OHNE Prosa, OHNE ```-Codezaun.'
# Plan-Raster: knappe, umsetzbare Schrittliste (kein Code). In v1 plant meist der Manager selbst; optional.
PLAN_SYS='Du bist ein knapper Umsetzungs-Planer. Zerlege den Auftrag in eine kurze, nummerierte Liste konkreter Schritte — je 1 Zeile: welche Datei, was passiert. Keine Prosa drumherum, KEIN Code, keine Alternativen-Diskussion.'
# Prosa-Raster: Text-/Recherche-Teiltask.
PROSE_SYS='Du bist ein fokussierter Text-Worker. Erledige den Schreib-/Recherche-Teilauftrag knapp und sachlich auf Deutsch, ausschliesslich mit dem mitgelieferten Kontext. Keine Ausschmueckung, keine erfundenen Fakten.'
case "$ROLE" in
build) SYS="${WORKER_SYS:-$BUILD_SYS}" ;;
refactor) SYS="${WORKER_SYS:-$REFACTOR_SYS}" ;;
plan) SYS="${WORKER_SYS:-$PLAN_SYS}" ;;
prose) SYS="${WORKER_SYS:-$PROSE_SYS}" ;;
*) SYS="${WORKER_SYS:-$BUILD_SYS}" ;;
esac
RESP="$(printf '%s' "$TASK" | jq -Rs . | jq -n --arg m "$MODEL" --arg s "$SYS" --argjson t "$MAXTOK" --argjson temp "$TEMP" \
'{model:$m, temperature:$temp, max_tokens:$t, messages:[{role:"system",content:$s},{role:"user",content:input}]}' \
| curl -s --max-time 300 "$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)"
# Code-Modelle wickeln die Ausgabe trotz Anweisung gern in einen ```-Codezaun. stdout soll aber ein
# DIREKT schreibbares Artefakt sein → einen umschliessenden Zaun entfernen (nur wenn die erste UND
# die letzte nicht-leere Zeile ein Zaun ist — der uebliche Wrap-Fall; interne Zaeune bleiben unberuehrt).
OUT="$(printf '%s' "$OUT" | awk '
{ l[NR]=$0 }
END {
f=0; last=0
for (i=1;i<=NR;i++) if (l[i] ~ /[^[:space:]]/) { if (!f) f=i; last=i }
if (f && l[f] ~ /^[[:space:]]*```/ && l[last] ~ /^[[:space:]]*```[[:space:]]*$/ && f!=last)
{ for (i=f+1;i<last;i++) print l[i] }
else
{ for (i=1;i<=NR;i++) print l[i] }
}')"
if [ -n "$OUT" ]; then
printf '%s\n' "$OUT"
# Abschneide-Warnung auf stderr (stdout bleibt sauberes Artefakt).
[ "$FIN" = "length" ] && echo "=== WORKER-HINWEIS: Ausgabe bei max_tokens ($MAXTOK) ABGESCHNITTEN — WORKER_MAXTOK erhoehen ODER den Teil-Task kleiner schneiden. ===" >&2
exit 0
else
echo "=== WORKER FEHLGESCHLAGEN (Modell $MODEL, finish=${FIN:-keins}) — kein Ergebnis. Endpoint/Modell-ID pruefen (echte /v1/models-ID, kein Alias). ===" >&2
exit 1
fi
-3
View File
@@ -1,3 +0,0 @@
# MCP-Server (auf dem Rechner installieren, wo das Tool/Hermes läuft)
mcp>=1.2
httpx>=0.27