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:
co-authored by
Claude Opus 5.5
parent
e9f488b56c
commit
4cd856bd34
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}'
|
|
||||||
@@ -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("{}")
|
|
||||||
@@ -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}'
|
|
||||||
@@ -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({})
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
@@ -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 $?
|
||||||
|
|||||||
@@ -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)}).")
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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)."
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Projekte-Sync stuendlich
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=hourly
|
||||||
|
RandomizedDelaySec=5min
|
||||||
|
Persistent=true
|
||||||
|
AccuracySec=1min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -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 2–4). 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.
|
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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**
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# MCP-Server (auf dem Rechner installieren, wo das Tool/Hermes läuft)
|
|
||||||
mcp>=1.2
|
|
||||||
httpx>=0.27
|
|
||||||
Reference in New Issue
Block a user