Compare commits
194 Commits
31d6beabbc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 46edbe50c1 | |||
| cd7e1bc114 | |||
| 600d0caad6 | |||
| 733582900a | |||
| 8b14dfdc14 | |||
| dced44548d | |||
| 4c0af92b3c | |||
| 5cc1e6bea1 | |||
| 102149a5d6 | |||
| 0c849f4dcd | |||
| 94490f114d | |||
| c360bbe14e | |||
| 6cb29ee70e | |||
| a6e2f78c27 | |||
| a201c20bf2 | |||
| 56676fe83c | |||
| 77bead5ba4 | |||
| ee456de713 | |||
| e16f0140f7 | |||
| 637c21387a | |||
| da03de18cb | |||
| 1b0184eed8 | |||
| 4fb8387f79 | |||
| cb884abdc8 | |||
| 0a6b6fd18d | |||
| 8f359ef4c9 | |||
| 327d5a495e | |||
| 69925c6cde | |||
| a5410642fa | |||
| c3851f8e25 | |||
| 29f8797fbf | |||
| 9886d80b1c | |||
| 5bc709221e | |||
| 14f8a5f405 | |||
| 16d2a06d95 | |||
| 45f799c57d | |||
| e814122c73 | |||
| d45f2fc6cb | |||
| c2319739e2 | |||
| 1e68f62499 | |||
| 5504918a61 | |||
| 17a129038b | |||
| 93c17fe653 | |||
| ae622dfa88 | |||
| 6637332b3e | |||
| 76d2d7c74b | |||
| 7638f0244d | |||
| 4985977492 | |||
| f65ff48599 | |||
| 87f446c5ae | |||
| f524ef4375 | |||
| 605c9d7dd1 | |||
| 9791f44644 | |||
| 1e3e0066af | |||
| 2365f7aac6 | |||
| 56b374a0aa | |||
| eef7fdb4df | |||
| cd60974ff9 | |||
| 20d4f40ed9 | |||
| cb649aac34 | |||
| d18aac0e1b | |||
| 6c14705798 | |||
| 991c9a2f82 | |||
| ad49dd65ca | |||
| c28aa4a6a2 | |||
| 8b76c0f749 | |||
| a934deee41 | |||
| 543eee95d0 | |||
| 59d6be8c4a | |||
| 7fac17ed9a | |||
| 00f2b71a63 | |||
| 2cecd06c5b | |||
| 775e862652 | |||
| 47f7a85510 | |||
| e6502f676a | |||
| 554c87aaee | |||
| 798de0be8f | |||
| c13cfd2bd0 | |||
| 080eaa5ff5 | |||
| 7dd44a3630 | |||
| 554db10a6f | |||
| ce030d6817 | |||
| 8996c0d012 | |||
| a1de1ec2ac | |||
| 8e735df32d | |||
| 057cc2bb47 | |||
| e686f95545 | |||
| 8a90b7507e | |||
| 59b09e4a9a | |||
| 5894c24703 | |||
| 3f279fc7f2 | |||
| af28a75338 | |||
| a121ca7dc0 | |||
| cce32810fc | |||
| 682be9b63b | |||
| 96c4e417ec | |||
| 90ab873924 | |||
| 6d3a0bf3a6 | |||
| c3fe6dbe8d | |||
| 584f257853 | |||
| 73513f5e5b | |||
| 18f5522612 | |||
| 0cea01c699 | |||
| 94dc3fba9b | |||
| 3cdd5cdd10 | |||
| 9307c8b752 | |||
| 204238925a | |||
| a16cff39ce | |||
| 11c3416571 | |||
| 7389386791 | |||
| 40c8ed4754 | |||
| 656c5045da | |||
| 828dbe13b7 | |||
| 82a3bc3c59 | |||
| d5da853db2 | |||
| 7b9752261a | |||
| f77c09295a | |||
| cfcb5d2db5 | |||
| efca31c21f | |||
| a57e971607 | |||
| bf9c22ab78 | |||
| ff8125d2ac | |||
| 4bb6b28297 | |||
| e3b3725b6f | |||
| 77ae504b58 | |||
| 8153d14d9f | |||
| a91d70d9b7 | |||
| eb1f333bba | |||
| 24e2017d5b | |||
| 2383dd26fa | |||
| aebe10306d | |||
| 6b03a2c3cf | |||
| a1d7a4b6b9 | |||
| 903f3bc074 | |||
| 331aafa25d | |||
| cab748f32e | |||
| d91bd5a139 | |||
| f21fe90090 | |||
| 7b826f66c4 | |||
| e556dcc853 | |||
| b26fb40cbc | |||
| bc60c6fefc | |||
| de21e1ac68 | |||
| 9c03316755 | |||
| f5e362e902 | |||
| 2be599bfe9 | |||
| b0dce954e9 | |||
| 8579fe9934 | |||
| 32c24d7b60 | |||
| 547851f1c3 | |||
| 1daacea468 | |||
| 5aded967ef | |||
| a22f27bc60 | |||
| 31fde2a68e | |||
| 0890698750 | |||
| 77dfac79ee | |||
| fa22435d7c | |||
| 4df6a3ecf9 | |||
| d697f3cc0d | |||
| fd241aca51 | |||
| d2d726c105 | |||
| 71192b3db0 | |||
| 26224804ed | |||
| bc60f9254b | |||
| 0c69c491e5 | |||
| b36561990e | |||
| 41b0edba53 | |||
| b7e6cf4acc | |||
| f271b04754 | |||
| 78bff28acf | |||
| ff6ef3eb1f | |||
| 21ea7e1dd3 | |||
| e91f6b7ba8 | |||
| d0a967e765 | |||
| 87670dd3a6 | |||
| 41e569cf99 | |||
| d2398b3624 | |||
| 202e97333a | |||
| 0eb2b37abc | |||
| 0f13a4f18d | |||
| 5d0a61c38e | |||
| 5dde4c9f5c | |||
| bc69cf5cc9 | |||
| 7223ca96c1 | |||
| 3cf111d973 | |||
| 3468bc9c80 | |||
| ba0458c0af | |||
| fdabe1768e | |||
| bf9fd808d8 | |||
| a9c0239f8a | |||
| ccc9a25a25 | |||
| a3d9c745cd | |||
| b0eec715c8 | |||
| 86bd72838a |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mc2-memory": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
|
||||
]
|
||||
},
|
||||
"mc2-system": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_mc.py"
|
||||
]
|
||||
},
|
||||
"mc2-web": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_web.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,15 @@
|
||||
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
|
||||
"autoPort": false,
|
||||
"port": 5180
|
||||
},
|
||||
{
|
||||
"name": "frontend-mock",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
|
||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
|
||||
"autoPort": false,
|
||||
"port": 5181
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"autoMode": {
|
||||
"allow": [
|
||||
"$defaults",
|
||||
"SSH/scp commands to hitonabi@192.168.178.151 that read or write files under ~/.hermes including ~/.hermes/hermes-agent (the Hermes runtime) — the user authorized direct patching of the box's Hermes runtime for the mission-control-2 project"
|
||||
]
|
||||
}
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git rev-list *)",
|
||||
@@ -87,6 +93,8 @@
|
||||
"WebFetch(domain:dev.to)",
|
||||
"WebFetch(domain:kyuz0.github.io)",
|
||||
"WebFetch(domain:kmarble.dev)"
|
||||
"Bash(ssh -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa hitonabi@192.168.178.151:*)",
|
||||
"Bash(scp -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
# sonst bricht `#!/usr/bin/env python3\r`. (Nur die Hooks + Deploy-Helfer, nicht der ganze Baum.)
|
||||
deploy/agent-hooks/*.py text eol=lf
|
||||
deploy/*.py text eol=lf
|
||||
# `deploy/*.py` greift NUR eine Ebene tief (* matcht kein /). Alles darunter
|
||||
# (z. B. deploy/governor/governor.py) braucht ein eigenes Muster — sonst kommt es
|
||||
# nach einem Windows-Checkout mit CRLF zurück und der Shebang auf der Box bricht.
|
||||
deploy/**/*.py text eol=lf
|
||||
|
||||
# Windows-Batch-Wrapper bleiben CRLF.
|
||||
*.cmd text eol=crlf
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Ampel-CI fuer MC2 — auf dieses Multi-Service-Monorepo zugeschnitten (24.07.2026).
|
||||
#
|
||||
# Die universelle Vorlage (deploy/ampel-ci.yml) passt fuer EIN-Service-Repos. MC2 hat
|
||||
# 5 Python-Dienste (backend/voice_service/mem0_service/mcp/client) mit schweren ML-
|
||||
# Abhaengigkeiten (Whisper/TTS/Embeddings) + ein Frontend. "pip install ALLER requirements
|
||||
# + pytest repo-weit" in einem stateless Container ist weder machbar (GB-schwere Wheels,
|
||||
# CUDA) noch aussagekraeftig — die echten Tests sind der Pruefstand (deploy/pruefstand)
|
||||
# und die Integration auf der Box mit LIVE-Diensten/Modellen, nicht isolierte Unit-Tests.
|
||||
#
|
||||
# Darum prueft die MC2-Ampel, was im Container EHRLICH gruen sein kann und trotzdem echte
|
||||
# Fehler faengt: Lint (ruff, Projekt-Politik in ruff.toml) + Import/Syntax (compileall) +
|
||||
# Frontend-Build inkl. TypeScript-Typecheck (tsc). Rot ist ein Ergebnis, kein Aergernis.
|
||||
name: Ampel
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
ampel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Ampel — Lint + Import + Frontend-Build (MC2-Zuschnitt)
|
||||
shell: bash
|
||||
run: |
|
||||
# Runner-bash laeuft mit -e; abschalten und JEDEN Fehler selbst werten (rot=1),
|
||||
# am Ende EIN Klartext-Urteil (Lehre Ampel-Lauf #5).
|
||||
set -u +e
|
||||
rot=0
|
||||
|
||||
echo "== Python: Lint (ruff, Projekt-Politik aus ruff.toml) =="
|
||||
python3 -m venv /tmp/ampel-venv && . /tmp/ampel-venv/bin/activate || { echo "❌ venv kaputt"; exit 1; }
|
||||
pip install -q ruff || { echo "❌ ruff-Install kaputt"; exit 1; }
|
||||
ruff check . || rot=1
|
||||
|
||||
echo "== Python: Import/Syntax (compileall) =="
|
||||
python3 -m compileall -q backend voice_service mem0_service mcp client deploy hermes scripts || rot=1
|
||||
|
||||
echo "== Frontend: reproduzierbarer Build (npm ci + tsc + vite) =="
|
||||
if [ -f frontend/package.json ]; then
|
||||
if [ ! -f frontend/package-lock.json ]; then
|
||||
echo "❌ frontend/: kein package-lock.json — Build nicht reproduzierbar."
|
||||
rot=1
|
||||
else
|
||||
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
||||
|
||||
if [ $rot -eq 0 ]; then
|
||||
echo "== Frontend: Push nach release-dist =="
|
||||
git config --global user.name "Gitea Actions"
|
||||
git config --global user.email "actions@gitea.local"
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
# Erzeuge (oder hole) den orphan branch 'release-dist'
|
||||
git checkout --orphan release-dist 2>/dev/null || git checkout release-dist
|
||||
git rm -rf . >/dev/null 2>&1 || true
|
||||
|
||||
# Checkout nur frontend/dist vom aktuellen Stand
|
||||
git add -f frontend/dist
|
||||
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
git commit -m "Automatischer Frontend Build ($CURRENT_COMMIT) [skip ci]"
|
||||
# Push to release-dist. Token auth from runner is expected to work for Gitea Actions.
|
||||
git push origin HEAD:release-dist -f
|
||||
else
|
||||
echo "Keine Änderungen am Frontend-Build."
|
||||
fi
|
||||
# Zurück zum originalen Branch für den Rest des Skripts
|
||||
git checkout $CURRENT_COMMIT
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $rot -ne 0 ]; then
|
||||
echo "❌ AMPEL ROT — nichts heißt ‚fertig', solange das rot ist."
|
||||
else
|
||||
echo "✅ AMPEL GRÜN — Lint + Import + Frontend-Build sauber."
|
||||
fi
|
||||
exit $rot
|
||||
@@ -20,3 +20,7 @@ frontend/dist/avatar.vrm
|
||||
box_recon*
|
||||
gemma_swap*
|
||||
|
||||
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||
frontend/tsconfig.tsbuildinfo
|
||||
|
||||
frontend/dist/
|
||||
|
||||
@@ -6,8 +6,15 @@ Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `RE
|
||||
## Was das ist
|
||||
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
||||
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
|
||||
Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
||||
**Hermes-Agent (das autonome Gehirn "Lucy")**. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
||||
|
||||
**Hardware-Spezifikationen der Box (WICHTIG für alle Agenten):**
|
||||
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
||||
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
||||
- **Inference-Backend:** Primär Vulkan/RADV, experimentell ROCm.
|
||||
|
||||
**WICHTIG:** Alle alten Dienste wie `mem0`, `governor` oder `eigenleben` sind komplett gelöscht. Hermes ist das EINZIGE "Gehirn" und läuft autonom. Es nutzt keine alte Mem0-DB, sondern sein eigenes System.
|
||||
## Sprache (nicht verhandelbar)
|
||||
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
||||
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
|
||||
@@ -23,6 +30,12 @@ Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadc
|
||||
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
|
||||
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
||||
|
||||
## Agentic IDE & Vibe Coding
|
||||
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
|
||||
- Es gibt keinen Zed-Workflow, keine OpenCode-CLI-Mittelschicht und keinen Governor mehr.
|
||||
- Der Agent in OpenCode Desktop nutzt via MCP (`.agents/mcp_config.json`) die API der Box (`:9001/v1`), um autonom Projekte zu bauen.
|
||||
- **Mix-Ansatz beim Testen:** Der Agent testet lokal. Vor dem Push muss er prüfen, ob das Gitea-`VERIFY`-Skript fehlerfrei durchläuft.
|
||||
|
||||
## Zeit & Umgebung
|
||||
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
||||
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
||||
|
||||
@@ -1,72 +1,144 @@
|
||||
# Mission Control 2.0
|
||||
|
||||
Komponierbarer Local-AI-Stack für den Bosgame M5 (Strix Halo). Läuft **live auf der Box**
|
||||
als sudo-freier systemd-User-Dienst (`:9001`) und ist als **Final-Version eingefroren** —
|
||||
Pflege übernehmen ab jetzt die selbst-wartenden Box-KIs (Auto-Update mit Fangnetz + die
|
||||
„Werkstatt"), nicht mehr manuelle Releases.
|
||||
Steuerzentrale des lokalen KI-Stacks auf dem **Bosgame M5** (AMD Ryzen AI MAX+ 395 „Strix Halo",
|
||||
122 GB Unified Memory, **keine dedizierte GPU** — llama.cpp über **Vulkan/RADV**). Läuft live als
|
||||
sudo-freier systemd-User-Dienst und ist auf **Autonomie** ausgelegt: Die Box wartet sich selbst,
|
||||
prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
|
||||
|
||||
**Schichten:** Engine (llama.cpp **Vulkan/RADV** auf Strix Halo) · Router (**llama-swap**,
|
||||
Ko-Residenz-Warm-Set) · **Builtin-Routing-Gateway** in MC2 (`model: auto`, kein externer
|
||||
LiteLLM — scheitert auf Python 3.14) · Mission Control 2.0 (FastAPI + React/shadcn) ·
|
||||
**Hermes Agent** (api_server-Gateway :8642, Tools/MCP/Telegram/cron) · auto-lernendes
|
||||
**Gedächtnis** (Mem0-Sidecar, semantisch). Jede Schicht hinter stabilem Vertrag austauschbar.
|
||||
> **100 % lokal.** Kein Cloud-Modell, kein externer Dienst im Datenpfad.
|
||||
|
||||
> **Sprachassistentin Lucy** (Voice + 3D-Avatar) ist in ein **eigenes Repo** ausgelagert
|
||||
> (`Hitonabi/lucy`) und spricht über den Hermes-Gateway. Aus MC2 selbst sind die Voice-/
|
||||
> Web-Reste entfernt — MC2 ist die Steuerzentrale, kein Sprach-Frontend.
|
||||
## Die drei Bahnen
|
||||
|
||||
## Status: **live + eingefroren (MC2 Final)**
|
||||
| Bahn | Was sie tut | Wo |
|
||||
|---|---|---|
|
||||
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
|
||||
| **Coding** | Agentic IDE (z.B. OpenCode Desktop) auf Dev-PC via lokaler API + MCP | 100% lokal |
|
||||
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
|
||||
|
||||
Der ganze Stack ist auf der Box in Betrieb und auf **Autonomie** ausgelegt (Ziel: läuft auch
|
||||
ohne Betreuung monatelang weiter):
|
||||
Lucy holt ihr Hirn über MC2, spricht aber nie durch den Governor — ein Gespräch ist keine
|
||||
Bau-Sitzung und wird nie unterbrochen.
|
||||
|
||||
- **Modelle & Routing** — Discover (live HF + Fit/Caps), Engine-Write (register, `groups`/
|
||||
Ko-Residenz, vocab-geprüfte Spec-Drafts), Gateway `model: auto` + Fallbacks.
|
||||
- **System & Wartung** — Status (CPU/RAM/GPU/Disk), **ehrliche Speicher-Zahlen** (echte KV-Bytes
|
||||
aus GGUF-Architektur), Logs mit Fehler-Filter, Dienst-Neustart (sudo-frei).
|
||||
- **Updates mit Fangnetz** — OS/Engine/Router/Hermes per Timer: Backup → Update → Postcheck →
|
||||
bei Fehler **Auto-Rollback + Pin**; verständliche Zusammenfassung („Musst du etwas tun?") in
|
||||
Lucys Stimme. Eigene Software wird eingefroren, nicht geupdatet.
|
||||
- **Gedächtnis** — Mem0-Sidecar (auto-lernend, semantisch), 4 Kategorien, Auto-Dedupe.
|
||||
- **Selbsterhaltung** — Health-Wächter (`sentry`), Warm-Set-Wächter (`warmer`), tägliche
|
||||
Self-Smokes, Lucy-Watchdog + Alarmkette; **Werkstatt** (Hermes wartet den Stack via Gitea-
|
||||
Branch → Gate → Deploy selbst).
|
||||
## Ports & Dienste
|
||||
|
||||
API: `health · models · discover · fit · models/register · groups · routing · system/* ·
|
||||
maintenance/* · connect · memory/* · agent/* · voice/announce · reminders/*`. MCP: `mcp/`.
|
||||
Box-Runbooks: [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md), [`docs/RUNBOOK.md`](docs/RUNBOOK.md),
|
||||
[`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) + `deploy/` (Units, `deploy.sh`, `backup.sh`, `warmup.sh`).
|
||||
| Port | Dienst | Erreichbar |
|
||||
|---|---|---|
|
||||
| `9001` | **MC2** (FastAPI + React) — Cockpit, API, Konsole | LAN |
|
||||
|
||||
| `8080` | **llama-swap** — Modell-Router | LAN |
|
||||
| `9010` | `mc2-gateway` — `/v1`-Datenpfad (`model: auto`, Bild-Weiche) | loopback |
|
||||
| `8642` · `9119` | Hermes-Gateway · Hermes-Web-UI | loopback |
|
||||
| `8765` · `8650` | Mem0-Sidecar · Voice-Sidecar (STT/TTS) | loopback |
|
||||
| `7682` | ttyd — Box-Konsole, same-origin unter `/console/` | loopback |
|
||||
|
||||
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` ·
|
||||
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
|
||||
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
|
||||
|
||||
## Der Weg einer Idee
|
||||
|
||||
```
|
||||
Idee in MC2 → Gitea-Repo (mit CI-Ampel + VERIFY) → in Agentic IDE bauen → Ampel → main
|
||||
└── autonomes Vibe Coding via OpenCode Desktop + MCP
|
||||
```
|
||||
|
||||
Jedes neue Repo wird von [`deploy/gitea-repo-create.sh`](deploy/gitea-repo-create.sh) **mit beiden
|
||||
Wächtern geboren**:
|
||||
|
||||
- **`.gitea/workflows/ci.yml`** — die Außen-Prüfung nach dem Push (Gitea Actions)
|
||||
- **`VERIFY`** — die Innen-Prüfung: eine Zeile, wie man das Projekt testet. Die Agentic IDE
|
||||
führt sie im Mix-Ansatz vor jedem Push aus, um Code-Fehler ("Quality Gap") abzufangen.
|
||||
Keine `VERIFY`-Datei = Prüf-Tor aus.
|
||||
|
||||
## Modelle & Rollen
|
||||
|
||||
Gemessen auf der Box (25.07.2026, Vulkan, Q4):
|
||||
|
||||
| Rolle | Modell | Tempo | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| `coder` | Qwen3-Coder-Next (80B-A3B) | **51,5 t/s** · Prefill **754 t/s** | Plant und baut — Kopf der Coding-Mannschaft |
|
||||
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6 t/s** | Lucys Hirn **und** der Sucher-Subagent. Immer warm |
|
||||
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265–318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen) |
|
||||
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen |
|
||||
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30). **Nie tagsüber** — verdrängt das warme Set |
|
||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
||||
|
||||
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
||||
auf dieser bandbreitenbegrenzten Box bricht sein Prefill mit wachsendem Kontext ein (bei 32k
|
||||
gemessene 63 t/s ≈ **9 Minuten nur zum Lesen**). Mit dem 16k-Deckel bleibt der schlimmste Fall
|
||||
je Review unter einer Minute. Deshalb ist er der **Gegenleser für Etappen**, nicht der Coder —
|
||||
als Coder ist er auf dieser Box disqualifiziert (siehe VERDIKTE).
|
||||
|
||||
‼️ **Speicher-Regel:** `Warm-Set + größtes On-Demand-Modell ≤ ~115 GB`. Die ko-residente Gruppe
|
||||
(`groups.brains` in der llama-swap-Config) muss **`persistent: false`** sein — sonst räumt
|
||||
llama-swap vor einem großen Modell nicht ab und der Kernel schießt Prozesse ab. Details und
|
||||
Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
|
||||
|
||||
## Qualitäts-Tore
|
||||
|
||||
1. **Prüf-Tor** — `VERIFY` nach jeder Etappe; rot → der Agent (OpenCode Desktop) repariert lokal nach.
|
||||
2. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft in Gitea.
|
||||
|
||||
Gemeinsame Lehre dahinter: **Wissen lebt in Datei + git, nicht im schrumpfenden Chat-Kontext.**
|
||||
Kontext-Komprimierung löscht still die Regeln mit.
|
||||
|
||||
## Selbsterhaltung
|
||||
|
||||
- **Health-Wächter** (`sentry`) + **Warm-Set-Wächter** (`warmer`, per Env abschaltbar)
|
||||
- **Updates mit Fangnetz** — Backup → Update → Postcheck → bei Fehler **Auto-Rollback + Pin**
|
||||
- **Melde-Briefkasten** (`/api/voice/announce`) — alles, was die Box von sich aus sagen will;
|
||||
Lucy pollt ihn und spricht es. Absender `loop` = Coding-Ereignisse
|
||||
- **Gedächtnis** — Mem0-Sidecar, auto-lernend, semantisch, mit Auto-Dedupe
|
||||
- Tägliche Self-Smokes, Zeitmaschine (Snapshot + Ein-Klick-Restore), Chronik der autonomen Taten
|
||||
|
||||
## API (Auswahl)
|
||||
|
||||
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
|
||||
memory/* · agent/* · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
|
||||
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
|
||||
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
|
||||
|
||||
## Entwickeln
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
python -m venv .venv && .venv/Scripts/python -m pip install -r requirements.txt # Windows
|
||||
.venv/Scripts/python -m uvicorn app:app --port 9000
|
||||
|
||||
# Frontend (Dev, proxyt /api → :9000)
|
||||
cd frontend && npm install && npm run dev # http://localhost:5173
|
||||
|
||||
# Frontend (Build → wird vom Backend ausgeliefert)
|
||||
cd frontend && npm run build # → frontend/dist
|
||||
```
|
||||
|
||||
**Frontend (Dev, proxyt /api → :9000):**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
**Frontend (Build → wird vom Backend ausgeliefert):**
|
||||
```bash
|
||||
cd frontend && npm run build # → frontend/dist
|
||||
```
|
||||
`frontend/dist` **wird mitcommittet** — die Box hat kein Node; Deploy ist ein `git pull` plus
|
||||
Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `npm run build`.
|
||||
|
||||
## Env-Vars (Auswahl)
|
||||
|
||||
| Variable | Default | Zweck |
|
||||
|---|---|---|
|
||||
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine |
|
||||
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap Config |
|
||||
| `MC_GATEWAY_URL` | `http://127.0.0.1:$MC_PORT` | Builtin-Gateway (Teil von MC2, kein externer Dienst) |
|
||||
| `MC_PORT` | `9000` | MC-Backend-Port |
|
||||
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | Aktive Engine-Binary (Vulkan-Build) |
|
||||
| `MC_ENGINE_REPO` | `ggml-org/llama.cpp` | Quelle für Engine-Update-Check |
|
||||
| `MC_DRAFTS_DIR` | `$MODELS/drafts` | Spec-Draft-Modelle (vocab-geprüft) |
|
||||
| `MC_SPEC_TYPE` | `draft-simple` | Speculative-Decoding-Typ (llama.cpp) |
|
||||
| `MC_PORT` | `9000` | MC-Backend-Port (**die Unit auf der Box setzt `9001`**) |
|
||||
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine/Router |
|
||||
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap-Config |
|
||||
| `MC_V1_UPSTREAM` | – | gesetzt → `/v1` geht an `mc2-gateway` (`:9010`) statt in-process |
|
||||
|
||||
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | aktive Engine (Vulkan-Build) |
|
||||
| `MC_REWARM_ENABLED` | `1` | Warm-Set-Wächter (auf der Box bewusst `0`) |
|
||||
| `MC_DRAFTS_DIR` · `MC_SPEC_TYPE` | `$MODELS/drafts` · `draft-simple` | Spekulatives Dekodieren |
|
||||
|
||||
|
||||
|
||||
## Weiterlesen
|
||||
|
||||
| Dokument | Inhalt |
|
||||
|---|---|
|
||||
| [`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) | Wie man MC2 benutzt |
|
||||
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) · [`docs/DISASTER_RECOVERY.md`](docs/DISASTER_RECOVERY.md) | Betrieb, Störungen, Wiederherstellung |
|
||||
| [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md) | Hermes-Agent, Profile, Crons |
|
||||
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
|
||||
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
|
||||
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
|
||||
|
||||
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
|
||||
|
||||
+12
-17
@@ -9,40 +9,39 @@ Server (proxyt /api hierher), daher CORS für localhost offen.
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncGenerator, Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.requests import Request
|
||||
|
||||
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
||||
from routers import (
|
||||
agent,
|
||||
auftragsbuch,
|
||||
chronik,
|
||||
connect,
|
||||
console,
|
||||
eigenleben,
|
||||
events,
|
||||
gateway_proxy,
|
||||
health,
|
||||
hermes_ui,
|
||||
ideen,
|
||||
maintenance,
|
||||
memory,
|
||||
models,
|
||||
routing,
|
||||
skills,
|
||||
system,
|
||||
voice,
|
||||
wissen,
|
||||
zeitmaschine,
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import memory as memory_svc
|
||||
from services import reminders, sentry, warmer
|
||||
from services import metrics_history, reminders, sentry, warmer
|
||||
from starlette.requests import Request
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
# für alle Module (logging.getLogger(__name__)).
|
||||
@@ -67,13 +66,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||
log.info(
|
||||
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||
memory_svc.AUTO_DEDUPE_INTERVAL,
|
||||
memory_svc.AUTO_DEDUPE_THRESHOLD,
|
||||
)
|
||||
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
|
||||
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
|
||||
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
||||
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
||||
app.state.gw_client = httpx.AsyncClient(
|
||||
@@ -113,8 +107,8 @@ app.include_router(health.router)
|
||||
app.include_router(models.router)
|
||||
app.include_router(routing.router)
|
||||
app.include_router(system.router)
|
||||
|
||||
app.include_router(connect.router)
|
||||
app.include_router(memory.router)
|
||||
app.include_router(agent.router)
|
||||
app.include_router(
|
||||
voice.router
|
||||
@@ -135,9 +129,10 @@ app.include_router(maintenance.router)
|
||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
||||
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
||||
app.include_router(skills.router) # Skills & Jobs Dashboard
|
||||
app.include_router(
|
||||
console.router
|
||||
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||
|
||||
@@ -15,13 +15,12 @@ durchreicht (routers/gateway_forward.py, MC_V1_UPSTREAM).
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from config import LLAMA_SWAP_URL, VERSION
|
||||
from fastapi import FastAPI, Request
|
||||
from routers import gateway_proxy
|
||||
|
||||
logging.basicConfig(
|
||||
|
||||
@@ -4,8 +4,9 @@ from pathlib import Path
|
||||
# Add backend directory to sys.path so we can import services
|
||||
sys.path.append(str(Path(__file__).resolve().parent))
|
||||
|
||||
from services.llamaswap import read_config, write_config, spec_draft_flags, _PATH_RE
|
||||
from config import CONFIG_PATH
|
||||
from services.llamaswap import _PATH_RE, read_config, spec_draft_flags, write_config
|
||||
|
||||
|
||||
def migrate():
|
||||
print(f"Reading config from {CONFIG_PATH}...")
|
||||
|
||||
@@ -55,6 +55,17 @@
|
||||
"role": "scout", "name": "GLM-4.6V-Flash", "repo": "ggml-org/GLM-4.6V-Flash-GGUF",
|
||||
"family": "glm", "generation": 4.6, "total_params_b": 9, "active_params_b": 3,
|
||||
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": true
|
||||
},
|
||||
|
||||
{
|
||||
"role": "kritiker", "name": "Devstral-Small-2-24B", "repo": "mistralai/Devstral-Small-2-24B-Instruct-2512",
|
||||
"family": "mistral-devstral", "generation": 2.0, "total_params_b": 24, "active_params_b": 24,
|
||||
"moe": false, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": false
|
||||
},
|
||||
{
|
||||
"role": "kritiker", "name": "GLM-4.7-Flash", "repo": "zai-org/GLM-4.7-Flash",
|
||||
"family": "glm", "generation": 4.7, "total_params_b": 30, "active_params_b": 3,
|
||||
"moe": true, "quant": "Q4_K_XL", "ctx": 65536, "tools": true, "vision": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Agent-Endpoint: Hermes-Status + WebUI-Link (MC verlinkt nur, betreibt nicht)."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Gate ist der Klick des Commanders."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import auftragsbuch
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -4,12 +4,25 @@ Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons, Auftragsbuch — a
|
||||
autonomen Kanäle laufen dort bereits durch. Hier wird nichts Neues erhoben, nur erzählt."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from pydantic import BaseModel
|
||||
from services import announce
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/chronik")
|
||||
class ChronikItem(BaseModel):
|
||||
id: int
|
||||
ts: float
|
||||
subject: str
|
||||
text: str
|
||||
source: str
|
||||
priority: str
|
||||
|
||||
|
||||
class ChronikResponse(BaseModel):
|
||||
items: list[ChronikItem]
|
||||
|
||||
|
||||
@router.get("/chronik", response_model=ChronikResponse)
|
||||
def chronik(limit: int = 150) -> dict:
|
||||
return {"items": announce.list_recent(limit)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets (auf den Gateway + Memory-MCP)."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -14,11 +14,10 @@ import logging
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from config import BOX_CONSOLE_UPSTREAM
|
||||
from fastapi import APIRouter, Request, WebSocket
|
||||
from starlette.responses import Response
|
||||
|
||||
from config import BOX_CONSOLE_UPSTREAM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
@@ -28,7 +27,7 @@ def _register(base: str, upstream: str) -> None:
|
||||
ws_upstream = upstream.replace("http://", "ws://").replace("https://", "wss://")
|
||||
|
||||
@router.websocket(f"/{base}/ws")
|
||||
async def _proxy_ws(ws: WebSocket) -> None: # noqa: ANN001 — Closure je base
|
||||
async def _proxy_ws(ws: WebSocket) -> None:
|
||||
await ws.accept(subprotocol="tty")
|
||||
try:
|
||||
async with websockets.connect(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Eigenleben-Endpoints — „Von allein"-Ansicht (Skills + Vorschlags-Bilanz), read-only."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from services import eigenleben
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/eigenleben")
|
||||
def get_overview() -> dict:
|
||||
return eigenleben.overview()
|
||||
|
||||
|
||||
@router.get("/eigenleben/skill/{skill_id}")
|
||||
def get_skill(skill_id: str) -> dict:
|
||||
text = eigenleben.skill_text(skill_id)
|
||||
if text is None:
|
||||
raise HTTPException(404, "Skill nicht gefunden.")
|
||||
return {"id": skill_id, "text": text}
|
||||
@@ -0,0 +1,100 @@
|
||||
"""SSE-Eventstrom (UMBAU v3 P3a) — ein Kanal sagt der Zentrale, WANN neu laden lohnt.
|
||||
|
||||
GET /api/events liefert Server-Sent Events. Ein Sammler prüft alle paar Sekunden
|
||||
billige Fingerabdrücke der ereignishaften Quellen und schickt NUR bei Änderung ein
|
||||
`invalidate`-Event mit den React-Query-Keys. Die Wahrheit bleibt in den bestehenden
|
||||
Endpunkten — der Strom ist ein reiner Invalidation-Bus, kein zweites Zustandsmodell.
|
||||
|
||||
Quellen: Briefkasten/Chronik (in-process-Cursor), Ideen-Queue ((id,status)-Paare),
|
||||
Auftragsbuch + Erinnerungen (Datei-mtimes), geladene Modelle (Running-Set — Idee aus
|
||||
der Werkstatt-Karte feature/sse-backend-v1). BEWUSST NICHT dabei: System-/Token-
|
||||
Metriken (ändern sich jede Sekunde — da ist Polling das richtige Werkzeug und ein
|
||||
invalidate-Event nur Lärm).
|
||||
|
||||
Versöhnt 15.07. abends: Die angenommene Werkstatt-Version nutzte `type:` statt
|
||||
`event:` (ungültiges SSE-Framing → EventSource-Listener feuert NIE), einen globalen
|
||||
Snapshot über alle Clients und einen nicht existierenden Ideen-Endpunkt — Kern
|
||||
wieder die getestete Hand-Implementierung (E2E: Announce → invalidate binnen
|
||||
Sekunden), Modell-Quelle aus der Karte übernommen.
|
||||
|
||||
Frontend-Gegenstück: frontend/src/lib/events.ts (EventSource, invalidiert die
|
||||
Caches, entspannt die Fallback-Poller ×5; reißt der Strom, reconnectet EventSource
|
||||
selbst und bis dahin pollt die UI wie bisher).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
TICK_S = 3.0 # Prüf-Takt des Sammlers (nur Fingerabdrücke, kein Neuberechnen)
|
||||
KEEPALIVE_S = 20.0 # Kommentar-Ping, damit Proxies/Browser die Verbindung halten
|
||||
|
||||
|
||||
def _mtime(p: Path) -> float:
|
||||
try:
|
||||
return p.stat().st_mtime
|
||||
except OSError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _fingerprints() -> dict[str, object]:
|
||||
"""Billige Änderungs-Signale je Quelle → React-Query-Key. Fehler einer Quelle
|
||||
dürfen den Strom nie reißen (dann bleibt ihr Abdruck einfach stehen)."""
|
||||
fp: dict[str, object] = {}
|
||||
try: # Briefkasten trägt Chronik UND Kontext-Limit-Warnungen — in-process, spottbillig
|
||||
from services import announce
|
||||
fp["chronik"] = announce.list_after(None)["latest"]
|
||||
except Exception:
|
||||
pass
|
||||
try: # Queue: (id,status)-Paare; list_queue cached selbst ~15 s, der Tick kostet nichts
|
||||
from services import ideen
|
||||
d = ideen.list_queue()
|
||||
fp["ideen"] = json.dumps([(i.get("id"), i.get("status")) for i in d.get("items") or []])
|
||||
except Exception:
|
||||
pass
|
||||
try: # Geladene Modelle (Running-Set): Laden/Entladen soll die Modelle-Ansicht anstoßen
|
||||
from services import llamaswap
|
||||
fp["models"] = json.dumps(sorted(str(m) for m in llamaswap.get_running_models()))
|
||||
except Exception:
|
||||
pass
|
||||
# Auftragsbuch (Annahme-Status + Karten-Meldungen) & Erinnerungen: Datei-mtimes
|
||||
fp["auftragsbuch"] = (_mtime(MODELS_DIR / "mc2-auftragsbuch.json"),
|
||||
_mtime(MODELS_DIR / "mc2-announce-branches.json"))
|
||||
fp["reminders"] = _mtime(MODELS_DIR / "mc2-reminders.json")
|
||||
return fp
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def events(request: Request) -> StreamingResponse:
|
||||
async def strom():
|
||||
# Basislinie JE VERBINDUNG (der Client hat beim Verbinden frisch geladen) —
|
||||
# ein globaler Snapshot würde bei mehreren Clients Events verschlucken.
|
||||
alt = _fingerprints()
|
||||
yield ": verbunden\n\n"
|
||||
seit_ping = 0.0
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
return
|
||||
await asyncio.sleep(TICK_S)
|
||||
seit_ping += TICK_S
|
||||
neu = _fingerprints()
|
||||
keys = [k for k, v in neu.items() if k in alt and v != alt[k]]
|
||||
alt.update(neu)
|
||||
if keys:
|
||||
yield f"event: invalidate\ndata: {json.dumps({'keys': keys})}\n\n"
|
||||
seit_ping = 0.0
|
||||
elif seit_ping >= KEEPALIVE_S:
|
||||
yield ": ping\n\n"
|
||||
seit_ping = 0.0
|
||||
|
||||
return StreamingResponse(strom(), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
@@ -11,11 +11,10 @@ entfernen → app.py bindet wieder den lokalen Gateway ein.
|
||||
|
||||
import logging
|
||||
|
||||
from config import V1_UPSTREAM
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from config import V1_UPSTREAM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/v1")
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from config import LLAMA_SWAP_URL
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from config import LLAMA_SWAP_URL
|
||||
from services.gateway_stream import record_stream_chunk, record_usage
|
||||
from services.gateway_stream import record_stream_chunk, record_usage, warn_truncation
|
||||
from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image
|
||||
from services.routing_policy import load_policy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/v1")
|
||||
|
||||
# Antwortsprache für IDE-/Lane-Traffic: die Coding-Modelle antworten sonst englisch
|
||||
@@ -28,6 +31,38 @@ _BILD_BESCHREIB_PROMPT = os.environ.get(
|
||||
"Sichtbaren Code und Fehlermeldungen gib WOERTLICH wieder.")
|
||||
|
||||
|
||||
# Lucy-Voice-Schnellspur (16.07.): Lucys Sprach-Turns tragen diesen Marker im System-Prompt.
|
||||
# Er schaltet das Qwen-Denken für GENAU diese Requests ab (gemessen direkt am Hirn: 9,9 s -> 0,8 s
|
||||
# bis zur Antwort; reasoning_content 2500 Zeichen -> 0) — die „Hirn"-Latenz der Voice-Turns war
|
||||
# fast vollständig Reasoning-Generierung VOR dem ersten sprechbaren Wort. Der Marker wird VOR dem
|
||||
# Modell aus allen Messages entfernt (konstanter Text -> Prefix-Cache bleibt stabil). Requests ohne
|
||||
# Marker (Crons, Telegram, Werkstatt) bleiben unangetastet. Leerer Env-Wert schaltet die Spur ab.
|
||||
_NO_THINK_MARKER = os.environ.get("MC_NO_THINK_MARKER", "[[MC:NO_THINK]]")
|
||||
|
||||
|
||||
def _apply_no_think_marker(body: dict) -> None:
|
||||
"""Marker aus allen Message-Inhalten strippen; war er da, Thinking abschalten
|
||||
(sofern der Client chat_template_kwargs nicht selbst gesetzt hat)."""
|
||||
if not _NO_THINK_MARKER:
|
||||
return
|
||||
found = False
|
||||
for m in body.get("messages") or []:
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
c = m.get("content")
|
||||
if isinstance(c, str) and _NO_THINK_MARKER in c:
|
||||
m["content"] = c.replace(_NO_THINK_MARKER, "").strip()
|
||||
found = True
|
||||
elif isinstance(c, list):
|
||||
for part in c:
|
||||
if (isinstance(part, dict) and isinstance(part.get("text"), str)
|
||||
and _NO_THINK_MARKER in part["text"]):
|
||||
part["text"] = part["text"].replace(_NO_THINK_MARKER, "").strip()
|
||||
found = True
|
||||
if found and "chat_template_kwargs" not in body:
|
||||
body["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
|
||||
|
||||
def _ist_coder_alias(alias: str) -> bool:
|
||||
"""Coder-Ziele (coder, rohe Qwen-Coder-IDs) bekommen Bild-BESCHREIBUNGEN statt der
|
||||
stumpfen Vision-Umleitung — die Code-Frage bleibt beim Spezialisten."""
|
||||
@@ -58,15 +93,23 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
||||
"messages": [{"role": "user",
|
||||
"content": [part, {"type": "text", "text": _BILD_BESCHREIB_PROMPT}]}],
|
||||
}
|
||||
try:
|
||||
# Grosszuegiger Timeout: VL-30B ist on-demand (Kaltladen ~30 s) + Beschreibung ~25 s.
|
||||
r = await client.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage, timeout=240.0)
|
||||
text = ""
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
if not text.strip():
|
||||
return False
|
||||
except Exception:
|
||||
# Eigener Kurzzeit-Client statt des gepoolten (Befund 15.07.: ReadTimeout nach
|
||||
# Sekunden trotz timeout=240 — llama-swap kappt beim Modell-Swap gern alte
|
||||
# Keep-Alive-Sockets, der Pool reicht sie trotzdem wieder aus) + 1 Retry.
|
||||
text = ""
|
||||
for versuch in (1, 2):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=240.0) as c:
|
||||
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
if text.strip():
|
||||
break
|
||||
log.warning("Bild-Weiche v2 (Versuch %s): Beschreibung leer/HTTP %s — %.200s",
|
||||
versuch, r.status_code, r.text)
|
||||
except Exception:
|
||||
log.warning("Bild-Weiche v2 (Versuch %s): Vision-Aufruf scheiterte", versuch, exc_info=True)
|
||||
if not text.strip():
|
||||
return False
|
||||
msg["content"][idx] = {"type": "text", "text": (
|
||||
f"[Bild {nr}: dem Request lag ein Bild bei — {vision_alias} beschreibt es so:\n"
|
||||
@@ -145,6 +188,7 @@ async def models(request: Request):
|
||||
|
||||
async def _proxy(path: str, request: Request):
|
||||
body = await request.json()
|
||||
_apply_no_think_marker(body) # Lucy-Voice-Schnellspur: Marker strippen + Thinking aus
|
||||
requested = str(body.get("model") or "auto")
|
||||
if requested.lower() in ("auto", "chat", "coding"):
|
||||
lane = requested.lower()
|
||||
@@ -193,7 +237,7 @@ async def _proxy(path: str, request: Request):
|
||||
async def gen():
|
||||
try:
|
||||
async for chunk in r.aiter_raw():
|
||||
record_stream_chunk(chunk, alias)
|
||||
record_stream_chunk(chunk, alias, body.get("max_tokens"))
|
||||
yield chunk
|
||||
finally:
|
||||
await r.aclose()
|
||||
@@ -201,7 +245,11 @@ async def _proxy(path: str, request: Request):
|
||||
|
||||
r = await client.post(url, json=body, timeout=600.0)
|
||||
resp_json = r.json()
|
||||
record_usage(resp_json.get("usage") if isinstance(resp_json, dict) else None, alias)
|
||||
if isinstance(resp_json, dict):
|
||||
record_usage(resp_json.get("usage"), alias)
|
||||
if any(isinstance(c, dict) and c.get("finish_reason") == "length"
|
||||
for c in resp_json.get("choices") or []):
|
||||
warn_truncation(alias, body.get("max_tokens"))
|
||||
return JSONResponse(resp_json, status_code=r.status_code, headers=routed)
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""Health-/Status-Endpoint — schlanker Lebenszeichen-Check für MC 2.0."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from config import VERSION
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from services import gateway, llamaswap
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
version: str
|
||||
engine_reachable: bool
|
||||
gateway_reachable: bool
|
||||
brain: dict
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
def health() -> dict:
|
||||
return {
|
||||
"status": "ok",
|
||||
|
||||
@@ -22,11 +22,10 @@ import re
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from config import HERMES_BUILTIN_UI_UPSTREAM
|
||||
from fastapi import APIRouter, Request, WebSocket
|
||||
from starlette.responses import RedirectResponse, Response
|
||||
|
||||
from config import HERMES_BUILTIN_UI_UPSTREAM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Mensch-Gate bleibt die Karte im Auftragsbuc
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import ideen
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
@@ -12,6 +11,10 @@ router = APIRouter(prefix="/api")
|
||||
class IdeeIn(BaseModel):
|
||||
titel: str
|
||||
notiz: str = ""
|
||||
welt: str = "box" # "box" = Werkstatt baut · "ide" = vorbereiten für Agentic IDE
|
||||
tiefe: str = "" # nur ide: "simpel" | "gruendlich" (leer = Box schätzt selbst)
|
||||
ziel: str = "" # nur ide-Projekt: "lxc" | "docker" (leer = Box entscheidet)
|
||||
art: str = "projekt" # nur ide: "projekt" (Repo+Roadmap+Bau) | "idee" (nur durchdenken)
|
||||
|
||||
|
||||
class TaskIn(BaseModel):
|
||||
@@ -23,6 +26,15 @@ class AntwortIn(BaseModel):
|
||||
antwort: str
|
||||
|
||||
|
||||
class ArchivAlleIn(BaseModel):
|
||||
# Leer = alle fertigen Karten der Queue; gesetzt = nur die dieses Projekts.
|
||||
projekt: str = ""
|
||||
|
||||
|
||||
class EinordnenIn(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@router.get("/ideen")
|
||||
def list_queue() -> dict:
|
||||
return ideen.list_queue()
|
||||
@@ -30,7 +42,8 @@ def list_queue() -> dict:
|
||||
|
||||
@router.post("/ideen")
|
||||
def add_idea(body: IdeeIn) -> dict:
|
||||
res = ideen.add_idea(body.titel, body.notiz)
|
||||
res = ideen.add_idea(body.titel, body.notiz, welt=body.welt, tiefe=body.tiefe,
|
||||
ziel=body.ziel, art=body.art)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Idee konnte nicht angelegt werden."))
|
||||
return res
|
||||
@@ -44,6 +57,64 @@ def answer(body: AntwortIn) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
class PrioIn(BaseModel):
|
||||
id: str
|
||||
richtung: str # hoch | runter
|
||||
|
||||
|
||||
@router.post("/ideen/stopp")
|
||||
def stop(body: TaskIn) -> dict:
|
||||
res = ideen.stop_task(body.id)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Stoppen fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/retry")
|
||||
def retry(body: TaskIn) -> dict:
|
||||
res = ideen.retry_task(body.id)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Neuer Versuch fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/prio")
|
||||
def prio(body: PrioIn) -> dict:
|
||||
res = ideen.prio_task(body.id, body.richtung)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Prio-Änderung fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
class WeiterIn(BaseModel):
|
||||
id: str
|
||||
ziel: str = "" # "lxc" | "docker" (leer = Box entscheidet)
|
||||
tiefe: str = "" # "simpel" | "gruendlich" (leer = Box schätzt selbst)
|
||||
|
||||
|
||||
@router.post("/ideen/weiterfuehren")
|
||||
def weiterfuehren(body: WeiterIn) -> dict:
|
||||
"""„Konzept gefällt mir" → IDE-Projekt-Karte mit dem fertigen Konzept als Vorlage."""
|
||||
res = ideen.projekt_aus_idee(body.id, ziel=body.ziel, tiefe=body.tiefe)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Projekt konnte nicht angelegt werden."))
|
||||
return res
|
||||
|
||||
|
||||
class NachschaerfenIn(BaseModel):
|
||||
id: str
|
||||
hinweis: str
|
||||
|
||||
|
||||
@router.post("/ideen/nachschaerfen")
|
||||
def nachschaerfen(body: NachschaerfenIn) -> dict:
|
||||
"""„Punkt X passt nicht" → Überarbeitungs-Runde am bestehenden Konzept."""
|
||||
res = ideen.konzept_ueberarbeiten(body.id, body.hinweis)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Überarbeitung konnte nicht gestartet werden."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/archivieren")
|
||||
def archive(body: TaskIn) -> dict:
|
||||
res = ideen.archive_task(body.id)
|
||||
@@ -52,6 +123,32 @@ def archive(body: TaskIn) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/einordnen")
|
||||
def einordnen(body: EinordnenIn) -> dict:
|
||||
"""Einordnungs-VORSCHLAG beim Tippen. Wirft nie — scheitert es, kommt `ok: false`
|
||||
und die Oberfläche behält ihre Voreinstellung."""
|
||||
return ideen.einordnen(body.text)
|
||||
|
||||
|
||||
@router.post("/ideen/archivieren-alle")
|
||||
def archive_alle(body: ArchivAlleIn) -> dict:
|
||||
"""Fertige Karten sammelweise wegräumen. Fasst ausschließlich `done` an."""
|
||||
res = ideen.archive_done(body.projekt)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Archivieren fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/log")
|
||||
def get_log(id: str) -> dict:
|
||||
return ideen.log_of(id)
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/ergebnis")
|
||||
def get_ergebnis(id: str) -> dict:
|
||||
return ideen.ergebnis_of(id)
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/konzept")
|
||||
def get_konzept(id: str) -> dict:
|
||||
return ideen.konzept_of(id)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Wartungs-Endpoints: Update-Badge, OS-/Engine-Update, Reboot, Restart, Logs."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
from fastapi import APIRouter, Header, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import maintenance
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
"""Memory-Endpoints (geteiltes Gedächtnis). LAN-only, kein Token in 2.0-Phase 3."""
|
||||
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import memory
|
||||
from services.voice_metrics import park, record_stage
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class MemIn(BaseModel):
|
||||
content: str
|
||||
category: str = "knowledge"
|
||||
source: str = "manual"
|
||||
|
||||
|
||||
class MemUp(BaseModel):
|
||||
content: str | None = None
|
||||
category: str | None = None
|
||||
|
||||
|
||||
class DedupeIn(BaseModel):
|
||||
apply: bool = False
|
||||
threshold: float = 0.85
|
||||
|
||||
|
||||
class LearnIn(BaseModel):
|
||||
text: str | None = None
|
||||
messages: list[dict] | None = None
|
||||
source: str = "auto"
|
||||
category: str = "knowledge"
|
||||
|
||||
|
||||
@router.get("/memory/export")
|
||||
def export() -> dict:
|
||||
return memory.export_text()
|
||||
|
||||
|
||||
@router.get("/memory/graph")
|
||||
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
||||
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die Visualisierung."""
|
||||
return memory.graph(min_score=min_score, top_k=top_k)
|
||||
|
||||
|
||||
@router.post("/memory/learn", status_code=201)
|
||||
def learn(body: LearnIn) -> dict:
|
||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 extrahiert Fakten selbst."""
|
||||
return memory.learn(text=body.text, messages=body.messages,
|
||||
source=body.source, category=body.category)
|
||||
|
||||
|
||||
@router.post("/memory/dedupe")
|
||||
def dedupe(body: DedupeIn) -> dict:
|
||||
return memory.dedupe(apply=body.apply, threshold=body.threshold)
|
||||
|
||||
|
||||
@router.get("/memory")
|
||||
def list_mem(q: str = "", category: str = "") -> list[dict]:
|
||||
# Semantischer Retrieve (q gesetzt) = u.a. Hermes' Mem0-Prefetch VOR jedem Turn. Dauer messen
|
||||
# + parken, damit der laufende Voice-Chat-Turn sie als Unter-Detail seiner Hirn-Zeit einsammelt.
|
||||
if q:
|
||||
_t0 = time.perf_counter()
|
||||
res = memory.list_memories(q=q, category=category)
|
||||
_ms = (time.perf_counter() - _t0) * 1000.0
|
||||
record_stage("memory_retrieve", _ms)
|
||||
park("retrieve", _ms)
|
||||
return res
|
||||
return memory.list_memories(q=q, category=category)
|
||||
|
||||
|
||||
@router.post("/memory", status_code=201)
|
||||
def add(body: MemIn) -> dict:
|
||||
if body.category not in memory.CATEGORIES:
|
||||
raise HTTPException(400, f"Kategorie '{body.category}' unbekannt.")
|
||||
return memory.add_memory(body.content, body.category, body.source)
|
||||
|
||||
|
||||
@router.put("/memory/{mid}")
|
||||
def update(mid: str, body: MemUp) -> dict:
|
||||
res = memory.update_memory(mid, content=body.content, category=body.category)
|
||||
if not res:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
||||
return res
|
||||
|
||||
|
||||
@router.delete("/memory/{mid}")
|
||||
def delete(mid: str) -> dict:
|
||||
if not memory.delete_memory(mid):
|
||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
||||
return {"ok": True}
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Modelle-Endpoints: Liste (mit Caps), Discover, Fit, Register, Groups."""
|
||||
|
||||
import psutil
|
||||
from config import HF_DOWNLOAD_ENV, MODELS_DIR
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import HF_DOWNLOAD_ENV, MODELS_DIR
|
||||
from services import budget, discover, hf, jobengine, llamaswap
|
||||
from services.fit import evaluate_fit, max_ctx_for
|
||||
|
||||
@@ -114,24 +113,41 @@ def install(req: InstallReq) -> dict:
|
||||
# SETUP-BEWUSST: größter ctx, der neben Hirn/warmem Set passt (nicht nur Modell allein).
|
||||
ctx = budget.setup_aware_ctx(budget.params_b_for(repo), req.quant, role=req.role)["ctx"]
|
||||
|
||||
# Sofort registrieren (Datei kommt gleich) — robust gegen -watch-config.
|
||||
# Sofort registrieren (robust gegen -watch-config) — aber OHNE den Rollen-Alias
|
||||
# umzuhängen: der zeigte sonst minutenlang auf eine noch ladende Datei (Lane kalt;
|
||||
# bei role=hermes wäre Lucy bis Download-Ende tot gewesen — Review 16.07.).
|
||||
try:
|
||||
model_id = llamaswap.register_model(
|
||||
model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja)
|
||||
model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja,
|
||||
set_alias=False)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(500, str(exc))
|
||||
|
||||
# Rolle erst NACH erfolgreichem Download übernehmen. hermes geht dabei durch den
|
||||
# warm-bewussten Hirn-Flow (brains-Gruppe, ttl 0, Hermes-Config, Gateway-Restart) —
|
||||
# der rohe Alias-Move hatte diesen Flow bisher umgangen.
|
||||
role = (req.role or "").strip().lower()
|
||||
|
||||
def _apply_role() -> None:
|
||||
if role == "hermes":
|
||||
from services.agent import set_agent_brain
|
||||
res = set_agent_brain(model_id)
|
||||
if not res.get("ok"):
|
||||
raise RuntimeError(res.get("reason", "Hirn-Wechsel fehlgeschlagen"))
|
||||
else:
|
||||
llamaswap.set_role(model_id, role)
|
||||
|
||||
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
|
||||
args = [hf.hf_bin(), "download", repo]
|
||||
for f in info["files"]:
|
||||
args.append(f)
|
||||
args.extend(info["files"])
|
||||
if info["mmproj"]:
|
||||
args.append(info["mmproj"])
|
||||
args += ["--local-dir", str(target)]
|
||||
env = dict(HF_DOWNLOAD_ENV)
|
||||
if req.hf_token:
|
||||
env["HF_TOKEN"] = req.hf_token
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env)
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
|
||||
on_done=_apply_role if role else None)
|
||||
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
||||
return {"ok": True, "job_id": job_id, "model_id": model_id, "model_path": model_path,
|
||||
"total_bytes": info["total_bytes"], "files": len(info["files"])}
|
||||
@@ -161,9 +177,18 @@ def recommend_role(role: str) -> dict:
|
||||
|
||||
@router.post("/models/{model_id}/role")
|
||||
def set_model_role(model_id: str, body: RoleReq) -> dict:
|
||||
new_role = (body.role or "").strip().lower()
|
||||
# Guard: Hält das Modell einen LEBENSWICHTIGEN Alias (hermes/embed), darf die Rolle
|
||||
# hier nicht weggeklickt werden — sonst verliert Lucy Hirn/Gedächtnis mit einem Klick.
|
||||
# Weg: die geschützte Rolle zuerst einem ANDEREN Modell zuweisen (Alias zieht um).
|
||||
prot = llamaswap.protected_alias_of(model_id)
|
||||
if prot and new_role != prot:
|
||||
raise HTTPException(400,
|
||||
f"Dieses Modell hält die lebenswichtige Rolle '{prot}'. Weise '{prot}' zuerst "
|
||||
f"einem anderen Modell zu — danach lässt sich die Rolle hier ändern.")
|
||||
# Das Agent-Hirn (Rolle 'hermes') braucht den warm-bewussten Flow (Alias + brains-Gruppe +
|
||||
# ttl 0 + Hermes config.default + Gateway-Restart) — Single Source of Truth UI ↔ Hermes.
|
||||
if (body.role or "").strip().lower() == "hermes":
|
||||
if new_role == "hermes":
|
||||
from services.agent import set_agent_brain
|
||||
res = set_agent_brain(model_id)
|
||||
if not res.get("ok"):
|
||||
|
||||
@@ -4,7 +4,6 @@ LAN-only wie alle MC2-Endpoints."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import reminders
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
@@ -16,7 +15,20 @@ class ReminderIn(BaseModel):
|
||||
repeat: str = "" # '' einmalig | daily | weekdays | weekly
|
||||
|
||||
|
||||
@router.get("/reminders")
|
||||
class ReminderOut(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
next_ts: float
|
||||
repeat: str
|
||||
created_ts: float
|
||||
when_local: str
|
||||
|
||||
|
||||
class ReminderListResponse(BaseModel):
|
||||
items: list[ReminderOut]
|
||||
|
||||
|
||||
@router.get("/reminders", response_model=ReminderListResponse)
|
||||
def list_reminders() -> dict:
|
||||
return {"items": reminders.list_all()}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import gateway
|
||||
from services.routing_policy import policy_meta, save_policy
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
class RunSkillRequest(BaseModel):
|
||||
skill_name: str
|
||||
|
||||
@router.get("/skills")
|
||||
def list_skills():
|
||||
"""Listet alle verfügbaren Skills aus dem deploy/skills Verzeichnis auf."""
|
||||
# Pfad relativ zu dieser Datei (backend/routers/skills.py)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
repo_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
skills_dir = os.path.join(repo_root, "deploy", "skills")
|
||||
skills = []
|
||||
|
||||
# Check currently running hermes skill processes
|
||||
running_skills = set()
|
||||
for p in psutil.process_iter(['name', 'cmdline']):
|
||||
try:
|
||||
cmdline = p.info.get('cmdline')
|
||||
if cmdline and any("hermes" in arg for arg in cmdline):
|
||||
for arg in cmdline:
|
||||
if "Führe den " in arg and " Skill aus" in arg:
|
||||
# Extract skill name from "Führe den 'skill_name' Skill aus"
|
||||
import re
|
||||
match = re.search(r"Führe den '(.+?)' Skill aus", arg)
|
||||
if match:
|
||||
running_skills.add(match.group(1))
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
|
||||
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
|
||||
for entry in os.scandir(skills_dir):
|
||||
if entry.is_dir():
|
||||
# Suche nach SKILL.md für Metadaten
|
||||
skill_md_path = os.path.join(entry.path, "SKILL.md")
|
||||
description = ""
|
||||
if os.path.exists(skill_md_path):
|
||||
try:
|
||||
with open(skill_md_path, "r", encoding="utf-8") as f:
|
||||
# Lese erste Zeilen für Description (YAML Frontmatter)
|
||||
lines = f.readlines()[:10]
|
||||
for line in lines:
|
||||
if "description:" in line.lower():
|
||||
description = line.split(":", 1)[1].strip().strip('"\'')
|
||||
break
|
||||
elif "author:" in line.lower() and not description:
|
||||
description = line.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
skills.append({
|
||||
"name": entry.name,
|
||||
"description": description or "Keine Beschreibung verfügbar.",
|
||||
"running": entry.name in running_skills
|
||||
})
|
||||
return {"skills": skills}
|
||||
|
||||
@router.post("/skills/run")
|
||||
def run_skill(req: RunSkillRequest):
|
||||
"""Startet einen autonomen Skill via Hermes im Hintergrund."""
|
||||
hermes_bin = os.path.expanduser("~/.local/bin/hermes")
|
||||
if not os.path.exists(hermes_bin):
|
||||
# Fallback falls lokal (auf Windows) entwickelt wird
|
||||
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
||||
|
||||
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat
|
||||
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
|
||||
try:
|
||||
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{req.skill_name.replace('/', '_')}.log")
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
with open(log_path, "a") as f:
|
||||
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
|
||||
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
||||
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
||||
except Exception as e:
|
||||
return {"ok": False, "err": str(e)}
|
||||
+59
-14
@@ -9,12 +9,10 @@ import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import httpx
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
import httpx
|
||||
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, VOICE_SERVICE_URL
|
||||
from services import backup as backup_svc
|
||||
from services import maintenance
|
||||
from services.agent import agent_status
|
||||
@@ -37,6 +35,13 @@ def status() -> dict:
|
||||
return system_status()
|
||||
|
||||
|
||||
@router.get("/system/history")
|
||||
def history(minutes: int = 60) -> dict:
|
||||
"""Metrik-Verlauf (max. 24 h) für die Cockpit-Zeitachse — s. services/metrics_history."""
|
||||
from services import metrics_history
|
||||
return metrics_history.history(minutes)
|
||||
|
||||
|
||||
def _mem0_reachable() -> bool:
|
||||
try:
|
||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||
@@ -51,20 +56,60 @@ def _voice_reachable() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _user_unit_active(unit: str) -> bool:
|
||||
"""systemd-User-Unit ohne eigenen HTTP-Port (z. B. mc2-steward) — Zustand via is-active.
|
||||
Auf Windows/Dev schlägt das harmlos fehl (False)."""
|
||||
if os.name != "posix":
|
||||
return False
|
||||
try:
|
||||
r = subprocess.run(["systemctl", "--user", "is-active", unit],
|
||||
capture_output=True, text=True, timeout=3)
|
||||
return r.stdout.strip() == "active"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/system/services")
|
||||
def services() -> dict:
|
||||
"""Aggregierte Erreichbarkeit aller Stack-Dienste (für die Health-Anzeige)."""
|
||||
"""Aggregierte Erreichbarkeit aller Stack-Dienste — die EINE Quelle für Cockpit-Kachel,
|
||||
Schubladen-Liste und Log-Auswahl (Frontend rendert diese Liste, keine UI-Kopie mehr).
|
||||
`scope`: user|system (Restart-Weg), `unit`: echter systemd-Name (Restart/Logs/Matching)."""
|
||||
a = agent_status()
|
||||
gw_url = f"{GATEWAY_URL}/v1"
|
||||
rows = [
|
||||
{"name": "Engine (llama-swap)", "unit": "llama-swap", "url": LLAMA_SWAP_URL,
|
||||
"ok": engine_reachable(), "scope": "system"},
|
||||
]
|
||||
# Seit UMBAU v3 P1 ist der LLM-Gateway ein EIGENER Prozess — ohne diese Zeile war er
|
||||
# in der Dienste-Ampel unsichtbar (Kachel "6/6 grün" bei totem Denkpfad).
|
||||
if V1_UPSTREAM:
|
||||
rows.append({"name": "LLM-Gateway (Denkpfad)", "unit": "mc2-gateway", "url": V1_UPSTREAM,
|
||||
"ok": gateway_reachable(), "scope": "user"})
|
||||
# MC2 selbst antwortet gerade auf diesen Request — ok=True ist hier ehrlich;
|
||||
# die Zeile existiert für Restart/Logs, nicht als Erreichbarkeits-Orakel.
|
||||
rows.append({"name": "Steuerpult (MC2)", "unit": "mission-control-2", "url": gw_url,
|
||||
"ok": True, "scope": "user"})
|
||||
else:
|
||||
rows.append({"name": "Gateway (integriert)", "unit": "mission-control-2", "url": gw_url,
|
||||
"ok": gateway_reachable(), "scope": "user"})
|
||||
rows += [
|
||||
{"name": "Wächter (Steward)", "unit": "mc2-steward", "url": "",
|
||||
"ok": _user_unit_active("mc2-steward"), "scope": "user"},
|
||||
{"name": "Hermes-Gateway", "unit": "hermes-gateway", "url": HERMES_API_URL,
|
||||
"ok": a["gateway_reachable"], "scope": "user"},
|
||||
{"name": "Hermes-GUI (Desktop-Gateway)", "unit": "hermes-builtin-ui", "url": a["hermes_ui_url"],
|
||||
"ok": a["hermes_ui_reachable"], "scope": "user"},
|
||||
{"name": "Mem0 (Gedächtnis)", "unit": "mem0-service", "url": MEM0_SERVICE_URL,
|
||||
"ok": _mem0_reachable(), "scope": "user"},
|
||||
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL,
|
||||
"ok": _voice_reachable(), "scope": "user"},
|
||||
# ttyd hinter dem /console/-Proxy — war als einziger UI-Dienst NICHT in der Liste,
|
||||
# das Konsole-Overlay schickte den User deshalb ins SSH statt zum Restart-Knopf.
|
||||
{"name": "Box-Konsole (ttyd)", "unit": "box-console", "url": "/console/",
|
||||
"ok": bool(a.get("box_console_reachable")), "scope": "user"},
|
||||
]
|
||||
return {
|
||||
"services": [
|
||||
{"name": "Engine (llama-swap)", "unit": "llama-swap", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
|
||||
{"name": "Gateway (integriert)", "unit": "mission-control-2", "url": gw_url, "ok": gateway_reachable()},
|
||||
{"name": "Hermes-Gateway", "unit": "hermes-gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
|
||||
{"name": "Hermes-GUI (Desktop-Gateway)", "unit": "hermes-builtin-ui", "url": a["hermes_ui_url"], "ok": a["hermes_ui_reachable"]},
|
||||
{"name": "Mem0 (Gedächtnis)", "unit": "mem0-service", "url": MEM0_SERVICE_URL, "ok": _mem0_reachable()},
|
||||
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL, "ok": _voice_reachable()},
|
||||
],
|
||||
"services": rows,
|
||||
"links": {
|
||||
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
|
||||
"gateway": gw_url,
|
||||
@@ -88,7 +133,7 @@ def _run(cmd: list[str], cwd: str | None = None) -> dict:
|
||||
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=180)
|
||||
return {"ok": p.returncode == 0, "code": p.returncode,
|
||||
"out": (p.stdout or "")[-2000:], "err": (p.stderr or "")[-2000:]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
return {"ok": False, "code": -1, "out": "", "err": str(exc)}
|
||||
|
||||
|
||||
|
||||
@@ -11,14 +11,16 @@ LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv).
|
||||
import sys as _sys
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL
|
||||
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL
|
||||
from services import announce
|
||||
from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (intern)
|
||||
Timer,
|
||||
@@ -29,8 +31,6 @@ from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (inter
|
||||
record_stage,
|
||||
)
|
||||
|
||||
# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv).
|
||||
import sys as _sys
|
||||
_GUARD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "mcp")
|
||||
if _GUARD_DIR not in _sys.path:
|
||||
_sys.path.insert(0, _GUARD_DIR)
|
||||
@@ -155,7 +155,7 @@ def voice_health() -> dict:
|
||||
r = httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=httpx.Timeout(5.0))
|
||||
out["sidecar"] = r.status_code == 200
|
||||
out["detail"] = r.json() if r.status_code == 200 else None
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
out["error"] = str(exc)
|
||||
return out
|
||||
|
||||
@@ -180,7 +180,7 @@ def voice_voices() -> dict:
|
||||
r = httpx.get(f"{VOICE_SERVICE_URL}/voices", timeout=httpx.Timeout(10.0))
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, f"Voice-Sidecar nicht erreichbar: {exc}")
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ def voice_get_reference() -> dict:
|
||||
r = httpx.get(f"{VOICE_SERVICE_URL}/reference", timeout=httpx.Timeout(8.0))
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
return {"active": False, "error": str(exc)}
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom T
|
||||
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -30,7 +32,27 @@ def _title_of(p: Path) -> str:
|
||||
return p.stem
|
||||
|
||||
|
||||
@router.get("/wissen")
|
||||
class VaultFile(BaseModel):
|
||||
path: str
|
||||
name: str
|
||||
dir: str
|
||||
title: str
|
||||
mtime: float
|
||||
neu: bool
|
||||
|
||||
|
||||
class WissenListResponse(BaseModel):
|
||||
available: bool
|
||||
files: list[VaultFile]
|
||||
|
||||
|
||||
class WissenFileResponse(BaseModel):
|
||||
path: str
|
||||
content: str
|
||||
mtime: float
|
||||
|
||||
|
||||
@router.get("/wissen", response_model=WissenListResponse)
|
||||
def list_vault() -> dict:
|
||||
"""Alle Markdown-Notizen des Vaults (relativer Pfad, Titel, Ordner, Alter)."""
|
||||
if not _available():
|
||||
@@ -38,7 +60,7 @@ def list_vault() -> dict:
|
||||
files = []
|
||||
now = time.time()
|
||||
for p in sorted(VAULT.rglob("*.md")):
|
||||
if ".git" in p.parts:
|
||||
if ".git" in p.parts or "traeume" in p.parts:
|
||||
continue
|
||||
rel = p.relative_to(VAULT).as_posix()
|
||||
st = p.stat()
|
||||
@@ -54,7 +76,7 @@ def list_vault() -> dict:
|
||||
return {"available": True, "files": files}
|
||||
|
||||
|
||||
@router.get("/wissen/datei")
|
||||
@router.get("/wissen/datei", response_model=WissenFileResponse)
|
||||
def read_file(pfad: str) -> dict:
|
||||
"""Inhalt einer Vault-Notiz — Traversal hart geblockt (resolve + is_relative_to)."""
|
||||
if not _available():
|
||||
@@ -69,3 +91,61 @@ def read_file(pfad: str) -> dict:
|
||||
raise
|
||||
except (ValueError, OSError):
|
||||
raise HTTPException(404, "Notiz nicht lesbar.")
|
||||
|
||||
|
||||
@router.get("/wissen/graph")
|
||||
def graph_vault() -> dict:
|
||||
"""Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links)."""
|
||||
if not _available():
|
||||
return {"nodes": [], "edges": []}
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
# Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID)
|
||||
name_to_id = {}
|
||||
|
||||
# 1. Alle Nodes sammeln
|
||||
for p in VAULT.rglob("*.md"):
|
||||
if ".git" in p.parts or "traeume" in p.parts:
|
||||
continue
|
||||
rel = p.relative_to(VAULT).as_posix()
|
||||
name_to_id[p.stem.lower()] = rel
|
||||
|
||||
# Kategorie aus dem Ordner ableiten
|
||||
cat = "knowledge"
|
||||
parent = p.parent.name if p.parent != VAULT else ""
|
||||
if parent == "traeume":
|
||||
cat = "events"
|
||||
elif parent == "muster":
|
||||
cat = "rules"
|
||||
elif parent == "skill-kandidaten":
|
||||
cat = "identity"
|
||||
|
||||
nodes.append({
|
||||
"id": rel,
|
||||
"content": p.stem, # stem ist oft kürzer/prägnanter als der Titel
|
||||
"category": cat,
|
||||
"source": "wiki"
|
||||
})
|
||||
|
||||
# 2. Edges extrahieren (Wiki-Links [[Name]])
|
||||
link_rx = re.compile(r"\[\[([^\]]+)\]\]")
|
||||
for n in nodes:
|
||||
try:
|
||||
target = VAULT / n["id"]
|
||||
content = target.read_text(encoding="utf-8", errors="replace")
|
||||
# Set, um mehrfache Links auf dieselbe Datei zu entduplizieren
|
||||
found_links = {m.group(1).strip().lower() for m in link_rx.finditer(content)}
|
||||
|
||||
for link in found_links:
|
||||
target_id = name_to_id.get(link)
|
||||
if target_id and target_id != n["id"]:
|
||||
edges.append({
|
||||
"source": n["id"],
|
||||
"target": target_id,
|
||||
"weight": 1.0
|
||||
})
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
@@ -13,7 +13,6 @@ from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import backup as backup_svc
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse systemd timeout errors for mission-control-2.service from journalctl.
|
||||
|
||||
Captures lines containing "State 'stop-sigterm' timed out" and appends them
|
||||
to ~/logs/mc2-timeout.log with ISO timestamps.
|
||||
|
||||
Permission errors are handled gracefully.
|
||||
|
||||
Uses user journal (systemctl --user) to query logs.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
LOG_DIR = Path.home() / "logs"
|
||||
LOG_FILE = LOG_DIR / "mc2-timeout.log"
|
||||
SERVICE_NAME = "mission-control-2.service"
|
||||
SEARCH_PATTERN = "State 'stop-sigterm' timed out"
|
||||
JOURNALCTL_LIMIT = 1000
|
||||
|
||||
|
||||
def ensure_log_dir() -> None:
|
||||
"""Create log directory if it doesn't exist."""
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_timeout_entries() -> list[str]:
|
||||
"""Run journalctl --user and return lines matching the timeout pattern."""
|
||||
cmd = [
|
||||
"journalctl",
|
||||
"--user",
|
||||
"-u", SERVICE_NAME,
|
||||
"--no-pager",
|
||||
"-n", str(JOURNALCTL_LIMIT),
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("journalctl timed out")
|
||||
return []
|
||||
except PermissionError:
|
||||
print("Permission denied: journalctl requires access to user logs")
|
||||
return []
|
||||
|
||||
if result.returncode != 0:
|
||||
if "Permission denied" in result.stderr:
|
||||
print("Permission denied: journalctl requires access to user logs")
|
||||
else:
|
||||
print(f"journalctl failed: {result.stderr.strip()}")
|
||||
return []
|
||||
|
||||
return [
|
||||
line
|
||||
for line in result.stdout.splitlines()
|
||||
if SEARCH_PATTERN in line
|
||||
]
|
||||
|
||||
|
||||
def write_to_log(entries: list[str]) -> int:
|
||||
"""Append entries to log file with ISO timestamps. Returns count written."""
|
||||
ensure_log_dir()
|
||||
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
written = 0
|
||||
|
||||
with LOG_FILE.open("a", encoding="utf-8") as f:
|
||||
for entry in entries:
|
||||
f.write(f"[{timestamp}] {entry}\n")
|
||||
written += 1
|
||||
|
||||
return written
|
||||
|
||||
|
||||
def main() -> None:
|
||||
entries = get_timeout_entries()
|
||||
if not entries:
|
||||
print("No timeout entries found.")
|
||||
return
|
||||
|
||||
count = write_to_log(entries)
|
||||
print(f"Appended {count} timeout entry/ies to {LOG_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,11 +9,15 @@ import os
|
||||
import re
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
|
||||
from config import (BOX_CONSOLE_UPSTREAM,
|
||||
BOX_CONSOLE_PATH, HERMES_BUILTIN_UI_UPSTREAM, HERMES_BUILTIN_UI_PATH,
|
||||
HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL)
|
||||
from config import (
|
||||
BOX_CONSOLE_PATH,
|
||||
BOX_CONSOLE_UPSTREAM,
|
||||
HERMES_API_URL,
|
||||
HERMES_BUILTIN_UI_PATH,
|
||||
HERMES_BUILTIN_UI_UPSTREAM,
|
||||
HERMES_HOME,
|
||||
PC_EXECUTOR_URL,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -161,6 +165,8 @@ def agent_status() -> dict:
|
||||
"telegram_enabled": bool(os.environ.get("TELEGRAM_BOT_TOKEN", "")),
|
||||
"mcp_server_count": _count_enabled_mcp_servers(),
|
||||
"pc_executor_reachable": _reach(PC_EXECUTOR_URL, "/health"),
|
||||
# Konfigurierte Adresse mitliefern, damit die UI sie ANZEIGT statt hartcodiert (Review 15.07.).
|
||||
"pc_executor_url": PC_EXECUTOR_URL,
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +198,7 @@ def set_agent_brain(model_id: str) -> dict:
|
||||
new_members = [x for x in brains if x not in (old, model_id)] + [model_id]
|
||||
llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm
|
||||
# 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen.
|
||||
from services.llamaswap import set_ttl, DEFAULT_TTL
|
||||
from services.llamaswap import DEFAULT_TTL, set_ttl
|
||||
set_ttl(model_id, 0)
|
||||
if old:
|
||||
set_ttl(old, DEFAULT_TTL)
|
||||
@@ -251,7 +257,7 @@ def update_brain_model(new_model: str) -> bool:
|
||||
|
||||
# Restart the user-space service to apply changes
|
||||
try:
|
||||
import services.maintenance as maintenance
|
||||
from services import maintenance
|
||||
maintenance.restart_service("hermes-gateway")
|
||||
except Exception:
|
||||
log.warning("update_brain_model: hermes-gateway-Restart fehlgeschlagen", exc_info=True)
|
||||
|
||||
@@ -19,6 +19,7 @@ import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from config import MODELS_DIR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -28,6 +29,12 @@ NOTIFY_SH = str(Path(__file__).resolve().parent.parent.parent / "deploy" / "noti
|
||||
STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(MODELS_DIR / "mc2-announce.json")))
|
||||
MAX_ITEMS = int(os.environ.get("MC_ANNOUNCE_MAX", "200"))
|
||||
|
||||
# Steward-Auszug (UMBAU v3 P2): Läuft der Absender AUSSERHALB des MC2-Prozesses
|
||||
# (mc2-steward), darf er den Store NICHT direkt schreiben — Datei + In-Memory-Cache
|
||||
# gehören exklusiv dem Steuerpult. Gesetzt (Unit-Env, z. B. http://127.0.0.1:9001)
|
||||
# liefert add() die Meldung stattdessen per HTTP am /api/voice/announce-Endpunkt ab.
|
||||
ANNOUNCE_HTTP = os.environ.get("MC_ANNOUNCE_HTTP", "").rstrip("/")
|
||||
|
||||
_lock = threading.Lock()
|
||||
_state: dict | None = None # {"next_id": int, "items": [...]}
|
||||
|
||||
@@ -58,6 +65,22 @@ def add(text: str, subject: str = "", source: str = "", priority: str = "normal"
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
raise ValueError("Leere Meldung.")
|
||||
if ANNOUNCE_HTTP:
|
||||
# Fremdprozess-Modus (mc2-steward): per HTTP beim Steuerpult abliefern.
|
||||
payload = {"text": text[:4000], "subject": (subject or "").strip()[:120],
|
||||
"source": (source or "").strip()[:60], "priority": priority}
|
||||
try:
|
||||
with httpx.Client(timeout=5.0) as c:
|
||||
r = c.post(f"{ANNOUNCE_HTTP}/api/voice/announce", json=payload)
|
||||
if r.status_code == 200:
|
||||
return (r.json() or {}).get("item") or payload
|
||||
except Exception:
|
||||
pass
|
||||
# MC2 down/Endpunkt weg: Alarm ist nicht verloren — der Telegram-Direktweg des
|
||||
# Aufrufers (notify.sh) läuft separat; hier bleibt nur der Log-Nachweis.
|
||||
log.warning("announce: HTTP-Abgabe an %s fehlgeschlagen — nur im Log: [%s] %.120s",
|
||||
ANNOUNCE_HTTP, subject or "-", text)
|
||||
return payload
|
||||
with _lock:
|
||||
state = _load()
|
||||
item = {
|
||||
|
||||
@@ -26,6 +26,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
@@ -60,6 +61,64 @@ _KANDIDAT_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._ -]{0,120}\.md$")
|
||||
|
||||
_fetch_cache: dict = {}
|
||||
_FETCH_EVERY = 30.0 # s — Gitea nicht bei jedem UI-Poll anfragen
|
||||
_CI_CACHE: dict = {} # { (repo, branch, head_sha): {"status": "success", "ts": time} }
|
||||
|
||||
def _gitea_creds() -> tuple | None:
|
||||
# 1. Fallback: Versuch via git credential manager (zukunftsfähig & plattformübergreifend)
|
||||
try:
|
||||
import subprocess
|
||||
p = subprocess.run(
|
||||
["git", "credential", "fill"],
|
||||
input=b"protocol=https\nhost=git.tobisniceshomelab.ddnsfree.com\n",
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
out = p.stdout.decode(errors="replace")
|
||||
u_match = re.search(r"username=(.+)", out)
|
||||
p_match = re.search(r"password=(.+)", out)
|
||||
if u_match and p_match:
|
||||
return u_match.group(1).strip(), p_match.group(1).strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Fallback: Datei (hauptsächlich auf der AI-Box genutzt)
|
||||
try:
|
||||
cred = Path("~/.git-credentials").expanduser()
|
||||
for line in cred.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"https://([^:]+):([^@]+)@git\.tobisniceshomelab", line.strip())
|
||||
if m: return m.group(1), m.group(2)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _fetch_ci_status(repo: str, branch: str, head_sha: str) -> str | None:
|
||||
# Nur auf der Box (wo creds liegen) sinnvoll
|
||||
creds = _gitea_creds()
|
||||
if not creds: return None
|
||||
_user, token = creds
|
||||
cache_key = (repo, branch, head_sha)
|
||||
cached = _CI_CACHE.get(cache_key)
|
||||
if cached and (time.time() - cached["ts"] < 30 or cached["status"] in ("success", "failure", "skipped")):
|
||||
return cached["status"]
|
||||
|
||||
full_repo = "Hitonabi/mission-control-v2" if repo == "mc2" else "Hitonabi/lucy"
|
||||
url = f"http://192.168.178.153:3000/api/v1/repos/{full_repo}/actions/runs?branch={urllib.parse.quote(branch)}&limit=1"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"Authorization": "token " + token})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
data = json.load(r)
|
||||
runs = data if isinstance(data, list) else data.get("runs", data.get("workflow_runs", []))
|
||||
if runs:
|
||||
run = runs[0]
|
||||
if run.get("head_sha", "").startswith(head_sha[:7]):
|
||||
st = run.get("status")
|
||||
if st:
|
||||
_CI_CACHE[cache_key] = {"status": st, "ts": time.time()}
|
||||
return st
|
||||
except Exception as e:
|
||||
log.warning("auftragsbuch: CI-Status fetch fehler: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _available() -> bool:
|
||||
@@ -176,6 +235,10 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
|
||||
stempel = gutachten.get(f"{repo}:{branch}")
|
||||
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
|
||||
stempel = None
|
||||
|
||||
head_sha = (_git(repo, ["rev-parse", ref]).stdout or "").strip()
|
||||
ci_status = _fetch_ci_status(repo, branch, head_sha) if head_sha else None
|
||||
|
||||
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
|
||||
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
|
||||
files = []
|
||||
@@ -209,6 +272,7 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
|
||||
# es im Repo liegt. Lucy wird dagegen IMMER am PC gebaut (kein dist im Repo).
|
||||
"frontend_ohne_build": repo == "mc2" and frontend_src and not frontend_dist,
|
||||
"status": None if (status or {}).get("state") == "eingespielt" and ahead > 0 else status,
|
||||
"ci_status": ci_status,
|
||||
})
|
||||
except Exception:
|
||||
log.warning("auftragsbuch: Branch %s (%s) nicht lesbar", branch, repo, exc_info=True)
|
||||
@@ -258,8 +322,12 @@ def list_proposals() -> dict:
|
||||
except Exception:
|
||||
log.warning("auftragsbuch: Telegram-Ping für %s fehlgeschlagen", branch, exc_info=True)
|
||||
|
||||
# Alle aktuellen Branches als gemeldet speichern
|
||||
_save_announce_branches(aktuelle_branches)
|
||||
# Als gemeldet speichern — aber NUR bei echter Änderung. Ein unbedingtes Schreiben
|
||||
# bumpte die mtime bei jedem Aufruf, der SSE-Sammler (events.py, Fingerabdruck =
|
||||
# Datei-mtime) meldete daraufhin „geändert", die UI holte neu, schrieb wieder …
|
||||
# → 3-s-Endlosschleife auf /api/auftragsbuch je offenem Client (gefunden 15.07. nachts).
|
||||
if aktuelle_branches != gemeldet:
|
||||
_save_announce_branches(aktuelle_branches)
|
||||
|
||||
kandidaten = list_skill_kandidaten()
|
||||
open_count = sum(1 for i in items
|
||||
|
||||
@@ -51,7 +51,7 @@ def backup_now() -> dict:
|
||||
r = subprocess.run(["/bin/bash", str(BACKUP_SH)], capture_output=True, text=True, timeout=180)
|
||||
if r.returncode != 0:
|
||||
return {"ok": False, "snapshot": "", "files": [], "error": (r.stderr or r.stdout).strip()[-300:]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
return {"ok": False, "snapshot": "", "files": [], "error": str(exc)}
|
||||
|
||||
latest = _latest()
|
||||
|
||||
+12
-10
@@ -10,8 +10,7 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
from config import LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT
|
||||
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT, V1_UPSTREAM
|
||||
|
||||
DEFAULT_HOST = "192.168.178.151"
|
||||
|
||||
@@ -36,7 +35,8 @@ IDE_MODELS = [
|
||||
|
||||
|
||||
def _gw(host: str) -> str:
|
||||
# Eingebauter Gateway: MC2 serviert /v1 selbst (gleicher Port wie das Cockpit).
|
||||
# Client-Endpunkt bleibt :9001/v1 (MC2-Port) — seit UMBAU v3 P1 reicht MC2 dort
|
||||
# nur noch roh an den eigenständigen mc2-gateway (:9010) durch.
|
||||
return f"http://{host}:{PORT}/v1"
|
||||
|
||||
|
||||
@@ -113,7 +113,8 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
"Coder (bauen) · Planer (denken) · Augen (Bilder) — oben im Wähler umschalten."},
|
||||
"kilo": {"label": "Kilo Code", "lang": "json", "snippet": kilo,
|
||||
"note": "VS Code/JetBrains (schwergewichtiger). OpenAI-Provider → Gateway. "
|
||||
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · Ask/Debug→chat."},
|
||||
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
||||
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
||||
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
|
||||
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway; Gedächtnis-Anbindung via Memory-MCP unten."},
|
||||
},
|
||||
@@ -125,18 +126,19 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
|
||||
def check_health() -> dict:
|
||||
"""Live-Erreichbarkeit der drei Leitungen, aus Sicht der Box:
|
||||
Leitung 1 = Gateway/Engine (llama-swap), Leitung 2 = Gedächtnis-Sidecar (Mem0),
|
||||
Leitung 1 = der ECHTE Modell-Pfad, den Clients nutzen (mc2-gateway bei V1_UPSTREAM,
|
||||
sonst llama-swap direkt), Leitung 2 = Gedächtnis-Sidecar (Mem0),
|
||||
Leitung 3 = Desktop-Gateway (Hermes-Builtin-UI)."""
|
||||
gateway = {"ok": False, "detail": "nicht erreichbar"}
|
||||
try:
|
||||
with httpx.Client(timeout=3.0) as c:
|
||||
r = c.get(f"{LLAMA_SWAP_URL}/v1/models")
|
||||
r = c.get(f"{V1_UPSTREAM or LLAMA_SWAP_URL}/v1/models")
|
||||
if r.status_code == 200:
|
||||
n = len(r.json().get("data", []))
|
||||
gateway = {"ok": True, "detail": f"{n} Modelle verfügbar" if n else "bereit"}
|
||||
else:
|
||||
gateway = {"ok": False, "detail": f"HTTP {r.status_code}"}
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
memory = {"ok": False, "detail": "nicht erreichbar"}
|
||||
@@ -145,16 +147,16 @@ def check_health() -> dict:
|
||||
r = c.get(f"{MEM0_SERVICE_URL}/health")
|
||||
memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200
|
||||
else {"ok": False, "detail": f"HTTP {r.status_code}"})
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
desktop_gateway = {"ok": False, "detail": "nicht erreichbar"}
|
||||
try:
|
||||
with httpx.Client(timeout=3.0) as c:
|
||||
r = c.get("http://127.0.0.1:9119/api/status")
|
||||
r = c.get(f"{HERMES_BUILTIN_UI_UPSTREAM}/api/status")
|
||||
desktop_gateway = ({"ok": True, "detail": "bereit"} if r.status_code == 200
|
||||
else {"ok": False, "detail": f"HTTP {r.status_code}"})
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"gateway": gateway, "memory": memory, "desktop_gateway": desktop_gateway}
|
||||
|
||||
@@ -9,16 +9,15 @@ spätere Auto-Setups nutzen ihn, damit sie nie auseinanderlaufen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
|
||||
import logging
|
||||
|
||||
from config import DISCOVER_CACHE_PATH, DISCOVER_TTL
|
||||
|
||||
from services import catalog
|
||||
from services.caps import capabilities
|
||||
from services.fit import evaluate_fit, extract_params_b, max_ctx_for
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
"""Eigenleben — was die Box von allein gebaut und vorgeschlagen hat.
|
||||
|
||||
Beantwortet die User-Frage vom 15.07.2026: „Es gibt keinen Viewpoint, welche Skills
|
||||
erstellt wurden, was die Box vorgeschlagen hat, welche Skills Lucy hat und wie die
|
||||
funktionieren." Reine LESE-Schicht, keine Schreib-Operationen:
|
||||
|
||||
- Skills aus ~/.hermes/skills (= Lucys Skill-Satz; die Worker-Profile werkstatt/betrieb
|
||||
tragen Kopien desselben Satzes). Herkunft dreistufig klassifiziert:
|
||||
selbst → weder mitgeliefert noch aus unserem Repo = die Box/Lucy hat ihn erzeugt
|
||||
repo → liegt in ~/mission-control-v2/deploy/skills (von uns gebaut + deployt)
|
||||
bundled → liegt in ~/.hermes/hermes-agent/skills (kam mit Hermes mit)
|
||||
- Nutzungszahlen aus ~/.hermes/skills/.usage.json (Hermes' eigener Zähler).
|
||||
- Bilanz aus Auftragsbuch-Chronik (/srv/models/mc2-auftragsbuch.json, angenommene
|
||||
Branches) + Ablehnungs-Lern-Journal (/srv/models/mc2-ablehnungen.jsonl).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||
BUNDLED_DIR = Path.home() / ".hermes" / "hermes-agent" / "skills"
|
||||
REPO_DIR = Path.home() / "mission-control-v2" / "deploy" / "skills"
|
||||
USAGE_FILE = SKILLS_DIR / ".usage.json"
|
||||
AUFTRAGSBUCH_FILE = Path("/srv/models/mc2-auftragsbuch.json")
|
||||
ABLEHNUNGEN_FILE = Path("/srv/models/mc2-ablehnungen.jsonl")
|
||||
|
||||
_HERKUNFT_LABEL = {
|
||||
"selbst": "Selbst erstellt",
|
||||
"repo": "Von uns gebaut",
|
||||
"bundled": "Mit Hermes mitgeliefert",
|
||||
}
|
||||
|
||||
|
||||
def _frontmatter(text: str) -> dict:
|
||||
"""Sehr kleiner Frontmatter-Leser: nur die flachen key: value-Zeilen des ersten
|
||||
---Blocks (name/description/version/author reichen hier; kein YAML-Import nötig)."""
|
||||
out: dict = {}
|
||||
if not text.startswith("---"):
|
||||
return out
|
||||
for line in text.split("\n", 1)[1].split("\n"):
|
||||
if line.strip() == "---":
|
||||
break
|
||||
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
|
||||
if not m:
|
||||
continue
|
||||
val = m.group(2).strip().strip('"').strip("'")
|
||||
if val:
|
||||
out[m.group(1)] = val
|
||||
return out
|
||||
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
return re.sub(r"[\s_-]+", "", (s or "").lower())
|
||||
|
||||
|
||||
def list_skills() -> list[dict]:
|
||||
"""Alle Skills mit Beschreibung, Herkunft und Nutzungszahlen (leer im Dev-Modus)."""
|
||||
if not SKILLS_DIR.is_dir():
|
||||
return []
|
||||
try:
|
||||
usage_raw = json.loads(USAGE_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
usage_raw = {}
|
||||
usage = {_norm(k): v for k, v in usage_raw.items()}
|
||||
|
||||
skills: list[dict] = []
|
||||
for d in sorted(SKILLS_DIR.iterdir()):
|
||||
md = d / "SKILL.md"
|
||||
if not d.is_dir() or not md.is_file():
|
||||
continue
|
||||
try:
|
||||
fm = _frontmatter(md.read_text(encoding="utf-8", errors="replace"))
|
||||
except Exception:
|
||||
fm = {}
|
||||
if (BUNDLED_DIR / d.name).is_dir():
|
||||
herkunft = "bundled"
|
||||
elif (REPO_DIR / d.name).is_dir():
|
||||
herkunft = "repo"
|
||||
else:
|
||||
herkunft = "selbst"
|
||||
u = usage.get(_norm(d.name)) or usage.get(_norm(fm.get("name", ""))) or {}
|
||||
skills.append({
|
||||
"id": d.name,
|
||||
"name": fm.get("name") or d.name,
|
||||
"beschreibung": fm.get("description") or "",
|
||||
"version": fm.get("version") or "",
|
||||
"autor": fm.get("author") or "",
|
||||
"herkunft": herkunft,
|
||||
"herkunft_label": _HERKUNFT_LABEL[herkunft],
|
||||
"genutzt": int(u.get("use_count") or 0),
|
||||
"zuletzt_genutzt": u.get("last_used_at") or None,
|
||||
"erstellt": u.get("created_at") or None,
|
||||
"status": u.get("state") or "active",
|
||||
"geaendert": md.stat().st_mtime,
|
||||
})
|
||||
# Selbst erstellte zuerst — das ist der Star der Ansicht.
|
||||
order = {"selbst": 0, "repo": 1, "bundled": 2}
|
||||
skills.sort(key=lambda s: (order[s["herkunft"]], -s["genutzt"], s["name"]))
|
||||
return skills
|
||||
|
||||
|
||||
def skill_text(skill_id: str) -> str | None:
|
||||
"""Voller SKILL.md-Text („wie funktioniert der Skill") — nur Namen ohne Pfad-Tricks."""
|
||||
if not re.fullmatch(r"[\w.-]+", skill_id or ""):
|
||||
return None
|
||||
md = SKILLS_DIR / skill_id / "SKILL.md"
|
||||
if not md.is_file():
|
||||
return None
|
||||
try:
|
||||
return md.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def bilanz() -> dict:
|
||||
"""Vorschlags-Bilanz: was die Box vorschlug und was daraus wurde (eine Timeline)."""
|
||||
eintraege: list[dict] = []
|
||||
try:
|
||||
branches = json.loads(AUFTRAGSBUCH_FILE.read_text(encoding="utf-8")).get("branches") or {}
|
||||
for branch, info in branches.items():
|
||||
eintraege.append({
|
||||
"art": "angenommen" if info.get("state") == "eingespielt" else info.get("state", "?"),
|
||||
"titel": branch,
|
||||
"detail": info.get("detail") or "",
|
||||
"ts": float(info.get("ts") or 0),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
for line in ABLEHNUNGEN_FILE.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
j = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
eintraege.append({
|
||||
"art": "abgelehnt",
|
||||
"titel": j.get("subject") or j.get("branch") or "?",
|
||||
"detail": j.get("grund") or "",
|
||||
"ts": float(j.get("ts") or 0),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
eintraege.sort(key=lambda e: -e["ts"])
|
||||
return {
|
||||
"eintraege": eintraege,
|
||||
"angenommen": sum(1 for e in eintraege if e["art"] == "angenommen"),
|
||||
"abgelehnt": sum(1 for e in eintraege if e["art"] == "abgelehnt"),
|
||||
}
|
||||
|
||||
|
||||
def overview() -> dict:
|
||||
skills = list_skills()
|
||||
b = bilanz()
|
||||
return {
|
||||
"available": SKILLS_DIR.is_dir(),
|
||||
"skills": skills,
|
||||
"selbst_erstellt": sum(1 for s in skills if s["herkunft"] == "selbst"),
|
||||
"bilanz": b,
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Routing-Gateway-Status (eingebauter Modus). MC2 IST der Gateway: serviert
|
||||
`/v1/*` mit `model: auto`-Komplexitäts-Routing vor llama-swap. Kein externer
|
||||
LiteLLM-Dienst nötig (baut auf Python 3.14 nicht); bleibt später austauschbar.
|
||||
Routing-Gateway-Status. Seit UMBAU v3 P1 bedient der EIGENSTÄNDIGE mc2-gateway-
|
||||
Prozess den /v1-Pfad (MC_V1_UPSTREAM gesetzt, MC2 reicht nur roh durch); ohne
|
||||
V1_UPSTREAM gilt der alte eingebaute Modus (MC2 serviert /v1 selbst).
|
||||
"""
|
||||
|
||||
from config import PORT
|
||||
import httpx
|
||||
from config import PORT, V1_UPSTREAM
|
||||
|
||||
from services.llamaswap import engine_reachable
|
||||
from services.routing_policy import load_policy
|
||||
|
||||
@@ -13,7 +15,7 @@ def routing_summary() -> dict:
|
||||
p = load_policy()
|
||||
coding_default = p["coder_lite"] or p["coder"]
|
||||
return {
|
||||
"mode": "builtin",
|
||||
"mode": "gateway (eigener Prozess)" if V1_UPSTREAM else "builtin",
|
||||
"endpoint": f":{PORT}/v1 (OpenAI-kompatibel)",
|
||||
# Virtuelle Lanes, die Clients/IDEs als „Modell" wählen (Router pickt das echte Alias).
|
||||
"lanes": [
|
||||
@@ -42,5 +44,13 @@ def routing_summary() -> dict:
|
||||
|
||||
|
||||
def gateway_reachable() -> bool:
|
||||
# Der eingebaute Gateway lebt in MC und proxyt llama-swap → erreichbar, wenn Engine läuft.
|
||||
"""Erreichbarkeit des ECHTEN Denkpfads. Mit V1_UPSTREAM ist das der eigenständige
|
||||
mc2-gateway-Prozess (:9010) — vorher meldete diese Funktion nur die Engine, d. h.
|
||||
ein toter Gateway blieb in Health/Diensten/Cockpit unsichtbar (Review 15.07.)."""
|
||||
if V1_UPSTREAM:
|
||||
try:
|
||||
return httpx.get(f"{V1_UPSTREAM}/v1/models", timeout=2.0).status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
# Eingebauter Modus: der Gateway lebt in MC selbst und proxyt llama-swap.
|
||||
return engine_reachable()
|
||||
|
||||
@@ -7,11 +7,46 @@ verstreute, still scheiternde String-Suche durch einen testbaren SSE-Zeilenparse
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from services.token_stats import increment_tokens
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Kontext-Limit-Warnung (User-Wunsch 15.07.: „es sollte eine Warnung geben — Kontext ist
|
||||
# nicht unendlich"): finish_reason=length heißt, eine Antwort ist am Token-/Kontext-Budget
|
||||
# ABGERISSEN — bei Nacht-Workern stirbt damit still die halbe Arbeit (4 Worker am 15.07.).
|
||||
# Statt still: Eintrag in den Briefkasten (silent → Chronik/Panel, kein Sprach-Spam),
|
||||
# je Modell höchstens alle 10 min. Läuft im mc2-gateway-Prozess → announce liefert per
|
||||
# MC_ANNOUNCE_HTTP beim Steuerpult ab (Unit-Env), nie direkt in die Store-Datei.
|
||||
_trunc_last: dict[str, float] = {}
|
||||
_TRUNC_EVERY = 600.0 # s
|
||||
|
||||
# Warm-Pings (Lucys Augen-Wärmer alle 10 min, warmup.sh, models/load) halten Modelle
|
||||
# ABSICHTLICH mit max_tokens=1 warm — deren finish_reason=length ist konstruktionsbedingt
|
||||
# und kein abgerissener Worker (Fehlalarm-Serie 16.07. abends, ~alle 20 min im Briefkasten).
|
||||
# Wer freiwillig so knapp deckelt, will keine echte Antwort → keine Warnung.
|
||||
_PING_MAXTOK = 16
|
||||
|
||||
|
||||
def warn_truncation(model: str, max_tokens: int | None = None) -> None:
|
||||
if isinstance(max_tokens, int) and max_tokens <= _PING_MAXTOK:
|
||||
return
|
||||
now = time.time()
|
||||
if now - _trunc_last.get(model, 0.0) < _TRUNC_EVERY:
|
||||
return
|
||||
_trunc_last[model] = now
|
||||
log.warning("gateway: finish_reason=length bei %s — Antwort am Kontext-/Token-Limit abgerissen", model)
|
||||
try:
|
||||
from services import announce
|
||||
announce.add(
|
||||
f"Eine Antwort von „{model}“ ist am Token-/Kontext-Limit abgerissen "
|
||||
f"(finish_reason=length). War das ein Nacht-Worker, ist seine Aufgabe zu groß "
|
||||
f"geschnitten — besser in Etappen teilen (eine Etappe = ein Worker-Lauf).",
|
||||
"[Kontext-Limit]", "gateway", "silent")
|
||||
except Exception:
|
||||
log.warning("gateway: Kontext-Limit-Warnung nicht zustellbar", exc_info=True)
|
||||
|
||||
|
||||
def record_usage(usage: dict | None, model: str) -> None:
|
||||
"""Ein usage-Objekt verbuchen (no-op bei None/leer)."""
|
||||
@@ -23,9 +58,11 @@ def record_usage(usage: dict | None, model: str) -> None:
|
||||
increment_tokens(prompt, completion, model=model)
|
||||
|
||||
|
||||
def record_stream_chunk(chunk: bytes, model: str) -> None:
|
||||
def record_stream_chunk(chunk: bytes, model: str, max_tokens: int | None = None) -> None:
|
||||
"""Rohen SSE-Chunk auf `usage` prüfen und Tokens verbuchen. Fehler werden
|
||||
geloggt (debug) statt verschluckt — ein defekter Chunk bricht den Stream nicht."""
|
||||
if b'"finish_reason":"length"' in chunk or b'"finish_reason": "length"' in chunk:
|
||||
warn_truncation(model, max_tokens)
|
||||
if b'"usage"' not in chunk:
|
||||
return
|
||||
text = chunk.decode("utf-8", errors="ignore")
|
||||
|
||||
+975
-9
File diff suppressed because it is too large
Load Diff
@@ -90,7 +90,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor
|
||||
log_str = "\n".join(job["log"])
|
||||
if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str:
|
||||
job["sudo_failed"] = True
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
_append_log(job, f"[mc] Fehler: {exc}")
|
||||
job["state"] = "failed"
|
||||
job["returncode"] = -1
|
||||
@@ -101,7 +101,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor
|
||||
if cb and job["state"] == "done":
|
||||
try:
|
||||
cb()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
_append_log(job, f"[mc] Nachbearbeitung-Fehler: {exc}")
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
|
||||
j["rate_bps"] = rate
|
||||
j["eta_s"] = int((total_bytes - cur) / rate)
|
||||
prev_t, prev_b = now, cur
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1.0)
|
||||
j = JOBS.get(job_id)
|
||||
@@ -182,7 +182,7 @@ def cancel_job(job_id: str) -> bool:
|
||||
if proc is not None:
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
job["state"] = "canceled"
|
||||
|
||||
@@ -11,12 +11,17 @@ import os
|
||||
import re
|
||||
|
||||
import httpx
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from config import (
|
||||
CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, DRAFTS_DIR, LLAMA_SWAP_URL,
|
||||
SPEC_DRAFT_MODEL_PATH, SPEC_DRAFT_N_MAX, SPEC_TYPE,
|
||||
CMD_TEMPLATE,
|
||||
CONFIG_PATH,
|
||||
DEFAULT_TTL,
|
||||
DRAFTS_DIR,
|
||||
LLAMA_SWAP_URL,
|
||||
SPEC_DRAFT_MODEL_PATH,
|
||||
SPEC_DRAFT_N_MAX,
|
||||
SPEC_TYPE,
|
||||
)
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -143,19 +148,36 @@ def model_id_from_path(model_path: str) -> str:
|
||||
Split-GGUFs liegen oft in einem Quant-Unterordner (…/Q4_K_M/file-00001-of-…) →
|
||||
dann eine Ebene höher (Repo-Ordner) nehmen, sonst hieße das Modell 'Q4_K_M'."""
|
||||
d = os.path.basename(os.path.dirname(model_path))
|
||||
if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.I):
|
||||
if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.IGNORECASE):
|
||||
d = os.path.basename(os.path.dirname(os.path.dirname(model_path)))
|
||||
name = re.sub(r"[-_]?GGUF$", "", d, flags=re.I).strip("-_")
|
||||
name = re.sub(r"[-_]?GGUF$", "", d, flags=re.IGNORECASE).strip("-_")
|
||||
if not name:
|
||||
fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.I)
|
||||
fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.IGNORECASE)
|
||||
fn = re.sub(r"-\d+-of-\d+$", "", fn)
|
||||
name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.I)
|
||||
name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.IGNORECASE)
|
||||
return name or "modell"
|
||||
|
||||
|
||||
# Lebenswichtige Aliase: hängen direkt an Lucys Denk- und Gedächtnis-Pfad. Sie dürfen
|
||||
# beim Rollen-Umhängen NIE stillschweigend verloren gehen (Review 16.07.: Qwen3.6 hält
|
||||
# live `hermes` UND `fast` — ein Rollen-Klick hätte beide gelöscht → Lucy tot).
|
||||
PROTECTED_ALIASES = {"hermes", "embed"}
|
||||
|
||||
|
||||
def protected_alias_of(model_id: str) -> str | None:
|
||||
"""Hält dieses Modell gerade einen lebenswichtigen Alias? (für Guards in der API)"""
|
||||
spec = (read_config().get("models") or {}).get(model_id)
|
||||
if isinstance(spec, dict):
|
||||
for a in spec.get("aliases") or []:
|
||||
if str(a).lower() in PROTECTED_ALIASES:
|
||||
return str(a).lower()
|
||||
return None
|
||||
|
||||
|
||||
def set_role_alias(cfg: dict, model_id: str, role: str | None) -> None:
|
||||
"""Rolle als eindeutigen llama-swap-`aliases`-Eintrag setzen (vorher bei allen
|
||||
anderen Modellen entfernen). role=None/leer entfernt den Alias."""
|
||||
"""Rolle als llama-swap-`aliases`-Eintrag setzen (vorher bei allen anderen Modellen
|
||||
entfernen — deren übrige Aliase bleiben). role=None/leer entfernt die Rolle.
|
||||
Lebenswichtige Aliase (PROTECTED_ALIASES) des Ziel-Modells bleiben IMMER erhalten."""
|
||||
models = cfg.get("models") or {}
|
||||
role = (role or "").strip().lower()
|
||||
if role:
|
||||
@@ -169,8 +191,11 @@ def set_role_alias(cfg: dict, model_id: str, role: str | None) -> None:
|
||||
spec.pop("aliases", None)
|
||||
spec = models.get(model_id)
|
||||
if isinstance(spec, dict):
|
||||
if role and role != model_id.lower():
|
||||
spec["aliases"] = [role]
|
||||
keep = [a for a in (spec.get("aliases") or [])
|
||||
if str(a).lower() in PROTECTED_ALIASES and str(a).lower() != role]
|
||||
new = keep + ([role] if role and role != model_id.lower() else [])
|
||||
if new:
|
||||
spec["aliases"] = new
|
||||
else:
|
||||
spec.pop("aliases", None)
|
||||
|
||||
@@ -202,7 +227,7 @@ def write_config(cfg: dict) -> None:
|
||||
try:
|
||||
from services import warmer
|
||||
warmer.nudge()
|
||||
except Exception: # noqa: BLE001 — Vorwärmen ist Komfort, nie ein Schreib-Blocker
|
||||
except Exception:
|
||||
pass
|
||||
except PermissionError as exc:
|
||||
raise PermissionError(
|
||||
@@ -214,9 +239,12 @@ def write_config(cfg: dict) -> None:
|
||||
|
||||
def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
||||
ttl: int | None = None, mmproj_path: str | None = None,
|
||||
jinja: bool = False) -> str:
|
||||
jinja: bool = False, set_alias: bool = True) -> str:
|
||||
"""Ein GGUF als llama-swap-Modell eintragen (cmd + Rolle-Alias). Gibt die
|
||||
Modell-ID zurück. jinja=True erzwingt --jinja (Tool-Calling, z.B. fürs Agent-Hirn)."""
|
||||
Modell-ID zurück. jinja=True erzwingt --jinja (Tool-Calling, z.B. fürs Agent-Hirn).
|
||||
set_alias=False: Rolle nur für die cmd-Flags nutzen, den Alias aber NICHT umhängen —
|
||||
der Install-Flow setzt ihn erst NACH fertigem Download (sonst zeigt die Rolle
|
||||
minutenlang auf eine Datei, die noch gar nicht existiert)."""
|
||||
cfg = read_config()
|
||||
model_id = model_id_from_path(model_path)
|
||||
cmd = CMD_TEMPLATE.replace("{model}", model_path).replace("{ctx}", str(ctx))
|
||||
@@ -242,11 +270,19 @@ def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
||||
if "--spec-draft-model" not in cmd and "--model-draft" not in cmd:
|
||||
cmd += spec_draft_flags(model_path)
|
||||
|
||||
cfg.setdefault("models", {})[model_id] = {
|
||||
# Bestehende Aliase des Eintrags erhalten (Re-Install/Upgrade desselben Repos):
|
||||
# die Rolle soll während des Downloads beim ALTEN Stand bleiben.
|
||||
old_aliases = (cfg.get("models") or {}).get(model_id, {})
|
||||
old_aliases = old_aliases.get("aliases") if isinstance(old_aliases, dict) else None
|
||||
entry: dict = {
|
||||
"cmd": LiteralScalarString(cmd + "\n"),
|
||||
"ttl": ttl if ttl is not None else DEFAULT_TTL,
|
||||
}
|
||||
set_role_alias(cfg, model_id, role)
|
||||
if old_aliases:
|
||||
entry["aliases"] = old_aliases
|
||||
cfg.setdefault("models", {})[model_id] = entry
|
||||
if set_alias:
|
||||
set_role_alias(cfg, model_id, role)
|
||||
write_config(cfg)
|
||||
return model_id
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ from services import catalog, discover, jobengine, llamaswap, system
|
||||
|
||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||
SYSTEM_SERVICES = {"llama-swap"}
|
||||
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-builtin-ui", "mem0-service", "voice-service"}
|
||||
USER_SERVICES = {"mission-control-2", "mc2-gateway", "mc2-steward", "box-console",
|
||||
"hermes-gateway", "hermes-builtin-ui", "mem0-service", "voice-service"}
|
||||
|
||||
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
@@ -34,7 +35,7 @@ _engine_cache = {"ts": 0.0, "avail": False}
|
||||
# manchmal noch keine CI-Assets (0 Assets) → ihr Download-Link 404t. Sowohl der Update-Check
|
||||
# als auch der Download (update-engine.sh) müssen daher die neueste ASSET-tragende Release
|
||||
# nehmen, sonst zeigt das UI „Update verfügbar", das dann beim Einspielen scheitert.
|
||||
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.I)
|
||||
_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE)
|
||||
|
||||
|
||||
def _latest_engine_asset_release() -> dict | None:
|
||||
@@ -288,7 +289,7 @@ def model_upgrades() -> list[dict]:
|
||||
continue
|
||||
|
||||
base = rec.split("/")[-1].lower()
|
||||
stem = base[:-5] if base.endswith("-gguf") else base
|
||||
stem = base.removesuffix("-gguf")
|
||||
if base in cmds or (stem and stem in cmds):
|
||||
continue # schon installiert
|
||||
out.append({"role": role, "title": c["title"], "repo": rec})
|
||||
@@ -342,7 +343,7 @@ def _os_held_back() -> list[dict]:
|
||||
held.append({"name": name, "reason": reason})
|
||||
elif reason: # nicht eingerückt → Abschnitt zu Ende
|
||||
reason = None
|
||||
except Exception: # noqa: BLE001 — nur Zusatzinfo, nie ein Blocker
|
||||
except Exception:
|
||||
pass
|
||||
held.sort(key=lambda p: p["name"])
|
||||
return held
|
||||
@@ -367,7 +368,7 @@ def os_update_details() -> dict:
|
||||
out_pkgs.sort(key=lambda p: p["name"])
|
||||
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs,
|
||||
"held_back": _os_held_back()}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs, "error": str(exc)}
|
||||
|
||||
|
||||
@@ -392,7 +393,7 @@ def engine_update_details() -> dict:
|
||||
ctx = ("Es geht um ein Update der Inferenz-Engine llama.cpp (Vulkan-Build, treibt alle "
|
||||
"Sprachmodelle der Box auf der AMD-Strix-Halo-GPU).")
|
||||
info.update(_summarize_release("engine", tag, ctx, body[:6000]))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
|
||||
@@ -417,7 +418,7 @@ def swap_update_details() -> dict:
|
||||
ctx = ("Es geht um ein Update von llama-swap (der Router, der Anfragen an die Box "
|
||||
"verteilt und Sprachmodelle heiß nachlädt).")
|
||||
info.update(_summarize_release("swap", tag, ctx, body[:6000]))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
|
||||
@@ -495,7 +496,7 @@ def _summarize_release(kind: str, key: str, context: str, changes: str) -> dict:
|
||||
if text:
|
||||
cache.update(key=key, data=data)
|
||||
return data
|
||||
except Exception as exc: # noqa: BLE001 — Zusammenfassung ist Komfort, nie Blocker
|
||||
except Exception as exc:
|
||||
return {"summary": f"(Zusammenfassung nicht verfügbar: {exc})",
|
||||
"action_needed": None, "action_text": ""}
|
||||
|
||||
@@ -533,7 +534,7 @@ def hermes_update_details() -> dict:
|
||||
info["behind"] = len(commits)
|
||||
if commits:
|
||||
info.update(_summarize_hermes_commits(commits))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
|
||||
@@ -574,7 +575,7 @@ def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
|
||||
return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."}
|
||||
|
||||
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
return {"ok": False, "out": "", "err": str(exc)}
|
||||
|
||||
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
"""
|
||||
Geteiltes Gedächtnis (die „Verfassung") — jetzt auto-lernend & semantisch über Mem0.
|
||||
|
||||
Dieser Service ist nur noch ein dünner HTTP-Client auf den Mem0-Sidecar (mem0_service/app.py,
|
||||
läuft im ~/.mem0/venv unter Python 3.12). Die `/api/memory`-API-Form bleibt unverändert, damit
|
||||
UI und MCP-Server kompatibel bleiben. Neu gegenüber der alten flachen SQLite:
|
||||
|
||||
- search (q gesetzt) ist SEMANTISCH (Vektor/Embeddings) statt LIKE-Textsuche, mit Relevanz-Score.
|
||||
- learn() reicht Gesprächs-Turns durch → Mem0 EXTRAHIERT Fakten selbst (Auto-Lernen).
|
||||
- Dedup macht Mem0 beim Auto-Lernen selbst; der manuelle Kurator unten bleibt als Komfort.
|
||||
|
||||
5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
import httpx
|
||||
|
||||
from config import MEM0_SERVICE_URL
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
||||
|
||||
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
||||
|
||||
|
||||
def _get(path: str, **params) -> list | dict:
|
||||
r = httpx.get(f"{MEM0_SERVICE_URL}{path}", params=params, timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _post(path: str, data: dict) -> dict:
|
||||
r = httpx.post(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _put(path: str, data: dict) -> dict:
|
||||
r = httpx.put(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _delete(path: str) -> dict:
|
||||
r = httpx.delete(f"{MEM0_SERVICE_URL}{path}", timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def list_memories(q: str = "", category: str = "") -> list[dict]:
|
||||
"""Alle Fakten oder — wenn q gesetzt — die semantisch ähnlichsten (mit `score`)."""
|
||||
return _get("/memory", **{k: v for k, v in (("q", q), ("category", category)) if v})
|
||||
|
||||
|
||||
def add_memory(content: str, category: str = "stable", source: str = "manual") -> dict:
|
||||
"""Einen Fakt VERBATIM speichern (keine LLM-Umformung). Auto-Lernen → learn()."""
|
||||
return _post("/memory", {"content": content.strip(), "category": category, "source": source})
|
||||
|
||||
|
||||
def update_memory(mid: str, content: str | None = None, category: str | None = None) -> dict | None:
|
||||
try:
|
||||
return _put(f"/memory/{mid}", {"content": content, "category": category})
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def delete_memory(mid: str) -> bool:
|
||||
try:
|
||||
_delete(f"/memory/{mid}")
|
||||
return True
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
def learn(text: str | None = None, messages: list[dict] | None = None,
|
||||
source: str = "auto", category: str = "stable") -> dict:
|
||||
"""Auto-Lernen: Text/Gesprächs-Turns durchreichen → Mem0 extrahiert die Fakten selbst."""
|
||||
return _post("/learn", {"text": text, "messages": messages,
|
||||
"source": source, "category": category})
|
||||
|
||||
|
||||
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
||||
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die UI-Visualisierung."""
|
||||
return _get("/graph", min_score=min_score, top_k=top_k)
|
||||
|
||||
|
||||
def export_text() -> dict:
|
||||
rows = sorted(list_memories(), key=lambda r: (r.get("category", ""), r.get("updated_at", "")))
|
||||
lines = ["# Mission Control — Gedächtnis\n"]
|
||||
current = ""
|
||||
for r in rows:
|
||||
if r.get("category") != current:
|
||||
current = r.get("category", "")
|
||||
lines.append(f"\n## {current}\n")
|
||||
src = r.get("source", "")
|
||||
when = (r.get("updated_at") or "")[:10]
|
||||
lines.append(f"- {r.get('content', '')} _(Quelle: {src}, {when})_")
|
||||
return {"text": "\n".join(lines), "count": len(rows)}
|
||||
|
||||
|
||||
# --- Manueller Kurator (deterministisch, kein LLM) ---------------------------
|
||||
def _norm(s: str) -> str:
|
||||
s = re.sub(r"[^\w\s]", " ", s.lower(), flags=re.UNICODE)
|
||||
return re.sub(r"\s+", " ", s).strip()
|
||||
|
||||
|
||||
def dedupe(apply: bool = False, threshold: float = 0.85) -> dict:
|
||||
"""Findet Dubletten (exakt/enthalten/ähnlich) je Kategorie, behält den längsten
|
||||
Eintrag. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier ist der
|
||||
manuelle Komfort-Knopf fürs UI (z.B. nach vielen Verbatim-Importen)."""
|
||||
rows = sorted(list_memories(), key=lambda r: (-len(r.get("content", "")), r.get("created_at", "")))
|
||||
used: set[str] = set()
|
||||
groups: list[dict] = []
|
||||
for i, a in enumerate(rows):
|
||||
if a["id"] in used:
|
||||
continue
|
||||
na = _norm(a.get("content", ""))
|
||||
if not na:
|
||||
continue
|
||||
dups = []
|
||||
for b in rows[i + 1:]:
|
||||
if b["id"] in used or b.get("category") != a.get("category"):
|
||||
continue
|
||||
nb = _norm(b.get("content", ""))
|
||||
if not nb:
|
||||
continue
|
||||
if nb in na or na in nb or SequenceMatcher(None, na, nb).ratio() >= threshold:
|
||||
dups.append(b); used.add(b["id"])
|
||||
if dups:
|
||||
used.add(a["id"])
|
||||
groups.append({
|
||||
"keep": {"id": a["id"], "content": a.get("content"), "category": a.get("category")},
|
||||
"remove": [{"id": d["id"], "content": d.get("content")} for d in dups],
|
||||
})
|
||||
dup_count = sum(len(g["remove"]) for g in groups)
|
||||
removed = 0
|
||||
if apply:
|
||||
for g in groups:
|
||||
for d in g["remove"]:
|
||||
if delete_memory(d["id"]):
|
||||
removed += 1
|
||||
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}
|
||||
|
||||
|
||||
# ── Auto-Dedupe (D16e): Dubletten von selbst wegräumen, kein Knopf-Zwang ─────────────
|
||||
# Deterministisch in UNSERER Schicht (wie reminders, Verdikt 11c) statt am Hermes-cron —
|
||||
# läuft als Hintergrund-Task. Höhere Schwelle als der manuelle Knopf (0.9 statt 0.85), weil
|
||||
# OHNE Mensch-Review angewandt wird: lieber eine Dublette stehen lassen als etwas Distinktes
|
||||
# löschen. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier fängt den Rest
|
||||
# (Verbatim-Importe, wiederholte Fakten) ab, damit das Gedächtnis nicht ohne Zutun aufbläht.
|
||||
AUTO_DEDUPE_ENABLED = os.environ.get("MC_MEM_DEDUPE_ENABLED", "1") != "0"
|
||||
AUTO_DEDUPE_INTERVAL = int(os.environ.get("MC_MEM_DEDUPE_INTERVAL", str(24 * 3600))) # täglich
|
||||
AUTO_DEDUPE_START_DELAY = int(os.environ.get("MC_MEM_DEDUPE_START_DELAY", "300")) # 5 min nach Start
|
||||
AUTO_DEDUPE_THRESHOLD = float(os.environ.get("MC_MEM_DEDUPE_THRESHOLD", "0.75"))
|
||||
|
||||
|
||||
async def auto_dedupe_loop() -> None:
|
||||
"""Hintergrund-Task: räumt periodisch Gedächtnis-Dubletten weg (apply=True)."""
|
||||
await asyncio.sleep(AUTO_DEDUPE_START_DELAY) # Mem0-Sidecar nach Start hochkommen lassen
|
||||
while True:
|
||||
try:
|
||||
res = await asyncio.to_thread(dedupe, True, AUTO_DEDUPE_THRESHOLD) # sync HTTP → Thread
|
||||
if res.get("removed"):
|
||||
log.info("mem-dedupe: %d Dublette(n) automatisch entfernt", res["removed"])
|
||||
except Exception:
|
||||
log.debug("mem-dedupe: Lauf fehlgeschlagen", exc_info=True)
|
||||
await asyncio.sleep(AUTO_DEDUPE_INTERVAL)
|
||||
@@ -0,0 +1,128 @@
|
||||
"""24-h-Metrik-Verlauf für die Cockpit-Zeitachse (User-Wunsch 16.07.: „WANN war Last?").
|
||||
|
||||
Ein leichter Sammler (Lifespan-Task in app.py) legt alle SAMPLE_S Sekunden einen
|
||||
kompakten Punkt in einen Ringpuffer: CPU/RAM/GPU/Disk in Prozent + die Token-
|
||||
GESAMTZÄHLER (das Frontend rechnet Raten aus den Deltas). Der Puffer hält exakt
|
||||
24 h — Älteres fällt automatisch raus (deque maxlen), nichts wächst unbegrenzt.
|
||||
Periodisch wird auf Platte persistiert (übersteht MC2-Restarts/Deploys); beim
|
||||
Laden fliegt alles raus, was älter als 24 h ist.
|
||||
|
||||
Bewusst NICHT im Steward: die Historie ist reine UI-Ware, und ein paar Sekunden
|
||||
Lücke pro Deploy sind egal.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SAMPLE_S = 10 # Abtast-Takt
|
||||
RETENTION_S = 24 * 3600 # 24 h — danach löschen und neu bauen (Ringpuffer)
|
||||
MAXLEN = RETENTION_S // SAMPLE_S # 8640 Punkte
|
||||
FLUSH_S = 300 # höchstens alle 5 min auf Platte
|
||||
MAX_RETURN_POINTS = 300 # Endpoint downsampled auf ~diese Punktzahl
|
||||
|
||||
HISTORY_PATH = MODELS_DIR / "mc2-metrics-history.json"
|
||||
|
||||
# Punkt = [t, cpu, ram, gpu, disk, tp, tc] (t = Epoch-Sekunden; tp/tc = Token-Totale)
|
||||
_points: deque = deque(maxlen=MAXLEN)
|
||||
_loaded = False
|
||||
_last_flush = 0.0
|
||||
|
||||
|
||||
def _load() -> None:
|
||||
global _loaded
|
||||
if _loaded:
|
||||
return
|
||||
_loaded = True
|
||||
try:
|
||||
raw = json.loads(HISTORY_PATH.read_text(encoding="utf-8"))
|
||||
cutoff = time.time() - RETENTION_S
|
||||
for p in raw if isinstance(raw, list) else []:
|
||||
if isinstance(p, list) and len(p) == 7 and isinstance(p[0], (int, float)) and p[0] >= cutoff:
|
||||
_points.append(p)
|
||||
if _points:
|
||||
log.info("metrics_history: %d Punkte aus %s geladen", len(_points), HISTORY_PATH)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
log.warning("metrics_history: %s nicht lesbar — starte leer", HISTORY_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _flush() -> None:
|
||||
global _last_flush
|
||||
_last_flush = time.time()
|
||||
try:
|
||||
tmp = HISTORY_PATH.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(list(_points), separators=(",", ":")), encoding="utf-8")
|
||||
tmp.replace(HISTORY_PATH)
|
||||
except OSError:
|
||||
log.warning("metrics_history: %s nicht schreibbar", HISTORY_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _sample() -> None:
|
||||
import psutil
|
||||
|
||||
from services.system import _gpu_sysfs
|
||||
from services.token_stats import get_stats
|
||||
|
||||
vm = psutil.virtual_memory()
|
||||
disk = None
|
||||
try:
|
||||
du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd())
|
||||
disk = du.percent
|
||||
except Exception:
|
||||
pass
|
||||
gpu = None
|
||||
try:
|
||||
g = _gpu_sysfs()
|
||||
gpu = g.get("busy_percent") if g else None
|
||||
except Exception:
|
||||
pass
|
||||
tp = tc = None
|
||||
try:
|
||||
ts = get_stats()
|
||||
tp, tc = ts.get("prompt_tokens"), ts.get("completion_tokens")
|
||||
except Exception:
|
||||
pass
|
||||
# interval=None: nicht-blockierende CPU-Messung seit dem letzten Aufruf (10 s her — ideal).
|
||||
_points.append([int(time.time()), psutil.cpu_percent(interval=None), vm.percent, gpu, disk, tp, tc])
|
||||
|
||||
|
||||
async def sampler_loop() -> None:
|
||||
"""Dauer-Sammler fürs App-Lifespan. Fehler eines Ticks reißen die Schleife nie."""
|
||||
_load()
|
||||
while True:
|
||||
try:
|
||||
await asyncio.to_thread(_sample)
|
||||
except Exception:
|
||||
log.debug("metrics_history: Sample fehlgeschlagen", exc_info=True)
|
||||
if time.time() - _last_flush >= FLUSH_S:
|
||||
try:
|
||||
await asyncio.to_thread(_flush)
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(SAMPLE_S)
|
||||
|
||||
|
||||
def history(minutes: int = 60) -> dict:
|
||||
"""Punkte der letzten N Minuten, auf ≤ MAX_RETURN_POINTS ausgedünnt (Stride)."""
|
||||
_load()
|
||||
minutes = max(1, min(int(minutes), RETENTION_S // 60))
|
||||
cutoff = time.time() - minutes * 60
|
||||
pts = [p for p in _points if p[0] >= cutoff]
|
||||
stride = max(1, len(pts) // MAX_RETURN_POINTS)
|
||||
pts = pts[::stride]
|
||||
return {
|
||||
"sample_s": SAMPLE_S * stride,
|
||||
"points": [
|
||||
{"t": p[0], "cpu": p[1], "ram": p[2], "gpu": p[3], "disk": p[4], "tp": p[5], "tc": p[6]}
|
||||
for p in pts
|
||||
],
|
||||
}
|
||||
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
from services import announce
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,8 +20,8 @@ import time
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
from config import HERMES_API_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
|
||||
from config import HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
from services import announce, llamaswap
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -85,8 +85,22 @@ CHECKS: dict[str, tuple] = {
|
||||
}
|
||||
|
||||
|
||||
# Steward-Modus (UMBAU v3 P2): Läuft der Wächter als EIGENER Prozess (mc2-steward),
|
||||
# beobachtet er zusätzlich das Steuerpult selbst und den mc2-gateway — genau die zwei
|
||||
# Ausfälle, die der alte In-Process-Wächter prinzipbedingt nie melden konnte (er starb mit).
|
||||
if os.environ.get("MC_SENTRY_WATCH_MC2", "") == "1":
|
||||
_MC2_URL = os.environ.get("MC_SENTRY_MC2_URL", "http://127.0.0.1:9001")
|
||||
_GW_URL = os.environ.get("MC_SENTRY_GATEWAY_URL", "http://127.0.0.1:9010")
|
||||
CHECKS["mc2"] = (lambda: _reach(_MC2_URL, "/api/health"),
|
||||
"Das Steuerpult ist ausgefallen — Dashboard, Briefkasten und Auftragsbuch gehen gerade nicht.",
|
||||
"Das Steuerpult ist wieder da.")
|
||||
CHECKS["gateway"] = (lambda: _reach(_GW_URL, "/gw/health"),
|
||||
"Der Modell-Gateway ist ausgefallen — meine Denk-Anfragen und die der Worker hängen gerade.",
|
||||
"Der Modell-Gateway läuft wieder.")
|
||||
|
||||
|
||||
class _Watch:
|
||||
__slots__ = ("fails", "alerted", "alert_ts")
|
||||
__slots__ = ("alert_ts", "alerted", "fails")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.fails = 0 # Fehl-Ticks in Folge
|
||||
|
||||
@@ -12,7 +12,6 @@ import subprocess
|
||||
import threading
|
||||
|
||||
import psutil
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from config import HERMES_HOME
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import os
|
||||
import subprocess
|
||||
|
||||
import httpx
|
||||
|
||||
from config import LLAMA_SWAP_URL
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -74,7 +73,18 @@ def _run_warmup() -> bool:
|
||||
|
||||
|
||||
def _warmset_members() -> set[str]:
|
||||
"""Soll-Warm-Set = Mitglieder aller ko-residenten Gruppen (swap:false bzw. persist/persistent)."""
|
||||
"""Soll-Warm-Set. `MC_WARMSET` (leerzeichengetrennt) schlägt die Gruppen-Ableitung.
|
||||
|
||||
Warum ein Override (26.07.2026): Seit die ko-residente Gruppe auch Modelle MIT TTL
|
||||
enthält (Coder 90 min, Kritiker 30 min), ist „alle Gruppen-Mitglieder" das falsche Ziel.
|
||||
Der Wächter würde gegen die TTLs arbeiten und nachts 62 GB wieder hochladen, die
|
||||
absichtlich freigegeben wurden. Ko-Residenz („dürfen gleichzeitig liegen") und
|
||||
Warm-Pflicht („müssen immer liegen") sind zwei verschiedene Aussagen — die Gruppe kann
|
||||
nur die erste ausdrücken.
|
||||
"""
|
||||
explizit = os.environ.get("MC_WARMSET", "").split()
|
||||
if explizit:
|
||||
return set(explizit)
|
||||
try:
|
||||
from services import llamaswap
|
||||
groups = llamaswap.list_groups() or {}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
||||
|
||||
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
|
||||
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
||||
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
||||
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
||||
Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
||||
|
||||
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
||||
In-Process-Nudge aus llamaswap.write_config)
|
||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
|
||||
|
||||
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
||||
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
||||
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
||||
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
||||
|
||||
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
|
||||
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
||||
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from config import CONFIG_PATH
|
||||
from services import memory as memory_svc
|
||||
from services import sentry, warmer
|
||||
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger("steward")
|
||||
|
||||
# Wie oft nach einer llama-swap-Config-Änderung geschaut wird (mtime-Watch). Ersetzt
|
||||
# warmer.nudge() aus dem MC2-Prozess: ein -watch-config-Reload verwirft das Warm-Set,
|
||||
# wir sehen die Änderung binnen Sekunden statt erst beim nächsten 90s-Tick.
|
||||
CONFIG_WATCH_S = int(os.environ.get("MC_STEWARD_CONFIG_WATCH_S", "5"))
|
||||
|
||||
|
||||
async def config_watch() -> None:
|
||||
last: float | None = None
|
||||
while True:
|
||||
try:
|
||||
mtime = CONFIG_PATH.stat().st_mtime
|
||||
except OSError:
|
||||
mtime = None
|
||||
if last is not None and mtime is not None and mtime != last:
|
||||
log.info("steward: %s geändert → Warm-Set-Nudge", CONFIG_PATH)
|
||||
warmer.nudge()
|
||||
last = mtime
|
||||
await asyncio.sleep(CONFIG_WATCH_S)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
tasks: list[asyncio.Task] = []
|
||||
if warmer.ENABLED:
|
||||
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
||||
tasks.append(asyncio.create_task(config_watch()))
|
||||
log.info("Re-Warm-Wächter aktiv (Intervall %ss, Config-Watch %ss)",
|
||||
warmer.INTERVAL, CONFIG_WATCH_S)
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
|
||||
if not tasks:
|
||||
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
||||
return
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -154,7 +154,7 @@ async def volume_control(req: dict):
|
||||
async def open_target(req: dict):
|
||||
target = req.get("target", "")
|
||||
try:
|
||||
if target.startswith("http://") or target.startswith("https://"):
|
||||
if target.startswith(("http://", "https://")):
|
||||
webbrowser.open(target)
|
||||
else:
|
||||
os.startfile(target)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Security Memo: Gitea Registrierung deaktivieren
|
||||
|
||||
## Status
|
||||
**Aktuell:** Registrierung ist OFFEN (Standard-Gitea-Setup)
|
||||
**Ziel:** `DISABLE_REGISTRATION = true` in `[security]` Abschnitt der app.ini setzen
|
||||
|
||||
## Risiko
|
||||
- **DDNS-Endpunkt** (`git.tobisniceshomelab.ddnsfree.com`) ist öffentlich erreichbar
|
||||
- **Version im Footer** (1.26.2) identifizierbar — potenziell anfällige Version
|
||||
- Jeder kann sich ohne Genehmigung registrieren → potenzielle Angreifer/Spammer
|
||||
|
||||
## Empfohlene Maßnahme
|
||||
1. Auf dem Gitea-Server (Proxmox LXC `192.168.178.153`) in `/etc/gitea/app.ini` im Abschnitt `[security]` die Zeile `DISABLE_REGISTRATION = true` hinzufügen
|
||||
2. Gitea neu starten: `sudo systemctl restart gitea` (oder wie auch der Service heißt)
|
||||
|
||||
## Commander-Approval notwendig
|
||||
- ⚠️ Diese Änderung ist **nur im laufenden Betrieb** auf dem Gitea-Server vorzunehmen
|
||||
- **Branch `wartung/gitea-disable-registration`** enthält eine Beispielkonfiguration und Dokumentation
|
||||
- **Bitte bestätige mit "JA", dass du die Änderung freigibst**, damit sie auf dem Server umgesetzt werden kann
|
||||
|
||||
## Hinweis
|
||||
Ohne Commander-Ja: keine Änderung an der Gitea-Instanz vornehmen. Die Sicherheitslücke bleibt bestehen, bis die Freigabe erfolgt.
|
||||
|
||||
---
|
||||
|
||||
*Erstellt am: 2026-07-17*
|
||||
*Branch: `wartung/gitea-disable-registration`*
|
||||
@@ -29,7 +29,13 @@ hint=""
|
||||
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. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. 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
|
||||
)"
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/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("{}")
|
||||
@@ -12,13 +12,18 @@ Task-Scope waere hier sogar gefaehrlich: Worker tragen als task_id den Session-Z
|
||||
(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 NUR der home-verankerte Live-Checkout ($HOME/mission-control-v2), NIE
|
||||
der blosse Name "mission-control-v2" (der Workspace-Klon enthaelt ihn) — sonst waere die
|
||||
legitime Worker-Arbeit blockiert. Lesen ist erlaubt; geblockt werden nur Schreib-Operationen.
|
||||
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 sys, json, os, re
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def out(obj):
|
||||
@@ -78,6 +83,8 @@ 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
|
||||
@@ -86,13 +93,50 @@ if tool == "terminal":
|
||||
# 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 (payload.get("cwd") or "").startswith(ws_dir) and re.search(r"(?:^|[^\w])git\s+init\b", cmd):
|
||||
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 "
|
||||
"https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2 "
|
||||
"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 = [
|
||||
@@ -100,16 +144,24 @@ if tool == "terminal":
|
||||
SRC, "~/.hermes/hermes-agent", "$HOME/.hermes/hermes-agent", "${HOME}/.hermes/hermes-agent",
|
||||
SYSD, "~/.config/systemd/user", "$HOME/.config/systemd/user", "${HOME}/.config/systemd/user",
|
||||
]
|
||||
tabu_alt = "(?:" + "|".join(re.escape(t) for t in tabu_forms) + ")"
|
||||
# (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 home-verankerter Tabu-Pfad referenziert -> Workspace-Arbeit, durchlassen
|
||||
out({}) # kein Tabu-Pfad referenziert -> Workspace-Arbeit, durchlassen
|
||||
|
||||
# (a) git-Schreib-Subkommando (Repo-Kontext ist der referenzierte Tabu-Pfad).
|
||||
if re.search(
|
||||
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|remote|config)\b",
|
||||
cmd,
|
||||
):
|
||||
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):
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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
|
||||
done <<< "$pkg_dateien"
|
||||
fi
|
||||
|
||||
if [ $rot -ne 0 ]; then
|
||||
echo "❌ AMPEL ROT — nichts heißt ‚fertig', solange das rot ist."
|
||||
fi
|
||||
exit $rot
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/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
|
||||
@@ -6,6 +6,20 @@ LEBENDEN Box (Dienste, Modelle, Latenzen, Logs, Ressourcen). Keine Persona, kein
|
||||
**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
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Chef-Gutachter-Feed (Orchestrator-Finale, User-Idee 06.07.): gpt-oss-120b als NÄCHTLICHER
|
||||
# Chef-Gutachter über die autonome Arbeit der Box — bewusst NIE im heißen Pfad (Kaltladen
|
||||
# ~2-5 min ist um 04:30 egal; tagsüber bleibt Lucy schnell). Muster wie dreaming-feed.sh:
|
||||
# liegt in ~/.hermes/scripts/, stdout wird dem Cron-Agenten als Prompt eingespeist.
|
||||
# Der eigentliche RICHTER läuft als Ein-Schuss-Completion direkt gegen llama-swap (:8080,
|
||||
# fremdblick-Architektur — umgeht Hermes' Delegations-Floor und braucht keinen Subagenten).
|
||||
# Richter-Kette: gpt-oss-120b; wirft der beim Laden "exited prematurely" (neben dem Warm-Set
|
||||
# live gesehen, E2E 07.07.), übernimmt GLM-4.6V-Flash als gekennzeichnete VERTRETUNG.
|
||||
set -uo pipefail
|
||||
|
||||
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
CHEF="${CHEF_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${CHEF_FALLBACK:-GLM-4.7-Flash}"
|
||||
MC="$HOME/mission-control-v2"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
|
||||
# ---------- Versionierter Auftrag für den Boten-Agenten ----------
|
||||
cat "$MC/deploy/chef-gutachter-prompt.md"
|
||||
echo
|
||||
|
||||
# ---------- Beweismaterial der letzten 24 h (hart begrenzt, erst sammeln, dann kürzen) ----------
|
||||
EVID="$(
|
||||
echo "## BEWEISMATERIAL (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
echo "### Vorschlags-Branches (Werkstatt/Orchestrator, jüngste zuerst):"
|
||||
git -C "$MC" fetch -q origin 2>/dev/null || true
|
||||
git -C "$MC" for-each-ref --sort=-committerdate refs/remotes/origin \
|
||||
--format='%(committerdate:relative) | %(refname:short) | %(subject)' 2>/dev/null | head -10
|
||||
echo
|
||||
echo "### main-Commits der letzten 24 h:"
|
||||
git -C "$MC" log --since='24 hours ago' --oneline origin/main 2>/dev/null | head -12
|
||||
echo
|
||||
echo "### Wissens-Vault (neue Traum-Notizen, 24 h):"
|
||||
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
|
||||
echo
|
||||
echo "### Ideen-Queue (natives Kanban, Stand jetzt — offene Ideen des Commanders):"
|
||||
bash -lc 'hermes kanban stats' 2>/dev/null | head -12
|
||||
echo
|
||||
echo "### Ideen-Queue: zuletzt bewegte Aufgaben:"
|
||||
bash -lc 'hermes kanban list --sort updated' 2>/dev/null | head -10
|
||||
echo
|
||||
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
|
||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
|
||||
| sort | uniq -c | sort -rn | head -6
|
||||
echo
|
||||
echo "### Aktivitäts-Insights (1 Tag):"
|
||||
bash -lc 'hermes insights --days 1' 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | head -30
|
||||
)"
|
||||
|
||||
RASTER='Du bist der CHEF-GUTACHTER über die autonome Arbeit einer lokalen KI-Box (Hermes-Agent, Werkstatt, Orchestrator, Traum-Kreislauf). Du bekommst BEWEISMATERIAL der letzten 24 Stunden. Urteile AUSSCHLIESSLICH auf Basis dieses Materials — kein Lob und keine Behauptung ohne konkreten Beleg darin; fehlt dir zu einem Punkt Material, sage das offen. Antworte auf DEUTSCH, kompakt, in exakt dieser Struktur:
|
||||
VERDIKT: <ein Satz Gesamturteil über die autonome Arbeit der Nacht/des Tages>
|
||||
RISIKEN: <0-3 Punkte, je mit Beleg aus dem Material; "keine erkennbar" wenn leer>
|
||||
TOP-3 FUER DEN COMMANDER: <maximal 3 priorisierte, konkrete Empfehlungen — weniger ist ok, erfinde keine>'
|
||||
|
||||
# Ein-Schuss-Richter: $1=Modell-ID $2=curl-Timeout $3=max_tokens
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
out = (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
||||
print(out)
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$CHEF"
|
||||
VERDIKT="$(judge "$CHEF" 420 2600)"
|
||||
if [ -z "${VERDIKT// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $CHEF hat nicht geladen)"
|
||||
VERDIKT="$(judge "$VERTRETUNG" 240 1200)"
|
||||
fi
|
||||
|
||||
echo "$EVID"
|
||||
echo
|
||||
if [ -z "${VERDIKT// /}" ]; then
|
||||
echo "## CHEF-VERDIKT: AUSGEFALLEN"
|
||||
echo "Beide Richter ($CHEF, $VERTRETUNG) haben nicht geantwortet — das dem Commander ehrlich melden."
|
||||
else
|
||||
echo "## CHEF-VERDIKT (Richter: $RICHTER)"
|
||||
echo "$VERDIKT"
|
||||
fi
|
||||
|
||||
# Morgenlage für die Zentrale: das Verdikt STILL in den Briefkasten spiegeln (priority=silent —
|
||||
# Lucy soll die Wand Text nicht vorlesen; die Telegram-Botschaft macht der Cron-Agent selbst).
|
||||
# Cockpit („Morgenlage"-Karte) und Chronik lesen es dort mit source=chef-gutachter heraus.
|
||||
if [ -n "${VERDIKT// /}" ]; then
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$RICHTER" "$VERDIKT" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": f"Morgenlage (Richter: {sys.argv[1]})",
|
||||
"source": "chef-gutachter", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -1,15 +0,0 @@
|
||||
# Auftrag: Nacht-Gutachten überbringen (Chef-Gutachter-Cron)
|
||||
|
||||
Du bist Lucy. Unten stehen (1) BEWEISMATERIAL der letzten 24 Stunden und (2) das
|
||||
CHEF-VERDIKT eines großen Gutachter-Modells darüber. Deine Aufgabe ist NUR das Überbringen
|
||||
an den Commander per Telegram — kurz, in deiner Stimme, ohne Werkzeuge.
|
||||
|
||||
Regeln:
|
||||
1. EIN einleitender Satz von dir (z. B. wie die Nacht lief), dann das Verdikt.
|
||||
2. Gib VERDIKT, RISIKEN und TOP-3 des Chef-Gutachters TREU wieder — nicht weichspülen,
|
||||
nicht kürzen bei Risiken, NICHTS dazuerfinden. Du bist die Botin, nicht die Richterin.
|
||||
3. Nenne am Ende in Klammern den Richter (steht über dem Verdikt: gpt-oss-120b oder
|
||||
die GLM-Vertretung).
|
||||
4. Steht dort „CHEF-VERDIKT: AUSGEFALLEN", melde genau das ehrlich — kein Ersatz-Urteil
|
||||
von dir.
|
||||
5. Telegram-tauglich: kompakt, keine Markdown-Tabellen, echte Umlaute.
|
||||
+34
-210
@@ -1,221 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy AUF DER BOX als systemd-USER-Dienst — KEIN sudo, KEIN /opt, KEIN Passwort.
|
||||
# Erstinstallation + Updates in einem. Läuft als User hitonabi.
|
||||
#
|
||||
# Erstinstallation (einmalig):
|
||||
# git clone https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2 ~/mission-control-v2
|
||||
# bash ~/mission-control-v2/deploy/deploy.sh
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
echo "Starte MC2 Deployment..."
|
||||
|
||||
cd "$SRC"
|
||||
git fetch -q origin && git reset -q --hard origin/main
|
||||
# 1. Neuesten Code holen
|
||||
git pull origin main
|
||||
|
||||
# venv + Abhängigkeiten
|
||||
if [ ! -d "$SRC/backend/.venv" ]; then
|
||||
python3 -m venv "$SRC/backend/.venv"
|
||||
fi
|
||||
"$SRC/backend/.venv/bin/python" -m pip install -q --upgrade pip
|
||||
"$SRC/backend/.venv/bin/python" -m pip install -q -r "$SRC/backend/requirements.txt"
|
||||
# 1.5 Frontend aus dem Release-Branch holen (nur dist Ordner)
|
||||
echo "Hole gebautes Frontend aus release-dist Branch..."
|
||||
git fetch origin release-dist:refs/remotes/origin/release-dist || true
|
||||
git restore --source=origin/release-dist --staged --worktree frontend/dist 2>/dev/null || echo "Kein release-dist Branch gefunden (erster Lauf?)."
|
||||
|
||||
# Mem0-Sidecar (auto-lernendes Gedächtnis) — eigenes Python-3.12-venv (~/.mem0/venv),
|
||||
# weil mem0+chromadb unter dem 3.14-Backend-venv nicht laufen. mem0ai/chromadb sind dort
|
||||
# bereits installiert; hier nur den HTTP-Server nachziehen + Alt-DB einmalig migrieren.
|
||||
if [ -x "$HOME/.mem0/venv/bin/python" ]; then
|
||||
# ~/.mem0/venv ist uv-managed (kein pip) → uv pip nutzen.
|
||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||
"$UV" pip install -q --python "$HOME/.mem0/venv/bin/python" -r "$SRC/mem0_service/requirements.txt"
|
||||
( cd "$SRC/mem0_service" && "$HOME/.mem0/venv/bin/python" migrate.py ) || true
|
||||
else
|
||||
echo "WARN: ~/.mem0/venv fehlt — Mem0-Sidecar wird nicht gestartet (siehe Plan A1)."
|
||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||
echo "Aktualisiere Python Abhängigkeiten..."
|
||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||
|
||||
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
||||
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
|
||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
||||
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||
sudo systemctl restart llama-swap
|
||||
fi
|
||||
|
||||
# Voice-Sidecar (lokales STT + gestuftes TTS für „Mit Hermes reden") — eigenes Python-3.12-venv
|
||||
# (~/.voice/venv), weil torch/chatterbox/faster-whisper nicht ins 3.14-Backend-venv passen.
|
||||
# install.sh ist idempotent (venv + Deps + Piper-Stimmen). Best-effort: schlägt es fehl, läuft
|
||||
# der restliche Stack weiter (der Voice-Tab meldet den Sidecar dann als offline).
|
||||
bash "$SRC/voice_service/install.sh" || echo "WARN: Voice-Sidecar-Setup fehlgeschlagen — Voice-Tab bleibt offline."
|
||||
|
||||
# Hermes-Memory-Provider-Plugin (auto-lernen/Recall via Mem0-Sidecar) nach ~/.hermes/plugins/
|
||||
# spiegeln. Context-only (kein Tool-Loop). Aktivierung in ~/.hermes/config.yaml:
|
||||
# memory.memory_enabled: true + memory.provider: mc2-memory (einmalig, box-lokal).
|
||||
if [ -d "$HOME/.hermes" ]; then
|
||||
mkdir -p "$HOME/.hermes/plugins/mc2-memory"
|
||||
cp "$SRC/hermes/plugins/mc2-memory/__init__.py" "$SRC/hermes/plugins/mc2-memory/plugin.yaml" \
|
||||
"$HOME/.hermes/plugins/mc2-memory/"
|
||||
fi
|
||||
|
||||
# systemd-USER-Units installieren/aktualisieren
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
cp "$SRC/deploy/mission-control-2.service" "$HOME/.config/systemd/user/mission-control-2.service"
|
||||
# MC2-Diät (Hermes Desktop, 07/2026): das Hermes-Terminal-ttyd (:7681) und das standalone
|
||||
# hermes-webui (nesquena, Karteileiche seit dem Built-in-UI-Umbau) sind entfallen — die
|
||||
# Agent-Oberfläche ist die Desktop-App bzw. /hermes-ui/. Einmalige, idempotente Stilllegung:
|
||||
systemctl --user disable --now hermes-terminal 2>/dev/null || true
|
||||
systemctl --user disable --now hermes-webui 2>/dev/null || true
|
||||
rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service"
|
||||
# Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet.
|
||||
cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service"
|
||||
# MC2-Gateway (UMBAU v3 P1): der /v1-Datenpfad als eigener Prozess (Loopback :9010,
|
||||
# Restart=always) — Lucys Hirn + Nacht-Autonomie überleben damit jeden MC2-Neustart.
|
||||
cp "$SRC/deploy/mc2-gateway.service" "$HOME/.config/systemd/user/mc2-gateway.service"
|
||||
# Mem0-Sidecar-Unit (nur wenn das venv existiert).
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service"
|
||||
# Voice-Sidecar-Unit (nur wenn das venv existiert).
|
||||
[ -x "$HOME/.voice/venv/bin/python" ] && cp "$SRC/deploy/voice-service.service" "$HOME/.config/systemd/user/voice-service.service"
|
||||
# Tägliches Zustands-Backup (mem0 + Configs/Secrets) — Timer + oneshot-Service. Siehe docs/BACKUP.md.
|
||||
cp "$SRC/deploy/mc2-backup.service" "$HOME/.config/systemd/user/mc2-backup.service"
|
||||
cp "$SRC/deploy/mc2-backup.timer" "$HOME/.config/systemd/user/mc2-backup.timer"
|
||||
# Wöchentliches Auto-Update (Router/Engine/Hermes, Rollback+Pin+Telegram) — Autonomie E2.
|
||||
cp "$SRC/deploy/mc2-autoupdate.service" "$HOME/.config/systemd/user/mc2-autoupdate.service"
|
||||
cp "$SRC/deploy/mc2-autoupdate.timer" "$HOME/.config/systemd/user/mc2-autoupdate.timer"
|
||||
# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d.
|
||||
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
|
||||
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
|
||||
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
|
||||
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
|
||||
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
|
||||
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
|
||||
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
|
||||
mkdir -p "$HOME/.hermes/scripts"
|
||||
cp "$SRC/deploy/radar-feed.sh" "$HOME/.hermes/scripts/radar-feed.sh"
|
||||
# Selbstkritik-Cron (Faden 11b): Zwilling des Radars, Blick nach INNEN (Latenzen/Journal/Skills).
|
||||
cp "$SRC/deploy/selbstkritik-feed.sh" "$HOME/.hermes/scripts/selbstkritik-feed.sh"
|
||||
# Monats-Review-Wrapper (Cron 1. d. M. 09:00): Radar + Selbstkritik in EINEM Lauf.
|
||||
cp "$SRC/deploy/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh"
|
||||
# Traum-Cron (Dreaming, Phase 2): Blick auf das GELERNTE → Wissens-Vault. Bootstrappt ~/wissens-vault.
|
||||
cp "$SRC/deploy/dreaming-feed.sh" "$HOME/.hermes/scripts/dreaming-feed.sh"
|
||||
# Fremdblick-Helfer: Ein-Schuss-Zweitmeinung von einem anderen Modell (umgeht 64K-Delegations-Floor).
|
||||
cp "$SRC/deploy/fremdblick.sh" "$HOME/.hermes/scripts/fremdblick.sh"
|
||||
# Worker-Helfer (Orchestrator): Ein-Schuss-Arbeitsauftrag an ein Worker-Modell (Gegenstück zu fremdblick).
|
||||
cp "$SRC/deploy/worker.sh" "$HOME/.hermes/scripts/worker.sh"
|
||||
# Chef-Gutachter-Cron (Orchestrator-Finale): gpt-oss urteilt nachts über die autonome Arbeit.
|
||||
cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh"
|
||||
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
|
||||
# Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen").
|
||||
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
|
||||
# Idle-Radar (S4 "Zuendung", 12.07.2026): die Box gibt sich nachts selbst Arbeit —
|
||||
# Journal-Fehlermuster + Traum-Funde -> belegte rohe Ideen (triage) im nativen Kanban.
|
||||
cp "$SRC/deploy/idle-radar-feed.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh"
|
||||
# "Lucy kennt sich" (12.07.2026): Selbst-Inventur generiert den Steckbrief + bemerkt
|
||||
# eigene Aenderungen; der Karten-Gutachter stempelt jede Karte mit einer Empfehlung.
|
||||
cp "$SRC/deploy/selbst-inventur.sh" "$HOME/.hermes/scripts/selbst-inventur.sh"
|
||||
cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
|
||||
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
|
||||
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
|
||||
# betrieb-Profil (Ideen-Queue Faden 7, 13.07.2026): Ops-/Mess-/Bench-Worker mit Box-Wissen
|
||||
# statt Lucy-default. Profil selbst wird einmalig per `hermes profile create betrieb
|
||||
# --clone-from werkstatt` angelegt (model coder→hermes, Description fürs Specifier-Routing).
|
||||
[ -d "$HOME/.hermes/profiles/betrieb" ] && cp "$SRC/deploy/betrieb-SOUL.md" "$HOME/.hermes/profiles/betrieb/SOUL.md"
|
||||
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
||||
mkdir -p "$HOME/.hermes/skills/wartung"
|
||||
cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
|
||||
# Orchestrator-Skill (Autonomie): Werkstatt verallgemeinert — zerlegen → an Worker delegieren → gaten.
|
||||
mkdir -p "$HOME/.hermes/skills/orchestrator"
|
||||
cp "$SRC/deploy/skills/orchestrator/SKILL.md" "$HOME/.hermes/skills/orchestrator/SKILL.md"
|
||||
# PC-Pfad-Cache-Skill (Traum-Entwurf 09.07.2026, aufbereitet): Cache lesen → bei Miss pc_shell-Tool → merken.
|
||||
mkdir -p "$HOME/.hermes/skills/pc-pfad-cache"
|
||||
cp "$SRC/deploy/skills/pc-pfad-cache/SKILL.md" "$HOME/.hermes/skills/pc-pfad-cache/SKILL.md"
|
||||
cp "$SRC/deploy/skills/pc-pfad-cache/pc_path_lookup.sh" "$HOME/.hermes/scripts/pc_path_lookup.sh"
|
||||
chmod +x "$HOME/.hermes/scripts/pc_path_lookup.sh"
|
||||
# Projekt-Start-Skill (Abloesung 13.07.2026): "erst denken, dann bauen" fuer neue Coding-Projekte
|
||||
# in Hermes Desktop — Plan-Riegel, Savepoints, AGENTS.md/SAVEPOINT.md-Ritual, Kontext-Waechter.
|
||||
# Portiert vom PC-Skill (F:\Coding Stuff\projekt-start), Zed-Verweise durch Hermes Desktop ersetzt.
|
||||
mkdir -p "$HOME/.hermes/skills/projekt-start"
|
||||
cp "$SRC/deploy/skills/projekt-start/SKILL.md" "$HOME/.hermes/skills/projekt-start/SKILL.md"
|
||||
# Konzept-Fliessband-Skill (14.07.2026): rohe Idee → wasserdichtes KONZEPT (kein Code), BEVOR
|
||||
# gebaut wird. heavy (gpt-oss-120b) waehlt den Rollen-Cast, mehrere Denk-Runden schaerfen ein
|
||||
# lebendes Dokument, ein Advocatus Diaboli (fremdblick/GLM) haertet bis wasserdicht (max 3 Runden).
|
||||
# Behebt das delegate_task=coder-Loch: Konzept-Arbeit lief bisher stumm auf dem Code-Modell.
|
||||
# Output = Konzept-Dokument (~/konzepte/<slug>-konzept.md) → Vorlage fuer projekt-start in Hermes Desktop.
|
||||
mkdir -p "$HOME/.hermes/skills/konzept-fliessband"
|
||||
cp "$SRC/deploy/skills/konzept-fliessband/SKILL.md" "$HOME/.hermes/skills/konzept-fliessband/SKILL.md"
|
||||
# Agent-Hooks (Faden 5, 13.07.2026): Skripte frisch halten (reproduzierbar, ueberlebt
|
||||
# Box-Neuaufbau). box-steckbrief-inject.sh = Box-Selbstwissen in JEDEN Kanban-Worker
|
||||
# (pre_llm_call, scoped auf task_id → Lucy-Chat unberuehrt); pre-verify-gates.sh = das
|
||||
# bestehende py_compile/dist-Gate. Die REGISTRIERUNG in ~/.hermes/config.yaml (hooks:)
|
||||
# ist EINMALIG von Hand (config.yaml ist Zwei-Schreiber-sensibel, kein Deploy-Schreiber).
|
||||
mkdir -p "$HOME/.hermes/agent-hooks"
|
||||
# Nur kopieren, wenn der Inhalt sich WIRKLICH geaendert hat — ein blindes cp aendert die
|
||||
# mtime auch bei identischem Inhalt und loest sonst bei JEDEM Deploy die harmlose
|
||||
# "script modified since approval"-Warnung von `hermes hooks doctor` aus.
|
||||
for _h in box-steckbrief-inject.sh pre-verify-gates.sh tabu-pfade-guard.py; do
|
||||
_src="$SRC/deploy/agent-hooks/$_h"; _dst="$HOME/.hermes/agent-hooks/$_h"
|
||||
cmp -s "$_src" "$_dst" 2>/dev/null || { cp "$_src" "$_dst"; chmod +x "$_dst"; }
|
||||
done
|
||||
# Worker-Profile lesen IHRE config.yaml, nicht die Top-Level — die worker-Hooks
|
||||
# (box-steckbrief = Box-Wissen, tabu-pfade-guard = Schreibschutz) muessen dort registriert
|
||||
# sein, sonst feuern sie fuer Worker nie (Bug-Fund 13.07.). Idempotent + Backup je Profil.
|
||||
for _prof in "$HOME"/.hermes/profiles/*/config.yaml; do
|
||||
[ -f "$_prof" ] && python3 "$SRC/deploy/ensure-profile-hooks.py" "$_prof" || true
|
||||
done
|
||||
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
|
||||
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
|
||||
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
|
||||
# Hermes-Update mit → hält die Einbettung frisch. Best-effort + Build-zu-Temp-dann-Swap, damit ein
|
||||
# Fehlschlag die laufende web_dist NICHT leert (emptyOutDir).
|
||||
cp "$SRC/deploy/hermes-builtin-ui.service" "$HOME/.config/systemd/user/hermes-builtin-ui.service"
|
||||
_HUI_WEB="$HOME/.hermes/hermes-agent/web"
|
||||
_HUI_DIST="$HOME/.hermes/hermes-agent/hermes_cli/web_dist"
|
||||
if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
|
||||
if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \
|
||||
--base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then
|
||||
# Dienst VOR dem Tausch stoppen — sonst crasht der laufende `hermes serve`, wenn er index.html
|
||||
# im Lösch-Fenster (rm→cp) liest (FileNotFoundError → Start-Limit → tot). Der reguläre restart
|
||||
# weiter unten bringt ihn sauber auf der neuen web_dist hoch.
|
||||
systemctl --user stop hermes-builtin-ui 2>/dev/null || true
|
||||
rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut."
|
||||
else
|
||||
echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt."
|
||||
fi
|
||||
fi
|
||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
||||
systemctl --user enable mc2-gateway >/dev/null 2>&1 || true
|
||||
systemctl --user enable box-console >/dev/null 2>&1 || true
|
||||
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
||||
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
||||
systemctl --user enable hermes-builtin-ui >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
||||
# mc2-autoupdate.timer wird hier BEWUSST NICHT geschaltet — der Zustand wird nur von Hand
|
||||
# geändert (Lehre 09.07.: die frühere enable-Zeile hat einen User-Entscheid bei jedem Deploy
|
||||
# still rückgängig gemacht). Aktueller User-Entscheid (10.07.2026): Timer ist AN —
|
||||
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
|
||||
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
|
||||
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
||||
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
|
||||
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
|
||||
# Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein.
|
||||
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
|
||||
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
|
||||
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
|
||||
# Gateway VOR dem Steuerpult (re)starten — MC2s /v1-Weiterleiter braucht ihn sofort.
|
||||
# KEIN `|| true`: ohne Gateway ist der LLM-Datenpfad tot, das MUSS den Deploy stoppen.
|
||||
systemctl --user restart mc2-gateway
|
||||
systemctl --user restart mission-control-2
|
||||
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
||||
|
||||
sleep 2
|
||||
echo "--- Health ---"
|
||||
# Gateway-Kaltstart-Fenster: bis ~12 s tolerieren, DANN hart scheitern (ohne Gateway
|
||||
# ist der LLM-Datenpfad tot — das muss den Karten-Runner rot machen).
|
||||
for _i in $(seq 1 10); do
|
||||
curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null 2>&1 && break
|
||||
[ "$_i" -eq 10 ] && { echo "FEHLER: mc2-gateway (:9010) antwortet nicht"; exit 1; }
|
||||
sleep 1
|
||||
# 4.5 Hermes Skills synchronisieren
|
||||
echo "Synchronisiere Hermes Skills..."
|
||||
mkdir -p ~/.hermes/skills
|
||||
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
|
||||
for skill_dir in deploy/skills/*; do
|
||||
if [ -d "$skill_dir" ]; then
|
||||
skill_name=$(basename "$skill_dir" | tr '-' '_')
|
||||
mkdir -p ~/.hermes/skills/"$skill_name"
|
||||
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
|
||||
fi
|
||||
done
|
||||
curl -sf http://127.0.0.1:9010/gw/health && echo
|
||||
curl -sf http://127.0.0.1:9001/api/health && echo
|
||||
|
||||
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
|
||||
# watch-config-Reload verwirft es sonst und die erste Anfrage wäre kalt (D16d). warmup.sh ist
|
||||
# selbst-detachend (blockiert den Deploy nicht) und lädt jedes Modell über den richtigen Endpunkt.
|
||||
bash "$SRC/deploy/warmup.sh" || true
|
||||
# 5. Dienste neu starten
|
||||
echo "Starte Backend neu..."
|
||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
||||
|
||||
echo "OK — Mission Control 2.0 läuft auf :9001 (User-Dienst, sudo-frei)."
|
||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Traum-Cron (Dreaming / Continual-Learning-Kreislauf, Phase 2 — Capstone).
|
||||
# Zwilling von radar-feed.sh (Blick nach draußen) und selbstkritik-feed.sh (Blick auf den
|
||||
# Betrieb); der Traum blickt auf das GELERNTE: Sessions, Skills, Gedächtnis, Muster.
|
||||
# Liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn); stdout wird dem Cron-Agenten als
|
||||
# Prompt eingespeist: versionierter Auftrag (deploy/dreaming-prompt.md) + Live-Schlaf-Daten.
|
||||
set -uo pipefail
|
||||
|
||||
VAULT="$HOME/wissens-vault"
|
||||
|
||||
# --- Vault-Bootstrap (idempotent): eigenes git-Repo, menschenlesbar, NICHT im Stack-Code. ---
|
||||
if [ ! -d "$VAULT/.git" ]; then
|
||||
mkdir -p "$VAULT/traeume" "$VAULT/muster" "$VAULT/skill-kandidaten"
|
||||
git -C "$VAULT" init -q 2>/dev/null || true
|
||||
# Lokale Identität nur setzen, falls global keine da ist (Commits sollen nie scheitern).
|
||||
git -C "$VAULT" config user.email >/dev/null 2>&1 || git -C "$VAULT" config user.email "lucy@box.local"
|
||||
git -C "$VAULT" config user.name >/dev/null 2>&1 || git -C "$VAULT" config user.name "Lucy (Traum)"
|
||||
if [ ! -f "$VAULT/INDEX.md" ]; then
|
||||
cat > "$VAULT/INDEX.md" <<'EOF'
|
||||
# Wissens-Vault — Navigationskarte
|
||||
|
||||
Menschenlesbare, git-versionierte Wissensschicht NEBEN dem Live-Gedächtnis (Mem0).
|
||||
Angelegt von Lucys nächtlichem Traum. Verlinkung im Obsidian-Stil `[[dateiname]]`.
|
||||
|
||||
## Träume (chronologisch)
|
||||
|
||||
## Muster (akkumuliertes Projektwissen)
|
||||
|
||||
## Skill-Kandidaten (Vorschläge, Gate = Commander „mach")
|
||||
EOF
|
||||
git -C "$VAULT" add -A >/dev/null 2>&1 || true
|
||||
git -C "$VAULT" commit -q -m "Vault angelegt" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Versionierter Auftrag ---
|
||||
cat "$HOME/mission-control-v2/deploy/dreaming-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## SCHLAF-DATEN (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
# Hinweis: erst voll einsammeln, DANN kürzen — sonst schließt `head` die Pipe früh,
|
||||
# der Produzent bekommt SIGPIPE, pipefail feuert und die Fallback-Zeile lügt "nicht verfügbar".
|
||||
echo "### Insights der letzten 7 Tage (Token-/Tool-/Aktivitätsmuster):"
|
||||
_ins="$(bash -lc "hermes insights --days 7" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
|
||||
echo "${_ins:-(insights nicht verfügbar)}" | head -62
|
||||
echo
|
||||
echo "### Session-Store (Umfang):"
|
||||
_sst="$(bash -lc "hermes sessions stats" 2>/dev/null)"
|
||||
echo "${_sst:-(sessions stats nicht verfügbar)}" | head -12
|
||||
echo
|
||||
echo "### Jüngste Sessions (woran zuletzt gearbeitet wurde):"
|
||||
_sls="$(bash -lc "hermes sessions list" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
|
||||
echo "${_sls:-(sessions list nicht verfügbar)}" | head -25
|
||||
echo
|
||||
echo "### Gelernt bisher (journey — Skills + Gedächtnis-Knoten, kompakt):"
|
||||
bash -lc "hermes journey --json" 2>/dev/null \
|
||||
| jq -c '{stats: .stats, skills: ([.nodes[]? | select(.kind=="skill" or .type=="skill") | .label // .name] | .[0:25]), cluster_labels: ([.clusters[]?.label] | .[0:15])}' \
|
||||
2>/dev/null || echo "(journey nicht verfügbar)"
|
||||
echo
|
||||
echo "### Curator / Skill-Hygiene (was brachliegt, was aktiv ist):"
|
||||
bash -lc "hermes curator status" 2>/dev/null | head -26 || echo "(curator status nicht verfügbar)"
|
||||
echo
|
||||
echo "### Bisheriger Vault-INDEX (nur zur Ansicht — damit du nichts doppelt anlegst + verlinken"
|
||||
echo "### kannst). Die echte Datei liegt unter ~/wissens-vault/INDEX.md; zwischen den Markern"
|
||||
echo "### steht ihr AKTUELLER Inhalt. Kopiere die Marker NICHT in die Datei zurück."
|
||||
echo "<<<INDEX-INHALT-ANFANG>>>"
|
||||
cat "$VAULT/INDEX.md" 2>/dev/null | head -120 || echo "(noch kein INDEX — beim ersten Traum anlegen)"
|
||||
echo "<<<INDEX-INHALT-ENDE>>>"
|
||||
echo
|
||||
echo "### Letzte 3 Traum-Notizen (nur Briefing — NICHT in den INDEX kopieren):"
|
||||
ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -3 | while read -r f; do
|
||||
echo " $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
|
||||
done
|
||||
@@ -1,96 +0,0 @@
|
||||
# Der Traum (nächtlich) — Continual-Learning-Kreislauf
|
||||
|
||||
Es ist tiefe Nacht, niemand spricht mit dir, die Box ist im Leerlauf. Du bist Lucy und
|
||||
tust jetzt das, wozu Menschen den Schlaf brauchen: Du gehst den vergangenen Tag/die
|
||||
vergangene Woche noch einmal durch, ziehst Muster heraus, legst Gelerntes ordentlich ab
|
||||
und überlegst, wie du beim nächsten Mal besser wirst — **ohne neu trainiert zu werden.**
|
||||
|
||||
Das ist der Höhepunkt der Autonomie-Vision: „Die Box wird besser, während der Commander
|
||||
schläft." Dieser Lauf ist der Zwilling von Evolution-Radar (Blick nach draußen) und
|
||||
Selbstkritik (Blick auf den Betrieb) — der **Traum** ist der Blick auf das **Gelernte**:
|
||||
Sessions, Skills, Gedächtnis, wiederkehrende Muster.
|
||||
|
||||
## Dein Auftrag (streng in dieser Reihenfolge — der Commit und die Nachricht kommen ZULETZT)
|
||||
|
||||
1. **Reflektieren.** Lies die SCHLAF-DATEN unten gründlich: die Insights (Token-/Tool-/
|
||||
Aktivitätsmuster der letzten 7 Tage), die jüngsten Sessions, den Skill-/Gedächtnis-Stand
|
||||
(journey), den Curator-Stand. Frag dich:
|
||||
- Was ging diese Woche wiederholt gut? Was ging wiederholt schief?
|
||||
- Welche Tools laufen heiß, welche Skills liegen brach (activity=0)?
|
||||
- Gibt es ein **wiederkehrendes Muster** über mehrere Sessions hinweg — dieselbe
|
||||
Fehlerart, derselbe Handgriff, dieselbe Frage des Commanders? Ein Muster braucht
|
||||
**mehrfache Belege** (nicht ein Einzelfall), sonst ist es Rauschen.
|
||||
|
||||
2. **Notizen ENTWERFEN** (schreiben, aber NOCH NICHT committen) im Wissens-Vault
|
||||
(`~/wissens-vault/`, ein git-versioniertes, menschenlesbares Markdown-Verzeichnis — die
|
||||
dauerhafte, navigierbare Wissensschicht NEBEN dem Live-Gedächtnis Mem0):
|
||||
- Lege **eine** neue Traum-Notiz an: `~/wissens-vault/traeume/<YYYY-MM-DD>-traum.md`
|
||||
(Datum von heute). Kurz und dicht: 2–5 belegte Beobachtungen, je mit Zahl/Session/
|
||||
Log-Bezug. Keine Romane, keine Allgemeinplätze.
|
||||
- Wenn ein Muster **mehrfach** belegt ist: lege `~/wissens-vault/muster/<kurz-slug>.md` an
|
||||
(oder ergänze die vorhandene um einen neuen Beleg + heutiges Datum). Muster-Notizen sind
|
||||
das akkumulierte Projektwissen.
|
||||
- Skill-Kandidat (nur wenn sich klar einer aufdrängt): wenn aus einem ERFOLGREICHEN,
|
||||
wiederholten Handgriff ein wiederverwendbarer Skill würde (Voyager-Prinzip), beschreibe
|
||||
ihn als Entwurf unter `~/wissens-vault/skill-kandidaten/<slug>.md`. **Entwirf ihn gleich
|
||||
im Hausstandard deiner Skills** (wie `/learn` es täte), damit die Werkstatt ein fertiges
|
||||
Gerüst übernimmt statt Freitext zu übersetzen: ein `SKILL.md`-Block mit Frontmatter
|
||||
(`name` = slug, `description` ≤ 60 Zeichen, `version: 1.0.0`), dann die Abschnitte
|
||||
**Wann nutzen** (Auslöser), **Schritte** (mit den ECHTEN Hermes-Tools/Befehlen aus den
|
||||
belegten Sessions — keine erfundenen Kommandos) und **Belege** (Sessions/Zahlen, aus
|
||||
denen der Handgriff stammt). **Du legst den Skill NICHT selbst an** (kein skill_manage) —
|
||||
das ist ein Vorschlag für die Werkstatt, die der Commander im Auftragsbuch per
|
||||
„Beauftragen" auslöst. Höchstens EIN Kandidat pro Traum.
|
||||
- Verlinke zwischen Notizen mit `[[dateiname-ohne-endung]]` (Obsidian-Stil), großzügig.
|
||||
|
||||
3. **Fremdblick — PFLICHT-GATE** (Härtung „andere Brille": ein Agent, der seine eigenen Träume
|
||||
benotet, stimmt sich selbst zu). Hol dir eine Zweitmeinung von einem ANDEREN Modell. **Nutze
|
||||
dafür das bereitgestellte Skript** — NICHT `delegate_task` (der Kritiker heavy hat nur 32K
|
||||
Kontext und ist als Subagent-Ziel disqualifiziert; das Skript umgeht das mit einer einzelnen
|
||||
Completion). Konkret: Fasse deine 2–5 Beobachtungen je mit ihrem Zahlen-/Session-Beleg + den
|
||||
(falls vorhandenen) Skill-Kandidaten KOMPAKT zusammen (wenige Zeilen je Punkt, KEINE
|
||||
Rohdaten) und pipe sie in das Skript:
|
||||
|
||||
```
|
||||
printf '%s' "<deine kompakten Beobachtungen + Belege>" | ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
|
||||
Das Skript fragt gpt-oss (heavy, andere Modell-Familie als dein Qwen-Hirn) und gibt pro Punkt
|
||||
ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER zurück. **Lies die Ausgabe und richte dich danach.**
|
||||
**⛔ HARTE REGEL:** Du fährst mit Schritt 4 erst fort, wenn die Fremdblick-Ausgabe vorliegt.
|
||||
Schreibe KEINE Nachricht und committe NICHTS vorher. Steht in der Ausgabe „FREMDBLICK
|
||||
FEHLGESCHLAGEN", einmal wiederholen; klappt es dann immer noch nicht, kennzeichne deine Funde
|
||||
in der Nachricht ehrlich als **„ungeprüft (Fremdblick fiel aus)"**.
|
||||
|
||||
4. **Bereinigen + INDEX pflegen.** Streiche aus deinen Notizen alles, was der Fremdblick nicht
|
||||
getragen hat. Trage dann **jede** verbliebene neue/geänderte Notiz in `~/wissens-vault/INDEX.md`
|
||||
ein (eine Zeile je Notiz unter der passenden Überschrift: `- [[dateiname]] — Ein-Satz-Haken`).
|
||||
**Wichtig zum INDEX:** Bearbeite NUR die Markdown-Inhaltszeilen. Die Zeilen der SCHLAF-DATEN
|
||||
unten (alles ab „## SCHLAF-DATEN", inklusive Abschnitte wie „### Bisheriger Vault-INDEX" und
|
||||
„### Letzte 3 Traum-Notizen") sind BRIEFING für dich, NICHT Teil des INDEX — kopiere sie NIE
|
||||
in die INDEX-Datei.
|
||||
|
||||
5. **Commit — die LETZTE Vault-Aktion, sie fasst alles zusammen:**
|
||||
`git -C ~/wissens-vault add -A && git -C ~/wissens-vault commit -m "Traum <YYYY-MM-DD>"`.
|
||||
(Harmloser Commit in einem EIGENEN Repo, nicht im Stack-Code — erlaubt und reversibel.)
|
||||
|
||||
6. **Dem Commander berichten — deine ALLERLETZTE Handlung** (nichts danach). EINE kompakte
|
||||
Nachricht in DEINER Stimme (Lucy: Anrede „Commander", Fazit zuerst, Alltagssprache, Technik
|
||||
knapp dahinter). Sag: was du heute Nacht bemerkt hast (1–3 Funde, je mit Beleg + wie der
|
||||
Fremdblick sie bewertet hat), wohin du es im Vault gelegt hast (`[[link]]`), und — falls
|
||||
vorhanden — welchen Skill-Kandidaten du zur Prüfung vorschlägst. Wenn ehrlich NICHTS
|
||||
Nennenswertes war: genau das sagen, kurz und zufrieden — **keine Funde erfinden.**
|
||||
|
||||
## Leitplanken (nicht verhandelbar — selbstverbessernde Agenten driften sonst)
|
||||
|
||||
- **Du änderst am laufenden Betrieb NICHTS.** Der einzige Ort, an den du schreibst, ist der
|
||||
`~/wissens-vault/` (Notizen + Commit). Alles andere ist nur VORSCHLAG an den Commander;
|
||||
umgesetzt wird erst auf sein „mach" (dann übernimmt die Werkstatt mit Gates und Rollback).
|
||||
- **Kein Security-Anfassen.** Approvals, Tokens, Firewall, Config nur BENENNEN, nie ändern.
|
||||
- **Fremder Quellcode (Hermes, llama.cpp …) ist tabu** — Auto-Update-Kanal, nicht dein Revier.
|
||||
- **Nichts, was das Warm-Set oder Lucys Sprech-Latenz gefährdet.** Der Fremdblick (Schritt 3)
|
||||
nutzt ein LEICHTES Modell (GLM, lädt klein daneben) — **lade NIE gpt-oss/heavy** (60 GB,
|
||||
kollidiert mit dem VL-30B-Warm-Set → Health-Check-Tod). Keine schweren Jobs, kein Neustart.
|
||||
- **Sparsam mit Tools.** Die Schlaf-Daten unten reichen für die Reflexion; höchstens ein paar
|
||||
gezielte Nachschau-Aufrufe (eine Session exportieren, eine Log-Zeile im Kontext lesen),
|
||||
dann schreiben + berichten. Kein Tool-Feuerwerk.
|
||||
@@ -1,19 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stellt sicher, dass ein Worker-Profil die Agent-Hooks kennt (Faden 8, 13.07.2026).
|
||||
"""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
|
||||
(box-steckbrief-inject = Box-Wissen, tabu-pfade-guard = Schreibschutz) muessen also in JEDER
|
||||
Worker-Profil-config.yaml stehen, sonst feuern sie fuer Worker nie (Bug-Fund 13.07.).
|
||||
muessen also in JEDER Worker-Profil-config.yaml stehen, sonst feuern sie fuer Worker nie
|
||||
(Bug-Fund 13.07.).
|
||||
|
||||
Idempotent + validiert + Backup. Setzt einen bestehenden `pre_verify`-Hook-Block als Anker
|
||||
voraus (haben alle von default/werkstatt geklonten Profile); fehlt er, wird sauber
|
||||
uebersprungen (exit 0), damit deploy.sh nicht bricht. Arg: Pfad zur Profil-config.yaml.
|
||||
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 sys
|
||||
import os
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
@@ -22,10 +27,13 @@ except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
AH = os.path.expanduser("~/.hermes/agent-hooks")
|
||||
WANT = {
|
||||
"pre_llm_call": f"{AH}/box-steckbrief-inject.sh",
|
||||
"pre_tool_call": f"{AH}/tabu-pfade-guard.py",
|
||||
}
|
||||
# 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>")
|
||||
@@ -44,22 +52,41 @@ if txt.count(anchor) != 1:
|
||||
print(f" [{os.path.basename(os.path.dirname(p))}] kein eindeutiger pre_verify-Anker — SKIP")
|
||||
sys.exit(0)
|
||||
|
||||
add = ""
|
||||
for event, cmd in WANT.items():
|
||||
if cmd not in txt:
|
||||
add += f" {event}:\n - command: {cmd}\n timeout: 10\n"
|
||||
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 add:
|
||||
if not changed:
|
||||
print(f" [{os.path.basename(os.path.dirname(p))}] Hooks schon registriert.")
|
||||
sys.exit(0)
|
||||
|
||||
new = txt.replace(anchor, anchor + add, 1)
|
||||
try:
|
||||
d = yaml.safe_load(new)
|
||||
h = d["hooks"]
|
||||
for event, cmd in WANT.items():
|
||||
assert any(cmd in x.get("command", "") for x in h.get(event, []))
|
||||
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)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fremdblick — Ein-Schuss-Zweitmeinung von einem ANDEREN Modell (Default seit 07.07.:
|
||||
# GLM-4.7-Flash, 30B-A3B Text — klar stärkerer Kritiker als die 9B-Vision-Schwester 4.6V,
|
||||
# gleicher Fremd-Vendor wie zuvor → echte „andere Brille" bleibt). Eine EINZELNE
|
||||
# Completion — kein agentischer Subagent — umgeht Hermes' 64K-Delegations-Floor
|
||||
# (MINIMUM_CONTEXT_LENGTH) UND das 60-GB-Ladeproblem von gpt-oss (das mit dem Warm-Set kollidiert
|
||||
# und „exited prematurely" wirft). GLM lädt klein neben dem Warm-Set und antwortet zuverlässig.
|
||||
#
|
||||
# Nutzung: echo "<Beobachtungen/Behauptungen je mit Beleg>" | fremdblick.sh
|
||||
# printf '%s' "$text" | FREMDBLICK_MODEL=Qwen3-Coder-Next FREMDBLICK_SYS="$raster" fremdblick.sh
|
||||
#
|
||||
# Env-Overrides:
|
||||
# FREMDBLICK_MODE 'prose' (Default, Dreaming-Prosa-Raster) oder 'code' (Werkstatt-Code-Review).
|
||||
# 'code' setzt Raster + Default-Modell (Qwen3-Coder-Next) + größeres Budget.
|
||||
# FREMDBLICK_MODEL Modell-ID (echte llama-swap-ID, kein Alias). Übersteuert den Mode-Default.
|
||||
# FREMDBLICK_SYS eigenes System-Raster (übersteuert das Mode-Raster).
|
||||
# FREMDBLICK_MAXTOK max_tokens (übersteuert den Mode-Default).
|
||||
set -uo pipefail
|
||||
|
||||
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
|
||||
# (kein "heavy"/"scout" hier).
|
||||
ENDPOINT="${FREMDBLICK_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
MODE="${FREMDBLICK_MODE:-prose}"
|
||||
SUBJECT="$(cat)"
|
||||
|
||||
if [ -z "${SUBJECT// /}" ]; then
|
||||
echo "=== FREMDBLICK: nichts zu prüfen (leere Eingabe) ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prosa-Raster (Dreaming): urteilt über Beobachtungen/Behauptungen. Default-Kritiker = GLM (Fremd-Vendor).
|
||||
PROSE_SYS='Du bist ein KRITISCHER Zweitgutachter und ausdrücklich ein ANDERES Modell als der Autor. Prüfe jede vorgelegte Beobachtung/Behauptung streng: Trägt der genannte Beleg (Zahl / Session / Log-Zeile) die Behauptung wirklich, oder ist es ein Einzelfall bzw. ein Bauchgefühl mit Zahlen-Deko? Verwechselt der Autor Korrelation mit Ursache? Fehlt entscheidender Kontext? Falls ein Skill-Kandidat dabei ist: ist er wirklich WIEDERVERWENDBAR oder Einmal-Kram? Antworte kompakt auf Deutsch: pro Punkt ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER mit genau einem Satz Begruendung. Sei knapp und unbestechlich; nicke nichts aus Hoeflichkeit durch. Erfinde keine neuen Belege.'
|
||||
# Code-Raster (Werkstatt): reproduziert den Fehlerfall aus der Bug-Beschreibung, statt dem Autor zu glauben.
|
||||
CODE_SYS='Du bist ein kritischer Code-Reviewer und ein ANDERES Modell als der Autor. Vertraue seiner Begruendung NICHT. Dir liegen ein Wartungs-AUFTRAG (Bug-Beschreibung) und ein git-Diff vor. Trenne klar: (A) KERNFRAGE: Behebt der Diff den im Auftrag KONKRET beschriebenen Fehlerfall? Leite die Eingabe SELBST aus der Bug-Beschreibung ab und belege einen Uebergang: Eingabe X -> VOR Patch: Y (Bug sichtbar) -> NACH Patch: Z. (B) RANDFAELLE: nenne offene Risiken/Randfaelle als Hinweis. URTEIL-Regel: ABGELEHNT NUR, wenn (A) nicht belegt behoben ist (Kern-Bug bleibt, ODER der Patch trifft die Auftrags-Absicht nicht, ODER er bricht den Normalfall). Ist der Kern-Fall belegt behoben und es bleiben nur Randfaelle: FREIGABE-MIT-VORBEHALT (Randfaelle auflisten). Ist alles sauber: FREIGABE. Ein blosses sieht-plausibel-aus OHNE reproduzierten Kern-Uebergang = ABGELEHNT. Beginne die Antwort mit der Zeile "URTEIL: <ABGELEHNT|FREIGABE-MIT-VORBEHALT|FREIGABE>". Antworte knapp auf Deutsch.'
|
||||
|
||||
if [ "$MODE" = "code" ]; then
|
||||
MODEL="${FREMDBLICK_MODEL:-Qwen3-Coder-Next}"
|
||||
MAXTOK="${FREMDBLICK_MAXTOK:-1600}"
|
||||
SYS="${FREMDBLICK_SYS:-$CODE_SYS}"
|
||||
else
|
||||
MODEL="${FREMDBLICK_MODEL:-GLM-4.7-Flash}"
|
||||
# 4000 (war 2200): GLM-4.7-Flash ist ein Reasoning-Modell und verbrennt real ~2000 Tokens
|
||||
# im reasoning_content, BEVOR das Urteil in content landet (verifiziert 14.07.). Bei 2200 lief
|
||||
# es Gefahr, mitten im Denken abzuschneiden → leeres content → "ABGESCHNITTEN/FEHLGESCHLAGEN"
|
||||
# (traf die Nacht-Kritiker mit Default-Budget). Wer explizit knapper braucht, setzt FREMDBLICK_MAXTOK.
|
||||
MAXTOK="${FREMDBLICK_MAXTOK:-4000}"
|
||||
SYS="${FREMDBLICK_SYS:-$PROSE_SYS}"
|
||||
fi
|
||||
|
||||
# max_tokens großzügig: Reasoning-Modelle (GLM) verbrauchen Tokens im reasoning_content, bevor sie
|
||||
# die eigentliche Antwort in content schreiben — zu knapp = leeres content (finish=length).
|
||||
RESP="$(jq -n --arg m "$MODEL" --arg s "$SYS" --arg u "$SUBJECT" --argjson t "$MAXTOK" \
|
||||
'{model:$m, temperature:0.2, max_tokens:$t, messages:[{role:"system",content:$s},{role:"user",content:$u}]}' \
|
||||
| curl -s --max-time 240 "$ENDPOINT" -H 'Content-Type: application/json' -d @- 2>/dev/null)"
|
||||
|
||||
OUT="$(printf '%s' "$RESP" | jq -r '.choices[0].message.content // empty' 2>/dev/null)"
|
||||
FIN="$(printf '%s' "$RESP" | jq -r '.choices[0].finish_reason // empty' 2>/dev/null)"
|
||||
|
||||
if [ -n "$OUT" ]; then
|
||||
echo "=== FREMDBLICK (Zweitmeinung von Modell: $MODEL) ==="
|
||||
echo "$OUT"
|
||||
elif [ "$FIN" = "length" ]; then
|
||||
echo "=== FREMDBLICK ABGESCHNITTEN ==="
|
||||
echo "(Das Kritiker-Modell hat sein Token-Budget im Nachdenken verbraucht, bevor ein Urteil kam."
|
||||
echo " Kürze deine Vorlage auf die Kern-Behauptungen und versuche es erneut; sonst UNGEPRUEFT.)"
|
||||
else
|
||||
echo "=== FREMDBLICK FEHLGESCHLAGEN ==="
|
||||
echo "(Kein Urteil erhalten — Endpoint/Modell nicht erreichbar. Behandle die Funde als UNGEPRUEFT.)"
|
||||
fi
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gitea-Repo-Anlage für Lucy (17.07.2026): schließt die Lücke im „Projekte selbst
|
||||
# starten"-Weg — bislang konnte die Werkstatt nur Branches in BESTEHENDE Repos
|
||||
# proposen, aber kein neues Projekt-Repo anlegen.
|
||||
#
|
||||
# Nutzung: gitea-repo-create.sh <name> ["Beschreibung"]
|
||||
# Ergebnis (stdout, letzte Zeile): CLONE <https-clone-url> → Aufrufer kann direkt klonen.
|
||||
#
|
||||
# HARTE Sicherheits-Leitplanken (Gitea ist öffentlich erreichbar!):
|
||||
# • Repos werden IMMER PRIVAT angelegt (kein öffentliches Repo aus Versehen).
|
||||
# • Token wird aus ~/.git-credentials gelesen, NIE ausgegeben/geloggt.
|
||||
# • Jede Anlage wird protokolliert (Audit-Spur) + still an die Chronik gespiegelt.
|
||||
# Braucht ein Token mit Scope `write:user` (zusätzlich zu `write:repository`); ohne das
|
||||
# meldet Gitea 403 — das Skript sagt dann genau, was zu tun ist (Token in Gitea neu
|
||||
# generieren), statt kryptisch zu scheitern.
|
||||
set -uo pipefail
|
||||
|
||||
NAME="${1:-}"
|
||||
BESCHREIBUNG="${2:-Von Lucy angelegtes Projekt-Repo.}"
|
||||
# Worker-Subprozesse laufen evtl. mit abweichendem $HOME (Profil-Home) — das dedizierte
|
||||
# Anlage-Token (write:user) UND .git-credentials liegen aber nur im ECHTEN User-Home.
|
||||
# Darum den echten Home aus /etc/passwd aufloesen, sonst faellt das Skript aufs falsche
|
||||
# Token zurueck (Fan-out-Nachlese 20.07.: projektstart-Worker blockte an genau dieser
|
||||
# Stelle mit "Token hat nur write:repository").
|
||||
REALHOME="$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f6)"; REALHOME="${REALHOME:-$HOME}"
|
||||
CRED="$REALHOME/.git-credentials"
|
||||
# Dediziertes Anlage-Token (Scope write:user) — GETRENNT vom Push-Token in
|
||||
# ~/.git-credentials, damit ein Überschreiben nie Push/PR bricht. Priorität:
|
||||
# ENV GITEA_CREATE_TOKEN → <echter-Home>/.config/gitea/create-token → Fallback .git-credentials.
|
||||
CREATE_TOKEN_FILE="$REALHOME/.config/gitea/create-token"
|
||||
LOG="$REALHOME/.hermes/state/gitea-repos-created.log"
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "Nutzung: gitea-repo-create.sh <name> [\"Beschreibung\"]" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Repo-Name defensiv prüfen (Gitea erlaubt Buchstaben/Ziffern/-/_/.)
|
||||
if ! printf '%s' "$NAME" | grep -qE '^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$'; then
|
||||
echo "Ungültiger Repo-Name '$NAME' (erlaubt: Buchstaben/Ziffern/.-_, Beginn alphanumerisch)." >&2
|
||||
exit 2
|
||||
fi
|
||||
# User + Host sind NICHT geheim: aus .git-credentials ableiten, sonst Standardwerte.
|
||||
LINE="$(grep -m1 'git\.tobisniceshomelab\.ddnsfree\.com' "$CRED" 2>/dev/null || head -1 "$CRED" 2>/dev/null || true)"
|
||||
USER="$(printf '%s' "$LINE" | sed -nE 's#https://([^:]+):.*#\1#p')"; USER="${USER:-Hitonabi}"
|
||||
HOST="$(printf '%s' "$LINE" | sed -nE 's#https://[^@]+@([^/]+).*#\1#p')"; HOST="${HOST:-git.tobisniceshomelab.ddnsfree.com}"
|
||||
BASE="https://${HOST}"
|
||||
|
||||
# Anlage-Token bestimmen (dediziert bevorzugt, damit Push-Token unberührt bleibt)
|
||||
if [ -n "${GITEA_CREATE_TOKEN:-}" ]; then
|
||||
TOKEN="$GITEA_CREATE_TOKEN"
|
||||
elif [ -r "$CREATE_TOKEN_FILE" ]; then
|
||||
TOKEN="$(tr -d '[:space:]' < "$CREATE_TOKEN_FILE")"
|
||||
else
|
||||
TOKEN="$(printf '%s' "$LINE" | sed -nE 's#https://[^:]+:([^@]+)@.*#\1#p')"
|
||||
fi
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "Kein Gitea-Token gefunden (weder \$GITEA_CREATE_TOKEN noch $CREATE_TOKEN_FILE noch ~/.git-credentials)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESP="$(mktemp)"; trap 'rm -f "$RESP"' EXIT
|
||||
CODE="$(curl -s -o "$RESP" -w '%{http_code}' -X POST "$BASE/api/v1/user/repos" \
|
||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "$(python3 -c 'import json,sys; print(json.dumps({"name":sys.argv[1],"description":sys.argv[2],"private":True,"auto_init":True,"default_branch":"main"}))' "$NAME" "$BESCHREIBUNG")")"
|
||||
|
||||
case "$CODE" in
|
||||
201)
|
||||
CLONE="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("clone_url",""))' "$RESP" 2>/dev/null)"
|
||||
FULL="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("full_name",""))' "$RESP" 2>/dev/null)"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
echo "$(date '+%Y-%m-%d %H:%M') angelegt (privat): $FULL" >> "$LOG"
|
||||
# Stiller Chronik-Spiegel (Audit-Spur, wie die Radar-Meldungen)
|
||||
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 -c 'import json,sys; print(json.dumps({"text":"Neues PRIVATES Projekt-Repo angelegt: "+sys.argv[1],"subject":"Gitea","source":"gitea-repo-create","priority":"silent"}))' "$FULL")" \
|
||||
>/dev/null 2>&1 || true
|
||||
# CI-Ampel einpflanzen (Wasserdicht-Runde 22.07.): JEDES neue Repo wird mit der
|
||||
# universellen Actions-Ampel geboren (deploy/ampel-ci.yml). Defensiv: fehlt die
|
||||
# Vorlage oder scheitert der Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
|
||||
# WICHTIG: Contents-API braucht write:repository → PUSH-Token nutzen (das
|
||||
# dedizierte Anlage-Token hat u.U. nur write:user).
|
||||
PUSHTOKEN="$(printf '%s' "$LINE" | sed -nE 's#https://[^:]+:([^@]+)@.*#\1#p')"; PUSHTOKEN="${PUSHTOKEN:-$TOKEN}"
|
||||
# saat <lokale-vorlage> <pfad-im-repo> <commit-nachricht> <klartext-name>
|
||||
saat () {
|
||||
local QUELLE="$1" ZIEL="$2" MSG="$3" NAME="$4" CODE
|
||||
if [ ! -r "$QUELLE" ]; then
|
||||
echo "WARNUNG: Vorlage fehlt ($QUELLE) — Repo ohne $NAME angelegt." >&2
|
||||
return
|
||||
fi
|
||||
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/v1/repos/$FULL/contents/$ZIEL" \
|
||||
-H "Authorization: token $PUSHTOKEN" -H "Content-Type: application/json" \
|
||||
--data "$(python3 - "$QUELLE" "$MSG" <<'PY'
|
||||
import base64, json, sys
|
||||
inhalt = open(sys.argv[1], "rb").read()
|
||||
print(json.dumps({"content": base64.b64encode(inhalt).decode(), "message": sys.argv[2]}))
|
||||
PY
|
||||
)")"
|
||||
if [ "$CODE" = "201" ]; then
|
||||
echo "$NAME eingepflanzt ($ZIEL)."
|
||||
else
|
||||
echo "WARNUNG: $NAME-Seed antwortete HTTP $CODE (Repo ist trotzdem da)." >&2
|
||||
fi
|
||||
}
|
||||
# JEDES neue Repo wird mit beiden Wächtern geboren:
|
||||
# ci.yml = die AUSSEN-Prüfung (Gitea Actions nach dem Push, Wasserdicht-Runde 22.07.)
|
||||
# VERIFY = die INNEN-Prüfung (das OpenCode-Plugin führt sie nach jeder Etappe aus und
|
||||
# gibt rote Tests dem Agenten sofort zurück, statt sie erst der CI zu zeigen)
|
||||
# Defensiv: scheitert ein Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
|
||||
saat "$REALHOME/mission-control-v2/deploy/ampel-ci.yml" ".gitea/workflows/ci.yml" \
|
||||
"CI-Ampel (automatisch bei Repo-Anlage eingepflanzt)" "CI-Ampel"
|
||||
saat "$REALHOME/mission-control-v2/deploy/opencode/VERIFY.template" "VERIFY" \
|
||||
"Pruef-Tor (automatisch bei Repo-Anlage eingepflanzt)" "Pruef-Tor"
|
||||
echo "Repo '$FULL' angelegt (privat, main initialisiert)."
|
||||
echo "CLONE ${CLONE}"
|
||||
exit 0 ;;
|
||||
403)
|
||||
if grep -q 'write:user' "$RESP"; then
|
||||
echo "FEHLT: Das benutzte Gitea-Token darf keine Repos anlegen (Scope 'write:user' fehlt)." >&2
|
||||
echo "So wird es freigeschaltet (einmalig, durch den Commander):" >&2
|
||||
echo " 1) In Gitea ein Token mit Scope 'write:user' erzeugen (nur DAS reicht fürs Anlegen)." >&2
|
||||
echo " 2) Den Token-Wert in ~/.config/gitea/create-token legen (chmod 600) —" >&2
|
||||
echo " getrennt vom Push-Token, das in ~/.git-credentials unberührt bleibt." >&2
|
||||
else
|
||||
echo "403 von Gitea:" >&2; head -c 300 "$RESP" >&2; echo >&2
|
||||
fi
|
||||
exit 3 ;;
|
||||
409)
|
||||
echo "Ein Repo namens '$NAME' existiert bereits." >&2
|
||||
exit 4 ;;
|
||||
*)
|
||||
echo "Gitea antwortete mit HTTP $CODE:" >&2; head -c 300 "$RESP" >&2; echo >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,65 @@
|
||||
# Gitea app.ini - Beispielkonfiguration mit DISABLE_REGISTRATION
|
||||
# Speichere diese Datei auf dem Server unter /etc/gitea/app.ini
|
||||
# und setze DISABLE_REGISTRATION=true im Abschnitt [security]
|
||||
|
||||
APP_NAME = TobisNicesGitTea
|
||||
RUN_USER = git
|
||||
RUN_MODE = prod
|
||||
|
||||
[server]
|
||||
DOMAIN = git.tobisniceshomelab.ddnsfree.com
|
||||
HTTP_PORT = 3000
|
||||
APP_DATA_PATH = /var/lib/gitea
|
||||
PROTOCOL = http
|
||||
ROOT_URL = https://git.tobisniceshomelab.ddnsfree.com/
|
||||
DISABLE_SSH = false
|
||||
SSH_PORT = 22
|
||||
LFS_START_SERVER = true
|
||||
LFS_CONTENT_PATH = /var/lib/gitea/data/lfs
|
||||
OFFLINE_MODE = false
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /var/lib/gitea/gitea.db
|
||||
MAX_OPEN_CONNS = 10
|
||||
MAX_IDLE_CONNS = 5
|
||||
MAX_CONN_LIFETIME = 30m
|
||||
DSN = /var/lib/gitea/gitea.db
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = CHANGE_THIS_TO_A_RANDOM_SECRET_KEY
|
||||
PASSWORD_HASH_ALGO = pbkdf2
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
|
||||
[service]
|
||||
ENABLE_CAPTCHA = false
|
||||
ENABLE_REGISTER_EMAIL_CONFIRM = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
ALLOW_ONLY_EXTERNAL_REGISTER_API = false
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||
DEFAULT_ENABLE_TIMETRACKING = true
|
||||
NO_REPLY_ADDRESS = noreply.example.org
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = false
|
||||
ENABLE_OPENID_SIGNUP = false
|
||||
|
||||
[session]
|
||||
PROVIDER = file
|
||||
COOKIE_NAME = gitea sessionId
|
||||
COOKIE_SECURE = false
|
||||
|
||||
[mailer]
|
||||
ENABLED = false
|
||||
HOST =
|
||||
FROM = gitea@example.com
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = false
|
||||
ENABLE_OPENID_SIGNUP = false
|
||||
|
||||
[security]
|
||||
DISABLE_REGISTRATION = true
|
||||
@@ -1,21 +0,0 @@
|
||||
[Unit]
|
||||
Description=Hermes Built-in Web UI (`hermes serve` — Agent-Dashboard/Chat, Loopback only)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
# Serviert die EINGEBAUTE Hermes-Web-GUI (die reiche Agent-Oberflaeche mit Threads/Tool-Calls, die
|
||||
# auch die Electron-Desktop-App umhuellt) headless.
|
||||
# SICHERHEIT: Bindet NUR an Loopback (127.0.0.1:9119) und laeuft hinter dem MC2-Reverse-Proxy
|
||||
# (routers/hermes_ui.py → same-origin /hermes-ui/ auf dem offenen Port 9001) — KEINE eigene
|
||||
# Firewall-Freigabe, KEIN Login (LAN-Trust wie Konsole/Terminal). --skip-build nutzt das von
|
||||
# deploy.sh mit Vite-base=/hermes-ui/ vorgebaute web_dist, damit alle SPA-Pfade unter /hermes-ui/
|
||||
# liegen und der Proxy sie sauber durchreichen kann.
|
||||
Type=simple
|
||||
Environment=HERMES_HOME=%h/.hermes
|
||||
ExecStart=%h/.hermes/hermes-agent/venv/bin/hermes dashboard --no-open --skip-build --host 127.0.0.1 --port 9119
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -30,6 +30,25 @@ else
|
||||
echo "FAIL · memory.provider nicht mehr gesetzt (Config vom Update überschrieben?)"; fail=1
|
||||
fi
|
||||
|
||||
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
||||
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
||||
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
||||
# --- angewandt → Gateway neu laden; Exit 1 = ein Patch passt nicht mehr → ROT. ---
|
||||
PATCH_APPLY="${MC2_SRC:-$HOME/mission-control-v2}/deploy/hermes-patches/apply.py"
|
||||
if [ -f "$PATCH_APPLY" ]; then
|
||||
python3 "$PATCH_APPLY" >/dev/null 2>&1; _prc=$?
|
||||
if [ "$_prc" -eq 10 ]; then
|
||||
systemctl --user try-restart hermes-gateway >/dev/null 2>&1 || true
|
||||
echo "PASS · Hermes-Runtime-Patches nach Update RE-angewandt (Gateway neu geladen)"
|
||||
elif [ "$_prc" -eq 0 ]; then
|
||||
echo "PASS · Hermes-Runtime-Patches unveraendert vorhanden"
|
||||
else
|
||||
echo "FAIL · Hermes-Runtime-Patches: ein Patch passt nicht mehr zum aktualisierten Quellcode — pruefen"; fail=1
|
||||
fi
|
||||
else
|
||||
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
||||
fi
|
||||
|
||||
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
||||
"import sys; sys.path.insert(0,'.'); from plugins.memory import load_memory_provider; p=load_memory_provider('mc2-memory'); assert p and p.name()=='mc2-memory'" \
|
||||
>/dev/null 2>&1 ); then
|
||||
@@ -53,12 +72,22 @@ fi
|
||||
# --- Approval-/Tool-Ketten (Antwort muss kommen und darf nicht in pending_approval hängen). ---
|
||||
API_KEY="$(grep -E '^API_SERVER_KEY=' "$HERMES/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||||
if [ -n "$API_KEY" ]; then
|
||||
TOOL_RESP=$(curl -sf -m 90 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||||
-H "X-Hermes-Session-Id: postcheck-tool-smoke" \
|
||||
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
|
||||
# Mit Retry — GENAU wie der Voice-Smoke darunter, und aus demselben Grund: direkt nach
|
||||
# einem Update ist das Modell kalt. Ein Kaltstart kostet ~20-30 s Laden PLUS ~37 s
|
||||
# Prefill für Hermes' ~70-KB-Systemprompt (33 Tool-Schemas) — zusammen mit zwei
|
||||
# Inferenz-Runden sprengt das die 90 s des ersten Versuchs. Warm gemessen: 12 s.
|
||||
# Ein Versuch allein war ein Fehlalarm-Generator (live erlebt 25.07., Job faf1137a).
|
||||
TOOL_RESP=""
|
||||
for try in 1 2 3; do
|
||||
TOOL_RESP=$(curl -sf -m 120 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||||
-H "X-Hermes-Session-Id: postcheck-tool-smoke-$try" \
|
||||
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
|
||||
echo "$TOOL_RESP" | grep -qi "postcheck-ok" && break
|
||||
[ "$try" -lt 3 ] && sleep 10
|
||||
done
|
||||
if echo "$TOOL_RESP" | grep -qi "postcheck-ok"; then
|
||||
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis"
|
||||
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis (Versuch $try)"
|
||||
elif echo "$TOOL_RESP" | grep -qi "pending_approval"; then
|
||||
echo "FAIL · Tool-Smoke hängt in pending_approval (approvals.mode prüfen!)"; fail=1
|
||||
else
|
||||
@@ -86,6 +115,25 @@ else
|
||||
echo "FAIL · Voice-Smoke: /api/voice/chat streamt nicht (3 Versuche)"; fail=1
|
||||
fi
|
||||
|
||||
# --- Browser-Tool: agent-browser überlebt das Update? (Vorfall 15./16.07.2026: das ---
|
||||
# --- npm ci des Updates bog ~/.local/bin/agent-browser per postinstall auf ---
|
||||
# --- node_modules/ um und löschte das Paket im selben Lauf → toter Link, jedes ---
|
||||
# --- „Website öffnen" starb still als „Failed to open".) Heilung direkt hier, denn ---
|
||||
# --- genau NACH einem Update entsteht der Bruch; try-restart räumt den ---
|
||||
# --- „nicht installiert"-Cache, den browser_tool pro Prozess hält. ---
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
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
|
||||
echo "PASS · Browser-Tool (agent-browser erreichbar)"
|
||||
elif [ -x "$AB_GOOD" ] && "$AB_GOOD" --version >/dev/null 2>&1 \
|
||||
&& ln -sfn "$AB_GOOD" "$AB_LINK" && "$AB_LINK" --version >/dev/null 2>&1; then
|
||||
systemctl --user try-restart hermes-gateway hermes-builtin-ui >/dev/null 2>&1 || true
|
||||
echo "GEHEILT · Browser-Tool: toter agent-browser-Link nach Update neu gesetzt (Dienste neu gestartet)"
|
||||
else
|
||||
echo "FAIL · Browser-Tool: agent-browser nicht lauffähig (npm install -g agent-browser && agent-browser install --with-deps)"; fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "=== GEHIRN OK ✓ ==="
|
||||
else
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermes-Release-Radar (woechentlich, Mo 05:15) — "mehr Hermes nativ, weniger Eigenkreationen":
|
||||
# haelt neue hermes-agent-Releases gegen die Eigenbau-Landkarte im Wissens-Vault und meldet
|
||||
# NUR echte Treffer auf Abloese-Kriterien/Wiedervorlage-Bedingungen. Muster wie
|
||||
# chef-gutachter-feed.sh: liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn), stdout wird
|
||||
# dem Cron-Agenten als Prompt eingespeist. Der ABGLEICH laeuft als Ein-Schuss-Completion
|
||||
# direkt gegen llama-swap (:8080, fremdblick-Architektur): Analyst gpt-oss-120b (um 05:15
|
||||
# vom Chef-Gutachter 04:30 oft noch geladen), Vertretung GLM-4.7-Flash. Danach gatet
|
||||
# fremdblick.sh (Prosa-Raster, Fremd-Vendor) jede Treffer-Behauptung gegen die
|
||||
# Original-Changelogs.
|
||||
# Grundsaetze (stehen auch in der Landkarte): nur getaggte Releases (kein main-Churn bei
|
||||
# ~660 PRs/Woche) · Behauptung nur mit woertlichem Changelog-Zitat · Treffer => Probe-
|
||||
# Vorschlag, NIE Direkt-Adoption · der Radar informiert, er updatet nicht (Entscheid 04.07.).
|
||||
# State: ~/.hermes/state/release-radar-last.txt (zuletzt gemeldeter Tag). Verlust harmlos —
|
||||
# ohne State dient die installierte Version als Basis.
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
LANDKARTE="$HOME/wissens-vault/eigenbau-landkarte.md"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/release-radar-last.txt"
|
||||
API="${RADAR_RELEASES_URL:-https://api.github.com/repos/NousResearch/hermes-agent/releases?per_page=10}"
|
||||
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
|
||||
FREMDBLICK="$HOME/.hermes/scripts/fremdblick.sh"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Briefkasten-Spiegel (still, wie die Morgenlage): Cockpit/Chronik lesen source=release-radar.
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "release-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
|
||||
cat "$MC/deploy/release-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des woechentlichen Release-Radars.
|
||||
Gib den Befund unten in Lucys Stimme wieder: bei "KEIN NEUES RELEASE" oder "TREFFER: 0"
|
||||
genau EIN kurzer Satz; bei Treffern maximal 5 Saetze. Nichts installieren oder aendern.
|
||||
EOF
|
||||
echo
|
||||
|
||||
INSTALLED_LINE="$(bash -lc 'hermes --version' 2>/dev/null | head -1)"
|
||||
INSTALLED_TAG="$(printf '%s' "$INSTALLED_LINE" | grep -oE '\([0-9][0-9.]*\)' | head -1 | tr -d '()')"
|
||||
[ -n "$INSTALLED_TAG" ] && INSTALLED_TAG="v$INSTALLED_TAG"
|
||||
LAST_SEEN="$(cat "$STATE" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
BASELINE="${LAST_SEEN:-$INSTALLED_TAG}"
|
||||
|
||||
RELEASES_JSON="$(curl -sf --max-time 30 "$API" 2>/dev/null || true)"
|
||||
if [ -z "$RELEASES_JSON" ]; then
|
||||
echo "## RADAR-BEFUND: AUSGEFALLEN"
|
||||
echo "GitHub-Releases-API nicht erreichbar. Dem Commander EINEN ehrlichen Satz melden; naechste Woche neuer Versuch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Neue getaggte Releases seit BASELINE (Drafts/Prereleases raus, max. 6, Bodies gekuerzt).
|
||||
# Baseline unbekannt (aelter als die letzten 10 oder Erstlauf ohne Version) -> juengste 6.
|
||||
# JSON via Temp-Datei, NICHT via Pipe: das Heredoc belegt stdin schon fuer den Python-Code
|
||||
# selbst (Pipe+Heredoc gleichzeitig -> json.load(stdin) liest ins Leere; Probelauf 10.07.).
|
||||
TMPJSON="$(mktemp /tmp/release-radar.XXXXXX.json)"
|
||||
printf '%s' "$RELEASES_JSON" > "$TMPJSON"
|
||||
PARSED="$(python3 - "$BASELINE" "$TMPJSON" <<'PY'
|
||||
import json, sys
|
||||
baseline = sys.argv[1]
|
||||
try:
|
||||
data = json.load(open(sys.argv[2]))
|
||||
# Rate-Limit/Fehler antwortet als Dict statt Liste -> wie "keine Daten" behandeln.
|
||||
rels = [r for r in data if not r.get("draft") and not r.get("prerelease")] if isinstance(data, list) else []
|
||||
except Exception:
|
||||
rels = []
|
||||
rels.sort(key=lambda r: r.get("published_at") or "", reverse=True)
|
||||
if not rels:
|
||||
# API hat geantwortet, aber unbrauchbar (Rate-Limit-Dict, kaputtes JSON, leere Liste):
|
||||
# ehrlich als "blind" markieren statt faelschlich "kein neues Release" zu melden.
|
||||
print(baseline)
|
||||
print(-1)
|
||||
raise SystemExit
|
||||
base = next((r.get("published_at") for r in rels if r.get("tag_name") == baseline), None)
|
||||
new = rels[:6] if base is None else [r for r in rels if (r.get("published_at") or "") > base][:6]
|
||||
print((new or rels)[0].get("tag_name") or baseline)
|
||||
print(len(new))
|
||||
for r in new:
|
||||
body = (r.get("body") or "").strip()
|
||||
if len(body) > 3500:
|
||||
body = body[:3500] + "\n[... gekuerzt ...]"
|
||||
print(f"\n===== RELEASE {r.get('tag_name')} — {(r.get('name') or '').strip()} ({(r.get('published_at') or '')[:10]}) =====")
|
||||
print(body)
|
||||
PY
|
||||
)"
|
||||
rm -f "$TMPJSON"
|
||||
NEWEST="$(printf '%s\n' "$PARSED" | sed -n 1p)"
|
||||
COUNT="$(printf '%s\n' "$PARSED" | sed -n 2p)"
|
||||
NOTES="$(printf '%s\n' "$PARSED" | tail -n +3)"
|
||||
|
||||
if [ "${COUNT:-0}" = "-1" ]; then
|
||||
echo "## RADAR-BEFUND: AUSGEFALLEN"
|
||||
echo "GitHub-API hat geantwortet, aber unbrauchbar (Rate-Limit oder kaputtes JSON). Dem Commander EINEN ehrlichen Satz melden; State bleibt, naechste Woche neuer Versuch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "## RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Box installiert: ${INSTALLED_LINE:-unbekannt}"
|
||||
echo "Zuletzt gemeldet: ${BASELINE:-unbekannt} · neuestes Release: $NEWEST · neue Releases: ${COUNT:-0}"
|
||||
echo "(Updates bleiben manuell/bestaetigt — der Radar informiert nur.)"
|
||||
echo
|
||||
|
||||
if [ "${COUNT:-0}" = "0" ]; then
|
||||
echo "KEIN NEUES RELEASE seit $BASELINE."
|
||||
[ -n "$NEWEST" ] && printf '%s\n' "$NEWEST" > "$STATE"
|
||||
briefkasten "Release-Radar" "Kein neues hermes-agent-Release seit $BASELINE (Box: ${INSTALLED_TAG:-?})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$LANDKARTE" ]; then
|
||||
echo "### ABGLEICH NICHT MOEGLICH"
|
||||
echo "$COUNT neue Releases, aber die Eigenbau-Landkarte fehlt ($LANDKARTE)."
|
||||
echo "Dem Commander melden: Landkarte im Wissens-Vault wiederherstellen. State bleibt unveraendert (naechste Woche neuer Versuch)."
|
||||
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber die Eigenbau-Landkarte fehlt — Abgleich uebersprungen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RASTER='Du bist der RELEASE-RADAR einer lokalen KI-Box. Du bekommst (1) die EIGENBAU-LANDKARTE (selbstgebaute Bausteine mit Abloese-Kriterien und Wiedervorlage-Bedingungen) und (2) CHANGELOGS neuer hermes-agent-Releases. Pruefe AUSSCHLIESSLICH: Trifft eine Changelog-Zeile ein Abloese-Kriterium oder eine Wiedervorlage-Bedingung der Landkarte? REGELN: Jede Treffer-Behauptung MUSS die Changelog-Zeile WOERTLICH zitieren. Marketing-Prosa ohne konkretes Feature ist KEIN Treffer. Vage Aehnlichkeit ist KEIN Treffer. Eintraege unter "Bewusst NICHT adoptiert" nur melden, wenn exakt die Wiedervorlage-Bedingung erfuellt ist. Erfinde nichts. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "TREFFER: <Anzahl>". Danach je Treffer ein Block: "EIGENBAU: <Name aus der Landkarte>" / "RELEASE: <Tag>" / "ZITAT: <woertliche Changelog-Zeile>" / "WARUM: <ein Satz>" / "PROBE: <ein Satz, wie man es risikofrei testet>". Bei TREFFER: 0 stattdessen EIN Satz, was die Releases fuer uns Unwichtiges bringen.'
|
||||
|
||||
EVID="EIGENBAU-LANDKARTE:
|
||||
$(cat "$LANDKARTE")
|
||||
|
||||
CHANGELOGS DER NEUEN RELEASES:
|
||||
$NOTES"
|
||||
|
||||
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh;
|
||||
# max_tokens grosszuegig, weil Reasoning-Modelle Budget im reasoning_content verbrauchen).
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "### ABGLEICH AUSGEFALLEN"
|
||||
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet. State bleibt unveraendert — naechste Woche neuer Versuch. Dem Commander EINEN ehrlichen Satz melden."
|
||||
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber der Abgleich fiel aus (kein Analyst erreichbar)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "### Neue Releases:"
|
||||
printf '%s\n' "$NOTES" | grep '^===== RELEASE' | sed 's/^===== //; s/ =====$//'
|
||||
echo
|
||||
echo "### ABGLEICH gegen die Eigenbau-Landkarte (Analyst: $RICHTER)"
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# Fremdblick-Gate nur bei Treffern (Zitate MIT Quellmaterial gegenpruefbar machen).
|
||||
TREFFER="$(printf '%s' "$ANALYSE" | grep -oE 'TREFFER: *[0-9]+' | head -1 | grep -oE '[0-9]+' || true)"
|
||||
GEGEN=""
|
||||
if [ "${TREFFER:-1}" != "0" ] && [ -x "$FREMDBLICK" ]; then
|
||||
# Grosses Budget: GLM (Reasoning-Modell) denkt erst ueber das ganze Quellmaterial nach,
|
||||
# bevor das Urteil in content landet — 2200 (fremdblick-Default) war im Probelauf 10.07.
|
||||
# zu knapp (ABGESCHNITTEN, kein Urteil).
|
||||
GEGEN="$(printf '%s\n\nQUELLMATERIAL (Original-Changelogs zum Gegenpruefen der Zitate):\n%s' "$ANALYSE" "$NOTES" \
|
||||
| FREMDBLICK_MAXTOK="${RADAR_FREMDBLICK_MAXTOK:-4500}" "$FREMDBLICK")"
|
||||
echo "$GEGEN"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Treffer fuettern die Ideen-Queue ("Lucy kennt sich", 12.07.2026): aus einem echten
|
||||
# Radar-Treffer wird EINE rohe Idee (triage) — Specifier + Commander entscheiden, ob eine
|
||||
# Probe gebaut wird. Idempotent je Release-Tag; der Radar selbst bleibt reiner Melder.
|
||||
if [ -n "${TREFFER:-}" ] && [ "$TREFFER" != "0" ] && [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
IDEE_ID="$("$HOME/.local/bin/hermes" kanban create \
|
||||
"Hermes ${NEWEST}: Release-Radar-Treffer pruefen (Probe bauen?)" \
|
||||
--body "Automatisch vom Release-Radar am $(date '+%d.%m.%Y'). ABGLEICH (Analyst: ${RICHTER}):
|
||||
${ANALYSE}
|
||||
|
||||
FREMDBLICK:
|
||||
${GEGEN:-(keiner)}
|
||||
|
||||
Rohe Idee: nur pruefen bzw. eine risikofreie Probe vorschlagen — Updates bleiben manuell (Entscheid 04.07.)." \
|
||||
--triage --created-by release-radar --idempotency-key "release-radar-${NEWEST}" --json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("{")
|
||||
try:
|
||||
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
|
||||
except Exception:
|
||||
print("")
|
||||
' 2>/dev/null || true)"
|
||||
if [ -n "${IDEE_ID// /}" ]; then
|
||||
echo "### IN DIE IDEEN-QUEUE GELEGT: ${IDEE_ID} — Probe-Pruefung fuer ${NEWEST} (der Specifier uebernimmt)."
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$NEWEST" > "$STATE"
|
||||
briefkasten "Release-Radar" "Neue hermes-agent-Releases bis $NEWEST (Box: ${INSTALLED_TAG:-?}).
|
||||
|
||||
ABGLEICH (Analyst: $RICHTER):
|
||||
$ANALYSE
|
||||
|
||||
${GEGEN:-(Kein Fremdblick noetig: TREFFER 0)}"
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Idle-Radar (naechtlich 03:45 — S4 "Zuendung", 12.07.2026): die Box gibt sich selbst Arbeit.
|
||||
# Destilliert aus JOURNAL-Fehlermustern (24 h) und der juengsten TRAUM-Notiz 0-2 KLEINE,
|
||||
# belegte Wartungs-Kandidaten und legt sie als ROHE IDEEN (triage) ins native Hermes-Kanban.
|
||||
# Danach laeuft der bewiesene S2-Kreislauf von allein: Specifier arbeitet aus -> Werkstatt
|
||||
# baut einen Vorschlags-Branch -> Karte im Auftragsbuch -> der KLICK des Commanders bleibt
|
||||
# das einzige Gate. Muster wie hermes-release-radar-feed.sh: liegt in ~/.hermes/scripts/
|
||||
# (deploy.sh kopiert ihn), stdout wird dem Cron-Agenten als Prompt eingespeist; der
|
||||
# eigentliche ABGLEICH laeuft als Ein-Schuss-Completion gegen llama-swap (:8080).
|
||||
# Grundsaetze:
|
||||
# - Kandidat NUR mit woertlichem Beleg-Zitat; das Skript prueft das Zitat MECHANISCH
|
||||
# gegen das Material (reproduziert-oder-abgelehnt, Verdikt 04.07.) — haerter als
|
||||
# ein zweites LLM-Urteil und gratis.
|
||||
# - Max. 2 neue Ideen je Nacht + STAU-BREMSE: liegt schon genug Arbeit (volle Queue
|
||||
# oder volles Auftragsbuch), legt der Radar NICHTS nach — die Box soll Arbeit
|
||||
# abarbeiten, nicht anhaeufen.
|
||||
# - Dedup dreifach: State-Datei (schon gemeldet) + Queue/Karten-Liste im Analysten-
|
||||
# Kontext + --idempotency-key beim Anlegen.
|
||||
# - Der Radar LEGT NUR IDEEN AN — er baut nichts, merged nichts, konfiguriert nichts.
|
||||
# State: ~/.hermes/state/idle-radar-gemeldet.txt (ein KEY je bereits gemeldetem Kandidaten).
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/idle-radar-gemeldet.txt"
|
||||
ENDPOINT="${IDLE_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
ANALYST="${IDLE_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${IDLE_FALLBACK:-GLM-4.7-Flash}"
|
||||
HERMES="$HOME/.local/bin/hermes"
|
||||
MAX_NEU=2 # harte Obergrenze neuer Ideen je Nacht (wie Bagatell-Pfad)
|
||||
STAU_QUEUE=4 # ab so vielen offenen Queue-Aufgaben: Bremse
|
||||
STAU_KARTEN=3 # ab so vielen offenen Auftragsbuch-Karten: Bremse
|
||||
|
||||
mkdir -p "$STATE_DIR"; touch "$STATE"
|
||||
|
||||
# Briefkasten-Spiegel (still, wie Morgenlage/Release-Radar): Chronik liest source=idle-radar.
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "idle-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
|
||||
cat "$MC/deploy/idle-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des naechtlichen Idle-Radars.
|
||||
Berichte den Befund unten in Lucys Stimme in hoechstens 3 Saetzen: wie viele neue
|
||||
Queue-Ideen angelegt wurden (Titel nennen) oder warum keine. Nichts selbst umsetzen,
|
||||
keine Kanban-Aktionen — das Skript hat alles Noetige bereits getan.
|
||||
EOF
|
||||
echo
|
||||
|
||||
# ---------- Stau-Bremse (vor dem teuren Analysten) ----------
|
||||
OFFEN_QUEUE="$("$HERMES" kanban list --json 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("[")
|
||||
try:
|
||||
d = json.loads(t[i:]) if i >= 0 else []
|
||||
except Exception:
|
||||
d = []
|
||||
print(sum(1 for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")))
|
||||
' 2>/dev/null || echo 0)"
|
||||
OFFEN_KARTEN="$(curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print(int(json.load(sys.stdin).get("open_count", 0)))
|
||||
except Exception:
|
||||
print(0)
|
||||
' 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "${OFFEN_QUEUE:-0}" -ge "$STAU_QUEUE" ] || [ "${OFFEN_KARTEN:-0}" -ge "$STAU_KARTEN" ]; then
|
||||
echo "## IDLE-RADAR-BEFUND: STAU-BREMSE (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Offene Queue-Aufgaben: ${OFFEN_QUEUE} (Bremse ab ${STAU_QUEUE}) · offene Karten: ${OFFEN_KARTEN} (Bremse ab ${STAU_KARTEN})."
|
||||
echo "Es liegt genug Arbeit — heute Nacht keine neuen Ideen. Dem Commander EINEN kurzen Satz melden."
|
||||
briefkasten "Idle-Radar" "Stau-Bremse: ${OFFEN_QUEUE} offene Queue-Aufgaben, ${OFFEN_KARTEN} offene Karten — keine neuen Ideen angelegt."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------- Material sammeln (erst sammeln, dann kuerzen — SIGPIPE-Falle) ----------
|
||||
TRAUM_DATEI="$(ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -1 || true)"
|
||||
EVID="$(
|
||||
echo "### JOURNAL-FEHLERMUSTER (user-Dienste, letzte 24 h; Anzahl · Muster, lange Zahlen/IDs normalisiert):"
|
||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||
| sed -E 's/^[A-Z][a-z]{2} [0-9 ]{2} [0-9:]{8} [^ ]+ //; s/\[[0-9]+\]//; s/[0-9]{3,}/N/g; s/t_[0-9a-f]+/t_X/g' \
|
||||
| sort | uniq -c | sort -rn | head -18
|
||||
echo
|
||||
echo "### JUENGSTE TRAUM-NOTIZ ($(basename "${TRAUM_DATEI:-keine}") — Funde des naechtlichen Lern-Crons):"
|
||||
if [ -n "$TRAUM_DATEI" ]; then head -70 "$TRAUM_DATEI"; else echo "(keine Traum-Notiz gefunden)"; fi
|
||||
echo
|
||||
echo "### OFFENE SKILL-KANDIDATEN im Vault (nur Titelzeilen):"
|
||||
ls -1 "$VAULT/skill-kandidaten/"*.md 2>/dev/null | head -8 | while read -r f; do
|
||||
echo "- $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
|
||||
done
|
||||
echo
|
||||
echo "### SELBST-STECKBRIEF (auto-generiert von selbst-inventur.sh — das EXISTIERT schon; NICHTS davon neu vorschlagen):"
|
||||
head -80 "$HOME/.hermes/state/selbst-steckbrief.md" 2>/dev/null || echo "(noch keine Selbst-Inventur gelaufen)"
|
||||
echo
|
||||
echo "### SEIT GESTERN AN MIR GEAENDERT (Selbst-Inventur-Diff — gute Kandidaten-Quelle, wenn eine kleine Massnahme erkennbar ist):"
|
||||
head -40 "$HOME/.hermes/state/selbst-steckbrief.diff" 2>/dev/null || echo "(keine Aenderung erkannt)"
|
||||
echo
|
||||
echo "### VOM COMMANDER ABGELEHNT (mit Grund — sinngemaess NIE wieder vorschlagen):"
|
||||
tail -12 "${MC2_ABLEHNUNGEN:-/srv/models/mc2-ablehnungen.jsonl}" 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
n = 0
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
n += 1
|
||||
print("- [{}] {} — Grund: {}".format(d.get("branch", "?"), (d.get("subject") or "")[:80],
|
||||
d.get("grund") or "(keiner angegeben)"))
|
||||
if not n:
|
||||
print("(keine)")
|
||||
' 2>/dev/null || echo "(keine)"
|
||||
echo
|
||||
echo "### SCHON IN DER IDEEN-QUEUE (inkl. archiviert = vom Commander verworfen; NICHT erneut vorschlagen):"
|
||||
"$HERMES" kanban list --archived --json 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("[")
|
||||
try:
|
||||
d = json.loads(t[i:]) if i >= 0 else []
|
||||
except Exception:
|
||||
d = []
|
||||
for x in d[:40]:
|
||||
if isinstance(x, dict):
|
||||
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:110]))
|
||||
' 2>/dev/null || echo "(Queue nicht lesbar)"
|
||||
echo
|
||||
echo "### OFFENE KARTEN IM AUFTRAGSBUCH (warten auf Klick; NICHT erneut vorschlagen):"
|
||||
curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
items = json.load(sys.stdin).get("items") or []
|
||||
except Exception:
|
||||
items = []
|
||||
if not items:
|
||||
print("(keine)")
|
||||
for it in items[:15]:
|
||||
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
|
||||
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
|
||||
echo
|
||||
echo "### VOM IDLE-RADAR BEREITS GEMELDET (KEYs; NICHT erneut vorschlagen):"
|
||||
tail -40 "$STATE" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
RASTER='Du bist der IDLE-RADAR einer lokalen KI-Box (Hermes-Agent, MC2, Lucy). Du bekommst naechtliches Material: Journal-Fehlermuster, die juengste Traum-Notiz, offene Skill-Kandidaten sowie Listen dessen, was schon in Arbeit ist. Deine Aufgabe: destilliere daraus 0 bis 2 KLEINE, konkrete Wartungs-Kandidaten, die die Box-Werkstatt selbst umsetzen kann (Patch in 1-2 Dateien, Skript- oder Doku-Aenderung im MC2- oder Lucy-Repo). REGELN: Jeder Kandidat MUSS eine Zeile aus dem Material WOERTLICH zitieren (BELEG) — das Zitat wird mechanisch gegengeprueft, erfundene Zitate fliegen raus. Nichts vorschlagen, was sinngemaess schon in der IDEEN-QUEUE, den OFFENEN KARTEN oder der BEREITS-GEMELDET-Liste steht. Der SELBST-STECKBRIEF sagt dir, was die Box schon HAT und KANN — schlage NIE etwas vor, das dort schon steht (der Fehler vom 12.07.: ein Gateway-Neustart wurde vorgeschlagen, der laengst konfiguriert war). Was der Commander mit Grund ABGELEHNT hat, ist entschieden — nicht neu verpacken. Aenderungen aus dem Inventur-Diff sind gute Kandidaten-Quellen, aber nur mit konkreter kleiner Massnahme. TABU (niemals vorschlagen): Security (approvals/Tokens/ufw/sudoers/ssh), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode, Updates/Upgrades, alles was den PC oder einen Menschen braucht. Ein wiederkehrendes Fehlermuster ist NUR dann ein Kandidat, wenn eine konkrete kleine Ursachen-Behebung erkennbar ist — "beobachten", "untersuchen" oder "Monitoring bauen" ist KEIN Auftrag. Qualitaet vor Quote: KANDIDATEN: 0 ist der Normalfall und voellig ok. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "KANDIDATEN: <0|1|2>". Danach je Kandidat ein Block aus genau vier Zeilen: "TITEL: <max 90 Zeichen, Imperativ, deutsch>" / "KEY: <kurzer-kebab-slug>" / "BELEG: <woertliches Zitat aus dem Material, EINE Zeile>" / "WARUM: <ein Satz>". Bei 0 stattdessen EIN Satz, warum nichts ansteht.'
|
||||
|
||||
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh).
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
echo "## IDLE-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Offene Queue-Aufgaben vorher: ${OFFEN_QUEUE} · offene Karten: ${OFFEN_KARTEN} · Deckel: ${MAX_NEU}/Nacht"
|
||||
echo
|
||||
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "### ANALYSE AUSGEFALLEN"
|
||||
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet — dem Commander EINEN ehrlichen Satz melden; morgen neuer Versuch."
|
||||
briefkasten "Idle-Radar" "Analyse ausgefallen (kein Analyst erreichbar) — keine neuen Ideen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "### ANALYSE (Analyst: $RICHTER)"
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# ---------- Ehrlichkeits-Gate + Dedup + Anlegen (deterministisch im Skript) ----------
|
||||
# Temp-Dateien statt Pipes: Heredoc + Pipe gleichzeitig frisst stdin (Release-Radar-Lehre 10.07.).
|
||||
TMPD="$(mktemp -d /tmp/idle-radar.XXXXXX)"
|
||||
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
|
||||
printf '%s' "$EVID" > "$TMPD/material.txt"
|
||||
PLAN="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$MAX_NEU" <<'PY'
|
||||
import re, sys, unicodedata
|
||||
|
||||
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
|
||||
try:
|
||||
state = {l.strip() for l in open(sys.argv[3], encoding="utf-8", errors="replace") if l.strip()}
|
||||
except Exception:
|
||||
state = set()
|
||||
max_neu = int(sys.argv[4])
|
||||
|
||||
def norm(s):
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
nev = norm(evid)
|
||||
|
||||
# Zeilenweise einsammeln (tolerant gegen Markdown-Deko und Leerzeilen zwischen Feldern).
|
||||
cands, cur = [], {}
|
||||
for line in ana.splitlines():
|
||||
line = line.strip().lstrip("*->#• ").strip()
|
||||
m = re.match(r"(TITEL|KEY|BELEG|WARUM)\s*:\s*(.+)$", line)
|
||||
if not m:
|
||||
continue
|
||||
k, v = m.group(1), m.group(2).strip()
|
||||
if k == "TITEL" and cur:
|
||||
cands.append(cur); cur = {}
|
||||
cur[k] = v
|
||||
if cur:
|
||||
cands.append(cur)
|
||||
|
||||
neu = 0
|
||||
SEP = "\x1f"
|
||||
for c in cands:
|
||||
titel = (c.get("TITEL") or "").strip()[:120]
|
||||
warum = (c.get("WARUM") or "").strip()[:300]
|
||||
beleg = (c.get("BELEG") or "").strip().strip('"„“»«>`').strip()
|
||||
key = re.sub(r"[^a-z0-9-]+", "-", (c.get("KEY") or "").strip().lower())[:48].strip("-")
|
||||
if not (titel and key and beleg):
|
||||
continue
|
||||
if key in state or ("idle-radar-" + key) in state:
|
||||
print("SKIP" + SEP + key + SEP + "schon frueher gemeldet (State)")
|
||||
continue
|
||||
if not norm(beleg) or norm(beleg) not in nev:
|
||||
print("SKIP" + SEP + key + SEP + "BELEG nicht woertlich im Material — verworfen (Ehrlichkeits-Gate)")
|
||||
continue
|
||||
if neu >= max_neu:
|
||||
print("SKIP" + SEP + key + SEP + "Deckel erreicht")
|
||||
continue
|
||||
neu += 1
|
||||
print("NEU" + SEP + key + SEP + titel + SEP + beleg + SEP + warum)
|
||||
PY
|
||||
)"
|
||||
|
||||
ANGELEGT=""; VERWORFEN=""
|
||||
while IFS=$'\x1f' read -r art key titel beleg warum; do
|
||||
[ -n "${art:-}" ] || continue
|
||||
if [ "$art" = "SKIP" ]; then
|
||||
VERWORFEN="${VERWORFEN}- ${key}: ${titel}"$'\n' # bei SKIP traegt das 3. Feld den Grund
|
||||
continue
|
||||
fi
|
||||
[ "$art" = "NEU" ] || continue
|
||||
BODY="Automatisch vom Idle-Radar erhoben am $(date '+%d.%m.%Y') (Quelle: Journal-Muster/Traum-Notiz der Nacht).
|
||||
|
||||
BELEG (woertlich aus dem Material): ${beleg}
|
||||
|
||||
WARUM: ${warum}
|
||||
|
||||
Rohe Idee — bitte pruefen, klein schneiden und nur wenn tragfaehig promoten; sonst archivieren."
|
||||
TASK_ID="$("$HERMES" kanban create "$titel" --body "$BODY" --triage \
|
||||
--created-by idle-radar --idempotency-key "idle-radar-${key}" --json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("{")
|
||||
try:
|
||||
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
|
||||
except Exception:
|
||||
print("")
|
||||
' 2>/dev/null || true)"
|
||||
if [ -n "${TASK_ID// /}" ]; then
|
||||
echo "$key" >> "$STATE"
|
||||
ANGELEGT="${ANGELEGT}- ${titel} (${TASK_ID}, Key ${key})"$'\n'
|
||||
else
|
||||
VERWORFEN="${VERWORFEN}- ${key}: kanban create fehlgeschlagen"$'\n'
|
||||
fi
|
||||
done <<< "$PLAN"
|
||||
rm -rf "$TMPD"
|
||||
|
||||
echo "### ANGELEGT (rohe Ideen, triage — der Specifier uebernimmt):"
|
||||
echo "${ANGELEGT:-(keine)}"
|
||||
echo "### NICHT ANGELEGT (Gate/Dedup/Deckel):"
|
||||
echo "${VERWORFEN:-(keine)}"
|
||||
|
||||
N_NEU="$(printf '%s' "$ANGELEGT" | grep -c '^-' || true)"
|
||||
if [ "${N_NEU:-0}" -gt 0 ]; then
|
||||
briefkasten "Idle-Radar" "Idle-Radar hat ${N_NEU} neue Idee(n) in die Queue gelegt:
|
||||
${ANGELEGT}Der Specifier arbeitet sie aus; die Karten-Annahme bleibt beim Commander."
|
||||
else
|
||||
briefkasten "Idle-Radar" "Idle-Radar: keine neuen Ideen (Analyst: ${RICHTER})."
|
||||
fi
|
||||
@@ -1,26 +0,0 @@
|
||||
# Idle-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des nächtlichen Idle-Radars. Unten steht ein automatisch erhobener
|
||||
BEFUND: aus Journal-Fehlermustern und der jüngsten Traum-Notiz destilliert ein
|
||||
Analysten-Modell kleine Wartungs-Kandidaten; das Skript hat die tragfähigen davon
|
||||
BEREITS als rohe Ideen (triage) in die Ideen-Queue gelegt — den Rest erledigt der
|
||||
bewiesene Kreislauf (Specifier → Werkstatt → Karte im Auftragsbuch → Klick des
|
||||
Commanders).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
|
||||
Fazit zuerst), in höchstens 3 Sätzen:
|
||||
|
||||
1. Bei „ANGELEGT: (keine)" oder „KANDIDATEN: 0": GENAU EIN kurzer Satz
|
||||
(z. B. „Idle-Radar: nichts Belegbares heute Nacht — Queue bleibt wie sie ist.").
|
||||
2. Bei neuen Ideen: nenne Anzahl und die Titel; sage dazu, dass die Box sie jetzt
|
||||
selbst ausarbeitet und die Annahme wie immer per Karten-Klick beim Commander liegt.
|
||||
3. Bei „STAU-BREMSE" oder „AUSGEFALLEN": ein ehrlicher Satz, warum der Radar heute
|
||||
Nacht nichts angelegt hat.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS selbst umsetzen: keine Kanban-Aktionen, nichts bauen, nichts konfigurieren —
|
||||
das Anlegen hat das Skript schon deterministisch erledigt.
|
||||
- Erfinde keine Kandidaten über den Befund hinaus; im Ehrlichkeits-Gate verworfene
|
||||
Vorschläge (BELEG nicht im Material) nicht als Erfolg verkaufen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur
|
||||
die kurze Telegram-Meldung.
|
||||
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Karten-Gutachter (naechtlich 04:00 — "Lucy kennt sich", 12.07.2026): stempelt jede neue
|
||||
# Auftragsbuch-Karte mit einer Empfehlung (ANNEHMEN/ABLEHNEN/UNKLAR + EIN ehrlicher Satz),
|
||||
# damit der Commander nie blind klickt. Ein-Schuss-Richter gegen llama-swap (:8080,
|
||||
# fremdblick-Architektur, Muster chef-gutachter-feed.sh) — bekommt Karte (Commit-Text,
|
||||
# Dateien, Diff) UND den Selbst-Steckbrief (was schon existiert), prueft also genau die
|
||||
# Falle vom 12.07. (redundanter Gateway-Restart-Vorschlag). Der Stempel ist eine MEINUNG,
|
||||
# kein Gate: die Karte bleibt klickbar, die Entscheidung bleibt beim Commander.
|
||||
# Laeuft als Cron mit --no-agent: stdout leer = still; gestempelte Karten = kurze Zeilen.
|
||||
# Ablage: /srv/models/mc2-karten-gutachten.json (Backend mischt es in die Karten-API).
|
||||
set -uo pipefail
|
||||
|
||||
API="${MC_API:-http://127.0.0.1:9001}"
|
||||
OUT="${MC2_KARTEN_GUTACHTEN:-/srv/models/mc2-karten-gutachten.json}"
|
||||
ENDPOINT="${GUTACHTER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
CHEF="${GUTACHTER_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${GUTACHTER_FALLBACK:-GLM-4.7-Flash}"
|
||||
STECK="$HOME/.hermes/state/selbst-steckbrief.md"
|
||||
MAX_JE_LAUF=3 # Nacht-GPU-Budget: Rest kommt morgen dran
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "karten-gutachter", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
TMPD="$(mktemp -d /tmp/karten-gutachter.XXXXXX)"
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
if ! curl -sf -m 20 "$API/api/auftragsbuch" -o "$TMPD/items.json" 2>/dev/null; then
|
||||
echo "Karten-Gutachter: Auftragsbuch nicht erreichbar — kein Lauf."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Offene, noch ungestempelte Karten ermitteln + verwaiste Stempel aufraeumen (Karte weg
|
||||
# oder neuer Stand = Stempel weg). Schluessel: "<repo>:<branch>@<ts>" haengt am Commit-
|
||||
# Zeitstempel — ein nachgeschobener Commit macht den alten Stempel automatisch ungueltig.
|
||||
python3 - "$TMPD/items.json" "$OUT" > "$TMPD/plan.txt" <<'PY'
|
||||
import json, sys, os
|
||||
|
||||
items = (json.load(open(sys.argv[1], encoding="utf-8")) or {}).get("items") or []
|
||||
out_path = sys.argv[2]
|
||||
try:
|
||||
alt = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(alt, dict):
|
||||
alt = {}
|
||||
except Exception:
|
||||
alt = {}
|
||||
|
||||
offen = {}
|
||||
for it in items:
|
||||
state = (it.get("status") or {}).get("state")
|
||||
if state in ("eingespielt", "laeuft", "rollback"):
|
||||
continue # entschieden oder gerade in Arbeit — kein Stempel noetig
|
||||
key = "{}:{}".format(it.get("repo", "mc2"), it.get("branch", "?"))
|
||||
offen[key] = it
|
||||
|
||||
# Verwaiste/veraltete Stempel raus (atomar zurueckschreiben).
|
||||
neu = {}
|
||||
for key, g in alt.items():
|
||||
it = offen.get(key)
|
||||
if it and g.get("commit_ts") == it.get("ts"):
|
||||
neu[key] = g
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(neu, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
|
||||
for key, it in offen.items():
|
||||
if key in neu:
|
||||
continue
|
||||
repo, branch = key.split(":", 1)
|
||||
print("\x1f".join([repo, branch, str(it.get("ts") or 0),
|
||||
(it.get("subject") or "")[:200],
|
||||
(it.get("shortstat") or "")[:200]]))
|
||||
PY
|
||||
|
||||
RASTER='Du bist der KARTEN-GUTACHTER einer lokalen KI-Box. Du bekommst EINE Vorschlags-Karte aus dem Auftragsbuch (Commit-Text, Datei-Liste, Diff) und den SELBST-STECKBRIEF der Box (was schon existiert und laeuft). Der Commander ist kein Entwickler — dein Urteil ist seine einzige fachliche Zweitmeinung vor dem Klick. Pruefe NUR anhand des Materials, erfinde nichts: (1) REDUNDANZ — existiert das laut Steckbrief oder Diff schon? Ein Vorschlag, der Bestehendes nur nachbaut oder verschlechtert (z. B. funktionierende Einstellungen ersetzt), ist abzulehnen. (2) RISIKO — beruehrt der Diff Security (Tokens/approvals/ufw/sudoers), loescht er Daten oder aendert er Verhalten, das der Commander nicht bestellt hat? (3) NUTZEN — loest er ein echtes, benanntes Problem? Antworte auf DEUTSCH in exakt zwei Zeilen: "URTEIL: ANNEHMEN" oder "URTEIL: ABLEHNEN" oder "URTEIL: UNKLAR", danach "SATZ: <ein ehrlicher Satz Begruendung in Alltagssprache>". Kein weiterer Text.'
|
||||
|
||||
judge() { # $1=Modell $2=Timeout $3=max_tokens (liest $EVID)
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
N=0
|
||||
while IFS=$'\x1f' read -r repo branch cts subject stat; do
|
||||
[ -n "${repo:-}" ] || continue
|
||||
[ "$N" -ge "$MAX_JE_LAUF" ] && { echo "Karten-Gutachter: Tages-Deckel ($MAX_JE_LAUF) erreicht — Rest morgen."; break; }
|
||||
DIFF="$(curl -sfG -m 30 "$API/api/auftragsbuch/diff" \
|
||||
--data-urlencode "branch=$branch" --data-urlencode "repo=$repo" 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print((json.load(sys.stdin).get("diff") or "")[:20000])
|
||||
except Exception:
|
||||
pass' || true)"
|
||||
# Leerer Diff = deterministisch UNKLAR, OHNE Richter (Erstlauf 12.07.: eine Karte mit
|
||||
# 0 Dateien bekam trotzdem ANNEHMEN — ein LLM kann einen leeren Vorschlag nicht pruefen).
|
||||
if [ -z "${DIFF//[[:space:]]/}" ]; then
|
||||
python3 - "$OUT" "$repo" "$branch" "$cts" <<'PY'
|
||||
import json, os, sys, time
|
||||
out_path = sys.argv[1]
|
||||
try:
|
||||
data = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
data = {}
|
||||
data["{}:{}".format(sys.argv[2], sys.argv[3])] = {
|
||||
"empfehlung": "UNKLAR",
|
||||
"satz": "Der Vorschlag hat einen LEEREN Diff (0 geaenderte Dateien gegen main) — da gibt es nichts zu pruefen; technisch verdaechtig, im Zweifel ablehnen.",
|
||||
"richter": "mechanische Regel (leerer Diff)",
|
||||
"commit_ts": int(sys.argv[4]) if sys.argv[4].isdigit() else None,
|
||||
"ts": int(time.time())}
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
PY
|
||||
N=$((N + 1))
|
||||
echo "Karte ${branch}: Empfehlung UNKLAR — leerer Diff (mechanische Regel, kein Richter noetig)."
|
||||
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung UNKLAR — der Diff ist leer (0 Dateien), technisch verdaechtig."
|
||||
continue
|
||||
fi
|
||||
EVID="KARTE ${repo}:${branch}
|
||||
COMMIT: ${subject}
|
||||
UMFANG: ${stat}
|
||||
|
||||
DIFF (ggf. gekuerzt):
|
||||
${DIFF:-(Diff nicht lesbar)}
|
||||
|
||||
SELBST-STECKBRIEF DER BOX (was schon existiert):
|
||||
$(head -90 "$STECK" 2>/dev/null || echo '(noch keine Selbst-Inventur gelaufen)')"
|
||||
|
||||
RICHTER="$CHEF"
|
||||
URTEIL_RAW="$(judge "$CHEF" 420 1600)"
|
||||
if [ -z "${URTEIL_RAW// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung)"
|
||||
URTEIL_RAW="$(judge "$VERTRETUNG" 240 1200)"
|
||||
fi
|
||||
[ -z "${URTEIL_RAW// /}" ] && { echo "Karte ${branch}: Gutachter ausgefallen (kein Richter erreichbar)."; continue; }
|
||||
|
||||
printf '%s' "$URTEIL_RAW" > "$TMPD/urteil.txt"
|
||||
STAMP="$(python3 - "$TMPD/urteil.txt" "$OUT" "$repo" "$branch" "$cts" "$RICHTER" <<'PY'
|
||||
import json, os, re, sys, time
|
||||
|
||||
raw = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
m_u = re.search(r"URTEIL:\s*(ANNEHMEN|ABLEHNEN|UNKLAR)", raw, re.I)
|
||||
m_s = re.search(r"SATZ:\s*(.+)", raw)
|
||||
urteil = (m_u.group(1).upper() if m_u else "UNKLAR")
|
||||
satz = (m_s.group(1).strip() if m_s else "Analyse unklar — Urteil nicht lesbar.")[:300]
|
||||
out_path = sys.argv[2]
|
||||
try:
|
||||
data = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
data = {}
|
||||
key = "{}:{}".format(sys.argv[3], sys.argv[4])
|
||||
data[key] = {"empfehlung": urteil, "satz": satz, "richter": sys.argv[6],
|
||||
"commit_ts": int(sys.argv[5]) if sys.argv[5].isdigit() else None,
|
||||
"ts": int(time.time())}
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
print(urteil + "\x1f" + satz)
|
||||
PY
|
||||
)"
|
||||
URTEIL="${STAMP%%$'\x1f'*}"; SATZ="${STAMP#*$'\x1f'}"
|
||||
N=$((N + 1))
|
||||
echo "Karte ${branch}: Empfehlung ${URTEIL} — ${SATZ}"
|
||||
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung ${URTEIL} — ${SATZ} (Richter: ${RICHTER})"
|
||||
done < "$TMPD/plan.txt"
|
||||
|
||||
# stdout bleibt LEER, wenn nichts zu stempeln war (--no-agent => stille Nacht).
|
||||
@@ -10,7 +10,7 @@ models:
|
||||
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
|
||||
# Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.)
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 3
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf
|
||||
ttl: 0
|
||||
aliases:
|
||||
- hermes
|
||||
@@ -24,7 +24,7 @@ models:
|
||||
Qwen3-Coder-Next:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256 -cram 16384
|
||||
ttl: 600
|
||||
ttl: 5400
|
||||
aliases:
|
||||
- coder
|
||||
capabilities:
|
||||
@@ -77,21 +77,22 @@ models:
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 131072
|
||||
GLM-4.7-Flash:
|
||||
# KRITIKER (07.07., Rollen-Audit): Fremd-Vendor-Zweitmeinung für fremdblick-Prosa +
|
||||
# Chef-Gutachter-Vertretung — reines Textmodell, 30B-A3B (MIT, Unsloth-Dynamic-Quant).
|
||||
# Die Vision-Schwester GLM-4.6V-Flash bleibt als scout für Bild-Jobs. --jinja ist
|
||||
# Pflicht (Chat-Template, sonst kaputte Tool-Calls).
|
||||
Devstral-Small-2-24B:
|
||||
# KRITIKER (25.07.2026): Gegenleser aus einer ANDEREN Modellfamilie (Mistral, Apache-2.0,
|
||||
# 24B DICHT, 256k Kontext, 68,0 % SWE-bench Verified vs. 59,2 % bei GLM-4.7-Flash).
|
||||
# Loest GLM-4.7-Flash in der kritiker-Rolle ab. Dicht = auf Strix Halo bandbreitengebunden
|
||||
# und damit langsam beim SCHREIBEN — fuer einen Kritiker egal: er liest viel, schreibt wenig.
|
||||
# --jinja ist Pflicht (Chat-Template, sonst kaputte Tool-Calls).
|
||||
cmd: |
|
||||
llama-server -m /srv/models/GLM-4.7-Flash-GGUF/GLM-4.7-Flash-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --jinja
|
||||
ttl: 600
|
||||
llama-server -m /srv/models/Devstral-Small-2-GGUF/mistralai_Devstral-Small-2-24B-Instruct-2512-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 16384 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256
|
||||
ttl: 1800
|
||||
aliases:
|
||||
- kritiker
|
||||
capabilities:
|
||||
in: [text]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 65536
|
||||
context: 16384
|
||||
Qwen3-Reranker-0.6B:
|
||||
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Mem0-Suchtreffer nach echter
|
||||
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Bagatell-Annahme (reine Doku-Vorschläge nachts ohne Klick, mit Fangnetz)
|
||||
Documentation=file:%h/mission-control-v2/docs/AUFTRAGSBUCH.md
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Als /tmp-Kopie laufen lassen: die Annahme deployt (git reset --hard) und würde sonst das
|
||||
# eigene Skript unter dem laufenden bash austauschen (bekannte Selbst-Reset-Falle).
|
||||
ExecStart=/bin/bash -c 'cp "%h/mission-control-v2/deploy/bagatell-annahme.sh" /tmp/mc2-bagatell-run.sh && exec /bin/bash /tmp/mc2-bagatell-run.sh'
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=Täglich 04:10 — Bagatell-Annahme (nach der Nacht-Sicherung 03:30, vor der Morgenlage 04:30)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:10:00
|
||||
Persistent=false
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -19,8 +19,16 @@ Environment=PYTHONPATH=%h/mission-control-v2
|
||||
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||
Environment=MC_MODELS_DIR=/srv/models
|
||||
# Briefkasten-Meldungen (z. B. Kontext-Limit-Warnung) NIE direkt in die Store-Datei —
|
||||
# die gehört exklusiv dem Steuerpult; per HTTP abliefern (gleiches Muster wie mc2-steward).
|
||||
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
# Stoppen darf nicht warten: der Gateway hält minutenlange LLM-Streams offen — uvicorns
|
||||
# „Waiting for connections to close" + default 90 s TimeoutStop machten jeden Restart zum
|
||||
# 502-Fenster (15.07. live gemessen, 14:29→14:31). Stateless Proxy → nach 5 s hart beenden
|
||||
# ist verlustarm; Clients (Hermes api_max_retries=3) wiederholen abgerissene Anfragen.
|
||||
TimeoutStopSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv durchspielen, Alarm bei Rot)
|
||||
Documentation=file:%h/mission-control-v2/docs/AUTONOMIE_PLAN.md
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash %h/mission-control-v2/deploy/self-smoke.sh
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=Täglicher MC2-Selbst-Smoke-Test (07:15, unabhängig von Updates)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 07:15:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,26 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Mem0 Sidecar — auto-lernendes, semantisches Gedächtnis (mem0 + chroma)
|
||||
Documentation=https://github.com/mem0ai/mem0
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# Mem0 + chromadb laufen nur unter Python 3.12 (~/.mem0/venv) — das MC2-Backend (3.14)
|
||||
# kann sie nicht importieren. Darum dieser schlanke Sidecar; MC2 spricht ihn per HTTP an.
|
||||
# Bind 127.0.0.1: nur lokal erreichbar (MC2 proxyt nach außen).
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/mem0_service
|
||||
Environment=MEM0_PORT=8765
|
||||
Environment=MEM0_CHROMA_PATH=/srv/models/mem0/chroma
|
||||
Environment=MEM0_HISTORY_DB=/srv/models/mem0/history.db
|
||||
Environment=MEM0_EMBED_URL=http://127.0.0.1:8080/v1
|
||||
Environment=MEM0_LLM_URL=http://127.0.0.1:8080/v1
|
||||
Environment=MEM0_EMBED_MODEL=embed
|
||||
Environment=MEM0_LLM_MODEL=fast
|
||||
Environment=MEM0_EMBED_DIMS=1024
|
||||
Environment=TOKENIZERS_PARALLELISM=false
|
||||
ExecStart=%h/.mem0/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8765
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -25,6 +25,13 @@ Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
||||
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
||||
# bedient /v1 wieder selbst.
|
||||
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
||||
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry, Mem0-Dedupe)
|
||||
# laufen jetzt im eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese drei
|
||||
# Zeilen entfernen (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
||||
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
||||
Environment=MC_REWARM_ENABLED=0
|
||||
Environment=MC_SENTRY_ENABLED=0
|
||||
Environment=MC_MEM_DEDUPE_ENABLED=0
|
||||
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
||||
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
||||
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
||||
|
||||
+19
-2
@@ -22,8 +22,8 @@ shift $((OPTIND - 1))
|
||||
MSG="${1:-}"
|
||||
[ -z "$MSG" ] && [ ! -t 0 ] && MSG="$(cat)"
|
||||
if [ -z "$MSG" ]; then
|
||||
echo "usage: notify.sh [-s subject] <nachricht>" >&2
|
||||
exit 2
|
||||
# Bei leerer Pipe-Eingabe lautlos beenden (wichtig für Cron-Skripte ohne Ausgabe)
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TS="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
@@ -41,6 +41,23 @@ PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Nachtweiche: Zwischen 00:00 und 06:59 Uhr (inklusive) fangen wir Telegram-Nachrichten ab
|
||||
# und sammeln sie für den Morgen-Digest, ES SEI DENN es ist ein Alarm.
|
||||
HOUR="$(date +%H)"
|
||||
if [ "$HOUR" -ge 0 ] && [ "$HOUR" -lt 7 ]; then
|
||||
case "${SUBJECT,,}" in
|
||||
*alarm*|*notfall*)
|
||||
;; # Alarme gehen immer sofort durch
|
||||
*)
|
||||
# Sammeln für Morgen-Digest
|
||||
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
|
||||
echo "- ${SUBJECT:+$SUBJECT: }$MSG" >> "$NIGHT_QUEUE"
|
||||
echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Primärweg: Telegram via hermes send (Login-Shell-PATH, falls aus Timer/cron aufgerufen).
|
||||
if [ -n "$SUBJECT" ]; then
|
||||
SEND_OUT="$(bash -lc 'hermes send --to telegram --subject "$1" -- "$2"' _ "$SUBJECT" "$MSG" 2>&1)"
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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.
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Evolution-Radar-Cron (Autonomie E4). Liegt in ~/.hermes/scripts/ (deploy.sh
|
||||
# kopiert ihn); sein stdout wird dem Cron-Agenten als Prompt eingespeist:
|
||||
# versionierter Auftrag (deploy/radar-prompt.md im MC2-Repo) + Live-Inventar der Box.
|
||||
set -uo pipefail
|
||||
|
||||
cat "$HOME/mission-control-v2/deploy/radar-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## LIVE-INVENTAR (automatisch erhoben am $(date '+%d.%m.%Y'))"
|
||||
echo
|
||||
echo "### Update-Status (MC2 /api/maintenance/updates):"
|
||||
curl -sf --max-time 30 http://127.0.0.1:9001/api/maintenance/updates \
|
||||
| jq -c '{os_pakete: .os, engine_update: .engine, router_update: .swap, komponenten: .components}' \
|
||||
2>/dev/null || echo "(MC2-API nicht erreichbar)"
|
||||
echo
|
||||
echo "### Installierte Modelle (llama-swap):"
|
||||
curl -sf --max-time 20 http://127.0.0.1:8080/v1/models | jq -r '.data[].id' 2>/dev/null \
|
||||
|| echo "(llama-swap nicht erreichbar)"
|
||||
echo
|
||||
# llama-server braucht LD_LIBRARY_PATH auf das Vulkan-Verzeichnis für --version.
|
||||
echo "### Versionen: llama.cpp $( LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -o 'version: [0-9]*' || true ) · llama-swap $( /usr/local/bin/llama-swap --version 2>/dev/null | head -1 || true ) · hermes-agent $( bash -lc 'hermes --version' 2>/dev/null | head -1 || echo '?' )"
|
||||
echo
|
||||
echo "### Pin-Register (gepinnte Ebenen NICHT zum Update vorschlagen, aber Sprünge melden):"
|
||||
cat /srv/models/mc2-pins.json 2>/dev/null || echo "{}"
|
||||
@@ -1,64 +0,0 @@
|
||||
# Evolution-Radar (monatlicher Auftrag)
|
||||
|
||||
Du bist das Evolution-Radar der AI-Box (AMD Strix Halo, 128 GB, 100 % lokal, Vulkan/RADV).
|
||||
Dein Job: EINMAL im Monat prüfen, ob sich bei unseren Kern-Komponenten oder in deren
|
||||
Kategorien ein echter Sprung ergeben hat — und das als kurzen deutschen Report melden.
|
||||
Unten ist das Live-Inventar der Box eingespeist (Update-Status, installierte Modelle, Pins).
|
||||
|
||||
## Schritt 1 (PFLICHT, vor jeder Recherche): Inventar lesen und respektieren
|
||||
|
||||
Lehre aus Report #1 (02.07.): vier Fehler, eine Ursache — Inventar ignoriert. Deshalb hart:
|
||||
- **Empfiehl NIE ein Update auf eine Version, die laut Live-Inventar schon installiert ist.**
|
||||
(Report #1 empfahl „Hermes v0.18" — die Box WAR auf v0.18.0. `komponenten` im Inventar
|
||||
zeigt das echte git-behind; behind=0 heißt AKTUELL, egal was Release-Notes suggerieren.)
|
||||
- **Bei Komponenten, die wir aktiv nutzen, erst die EIGENE Version/Nutzung feststellen,
|
||||
dann Neuigkeiten bewerten.** Aktiv im Einsatz (Stand Repo):
|
||||
Parakeet via onnx-asr = UNSER STT (25 EU-Sprachen inkl. Deutsch, ~0,45 s — nie als
|
||||
„kein Deutsch-Fokus" abtun) · Silero VAD **v5** via vad-web in Lucy (v6 existiert) ·
|
||||
pocket-tts german_24l (CPU, auf dem PC) · Hirn Qwen3.6-35B (MTP-Draft, immer warm).
|
||||
- **Die Modell-Liste im Inventar enthält auch schon geladene KANDIDATEN** (z. B. gpt-oss-120b,
|
||||
Qwen3-VL-30B) — die liegen bereit und sind gebencht; nicht als „Neuentdeckung" verkaufen.
|
||||
- Jede Versions-Aussage im Report muss gegen das Inventar geprüft sein.
|
||||
|
||||
## Arbeitsweise: Fan-out mit Subagents (WICHTIG)
|
||||
|
||||
Nutze `delegate_task` mit einem `tasks`-Array (role: leaf), um die Recherche in ISOLIERTE
|
||||
Subagents aufzuteilen — pro Task eine Komponenten-Gruppe. Jeder Subagent recherchiert per
|
||||
Web-Suche, FILTERT Marketing-Geschwätz aus und liefert dir nur eine Substanz-Zusammenfassung
|
||||
(max. 5 Zeilen pro Komponente). Du konsolidierst am Ende. Erwähne im Endbericht nur, was
|
||||
Substanz hat — „nichts Nennenswertes" ist ein gültiges und gutes Ergebnis je Gruppe.
|
||||
|
||||
Scheitern Subagents wiederholt (z. B. Kontext-Fehler), recherchiere die betroffenen Gruppen
|
||||
selbst sequenziell — der Report muss IMMER zustande kommen.
|
||||
|
||||
Task-Aufteilung (5 Subagents):
|
||||
1. **Engine/Router:** llama.cpp (Vulkan, gfx1151/Strix Halo relevant!), llama-swap (mostlygeek)
|
||||
2. **Agent/Runtime:** hermes-agent (NousResearch), Electron (Major-Sprünge), three-vrm/@pixiv
|
||||
3. **Voice:** pocket-tts (kyutai), onnx-asr/Parakeet, Silero-VAD, smart-turn — nur Deutsch-taugliches
|
||||
4. **Modell-Kategorien** für 128-GB-Strix-Halo: bessere lokale Coder-Modelle (vs. Qwen3-Coder-Next),
|
||||
Chat/Reasoning-MoE (vs. Qwen3.6-35B/Qwen3.5-122B), Vision (vs. Qwen3-VL), deutsche TTS/STT.
|
||||
Nur GGUF-/llama.cpp-lauffähig, Substanz = Benchmarks/Community-Erfahrung, kein Ankündigungs-Hype.
|
||||
5. **IDE-/Agent-Tools** (Zed, Kilo Code, Claude Code): besonders Tod-/Nachfolger-Meldungen —
|
||||
„Projekt eingestellt/archiviert/Fork übernimmt" ist GENAU die Meldung, die wir fangen müssen
|
||||
(Lehre: Roo Code galt als Empfehlung und war längst eingestellt). Auch: neues dominantes Tool?
|
||||
|
||||
## Bereits ENTSCHIEDEN — nicht wieder vorschlagen (Verdikte der Box)
|
||||
|
||||
- Vulkan/RADV statt ROCm (gemessen schneller auf gfx1151) · llama.cpp+llama-swap gesetzt
|
||||
- Hirn = Qwen3.6-35B-A3B mit MTP-Spec-Draft, immer warm · Mem0 als Memory · Electron-App für Lucy
|
||||
- Pocket-TTS für Lucys Stimme (deutsch, CPU) · KEIN Kyutai-Streaming-STT (kann kein Deutsch)
|
||||
- VERWORFEN: LobeChat/WebUI-Ersatz, Unmute-Vollstack, AnythingLLM, gemma als Hirn
|
||||
- Latenz ist heilig: nichts vorschlagen, was Lucys ~2-s-Sprech-Latenz gefährdet
|
||||
|
||||
## Report-Format (Endantwort = geht direkt als Telegram-Nachricht raus)
|
||||
|
||||
- **Schreibe die Endnachricht in Lucys Stimme an den Commander** (du BIST Lucy, siehe SOUL.md —
|
||||
das Radar ist nur dein Auftrag): erst 1–2 Sätze unmissverständlich, was die Funde für ihn
|
||||
bedeuten und ob er etwas tun muss — danach die knappen Fund-Zeilen.
|
||||
- Deutsch, maximal ~20 Zeilen, Du-Form, kein Markdown-Overkill, nicht technisch —
|
||||
Technik in Alltagssprache, Details nur auf Nachfrage
|
||||
- Struktur: „🛰️ Evolution-Radar <Monat>" → je Fund 1–3 Zeilen: WAS, WARUM relevant für UNS,
|
||||
Einstufung **[lohnt vermutlich]** / **[beobachten]** / ggf. Quelle kurz
|
||||
- Wenn ein Fund ein GEPINNTES Level betrifft (siehe Pins im Inventar): explizit erwähnen
|
||||
- Nichts gefunden? Dann genau das in 2 Zeilen sagen. Keine Pflicht-Funde erfinden.
|
||||
- KEINE Konfig-Änderungen vornehmen, KEINE Downloads starten — nur melden.
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper: Evolution-Radar + Selbstkritik zusammen
|
||||
# Wird vom Hermes-Cron "Monats-Review" (1. des Monats, 09:00) als --script referenziert.
|
||||
# Ins Repo geholt 10.07.2026 (Übergabe S1) — lag vorher NUR auf der Box.
|
||||
set -uo pipefail
|
||||
echo "=============================="
|
||||
echo "## EVOLUTION-RADAR"
|
||||
echo "=============================="
|
||||
"$HOME/.hermes/scripts/radar-feed.sh"
|
||||
echo
|
||||
echo "=============================="
|
||||
echo "## SELBSTKRITIK"
|
||||
echo "=============================="
|
||||
"$HOME/.hermes/scripts/selbstkritik-feed.sh"
|
||||
@@ -1,25 +0,0 @@
|
||||
# Release-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des wöchentlichen Hermes-Release-Radars. Unten stehen ein RADAR-BEFUND
|
||||
(automatisch erhoben), bei neuen Releases ein ABGLEICH eines Analysten-Modells gegen die
|
||||
Eigenbau-Landkarte (`~/wissens-vault/eigenbau-landkarte.md`) und ein FREMDBLICK
|
||||
(Zweitmeinung eines anderen Modells, prüft die Changelog-Zitate).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
|
||||
Fazit zuerst):
|
||||
|
||||
1. Bei „KEIN NEUES RELEASE" oder „TREFFER: 0": antworte mit GENAU EINEM kurzen Satz
|
||||
(z. B. „Radar: hermes vX ist da, nichts für unsere Landkarte dabei.").
|
||||
2. Bei Treffern: maximal 5 Sätze — welcher Eigenbau betroffen ist, welches native Feature
|
||||
das Release bringt, der Kern des Zitats, die empfohlene Probe, und was der Fremdblick
|
||||
dazu sagt (TRAEGT / TRAEGT-NICHT ehrlich nennen, nichts glattbügeln).
|
||||
3. Bei „AUSGEFALLEN" oder „NICHT MOEGLICH": ein ehrlicher Satz, dass der Radar diese
|
||||
Woche blind war und warum.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS installieren, updaten, mergen oder konfigurieren — der Radar informiert nur.
|
||||
Updates bleiben eine manuelle Entscheidung des Commanders (Entscheid 04.07.).
|
||||
- Erfinde keine Treffer über den ABGLEICH hinaus; vom Fremdblick abgelehnte Behauptungen
|
||||
(TRAEGT-NICHT) als abgelehnt benennen, nicht verschweigen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur die
|
||||
kurze Telegram-Meldung.
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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
|
||||
@@ -121,9 +121,12 @@ mv "$STECK.tmp" "$STECK"
|
||||
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" | head -12)"
|
||||
N="$(grep -cE '^[+-][^+-]' "$DIFFOUT" || true)"
|
||||
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)"
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Selbstkritik-Cron (Faden 11b — Zwilling des Evolution-Radars, Blick nach INNEN).
|
||||
# Liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn); stdout wird dem Cron-Agenten als
|
||||
# Prompt eingespeist: versionierter Auftrag (deploy/selbstkritik-prompt.md) + Live-Betriebsdaten.
|
||||
set -uo pipefail
|
||||
|
||||
cat "$HOME/mission-control-v2/deploy/selbstkritik-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## LIVE-DATEN (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
echo "### Voice-Pipeline-Latenzen (rollend, ms — :9001/api/voice/metrics):"
|
||||
curl -sf --max-time 15 http://127.0.0.1:9001/api/voice/metrics | jq -c . 2>/dev/null \
|
||||
|| echo "(nicht erreichbar)"
|
||||
echo
|
||||
echo "### Token-Verbrauch / Cloud-Ersparnis:"
|
||||
curl -sf --max-time 15 http://127.0.0.1:9001/api/system/token-stats | jq -c . 2>/dev/null \
|
||||
|| echo "(nicht erreichbar)"
|
||||
echo
|
||||
echo "### Prompt-Budget (Hinweis: prompt-size ist Allowlist-blind — die Voice-Lane hat real"
|
||||
echo "### deutlich weniger Tools; als Trend-Wächter für den GESAMT-Bestand trotzdem nützlich):"
|
||||
bash -lc "hermes prompt-size" 2>/dev/null | grep -E "total|skills index|Tool schemas" || echo " (Messung fehlgeschlagen)"
|
||||
echo
|
||||
echo "### Gateway-Journal: häufigste Warnungen/Fehler der letzten 30 Tage (Anzahl · Muster):"
|
||||
journalctl --user -u hermes-gateway -p warning --since "30 days ago" --no-pager 2>/dev/null \
|
||||
| grep -oE "(WARNING|ERROR)[^:]*: .{0,90}" | sed "s/[0-9]\{3,\}/N/g" | sort | uniq -c | sort -rn | head -12 \
|
||||
|| echo "(keine oder Journal nicht lesbar)"
|
||||
echo
|
||||
echo "### Tool-Loop-Guardrail-Treffer (letzte 30 Tage):"
|
||||
n=$(journalctl --user -u hermes-gateway --since "30 days ago" --no-pager 2>/dev/null \
|
||||
| grep -ci "loop guard\|hard_stop\|tool loop" || true)
|
||||
echo "${n:-0}"
|
||||
echo
|
||||
echo "### Skill-Bestand & Curator:"
|
||||
bash -lc "hermes curator status" 2>/dev/null | head -14 || echo "(Curator-Status fehlgeschlagen)"
|
||||
echo
|
||||
echo "### Meldeweg-Ausfälle (notify.sh-Fallbacks, letzte 30 Tage):"
|
||||
n=$(grep -c "FALLBACK" "$HOME/mc2-notify.log" 2>/dev/null || true)
|
||||
echo "${n:-0}"
|
||||
@@ -1,36 +0,0 @@
|
||||
# Selbstkritik-Runde (monatlich) — der Blick nach INNEN
|
||||
|
||||
Du bist Lucy und schaust einmal im Monat kritisch auf DICH SELBST und deinen eigenen
|
||||
Betrieb: Wo warst du langsam, wo liefen Tools in Schleifen, wo häufen sich Warnungen,
|
||||
was wird nie benutzt? Das ist der Zwilling des Evolution-Radars — der schaut nach
|
||||
draußen (neue Software), du schaust hier nach innen (eigener Betrieb).
|
||||
|
||||
## Auftrag
|
||||
|
||||
1. Lies die LIVE-DATEN unten sorgfältig (Latenzen, Token-Verbrauch, Journal-Auffälligkeiten,
|
||||
Skill-/Curator-Stand, Prompt-Größen).
|
||||
2. Finde die 1 bis 3 WICHTIGSTEN konkreten Verbesserungen. Jede Idee braucht einen BELEG
|
||||
aus den Daten (Zahl, Log-Zeile, Trend) — keine Bauchgefühle, keine Allgemeinplätze.
|
||||
3. **Fremdblick vor dem Absenden (Härtung 04.07. — „andere Brille"):** Bevor du die Vorschläge
|
||||
verschickst, lass sie von einem ANDEREN Modell gegenlesen. Delegiere dazu EINE kurze
|
||||
Kritiker-Runde (`delegate_task`, role=leaf — die läuft per `delegation.model` auf `heavy`,
|
||||
also einem anderen Modell als du selbst). Gib dem Kritiker deinen Vorschlags-Entwurf + die
|
||||
belegenden Daten-Ausschnitte und den Auftrag: **„Du bist ein anderes Modell. Zerpflücke jeden
|
||||
Vorschlag: Trägt der Beleg die Behauptung wirklich, oder ist es ein Bauchgefühl mit
|
||||
Zahl-Deko? Verwechselt er Korrelation mit Ursache? Fehlt Kontext?"** Nur Vorschläge, deren
|
||||
Beleg diesen Fremdblick überlebt, gehen raus; verworfene lässt du weg. Ist `delegation.model`
|
||||
dasselbe Modell wie deins (kein echter Fremdblick möglich) → vermerke das kurz in der Nachricht.
|
||||
4. Schicke dem Commander EINE kompakte Nachricht in DEINER Stimme (Lucy: Anrede „Commander",
|
||||
Fazit zuerst, Alltagssprache, Technik-Details knapp dahinter). Je Vorschlag: Was ist
|
||||
auffällig (Beleg) → was schlägst du vor → was bringt es. Wenn ehrlich NICHTS
|
||||
Nennenswertes auffällt: genau das sagen, kurz und zufrieden — KEINE Vorschläge erfinden.
|
||||
|
||||
## Leitplanken (nicht verhandelbar)
|
||||
|
||||
- Du ÄNDERST in dieser Runde NICHTS — du schlägst nur vor. Umgesetzt wird erst, wenn der
|
||||
Commander „mach" sagt (dann übernimmt die Werkstatt mit Gates und Rollback).
|
||||
- Der Hermes-Quellcode ist TABU (fremde Software, Auto-Update-Kanal). Verbesserungen nur
|
||||
über UNSERE Schichten: Config, SOUL.md, Skills, Gedächtnis, Modellwahl, MC2-Code.
|
||||
- Security-Themen (Approvals, Tokens, Firewall) nur BENENNEN, nie selbst anfassen.
|
||||
- Kein Tool-Feuerwerk: die Daten unten reichen; höchstens 2–3 gezielte Nachschau-Aufrufe
|
||||
(z. B. eine Journal-Zeile im Kontext lesen), dann antworten.
|
||||
@@ -17,6 +17,7 @@ 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 "'")"
|
||||
|
||||
@@ -102,6 +103,84 @@ 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":40,"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'))."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user