Auftragsbuch ausgebaut: Karten-Annahme, Bagatell-Annahme, Lucy-Annahme raus — Gate ist Ampel + User-Merge
Ampel / ampel (push) Failing after 24s

Die Vorschlags-Inbox (routers/services auftragsbuch, AuftragsbuchView, /auftraege, Cockpit-
Kachel + Handlungsbedarf-Eintrag, deploy/auftrag-annehmen.sh, lucy-annahme.sh, bagatell-
annahme.sh, docs/AUFTRAGSBUCH.md) war seit 02.08.2026 ungenutzt: Kanban-Tools seit 21.08. aus,
v3-Umbau lief ueber Branch -> Ampel -> Merge -> deploy.sh, PC-Executor-IP in Box und Config
veraltet. Doku behauptete den Karten-Weg trotzdem als Standard.

Jetzt:
- Cockpit: Kachel "Ideen" (offen/haengend) statt "Auftragsbuch"; Handlungsbedarf zeigt haengende
  Ideen und springt in die Ideen-Ansicht.
- Guide/Schaubild: Kreislauf Idee -> Ideen-Queue -> IDE am PC -> Ampel -> DEIN Merge -> Live.
- events.py: kein Auftragsbuch-Fingerprint mehr; config.py: PC_EXECUTOR_URL-Default auf .22.
- Skills/Skripte: selbst-inventur ohne Karten-Abschnitt, morning-report nennt offene Branches,
  konzept-fliessband verweist auf die Ideen-Ansicht.
- Doku: STACK "Deploy & Pipeline" = Branch -> Ampel -> User-Merge -> deploy.sh (einziger Weg),
  GRENZEN/ARBEITSWEISE/FALLEN/README/BEDIENUNG/RUNBOOK/GEMINI_BRIEFING/gitea-workflow angepasst,
  ZIELBILD Punkt 9, OFFENE-FAEDEN + RAPHAEL.md: erledigt.
- Bewusst geblieben: werkstatt-SOUL/projektstart-SOUL (Werkstatt-Persona, eigener Faden),
  Chronik-Kategorie "auftragsbuch" fuer historische Eintraege.
Auf der Box bereits erledigt (Hand): mc2-bagatell.timer deaktiviert + Units archiviert,
PC-Executor-URL in ~/.hermes/config.yaml auf .22.

Baut auf wartung/lucy-stimme-proxy-raphael auf (Proxy /api/lucy/stimme/*).
Gates: eslint 0 Fehler, vitest 59/59, tsc + vite build gruen, frontend/dist committet,
py_compile gruen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-04 16:25:05 +02:00
co-authored by Claude Fable 5.1
parent df8d088fac
commit 479fecd2c6
72 changed files with 569 additions and 2323 deletions
+4 -5
View File
@@ -11,9 +11,9 @@ prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
| Bahn | Was sie tut | Wo |
|---|---|---|
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue | 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` |
| **Lucy** | Innere Stimme am PC (HUD, kein Avatar) und auf Telegram — eine Stimme, ein Hirn | eigenes Repo `Hitonabi/lucy` |
Lucy holt ihr Hirn direkt über das MC2-Gateway (`:9010/v1`) mit nativer Bildweiche und Voice-Streaming.
@@ -30,7 +30,7 @@ Lucy holt ihr Hirn direkt über das MC2-Gateway (`:9010/v1`) mit nativer Bildwei
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.
Auto-Update und Self-Smoke. `llama-swap` läuft als System-Unit.
## Die Bau-Pipeline
@@ -98,7 +98,7 @@ Kontext-Komprimierung löscht still die Regeln mit.
## API (Auswahl)
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
memory/* · agent/* · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
memory/* · agent/* · ideen/* · lucy/stimme/* · chronik · eigenleben · wissen ·
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
@@ -142,7 +142,6 @@ Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `n
| [`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 |
-2
View File
@@ -21,7 +21,6 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from routers import (
agent,
auftragsbuch,
chronik,
connect,
console,
@@ -126,7 +125,6 @@ if V1_UPSTREAM:
else:
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
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(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
+142 -142
View File
@@ -1,142 +1,142 @@
"""
Zentrale Konfiguration für Mission Control 2.0.
Eine Quelle der Wahrheit für Pfade, URLs und Defaults — alles über Env-Vars
überschreibbar. Bewusst schlank: MC 2.0 ist ein Glue-Cockpit, das vorhandene
Dienste (llama-swap, LiteLLM-Gateway, Hermes) steuert, statt sie nachzubauen.
"""
import os
from pathlib import Path
from ruamel.yaml import YAML
# --- Engine (llama-swap) -----------------------------------------------------
LLAMA_SWAP_URL = os.environ.get("MC_LLAMA_SWAP_URL", "http://127.0.0.1:8080").rstrip("/")
CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/etc/llama-swap/config.yaml"))
MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
# Cache der Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace).
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
# Gedächtnis: MC2 hält KEINS mehr. Bis August 2026 lief hier erst eine eigene SQLite-DB,
# dann ein semantischer Sidecar auf :8765 — beides ist abgelöst, der Port ist tot.
# Einzige Gedächtnis-Wahrheit ist jetzt Hermes selbst (HERMES_API_URL, siehe unten).
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
# macht llama-server ohnehin automatisch pro Slot (KV-Reuse).
_DEFAULT_CMD_TEMPLATE = (
"llama-server -m {model} --host 127.0.0.1 --port ${PORT} "
"-c {ctx} -ngl 999 -fa on --no-mmap"
)
CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
if "{model}" not in CMD_TEMPLATE:
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
# Verzeichnis mit Draft-Modellen für Speculative Decoding. Beim Hinzufügen eines
# fast/coder-Modells wird hieraus automatisch ein **vocab-kompatibler** Draft gewählt
# (Vocab-Check via services.gguf_meta; ein inkompatibler Draft lässt llama.cpp scheitern).
DRAFTS_DIR = Path(os.environ.get("MC_DRAFTS_DIR", str(MODELS_DIR / "drafts")))
# Optionaler expliziter Default-Draft (leer = Auto-Erkennung aus DRAFTS_DIR). Wird nur
# verwendet, wenn er zum Ziel-Modell vocab-kompatibel ist. (Früher fix qwen2.5 → entfernt,
# weil das mit neueren Vocabs wie Qwen3.6 inkompatibel ist und Spec stillschweigend brach.)
SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "")
# Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich
# zu --spec-draft-model, sonst ist Spec inaktiv).
SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple")
# MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen
# MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit
# `--model-draft <mtp.gguf> --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT
# --spec-draft-model/draft-simple). 1,52× Durchsatz bei null Qualitätsverlust.
SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4"))
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
# --- Routing-Gateway (builtin in MC2, model: auto) ---------------------------
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
# Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B.
# http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad
# und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes
# Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback.
V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
# (Tools + eigenes Gedächtnis) wie CLI/Telegram, nur über HTTP.
def _read_hermes_env(key: str) -> str:
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
try:
env_path = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) / ".env"
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith(f"{key}="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return ""
HERMES_API_KEY = (
os.environ.get("HERMES_API_KEY")
or os.environ.get("API_SERVER_KEY")
or _read_hermes_env("API_SERVER_KEY")
)
# Modellfeld im OpenAI-Request; die api_server-Plattform nutzt ihr konfiguriertes Hirn,
# das Feld ist i.d.R. kosmetisch. Override via Env, falls die Plattform strikt prüft.
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
# Eigenes Python-3.12-venv (~/.voice/venv), weil Parakeet/Piper nicht ins 3.14-Backend-venv
# passen. MC2 proxyt nach außen.
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
# --- Lucys Stimme (lucy-stimme.service, pocket-tts german_24l, Klon aus ref.mp3) -----------
# Eigener User-Dienst in ~/.lucy-stimme (nur Loopback :8021). Spricht die Telegram-Sprachnachrichten
# UND — seit dem Raphael-Umbau der Desktop-Lucy (04.09.2026) — auch den PC: MC2 reicht ihn unter
# /api/lucy/stimme/* ins LAN (routers/voice.py). EINE Stimme für alle Türen. :8650 ist NICHT Lucy.
LUCY_STIMME_URL = os.environ.get("MC_LUCY_STIMME_URL", "http://127.0.0.1:8021").rstrip("/")
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
# KEINE eigene Firewall-Freigabe (Port 7682 ist von außen dicht) und keinen sudo-Eingriff.
# Direkter, SSH-artiger Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie das Dashboard.
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
BOX_CONSOLE_PATH = "/console/"
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). ‼️ Sie bindet NICHT auf Loopback:
# ein systemd-Drop-in überschreibt `--host 127.0.0.1` mit `0.0.0.0` und setzt dafür ein
# Session-Token. Dass sie trotzdem nicht im LAN hängt, liegt allein an ufw (9119 ist nicht
# freigegeben — am 27.08.2026 von einem zweiten Rechner aus gegengeprüft). Wer die Firewall
# anfasst, öffnet damit auch diese Oberfläche. MC2 erreicht sie über Loopback und reicht sie
# same-origin unter
# /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle
# SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/")
HERMES_BUILTIN_UI_PATH = "/hermes-ui/"
# GitHub-Repo für Update-Checks.
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/")
# --- Server ------------------------------------------------------------------
HOST = os.environ.get("MC_HOST", "0.0.0.0")
PORT = int(os.environ.get("MC_PORT", "9000"))
# Gebautes React-Frontend (frontend/dist). In Prod liefert FastAPI es statisch aus;
# im Dev läuft der Vite-Dev-Server separat und proxyt /api hierher.
FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist")))
# Version (Phase 0 — Greenfield-Skeleton).
VERSION = "2.0.0-w8"
# Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml).
yaml = YAML()
yaml.preserve_quotes = True
"""
Zentrale Konfiguration für Mission Control 2.0.
Eine Quelle der Wahrheit für Pfade, URLs und Defaults — alles über Env-Vars
überschreibbar. Bewusst schlank: MC 2.0 ist ein Glue-Cockpit, das vorhandene
Dienste (llama-swap, LiteLLM-Gateway, Hermes) steuert, statt sie nachzubauen.
"""
import os
from pathlib import Path
from ruamel.yaml import YAML
# --- Engine (llama-swap) -----------------------------------------------------
LLAMA_SWAP_URL = os.environ.get("MC_LLAMA_SWAP_URL", "http://127.0.0.1:8080").rstrip("/")
CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/etc/llama-swap/config.yaml"))
MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
# Cache der Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace).
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
# Gedächtnis: MC2 hält KEINS mehr. Bis August 2026 lief hier erst eine eigene SQLite-DB,
# dann ein semantischer Sidecar auf :8765 — beides ist abgelöst, der Port ist tot.
# Einzige Gedächtnis-Wahrheit ist jetzt Hermes selbst (HERMES_API_URL, siehe unten).
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
# macht llama-server ohnehin automatisch pro Slot (KV-Reuse).
_DEFAULT_CMD_TEMPLATE = (
"llama-server -m {model} --host 127.0.0.1 --port ${PORT} "
"-c {ctx} -ngl 999 -fa on --no-mmap"
)
CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
if "{model}" not in CMD_TEMPLATE:
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
# Verzeichnis mit Draft-Modellen für Speculative Decoding. Beim Hinzufügen eines
# fast/coder-Modells wird hieraus automatisch ein **vocab-kompatibler** Draft gewählt
# (Vocab-Check via services.gguf_meta; ein inkompatibler Draft lässt llama.cpp scheitern).
DRAFTS_DIR = Path(os.environ.get("MC_DRAFTS_DIR", str(MODELS_DIR / "drafts")))
# Optionaler expliziter Default-Draft (leer = Auto-Erkennung aus DRAFTS_DIR). Wird nur
# verwendet, wenn er zum Ziel-Modell vocab-kompatibel ist. (Früher fix qwen2.5 → entfernt,
# weil das mit neueren Vocabs wie Qwen3.6 inkompatibel ist und Spec stillschweigend brach.)
SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "")
# Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich
# zu --spec-draft-model, sonst ist Spec inaktiv).
SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple")
# MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen
# MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit
# `--model-draft <mtp.gguf> --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT
# --spec-draft-model/draft-simple). 1,52× Durchsatz bei null Qualitätsverlust.
SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4"))
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
# --- Routing-Gateway (builtin in MC2, model: auto) ---------------------------
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
# Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B.
# http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad
# und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes
# Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback.
V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
# (Tools + eigenes Gedächtnis) wie CLI/Telegram, nur über HTTP.
def _read_hermes_env(key: str) -> str:
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
try:
env_path = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) / ".env"
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith(f"{key}="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return ""
HERMES_API_KEY = (
os.environ.get("HERMES_API_KEY")
or os.environ.get("API_SERVER_KEY")
or _read_hermes_env("API_SERVER_KEY")
)
# Modellfeld im OpenAI-Request; die api_server-Plattform nutzt ihr konfiguriertes Hirn,
# das Feld ist i.d.R. kosmetisch. Override via Env, falls die Plattform strikt prüft.
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
# Eigenes Python-3.12-venv (~/.voice/venv), weil Parakeet/Piper nicht ins 3.14-Backend-venv
# passen. MC2 proxyt nach außen.
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
# --- Lucys Stimme (lucy-stimme.service, pocket-tts german_24l, Klon aus ref.mp3) -----------
# Eigener User-Dienst in ~/.lucy-stimme (nur Loopback :8021). Spricht die Telegram-Sprachnachrichten
# UND — seit dem Raphael-Umbau der Desktop-Lucy (04.09.2026) — auch den PC: MC2 reicht ihn unter
# /api/lucy/stimme/* ins LAN (routers/voice.py). EINE Stimme für alle Türen. :8650 ist NICHT Lucy.
LUCY_STIMME_URL = os.environ.get("MC_LUCY_STIMME_URL", "http://127.0.0.1:8021").rstrip("/")
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
# KEINE eigene Firewall-Freigabe (Port 7682 ist von außen dicht) und keinen sudo-Eingriff.
# Direkter, SSH-artiger Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie das Dashboard.
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
BOX_CONSOLE_PATH = "/console/"
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). ‼️ Sie bindet NICHT auf Loopback:
# ein systemd-Drop-in überschreibt `--host 127.0.0.1` mit `0.0.0.0` und setzt dafür ein
# Session-Token. Dass sie trotzdem nicht im LAN hängt, liegt allein an ufw (9119 ist nicht
# freigegeben — am 27.08.2026 von einem zweiten Rechner aus gegengeprüft). Wer die Firewall
# anfasst, öffnet damit auch diese Oberfläche. MC2 erreicht sie über Loopback und reicht sie
# same-origin unter
# /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle
# SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/")
HERMES_BUILTIN_UI_PATH = "/hermes-ui/"
# GitHub-Repo für Update-Checks.
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.22:7777").rstrip("/") # PC-IP seit 09/2026 .22 (vorher .98)
# --- Server ------------------------------------------------------------------
HOST = os.environ.get("MC_HOST", "0.0.0.0")
PORT = int(os.environ.get("MC_PORT", "9000"))
# Gebautes React-Frontend (frontend/dist). In Prod liefert FastAPI es statisch aus;
# im Dev läuft der Vite-Dev-Server separat und proxyt /api hierher.
FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist")))
# Version (Phase 0 — Greenfield-Skeleton).
VERSION = "2.0.0-w8"
# Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml).
yaml = YAML()
yaml.preserve_quotes = True
-63
View File
@@ -1,63 +0,0 @@
"""Auftragsbuch-Endpoints (Vorschlags-Inbox) — dünner REST-Layer über services/auftragsbuch.py.
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")
class BranchIn(BaseModel):
branch: str
repo: str = "mc2" # "mc2" (Box-Stack) oder "lucy" (Desktop-App, Annahme baut am PC)
grund: str = "" # nur beim Ablehnen: optionaler Grund → Lern-Gedächtnis des Kreislaufs
class KandidatIn(BaseModel):
file: str
@router.get("/auftragsbuch")
def list_proposals() -> dict:
return auftragsbuch.list_proposals()
@router.get("/auftragsbuch/diff")
def diff(branch: str, repo: str = "mc2") -> dict:
res = auftragsbuch.diff_of(branch, repo)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Diff nicht verfügbar."))
return res
@router.post("/auftragsbuch/annehmen")
def accept(body: BranchIn) -> dict:
res = auftragsbuch.accept(body.branch, body.repo)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Annehmen fehlgeschlagen."))
return res
@router.post("/auftragsbuch/ablehnen")
def reject(body: BranchIn) -> dict:
res = auftragsbuch.reject(body.branch, body.repo, body.grund)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Ablehnen fehlgeschlagen."))
return res
@router.post("/auftragsbuch/skill/annehmen")
def skill_accept(body: KandidatIn) -> dict:
res = auftragsbuch.skill_accept(body.file)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Beauftragen fehlgeschlagen."))
return res
@router.post("/auftragsbuch/skill/ablehnen")
def skill_reject(body: KandidatIn) -> dict:
res = auftragsbuch.skill_reject(body.file)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "Verwerfen fehlgeschlagen."))
return res
+1 -1
View File
@@ -1,6 +1,6 @@
"""Chronik — die lesbare Timeline dessen, was die Box von allein getan und gemeldet hat.
Quelle ist der persistente Melde-Briefkasten (services/announce.py): Health-Wächter,
Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons, Auftragsbuch — alle
Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons — alle
autonomen Kanäle laufen dort bereits durch. Hier wird nichts Neues erhoben, nur erzählt."""
from fastapi import APIRouter
+1 -3
View File
@@ -79,9 +79,7 @@ def _fingerprints() -> dict[str, object]:
)
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"))
# Erinnerungen: Datei-mtime (das Auftragsbuch ist seit 04.09.2026 ausgebaut)
fp["reminders"] = _mtime(MODELS_DIR / "mc2-reminders.json")
return fp
-525
View File
@@ -1,525 +0,0 @@
"""
Auftragsbuch — die Vorschlags-Inbox der Box (das Mensch-Gate als Klick statt Git-Handarbeit).
Quellen der Karten:
• Vorschlags-Branches auf Gitea (wartung/*, orchestrator/*, doku/*) — die Werkstatt und der
Orchestrator arbeiten propose-only und lassen ihre Ergebnisse dort liegen.
Seit S3 (lucy-pipeline) aus ZWEI Repos: mission-control-v2 (Box-Stack) UND lucy (Desktop-App).
• Skill-Kandidaten aus dem Wissens-Vault (~/wissens-vault/skill-kandidaten/) — Vorschläge des
nächtlichen Traum-Crons, Gate war bisher „Commander sagt mach".
Annehmen (Branch) startet den Repo-eigenen Runner als EIGENE systemd-Unit (detached):
• mc2: deploy/auftrag-annehmen.sh — Merge im Worktree → Push main → Deploy → Health → Revert bei Rot.
• lucy: deploy/lucy-annahme.sh — Merge im Worktree → Push main → PC baut dist (via PC-Executor)
und startet Lucy neu, wenn sie lief → bei Rot Revert auf main (die laufende Lucy bleibt die alte).
Detached, weil deploy.sh mission-control-2 neu startet — ein Kind des Backends stürbe mittendrin.
Der Fortschritt landet in STATUS_PATH (JSON), das Skript schreibt, die API liest nur.
Status-Schlüssel: mc2 = Branch-Name pur (Bestand), lucy = "lucy:<branch>" (kollisionsfrei).
Lokal (Windows-Dev) ist alles harmlos: available=False, Aktionen geben Fehler statt zu crashen.
"""
import json
import logging
import os
import re
import shutil
import subprocess
import time
import urllib.request
from pathlib import Path
from config import MODELS_DIR
log = logging.getLogger(__name__)
REPO = Path(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2")).expanduser()
LUCY = Path(os.environ.get("MC2_LUCY_DIR", "~/lucy")).expanduser()
VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
STATUS_PATH = Path(os.environ.get("MC2_AUFTRAG_STATUS", str(MODELS_DIR / "mc2-auftragsbuch.json")))
# Empfehlungs-Stempel des nächtlichen Karten-Gutachters (deploy/karten-gutachter.sh schreibt,
# die API liest nur) und das Lern-Gedächtnis der Ablehnungen (Radar/Specifier lesen es).
GUTACHTEN_PATH = Path(os.environ.get("MC2_KARTEN_GUTACHTEN", str(MODELS_DIR / "mc2-karten-gutachten.json")))
ABLEHNUNGEN_PATH = Path(os.environ.get("MC2_ABLEHNUNGEN", str(MODELS_DIR / "mc2-ablehnungen.jsonl")))
# Karten-Quellen. Die Runner-Skripte liegen IMMER im MC2-Checkout (deploy/) — auch der
# Lucy-Runner, denn er läuft auf der Box; nur der Build passiert am PC.
REPOS: dict[str, dict] = {
"mc2": {"path": REPO, "runner": "auftrag-annehmen.sh", "key": ""},
"lucy": {"path": LUCY, "runner": "lucy-annahme.sh", "key": "lucy:"},
}
# Persistenz für gemeldete Branches (Idempotenz: nur EINMAL pro Branch pingen).
# Muster wie mc2-announce.json unter MODELS_DIR.
ANNOUNCE_BRANCHES_PATH = Path(os.environ.get("MC2_ANNOUNCE_BRANCHES", str(MODELS_DIR / "mc2-announce-branches.json")))
# Nur diese Branch-Familien sind Vorschläge (main/HEAD & Fremdes bleiben draußen).
PREFIXES = ("wartung/", "orchestrator/", "doku/", "feature/")
# Branch-Namen kommen vom Client zurück → hart validieren (keine Shell-/Git-Injektion).
_BRANCH_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,120}$")
_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:
return os.name == "posix" and (REPO / ".git").exists()
def _repo_ok(repo: str) -> bool:
r = REPOS.get(repo)
return bool(r) and os.name == "posix" and (r["path"] / ".git").exists()
def _git(repo: str, args: list[str], timeout: int = 20) -> subprocess.CompletedProcess:
return subprocess.run(["git", "-C", str(REPOS[repo]["path"]), *args],
capture_output=True, text=True, timeout=timeout)
def _status_key(repo: str, branch: str) -> str:
return f"{REPOS[repo]['key']}{branch}"
def _fetch_throttled(repo: str) -> None:
now = time.time()
if now - _fetch_cache.get(repo, 0.0) < _FETCH_EVERY:
return
_fetch_cache[repo] = now
try:
_git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30)
except Exception:
log.warning("auftragsbuch: git fetch (%s) fehlgeschlagen", repo, exc_info=True)
def _load_announce_branches() -> set:
"""Liefert die Menge der bereits gemeldeten Branch-Namen (Idempotenz)."""
try:
data = json.loads(ANNOUNCE_BRANCHES_PATH.read_text(encoding="utf-8"))
return set(data) if isinstance(data, list) else set()
except Exception:
return set()
def _save_announce_branches(branches: set) -> None:
"""Speichert die Menge gemelder Branch-Namen."""
try:
tmp = ANNOUNCE_BRANCHES_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(sorted(branches), ensure_ascii=False), encoding="utf-8")
tmp.replace(ANNOUNCE_BRANCHES_PATH)
except OSError:
log.warning("auftragsbuch: Announce-Branches %s nicht schreibbar", ANNOUNCE_BRANCHES_PATH, exc_info=True)
def _statuses() -> dict:
try:
return (json.loads(STATUS_PATH.read_text(encoding="utf-8")) or {}).get("branches", {})
except Exception:
return {}
def _set_status(key: str, state: str, detail: str) -> None:
try:
try:
data = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
except Exception:
data = {}
data.setdefault("branches", {})[key] = {"state": state, "detail": detail, "ts": time.time()}
tmp = STATUS_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
tmp.replace(STATUS_PATH)
except OSError:
log.warning("auftragsbuch: Status %s nicht schreibbar", STATUS_PATH, exc_info=True)
def _gutachten() -> dict:
"""Alle Karten-Stempel ("<repo>:<branch>"{empfehlung, satz, richter, commit_ts, ts})."""
try:
data = json.loads(GUTACHTEN_PATH.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _remote_branches(repo: str) -> list[str]:
r = _git(repo, ["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"])
out = []
for line in (r.stdout or "").splitlines():
name = line.strip().removeprefix("origin/")
if name and name != "HEAD" and name.startswith(PREFIXES):
out.append(name)
return out
def _valid_branch(branch: str) -> bool:
return bool(_BRANCH_RX.match(branch)) and ".." not in branch and branch.startswith(PREFIXES)
def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
items = []
for branch in _remote_branches(repo):
ref = f"origin/{branch}"
status = statuses.get(_status_key(repo, branch))
try:
ahead = int((_git(repo, ["rev-list", "--count", f"origin/main..{ref}"]).stdout or "0").strip() or 0)
if ahead == 0 and ((status or {}).get("state") not in ("laeuft", "rollback")):
continue # bereits in main enthalten → keine offene Entscheidung mehr
behind = int((_git(repo, ["rev-list", "--count", f"{ref}..origin/main"]).stdout or "0").strip() or 0)
# Kaputt aufgesetzte Branches entlarven (Worker machte git init/Shallow statt zu
# klonen): ohne gemeinsamen Vorfahren kann git NIE mergen — Annehmen wäre ein
# garantierter Fehllauf („refusing to merge unrelated histories").
verwaist = _git(repo, ["merge-base", "origin/main", ref]).returncode != 0
show = _git(repo, ["show", "-s", "--format=%s%x1f%b%x1f%ct%x1f%an", ref])
subject, body, cts, author = ((show.stdout or "").split("\x1f") + ["", "", "", ""])[:4]
ts_val = int(cts) if cts.strip().isdigit() else None
# Stempel nur zeigen, wenn er zum AKTUELLEN Commit gehört (nachgeschobener
# Commit macht das alte Gutachten ungültig — der Nacht-Lauf stempelt neu).
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 = []
for line in files_raw[:60]:
parts = line.split("\t")
if len(parts) >= 2:
files.append({"status": parts[0][:2], "path": parts[-1]})
paths = [f["path"] for f in files]
frontend_src = any(p.startswith("frontend/src") for p in paths)
frontend_dist = any(p.startswith("frontend/dist") for p in paths)
items.append({
"repo": repo,
"branch": branch,
"kind": branch.split("/", 1)[0],
"subject": subject.strip(),
"body": body.strip()[:2000],
"author": author.strip(),
"ts": ts_val,
"empfehlung": stempel,
"ahead": ahead,
"behind": behind,
"verwaist": verwaist,
# Diff gegen main ist leer → der Branch brächte nichts (Inhalt schon in main
# oder Worker hat am Repo vorbei gearbeitet). Bei verwaist ist der Diff
# technisch leer (kein merge-base) — dann zählt nur das verwaist-Flag.
"leer": not verwaist and ahead > 0 and not files_raw,
"shortstat": stat,
"files": files,
"files_truncated": len(files_raw) > 60,
# Nur mc2: Frontend-Quelltext ohne gebautes Bundle — die Box deployt dist so, wie
# 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)
return items
def list_proposals() -> dict:
"""Alle offenen Vorschläge: Branches aus beiden Repos (mit Commit-/Diff-Zusammenfassung +
Status) und Skill-Kandidaten aus dem Vault. Eine Antwort für die ganze Auftragsbuch-Seite.
Nebenbei: Pinge neu auftauchende Vorschlags-Branches per Telegram und füge sie dem
Briefkasten hinzu (Idempotenz: nur EINMAL pro Branch)."""
if not _available():
return {"available": False, "items": [], "skill_kandidaten": [], "open_count": 0}
statuses = _statuses()
gutachten = _gutachten()
items = []
for repo in REPOS:
if not _repo_ok(repo):
continue # z. B. ~/lucy (noch) nicht geklont → Karten dieses Repos einfach weglassen
_fetch_throttled(repo)
items.extend(_repo_items(repo, statuses, gutachten))
items.sort(key=lambda i: i.get("ts") or 0, reverse=True)
# --- Telegram-Ping für NEUE Vorschlags-Branches (Idempotenz) ---
# Alle aktuellen Branch-Namen aus beiden Repos sammeln
aktuelle_branches = set()
for repo in REPOS:
if _repo_ok(repo):
aktuelle_branches.update(_remote_branches(repo))
# Bereits gemeldete Branches laden
gemeldet = _load_announce_branches()
# Nur wirklich NEUE Branches pingen
neue = aktuelle_branches - gemeldet
for branch in neue:
# NotifyTelegram best-effort, niemals crashen
try:
from services import announce
subject = "[Auftragsbuch]"
text = f"Neue Karte wartet auf deinen Klick: {branch}"
announce.notify_telegram(subject, text)
# Und ein Eintrag für den Briefkasten (damit Lucy es spricht)
announce.add(text, subject, "auftragsbuch", "normal")
except Exception:
log.warning("auftragsbuch: Telegram-Ping für %s fehlgeschlagen", branch, exc_info=True)
# 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
if (i.get("status") or {}).get("state") not in ("eingespielt",)) + len(kandidaten)
return {"available": True, "items": items, "skill_kandidaten": kandidaten, "open_count": open_count}
def diff_of(branch: str, repo: str = "mc2") -> dict:
if not _repo_ok(repo):
return {"ok": False, "error": "Nur auf der Box verfügbar."}
if not _valid_branch(branch) or branch not in _remote_branches(repo):
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
r = _git(repo, ["diff", f"origin/main...origin/{branch}"], timeout=30)
text = r.stdout or ""
truncated = len(text) > 200_000
return {"ok": True, "diff": text[:200_000], "truncated": truncated}
def accept(branch: str, repo: str = "mc2") -> dict:
"""Annehmen: detached Runner starten. mc2: Merge→Push→Deploy→Health→ggf. Rollback.
lucy: Merge→Push→PC-Build+Neustart→ggf. Revert (der Runner spricht den PC-Executor an)."""
if not _repo_ok(repo):
return {"ok": False, "error": "Annehmen geht nur auf der Box."}
if not _valid_branch(branch) or branch not in _remote_branches(repo):
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
key = _status_key(repo, branch)
state = (_statuses().get(key) or {}).get("state")
if state in ("laeuft", "rollback"):
return {"ok": False, "error": "Für diesen Vorschlag läuft bereits ein Annahme-Lauf."}
# Kaputt aufgesetzter Branch (kein gemeinsamer Vorfahre) → Merge kann NIE gelingen;
# gar nicht erst einen Runner starten (die UI blendet den Knopf aus, die API hält dicht).
if _git(repo, ["merge-base", "origin/main", f"origin/{branch}"]).returncode != 0:
return {"ok": False, "error": "Dieser Branch hat keinen gemeinsamen Ursprung mit main "
"(kaputt aufgesetzt, z. B. git init statt Klonen) — Annehmen ist technisch unmöglich. "
"Bitte ablehnen (gern mit Grund) und die Idee neu in die Queue geben."}
# Runner als /tmp-Kopie starten: deploy.sh resettet das Repo hart — das Original-Skript
# würde einem laufenden bash unter den Füßen getauscht (bekannte Selbst-Reset-Falle).
src = REPO / "deploy" / REPOS[repo]["runner"]
if not src.is_file():
return {"ok": False, "error": f"Runner fehlt im Checkout: {src.name}"}
runner = Path(f"/tmp/mc2-auftrag-runner-{int(time.time())}.sh")
try:
shutil.copyfile(src, runner)
except OSError as exc:
return {"ok": False, "error": f"Runner nicht kopierbar: {exc}"}
slug = re.sub(r"[^a-z0-9-]+", "-", f"{repo}-{branch}".lower())[:40].strip("-")
unit = f"mc2-auftrag-{slug}-{int(time.time())}"
r = subprocess.run(
["systemd-run", "--user", "--collect", f"--unit={unit}",
"/bin/bash", str(runner), branch],
capture_output=True, text=True, timeout=20)
if r.returncode != 0:
return {"ok": False, "error": f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}"}
_set_status(key, "laeuft", "Annahme-Lauf gestartet")
return {"ok": True, "unit": unit}
def reject(branch: str, repo: str = "mc2", grund: str = "") -> dict:
"""Ablehnen: Remote-Branch löschen (die Arbeit bleibt in der Gitea-Historie referenzierbar,
aber die Entscheidung ist gefallen). Der optionale GRUND ist das Lern-Gedächtnis des
Kreislaufs: er landet in ABLEHNUNGEN_PATH (jsonl), und Idle-Radar/Analysten bekommen
ihn als „NIE wieder vorschlagen"-Material vorgelegt. Meldung in den Briefkasten."""
if not _repo_ok(repo):
return {"ok": False, "error": "Ablehnen geht nur auf der Box."}
if not _valid_branch(branch) or branch not in _remote_branches(repo):
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
grund = (grund or "").strip()[:400]
# Betreff VOR dem Löschen sichern — danach ist der Ref weg.
subject = (_git(repo, ["show", "-s", "--format=%s", f"origin/{branch}"]).stdout or "").strip()[:200]
r = _git(repo, ["push", "origin", "--delete", branch], timeout=30)
if r.returncode != 0:
return {"ok": False, "error": f"Löschen fehlgeschlagen: {(r.stderr or '').strip()[:300]}"}
_git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30)
detail = "Vom Commander abgelehnt — Branch gelöscht"
if grund:
detail += f" (Grund: {grund})"
_set_status(_status_key(repo, branch), "abgelehnt", detail)
try:
with ABLEHNUNGEN_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps({"ts": int(time.time()), "repo": repo, "branch": branch,
"subject": subject, "grund": grund}, ensure_ascii=False) + "\n")
except OSError:
log.warning("auftragsbuch: Ablehn-Gedächtnis %s nicht schreibbar", ABLEHNUNGEN_PATH, exc_info=True)
try:
from services import announce
wo = "im Lucy-Repo " if repo == "lucy" else ""
warum = f" Grund: {grund}" if grund else ""
announce.add(f"Vorschlag '{branch}' {wo}wurde abgelehnt und der Branch gelöscht.{warum}",
"[Auftragsbuch]", "auftragsbuch", "silent")
except Exception:
pass
return {"ok": True}
# ── Skill-Kandidaten (Wissens-Vault) ─────────────────────────────────────────
def _kandidaten_dir() -> Path:
return VAULT / "skill-kandidaten"
def list_skill_kandidaten() -> list[dict]:
d = _kandidaten_dir()
if os.name != "posix" or not d.is_dir():
return []
out = []
for p in sorted(d.glob("*.md"), key=lambda x: x.stat().st_mtime, reverse=True):
try:
text = p.read_text(encoding="utf-8", errors="replace")
first = next((ln.strip().lstrip("# ") for ln in text.splitlines() if ln.strip()), p.stem)
out.append({"file": p.name, "title": first[:160],
"preview": text[:3000], "mtime": p.stat().st_mtime})
except OSError:
continue
return out
def _vault_git(args: list[str]) -> None:
try:
subprocess.run(["git", "-C", str(VAULT), *args], capture_output=True, text=True, timeout=15)
except Exception:
pass
def _kandidat_path(fname: str) -> Path | None:
if not _KANDIDAT_RX.match(fname):
return None
p = (_kandidaten_dir() / fname).resolve()
if not p.is_relative_to(_kandidaten_dir().resolve()) or not p.is_file():
return None
return p
def skill_accept(fname: str) -> dict:
"""Skill-Kandidat beauftragen: Inhalt als Werkstatt-Auftrag an die bewährte
`hermes -z`-CLI-Lane (detached — der Lauf dauert Minuten und braucht das Backend nicht).
Ergebnis ist wieder propose-only: ein neuer Branch, der hier als Karte auftaucht."""
if not _available():
return {"ok": False, "error": "Beauftragen geht nur auf der Box."}
p = _kandidat_path(fname)
if not p:
return {"ok": False, "error": f"Kandidat nicht gefunden: {fname}"}
beauftragt = _kandidaten_dir() / "beauftragt"
beauftragt.mkdir(parents=True, exist_ok=True)
target = beauftragt / p.name
try:
content = p.read_text(encoding="utf-8", errors="replace")
p.rename(target)
except OSError as exc:
return {"ok": False, "error": f"Kandidat nicht verschiebbar: {exc}"}
_vault_git(["add", "-A"])
_vault_git(["commit", "-q", "-m", f"Skill-Kandidat beauftragt: {p.name}"])
order = ("Nutze den orchestrator-Skill (propose-only, Zwei-Kritiker-Gate). "
"Auftrag aus dem Wissens-Vault (vom Commander im Auftragsbuch freigegeben):\n\n"
+ content)
order_file = Path(f"/tmp/mc2-skill-auftrag-{int(time.time())}.txt")
try:
order_file.write_text(order, encoding="utf-8")
except OSError as exc:
return {"ok": False, "error": f"Auftrag nicht schreibbar: {exc}"}
unit = f"mc2-skill-auftrag-{int(time.time())}"
r = subprocess.run(
["systemd-run", "--user", "--collect", f"--unit={unit}",
"/bin/bash", "-lc", f'hermes -z "$(cat {order_file})"'],
capture_output=True, text=True, timeout=20)
if r.returncode != 0:
return {"ok": False, "error": f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}"}
try:
from services import announce
announce.add(f"Skill-Kandidat '{fname}' wurde beauftragt — die Werkstatt arbeitet, "
"das Ergebnis erscheint als neuer Vorschlag im Auftragsbuch.",
"[Auftragsbuch]", "auftragsbuch", "silent")
except Exception:
pass
return {"ok": True, "unit": unit}
def skill_reject(fname: str) -> dict:
if not _available():
return {"ok": False, "error": "Verwerfen geht nur auf der Box."}
p = _kandidat_path(fname)
if not p:
return {"ok": False, "error": f"Kandidat nicht gefunden: {fname}"}
verworfen = _kandidaten_dir() / "verworfen"
verworfen.mkdir(parents=True, exist_ok=True)
try:
p.rename(verworfen / p.name)
except OSError as exc:
return {"ok": False, "error": f"Kandidat nicht verschiebbar: {exc}"}
_vault_git(["add", "-A"])
_vault_git(["commit", "-q", "-m", f"Skill-Kandidat verworfen: {p.name}"])
return {"ok": True}
+1 -1
View File
@@ -101,7 +101,7 @@ 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 ausgefallen — Dashboard und Briefkasten 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.",
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/env bash
# Auftragsbuch: einen Vorschlags-Branch ANNEHMEN — der letzte Meter des propose-only-Kreislaufs.
# Werkstatt/Orchestrator liefern Branches (wartung/*, orchestrator/*), der Commander klickt in
# der Zentrale „Annehmen" → dieses Skript macht die bisherige Git-Handarbeit: Merge im
# isolierten Worktree (Lehre: NIE im Live-Checkout arbeiten) → Push nach main → Deploy →
# Health-Check → bei Rot automatischer Revert + Redeploy + Alarm.
#
# WICHTIG: läuft als EIGENE systemd-Unit (systemd-run, startet routers/auftragsbuch.py),
# NICHT als Kind des Backends — deploy.sh startet mission-control-2 neu und würde sonst
# den eigenen Eltern-Prozess mitten im Lauf töten. Außerdem wird es vom Backend als
# /tmp-KOPIE gestartet (deploy.sh macht git reset --hard → die Datei unter den Füßen
# eines laufenden bash zu tauschen korrumpiert das Skript, bekannte Falle).
#
# Nutzung: auftrag-annehmen.sh <branch> (z. B. wartung/saubere-zusammenfassung)
set -uo pipefail
BRANCH="${1:?Nutzung: auftrag-annehmen.sh <branch>}"
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
API="${MC_API:-http://127.0.0.1:9001}"
SLUG="$(echo "$BRANCH" | tr '/' '-')"
WT="/tmp/annahme-$SLUG"
LOG="/tmp/annahme-$SLUG.log"
notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; }
# Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur).
status(){ # $1=state $2=detail
python3 - "$STATUS" "$BRANCH" "$1" "$2" <<'PY'
import json, os, sys, time
p, branch, state, detail = sys.argv[1:5]
try:
d = json.load(open(p, encoding="utf-8"))
except Exception:
d = {}
d.setdefault("branches", {})[branch] = {"state": state, "detail": detail, "ts": time.time()}
tmp = p + ".tmp"
json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False)
os.replace(tmp, p)
PY
}
cleanup_wt(){ git -C "$SRC" worktree remove --force "$WT" 2>/dev/null || true; }
fail(){
status "fehlgeschlagen" "$1"
notify "Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen."
cleanup_wt
exit 1
}
status "laeuft" "Merge wird vorbereitet"
cd "$SRC" || fail "Live-Checkout $SRC fehlt"
git fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen"
git rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \
|| fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)"
PRE="$(git rev-parse origin/main)"
# Kaputt aufgesetzte Branches (Worker machte git init/Shallow statt zu klonen) haben KEINEN
# gemeinsamen Vorfahren — git verweigert den Merge immer. Ehrlich sagen statt „Konflikt".
git merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \
|| fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben"
# Isolierter Worktree — der Live-Checkout bleibt bis zum Deploy unberührt.
cleanup_wt
git worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden"
MERGE_MSG="Auftragsbuch: '$BRANCH' angenommen (Ein-Klick-Gate)"
if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then
git -C "$WT" merge --abort 2>/dev/null || true
# Selbstheilung: veralteten Branch mechanisch auf main rebasen (zweiter Worktree am
# Branch-Kopf). Klappt das sauber, wird der rebasede Stand gemergt — kein PC nötig.
status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht"
WT2="/tmp/annahme-rebase-$SLUG"
git worktree remove --force "$WT2" 2>/dev/null || true
REBASED=""
if git worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \
&& git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
rebase origin/main >>"$LOG" 2>&1; then
REBASED="$(git -C "$WT2" rev-parse HEAD)"
else
git -C "$WT2" rebase --abort 2>/dev/null || true
fi
git worktree remove --force "$WT2" 2>/dev/null || true
[ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)"
git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \
|| { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; }
fi
# py_compile-Gate über die durch den Merge geänderten Python-Dateien (AGENTS.md-Regel).
PYS="$(git -C "$WT" diff --name-only "$PRE"..HEAD -- '*.py' 2>/dev/null | tr '\n' ' ')"
if [ -n "${PYS// /}" ]; then
PYBIN="$SRC/backend/.venv/bin/python"; [ -x "$PYBIN" ] || PYBIN="python3"
# shellcheck disable=SC2086
( cd "$WT" && "$PYBIN" -m py_compile $PYS ) >>"$LOG" 2>&1 || fail "py_compile-Gate rot — der Patch enthält kaputtes Python"
fi
status "laeuft" "Push nach main läuft"
PUSHED=0
for _ in 1 2 3; do
git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; }
sleep 5
done
[ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)"
cleanup_wt
status "laeuft" "Deploy läuft — die Zentrale startet gleich kurz neu"
notify "Vorschlag '$BRANCH' angenommen: Merge auf main ist durch, Deploy läuft. Die Zentrale ist gleich kurz weg."
# deploy.sh als Kopie ausführen (Selbst-Reset-Falle: es resettet das Repo, in dem es liegt).
cp "$SRC/deploy/deploy.sh" "/tmp/annahme-deploy-$SLUG.sh"
DEPLOY_OK=1
bash "/tmp/annahme-deploy-$SLUG.sh" >>"$LOG" 2>&1 || DEPLOY_OK=0
# Health-Check mit Geduld (Dienst-Neustart + Warmup brauchen einen Moment).
HEALTH=0
if [ "$DEPLOY_OK" = 1 ]; then
for _ in $(seq 1 18); do
curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { HEALTH=1; break; }
sleep 5
done
fi
if [ "$HEALTH" = 1 ]; then
# Gemergt + live + grün → der Remote-Branch hat seinen Zweck erfüllt.
git -C "$SRC" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true
git -C "$SRC" fetch -q --prune origin 2>/dev/null || true
status "eingespielt" "Deploy grün, Health-Check bestanden"
notify "Vorschlag '$BRANCH' ist LIVE — Deploy grün, Health-Check bestanden. ✅"
exit 0
fi
# ── Rollback: Merge revertieren, main zurückschieben, neu deployen, Alarm ──
status "rollback" "Health rot — automatischer Revert läuft"
git fetch -q origin || true
cleanup_wt
if git worktree add --detach "$WT" origin/main >/dev/null 2>&1 \
&& git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \
&& git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then
cleanup_wt
cp "$SRC/deploy/deploy.sh" "/tmp/annahme-rollback-$SLUG.sh"
bash "/tmp/annahme-rollback-$SLUG.sh" >>"$LOG" 2>&1 || true
R=0
for _ in $(seq 1 12); do
curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { R=1; break; }
sleep 5
done
if [ "$R" = 1 ]; then
status "zurueckgerollt" "Health blieb rot — Merge automatisch revertiert, Box läuft wieder"
notify "Vorschlag '$BRANCH' hat den Health-Check GERISSEN. Automatisch zurückgerollt — die Box läuft wieder auf dem alten Stand. Der Branch bleibt zur Analyse liegen. Log: $LOG"
exit 1
fi
fi
cleanup_wt
status "kritisch" "Rollback fehlgeschlagen — Box braucht Hilfe"
curl -sf -m 5 -X POST "$API/api/alarm" -H 'Content-Type: application/json' \
--data "{\"subject\":\"[Auftragsbuch]\",\"text\":\"KRITISCH: Annehmen von $BRANCH UND Rollback fehlgeschlagen — Zentrale prüfen. Log: $LOG\"}" >/dev/null 2>&1 \
|| notify "KRITISCH: Annehmen von '$BRANCH' UND Rollback fehlgeschlagen — bitte Box prüfen (restore.sh liegt bereit). Log: $LOG"
exit 2
-72
View File
@@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Bagatell-Annahme — „Harmloses darf selbst" (User-Entscheid 10.07.2026, konservativ geschnitten):
# Vorschlags-Branches, die AUSSCHLIESSLICH Markdown-Dateien anlegen oder ändern (Doku/Wiki —
# keine einzige Code-Zeile, keine Löschungen, keine Umbenennungen, keine Skripte), spielt die
# Box nachts OHNE Klick ein. Der Weg ist derselbe wie beim Klick im Auftragsbuch:
# auftrag-annehmen.sh (Merge im Worktree → py_compile-Gate → Push main → Deploy → Health →
# Auto-Revert). Fangnetz drumherum: Nacht-Sicherung 03:30 (Zeitmaschine) liegt davor, die
# Morgenlage 04:30 berichtet danach, jede Annahme steht in Chronik + Telegram.
# JEDE Code-Zeile bleibt Klick-pflichtig — dieses Skript weitet die Klasse NIE selbst aus.
#
# Läuft als mc2-bagatell.timer (04:10) über eine /tmp-Kopie (Selbst-Reset-Falle: die Annahme
# deployt und resettet damit das Repo, in dem dieses Skript liegt).
set -uo pipefail
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
MAX_PRO_NACHT=2 # jede Annahme startet die Zentrale kurz neu — Nachtruhe wahren
cd "$SRC" || exit 0
git fetch -q --prune origin 2>/dev/null || exit 0
angenommen=0
for REF in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin); do
[ "$angenommen" -ge "$MAX_PRO_NACHT" ] && break
BRANCH="${REF#origin/}"
case "$BRANCH" in wartung/*|orchestrator/*|doku/*|feature/*) ;; *) continue ;; esac
AHEAD="$(git rev-list --count "origin/main..$REF" 2>/dev/null || echo 0)"
[ "${AHEAD:-0}" -gt 0 ] || continue
# Nur unberührte Vorschläge: trägt der Branch schon irgendeinen Annahme-Status
# (läuft/fehlgeschlagen/zurückgerollt/…), entscheidet der Commander — nicht wir.
STATE="$(python3 - "$STATUS" "$BRANCH" <<'PY'
import json, sys
try:
print((json.load(open(sys.argv[1], encoding="utf-8")).get("branches", {}).get(sys.argv[2]) or {}).get("state", ""))
except Exception:
print("")
PY
)"
[ -z "$STATE" ] || continue
# Bagatell-Klasse: JEDE geänderte Datei endet auf .md UND ist nur angelegt (A) oder
# geändert (M). Alles andere (Löschung, Rename R…, Copy C…, Nicht-Markdown) → Klick-Pflicht.
DIFF="$(git diff --name-status "origin/main...$REF" 2>/dev/null)"
[ -n "$DIFF" ] || continue
OK=1
while IFS=$'\t' read -r st path _rest; do
[ -n "$st" ] || continue
case "$st" in A|M) ;; *) OK=0; break ;; esac
case "$path" in *.md) ;; *) OK=0; break ;; esac
done <<< "$DIFF"
[ "$OK" = 1 ] || continue
N="$(printf '%s\n' "$DIFF" | wc -l | tr -d ' ')"
echo "Bagatelle erkannt: $BRANCH ($N Markdown-Datei(en))"
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg t "Bagatelle (nur Doku, $N Datei(en)): '$BRANCH' wird ohne Klick eingespielt — Fangnetz aktiv, Details in der Morgenlage." \
'{text:$t, subject:"[Bagatell-Annahme]", source:"bagatell", priority:"silent"}')" >/dev/null 2>&1 || true
RUNNER="/tmp/mc2-bagatell-annehmen-$$.sh"
cp "$SRC/deploy/auftrag-annehmen.sh" "$RUNNER" || continue
if bash "$RUNNER" "$BRANCH"; then
angenommen=$((angenommen+1))
else
# auftrag-annehmen.sh hat selbst gemeldet (fehlgeschlagen/zurückgerollt) und aufgeräumt;
# der Branch trägt jetzt einen Status → nächste Nacht fasst ihn niemand mehr automatisch an.
echo "Bagatell-Annahme von $BRANCH nicht grün — Meldung kam vom Annahme-Runner."
fi
done
echo "Bagatell-Lauf fertig: $angenommen eingespielt."
-228
View File
@@ -1,228 +0,0 @@
#!/usr/bin/env bash
# Auftragsbuch: einen LUCY-Vorschlags-Branch ANNEHMEN — das PC-Gegenstück zu auftrag-annehmen.sh.
# Lucy (Electron) lebt auf dem Windows-PC (F:\Coding Stuff\lucy); dist liegt NICHT im Repo.
# Annehmen heißt darum: Merge auf main (hier, Box) → der PC zieht main, baut dist neu und
# startet Lucy neu, WENN sie lief (User-Entscheid: Lucy startet nur, wenn der Commander sie will).
# Schlägt am PC irgendetwas fehl, wird der Merge auf main automatisch revertiert — die laufende
# Lucy bleibt die alte (der PC-Runner stellt dist aus seinem Backup wieder her).
#
# Der PC wird über den PC-Executor (client/hermes-pc/executor.py, :7777, Bearer-Token aus
# ~/.hermes/config.yaml) gesteuert. /shell hat 60 s Timeout → der Build läuft DETACHED
# (deploy/lucy-annahme.ps1 im Lucy-Repo schreibt eine Status-Datei, wir pollen sie).
#
# Läuft wie der MC2-Runner als EIGENE systemd-Unit und als /tmp-Kopie (Selbst-Reset-Falle).
# Status-Schlüssel in der Auftragsbuch-JSON: "lucy:<branch>".
#
# Nutzung: lucy-annahme.sh <branch> (z. B. wartung/turncheck-default-aus)
set -uo pipefail
BRANCH="${1:?Nutzung: lucy-annahme.sh <branch>}"
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
LUCY="${MC2_LUCY_DIR:-$HOME/lucy}"
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
HERMES_CONF="${HERMES_CONF:-$HOME/.hermes/config.yaml}"
SLUG="lucy-$(echo "$BRANCH" | tr '/' '-')"
WT="/tmp/annahme-$SLUG"
LOG="/tmp/annahme-$SLUG.log"
# Windows-Seite (Pfade im PC-Dateisystem; einfach gehalten — EIN PC, EIN Checkout).
PC_REPO='F:\Coding Stuff\lucy'
PC_STATUSFILE='F:\Coding Stuff\lucy\.lucy-annahme.json'
PC_RUNNER='F:\Coding Stuff\lucy\deploy\lucy-annahme.ps1'
BUILD_TIMEOUT_S=1500 # 25 min Deckel für npm ci + tsc + electron-builder + Stimm-Warmup
notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; }
# Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur).
status(){ # $1=state $2=detail
python3 - "$STATUS" "lucy:$BRANCH" "$1" "$2" <<'PY'
import json, os, sys, time
p, key, state, detail = sys.argv[1:5]
try:
d = json.load(open(p, encoding="utf-8"))
except Exception:
d = {}
d.setdefault("branches", {})[key] = {"state": state, "detail": detail, "ts": time.time()}
tmp = p + ".tmp"
json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False)
os.replace(tmp, p)
PY
}
# PC-Executor: PowerShell-Kommando ausführen. stdout des PCs → stdout; rc 0 nur wenn
# Transport UND Kommando grün. Token/URL kommen aus der Hermes-Config (kein zweiter Ablageort).
PC_URL="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_URL:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')"
PC_TOKEN="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_TOKEN:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')"
pc_shell(){ # $1 = PowerShell-Kommando
python3 - "$PC_URL" "$PC_TOKEN" "$1" <<'PY'
import json, sys, urllib.request
url, token, cmd = sys.argv[1:4]
req = urllib.request.Request(url + "/shell", data=json.dumps({"command": cmd}).encode(),
headers={"Authorization": "Bearer " + token,
"Content-Type": "application/json"})
try:
r = json.load(urllib.request.urlopen(req, timeout=58))
except Exception as e:
print(f"(PC-Executor nicht erreichbar: {e})")
sys.exit(2)
out = (r.get("stdout") or "").strip()
if out:
print(out)
err = (r.get("stderr") or "").strip()
if err:
print(err, file=sys.stderr)
sys.exit(0 if r.get("returncode") == 0 else 1)
PY
}
cleanup_wt(){ git -C "$LUCY" worktree remove --force "$WT" 2>/dev/null || true; }
fail(){
status "fehlgeschlagen" "$1"
notify "Lucy-Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen."
cleanup_wt
exit 1
}
# Revert nach schon gepushtem Merge (PC-Phase rot): main zurückdrehen, PC-Checkout nachziehen.
revert_main(){ # $1 = Grund für die Meldung
status "rollback" "PC-Phase rot — Merge wird automatisch zurückgedreht"
git -C "$LUCY" fetch -q origin || true
cleanup_wt
if git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 \
&& git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \
&& git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then
cleanup_wt
# PC-Checkout best-effort auf den revertierten Stand nachziehen (Quelle = laufende Lucy).
pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 || true
status "zurueckgerollt" "$1 — Merge automatisch revertiert, Lucy läuft in der alten Version weiter"
notify "Lucy-Vorschlag '$BRANCH' hat den PC-Build/Neustart GERISSEN: $1. Merge automatisch zurückgedreht — Lucy läuft in der alten Version weiter. Der Branch bleibt zur Analyse liegen. Log: $LOG"
exit 1
fi
cleanup_wt
status "kritisch" "Revert fehlgeschlagen — Lucy-Repo und PC von Hand prüfen"
notify "KRITISCH: Lucy-Annahme '$BRANCH' scheiterte UND der automatische Revert schlug fehl — bitte Lucy-Repo (Gitea main) und PC prüfen. Log: $LOG"
exit 2
}
[ -n "$PC_URL" ] && [ -n "$PC_TOKEN" ] || fail "PC-Executor-Zugang fehlt in $HERMES_CONF (PC_EXECUTOR_URL/TOKEN)"
# ── Vorprüfungen am PC (VOR dem Merge — solange ist nichts kaputtzumachen) ──
status "laeuft" "Vorprüfung: ist der PC bereit?"
curl -sf -m 8 "$PC_URL/health" >/dev/null 2>&1 \
|| fail "PC-Executor nicht erreichbar — Lucy-Annahme braucht den eingeschalteten PC (Executor-Task läuft bei Anmeldung)"
PC_BRANCH="$(pc_shell "git -C '$PC_REPO' rev-parse --abbrev-ref HEAD" 2>>"$LOG")" \
|| fail "PC-Arbeitskopie nicht lesbar (git am PC): $PC_BRANCH"
[ "$PC_BRANCH" = "main" ] || fail "PC-Arbeitskopie steht auf '$PC_BRANCH' statt main — bitte am PC aufräumen"
PC_DIRTY="$(pc_shell "git -C '$PC_REPO' status --porcelain -uno" 2>>"$LOG")" \
|| fail "PC-Arbeitskopie nicht prüfbar"
[ -z "$PC_DIRTY" ] || fail "PC-Arbeitskopie hat ungespeicherte Änderungen — bitte erst am PC committen/aufräumen: $(echo "$PC_DIRTY" | head -3 | tr '\n' ' ')"
WAS_RUNNING="$(pc_shell "if (Get-Process Lucy -ErrorAction SilentlyContinue) { '1' } else { '0' }" 2>>"$LOG")" || WAS_RUNNING="0"
case "$WAS_RUNNING" in 1|0) ;; *) WAS_RUNNING="0" ;; esac
# ── Merge auf main (Box, isolierter Worktree — wie beim MC2-Runner) ─────────
status "laeuft" "Merge wird vorbereitet"
git -C "$LUCY" fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen"
git -C "$LUCY" rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \
|| fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)"
# Kaputt aufgesetzte Branches (kein gemeinsamer Vorfahre) → Merge kann NIE gelingen.
git -C "$LUCY" merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \
|| fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben"
cleanup_wt
git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden"
MERGE_MSG="Auftragsbuch: Lucy-Vorschlag '$BRANCH' angenommen (Ein-Klick-Gate)"
if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then
git -C "$WT" merge --abort 2>/dev/null || true
# Selbstheilung: veralteten Branch mechanisch auf main rebasen (wie im MC2-Runner).
status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht"
WT2="/tmp/annahme-rebase-$SLUG"
git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true
REBASED=""
if git -C "$LUCY" worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \
&& git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
rebase origin/main >>"$LOG" 2>&1; then
REBASED="$(git -C "$WT2" rev-parse HEAD)"
else
git -C "$WT2" rebase --abort 2>/dev/null || true
fi
git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true
[ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)"
git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \
|| { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; }
fi
status "laeuft" "Push nach main läuft"
PUSHED=0
for _ in 1 2 3; do
git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; }
sleep 5
done
[ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)"
cleanup_wt
# ── PC: main ziehen, Build detached starten ─────────────────────────────────
status "laeuft" "PC zieht den neuen Stand"
pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; if (\$LASTEXITCODE -ne 0) { exit 1 }; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 \
|| revert_main "PC konnte main nicht ziehen (git fetch/merge am PC rot)"
pc_shell "if (Test-Path '$PC_RUNNER') { exit 0 } else { exit 1 }" >>"$LOG" 2>&1 \
|| revert_main "PC-Runner-Skript fehlt im Lucy-Repo (deploy/lucy-annahme.ps1)"
# WICHTIG: -File-Pfad MUSS eingebettete Anführungszeichen tragen — Start-Process fügt
# die ArgumentList-Elemente OHNE Quoting zusammen; 'F:\Coding Stuff\…' zerbricht sonst am
# Leerzeichen und die gespawnte powershell stirbt still (erster Live-Lauf, 12.07.).
pc_shell "Remove-Item -Force '$PC_STATUSFILE' -ErrorAction SilentlyContinue; Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"$PC_RUNNER\"','-WasRunning','$WAS_RUNNING'" >>"$LOG" 2>&1 \
|| revert_main "PC-Build ließ sich nicht starten"
# ── Status-Datei pollen, Fortschritt in die UI spiegeln ─────────────────────
status "laeuft" "PC baut Lucy (dist) — das dauert ein paar Minuten"
DEADLINE=$(( $(date +%s) + BUILD_TIMEOUT_S ))
LAST_DETAIL=""
FINAL=""
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
sleep 10
RAW="$(pc_shell "if (Test-Path '$PC_STATUSFILE') { Get-Content -Raw '$PC_STATUSFILE' }" 2>>"$LOG")" || continue
[ -n "$RAW" ] || continue
# Parse OHNE f-String-Escapes: \" in f-String-Ausdruecken ist seit Python 3.12/3.14
# ein SyntaxError (alter Pre-3.12-Stil) — genau das legte den ersten Live-Lauf lahm.
PARSED="$(printf '%s' "$RAW" | python3 -c '
import json, sys
try:
d = json.loads(sys.stdin.read())
except Exception:
sys.exit(0)
detail = str(d.get("detail", "")).replace("|", "/").replace("\n", " ")
done = "1" if d.get("done") else "0"
ok = "1" if d.get("ok") else "0"
print(done + "|" + ok + "|" + detail)
')"
[ -n "$PARSED" ] || continue
DONE="${PARSED%%|*}"; REST="${PARSED#*|}"; OK="${REST%%|*}"; DETAIL="${REST#*|}"
if [ "$DETAIL" != "$LAST_DETAIL" ] && [ -n "$DETAIL" ]; then
LAST_DETAIL="$DETAIL"
status "laeuft" "PC: $DETAIL"
fi
if [ "$DONE" = "1" ]; then FINAL="$OK"; break; fi
done
[ -n "$FINAL" ] || revert_main "PC-Build-Timeout nach $((BUILD_TIMEOUT_S/60)) Minuten (Status-Datei blieb offen)"
[ "$FINAL" = "1" ] || revert_main "${LAST_DETAIL:-PC meldet Fehler ohne Detail}"
# ── Grün: Branch aufräumen, ehrlich melden ──────────────────────────────────
git -C "$LUCY" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true
git -C "$LUCY" fetch -q --prune origin 2>/dev/null || true
status "eingespielt" "${LAST_DETAIL:-Build grün}"
if [ "$WAS_RUNNING" = "1" ]; then
notify "Lucy-Vorschlag '$BRANCH' ist LIVE — dist am PC neu gebaut, Lucy neu gestartet, Stimme meldet sich. ✅"
else
notify "Lucy-Vorschlag '$BRANCH' ist eingespielt — dist am PC neu gebaut. Lucy war aus und bleibt aus; die neue Version startet beim nächsten Öffnen. ✅"
fi
exit 0
+1 -13
View File
@@ -97,21 +97,9 @@ for x in sorted(off, key=lambda x: x.get("title") or ""):
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:100]))
' 2>/dev/null || echo "(Queue nicht lesbar)"
echo
echo "## Offene Karten im Auftragsbuch"
curl -sf -m 10 "$API/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 sorted(items, key=lambda x: x.get("branch") or ""):
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
echo
echo "## Feste Leitplanken (Kurzfassung — Details docs/wissen/)"
echo "- Jede Code-Zeile geht NUR ueber eine Auftragsbuch-Karte live (Klick des Commanders)."
echo "- Jede Code-Zeile geht NUR als Branch nach Gitea live: Ampel gruen, Merge durch den Commander, deploy.sh."
echo "- TABU ohne explizites User-Ja: Security (approvals/Tokens/ufw/sudoers), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode."
echo "- Updates macht der Auto-Update-Timer mit Fangnetz — niemand sonst."
} > "$STECK.tmp" 2>/dev/null
+2 -2
View File
@@ -83,8 +83,8 @@ heavy-Rollen einer Runde am Stueck (heavy bleibt warm), DANN der Skeptiker (ein
dieses Projekt nicht (noetige Komponente gibt es nur als Image o. ae.), sag das im Konzept
AUSDRUECKLICH mit Begruendung — still ignorieren gilt nicht.
- **★ Explizite Commander-Wahl UEBERSTIMMT deine Einschaetzung.** Steht im Auftrag eine Zeile
„AUFWAND (vom Commander gewaehlt): SIMPEL …" bzw. „… GRUENDLICH …" (der Commander hat im
Auftragsbuch „IDE: simpel" oder „IDE: gruendlich" geklickt), dann HALTE DICH DARAN — SIMPEL =
„AUFWAND (vom Commander gewaehlt): SIMPEL …" bzw. „… GRUENDLICH …" (der Commander hat in der
Ideen-Ansicht „IDE: simpel" oder „IDE: gruendlich" gewaehlt), dann HALTE DICH DARAN — SIMPEL =
2 Rollen/1 Runde, GRUENDLICH = volle 3-5 Rollen/bis 3 Runden — auch wenn deine eigene
Schaetzung anders ausfaellt. Nur wenn KEINE solche Zeile da ist, entscheidest du selbst.
- **Max 3 Denk-Runden — keine Endlosschleife.** Nach der letzten Runde (je nach Groesse 1-3) kommt
+1 -1
View File
@@ -17,5 +17,5 @@ Dieser Skill fungiert als dein persönlicher, KI-gesteuerter Sysadmin, der dir j
1. **System-Metriken abrufen**: Ruft den Health-Status und Load der Box ab.
2. **Logs durchkämmen**: Sucht nach Auffälligkeiten, Fehlern oder Crashes in der Nacht.
3. **News Fetching**: Liest die Top-Posts von Hacker News (oder anderen Tech-Feeds), filtert sie nach AI-Relevanz und fasst die 3 wichtigsten Entwicklungen zusammen.
4. **Auftragsbuch Check**: Prüft, ob neue unentschiedene Vorschläge im Auftragsbuch liegen.
4. **Offene Branches**: Nennt ungemergte Vorschlags-Branches auf Gitea (wartung/*, feature/*, doku/*), falls welche warten.
5. **Report Generierung**: Bündelt all diese Informationen in eine kompakte, handyfreundliche (sehr kurze) Nachricht und schickt sie via Telegram an den Commander.
-169
View File
@@ -1,169 +0,0 @@
# Auftragsbuch — die Vorschlags-Inbox der Box
Das Auftragsbuch ist das Mensch-Gate des propose-only-Kreislaufs als Klick statt
Git-Handarbeit: alles, was die Box von allein baut oder sich ausdenkt, landet hier
als Karte — du entscheidest mit **Annehmen** oder **Ablehnen**.
## Die Ideen-Queue davor (natives Hermes-Kanban, seit 10.07.2026)
Vor dem Auftragsbuch liegt DIE eine Ideen-Queue: das native Hermes-Kanban
(`hermes kanban`, SQLite-Board, Dispatcher läuft im Gateway). Der Weg einer Idee:
1. **Idee rein — Tür egal:** Eingabefeld im Auftragsbuch-Tab (`POST /api/ideen`),
Telegram/Desktop (natives Tool `kanban_create`, Regel in SOUL.md) oder Lucys
Sprech-Leitung (`idee_notieren` in `mcp/mcp_voice.py`). Alles landet als
`triage`-Aufgabe auf demselben Board.
2. **Die Box arbeitet sie aus:** der Kanban-Specifier macht aus der rohen Idee
einen sauberen Auftrag und wählt das Worker-Profil (Routing über die
Profil-Beschreibungen: Code → `werkstatt` [Qwen3-Coder-Next, Leitplanken in
`deploy/werkstatt-SOUL.md`], Doku/Analyse → `default`).
3. **Der Worker baut** im isolierten Workspace; Code-Ergebnisse pusht er als
Vorschlags-Branch auf Gitea — der hier als Karte erscheint. **Das Gate bleibt
dein Klick.**
**Die Queue füttert sich auch selbst — Idle-Radar (S4 „Zündung", 12.07.2026):** ein
nächtlicher Hermes-Cron (03:45, `deploy/idle-radar-feed.sh`) destilliert aus den
Journal-Fehlermustern der letzten 24 h und der jüngsten Traum-Notiz bis zu **2 belegte**
Wartungs-Kandidaten und legt sie als rohe Ideen (`triage`, `created_by=idle-radar`) auf
dasselbe Board. Leitplanken: jedes Beleg-Zitat wird **mechanisch** gegen das Material
geprüft (erfundene Belege fliegen raus — reproduziert-oder-abgelehnt), **Stau-Bremse**
(ab 4 offenen Queue-Aufgaben oder 3 offenen Karten legt er nichts nach), dreifacher
Dedup (State-Datei `~/.hermes/state/idle-radar-gemeldet.txt`, Board-Abgleich inklusive
archivierter = verworfener Ideen, `--idempotency-key`), Security/Config/Updates tabu.
Der Radar legt nur Ideen an — gebaut wird über denselben Kreislauf, dein Klick bleibt
das Gate.
Die Queue-Sicht im Tab (Status, offene Ideen) kommt aus `hermes kanban list --json`
(`backend/services/ideen.py`); die Morgenlage bekommt `kanban stats` als
Beweismaterial. Entscheid nativ-vs-Eigenbau (Hermes-first): Probe 10.07.2026 —
Dispatcher, Specifier, Profil-Modellwahl und Artefakt-Tracking sind nativ E2E
bewiesen; ein Eigenbau hätte all das nur nachgebaut.
## Woher die Karten kommen
- **Fertige Patches** — Branches auf Gitea (`wartung/*`, `orchestrator/*`, `doku/*`,
`feature/*`), wie die Werkstatt und der Orchestrator sie hinterlassen. Die Karte
zeigt Commit-Botschaft, geänderte Dateien und den vollen Diff. Seit S3
(lucy-pipeline) aus **zwei Repos**: `mission-control-v2` (Box-Stack) und `lucy`
(Desktop-App, rosa „Lucy"-Badge auf der Karte).
- **Skill-Kandidaten** — Ideen des nächtlichen Traum-Crons aus dem Wissens-Vault
(`~/wissens-vault/skill-kandidaten/`). „Beauftragen" schickt sie als Auftrag an
die Werkstatt (Orchestrator-Skill); das Ergebnis kommt als neuer Patch zurück.
## Was „Annehmen" bei einem Patch macht (deploy/auftrag-annehmen.sh)
1. Merge des Branches in einem **isolierten Worktree** (Live-Checkout bleibt unberührt).
Kollidiert der Merge, weil main weitergelaufen ist, versucht der Runner **automatisch
einen Rebase** des Branches auf main (Selbstheilung für veraltete Vorschläge) — erst
wenn auch der kollidiert, fällt die Karte mit ehrlicher Meldung durch.
2. `py_compile`-Gate über die geänderten Python-Dateien.
3. Push nach `main` auf Gitea (mit Retry).
4. Deploy (als /tmp-Kopie von deploy.sh — Selbst-Reset-Falle) — die Zentrale
startet dabei kurz neu.
5. Health-Check mit Geduld. **Rot ⇒ automatischer Revert des Merges + Redeploy +
Alarm.** Grün ⇒ Branch wird aufgeräumt, Meldung an Telegram + Chronik.
Der Lauf ist eine eigene systemd-Unit (detached) — er überlebt den Neustart des
Backends, das ihn gestartet hat. Fortschritt steht live auf der Karte
(`/srv/models/mc2-auftragsbuch.json`).
**Kaputt aufgesetzte Branches** (Worker machte `git init`/Shallow statt zu klonen → kein
gemeinsamer Ursprung mit main, „refusing to merge unrelated histories") erkennt das
Auftragsbuch an der Quelle: rote Markierung „kein gemeinsamer Ursprung", der
Annehmen-Knopf fehlt (Annehmen wäre ein garantierter Fehllauf), API und Runner halten
zusätzlich dicht. Ebenso markiert: „Diff leer — bringt nichts" (Branch würde gegenüber
main nichts ändern). In beiden Fällen: **ablehnen mit Grund** und die Idee frisch in die
Queue geben; die Prävention (voll klonen, merge-base-Selbstcheck, fetch+rebase vor dem
Push) steht in der werkstatt-SOUL.
## Was „Annehmen" bei einer LUCY-Karte macht (deploy/lucy-annahme.sh)
Lucy (Electron) lebt auf dem Windows-PC; ihr `dist` liegt nicht im Repo — die Box
kann sie nicht bauen. Der Lucy-Runner spannt darum den PC ein (PC-Executor :7777,
Token aus `~/.hermes/config.yaml`):
1. **Vorprüfung am PC, VOR dem Merge:** Executor erreichbar? Arbeitskopie
`F:\Coding Stuff\lucy` auf `main` und sauber? Läuft Lucy gerade? Scheitert etwas,
bricht der Lauf ab, ohne irgendetwas zu verändern — der PC muss an sein.
2. Merge des Branches im isolierten Worktree von `~/lucy` → Push nach `main`.
3. Der PC zieht `main` (ff-only) und startet **detached**
`deploy/lucy-annahme.ps1` (liegt im Lucy-Repo): ggf. `npm ci` (nur wenn sich
das Lockfile geändert hat) → `tsc --noEmit`-Gate → dist-Backup → `npm run dist`
→ Lucy-Neustart **nur, wenn sie vorher lief** (User-Entscheid: Lucy startet
nur, wenn der Commander sie will) → Stimme-Health (`:8130`).
4. Die Box pollt die PC-Status-Datei (`.lucy-annahme.json`) und spiegelt den
Fortschritt live auf die Karte. **Rot ⇒ Merge wird automatisch revertiert**,
der PC stellt dist aus dem Backup wieder her — die laufende Lucy bleibt die
alte. Grün ⇒ Branch aufgeräumt, Meldung an Telegram + Chronik.
## Bagatellen ohne Klick (mc2-bagatell.timer, 04:10)
Einzige Ausnahme vom Klick-Gate (User-Entscheid 10.07.2026, bewusst eng geschnitten):
`deploy/bagatell-annahme.sh` spielt nachts Vorschlags-Branches ein, deren Diff
**ausschließlich `.md`-Dateien anlegt oder ändert** — keine Code-Zeile, keine
Löschungen, keine Renames, keine Skripte. Der Weg ist derselbe Annahme-Runner wie
beim Klick (Merge → Gate → Deploy → Health → Auto-Revert), maximal 2 pro Nacht und
nur Branches ohne vorherige Fehl-Läufe. Fangnetz: Nacht-Sicherung 03:30 liegt davor,
jede Annahme steht in Chronik + Telegram, die Morgenlage 04:30 berichtet. Die Klasse
erweitert nur der Commander, nie das Skript.
## „Lucy kennt sich" (12.07.2026): Selbstbild, Empfehlungs-Stempel, Lern-Gedächtnis
- **Selbst-Inventur (Cron 03:35, `deploy/selbst-inventur.sh`):** erhebt den
**Selbst-Steckbrief** der Box live aus der Realität (Versionen, Dienste, Modelle,
Crons, Queue, Karten → `~/.hermes/state/selbst-steckbrief.md`) und **bemerkt selbst,
wenn sich etwas ändert** (Diff gegen gestern → stille Chronik-Karte + kurze
Telegram-Zeile; keine Änderung = still). Der Steckbrief ist das „was es schon
gibt"-Gedächtnis: Idle-Radar-Analyst und Werkstatt (Realitäts-Check-Regel) lesen ihn,
bevor sie vorschlagen oder bauen.
- **Karten-Gutachter (Cron 04:00, `deploy/karten-gutachter.sh`):** stempelt jede neue
Karte mit **„Empfehlung: Annehmen/Ablehnen/Unklar" + einem ehrlichen Satz** —
geprüft werden Redundanz (existiert das laut Steckbrief schon?), Risiko und Nutzen.
Der Stempel ist eine Meinung, kein Gate; ein nachgeschobener Commit macht ihn
ungültig (wird neu gestempelt). Ablage `/srv/models/mc2-karten-gutachten.json`.
- **Ablehnen mit Grund:** Beim Ablehnen einer Karte kannst du der Box in einem Satz
sagen, *warum* („gibt es schon", „will ich nicht"). Der Grund landet in
`/srv/models/mc2-ablehnungen.jsonl` und wird Radar/Analysten als „NIE wieder
vorschlagen"-Material vorgelegt — dein Klick wird damit zum Lehrer, nicht nur zum Tor.
- **Radar-Treffer → Queue:** Meldet der wöchentliche Release-Radar einen echten Treffer,
legt er zusätzlich EINE rohe Idee in die Ideen-Queue („Probe bauen?", idempotent je
Release-Tag) — der Blick nach draußen füttert damit denselben Kreislauf.
## Curator Auto-Archivierung (Cron `0 3 * * *` — täglich um 03:00)
**Zweck:** Automatische Archivierung inaktiver Skills nach 14 Tagen ohne Nutzung.
**Skripte:**
- `~/.hermes/scripts/curator-archive-watchdog.sh` — Cron-Einstieg, ruft das Python-Skript auf.
- `~/.hermes/scripts/curator_archive_logic.py` — Hauptlogik. Liest `.curator_state` und `.usage.json` aus `~/.hermes/skills/`, prüft auf Inaktivität (>14 Tage seit letztem Gebrauch), überspringt gepinnte und exkludierte Skills, verschiebt Skills nach `.archive/<Kategorie>/`.
- `~/.hermes/scripts/curator-idle-watchdog.sh` — Zusätzlicher Watchdog (Sonntag 00:00, Cron `0 0 * * 0`): prüft, ob der Curator selbst ≥14 Tage idle ist und triggert ggf. eine Reaktivierung.
**Konfiguration** (`~/.hermes/skills/.curator_state`):
- `archivierung.enabled: false` — Archivierung muss explizit aktiviert werden.
- `archivierung.threshold_days: 14` — Tage ohne Nutzung bis zur Archivierung.
- `archivierung.excluded_skills: [...]` — Liste der nie zu archivierenden Skills.
- `archivierung.last_archive_run_at` — Zeitstempel des letzten Durchlaufs.
**Ablauf im Durchlauf:**
1. Prüft, ob Archivierung aktiviert ist.
2. Läuft alle Skills durch `.usage.json`.
3. Überspringt: gepinnte Skills, exkludierte Skills, aktive Skills (<14 Tage).
4. Archiviert: State auf "archived" setzen, Verzeichnis nach `.archive/<Kategorie>/` verschieben, Usage-Datei aktualisieren.
5. Protokolliert in `~/.hermes/logs/curator/archive-<Datum>.log`.
**Sicherheit:**
- Archivierung standardmäßig deaktiviert — passiert nie ohne explizite Freigabe.
- Gepinnte und exkludierte Skills werden nie berührt.
- Archivierung ist reversibel: Skills können manuell aus `.archive/` zurückverschoben werden.
## Leitplanken
- Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses
Buch; einzige Ausnahme sind die eng geschnittenen Doku-Bagatellen (oben).
- „Frontend ohne Build"-Warnung (nur mc2): enthält ein Branch `frontend/src` ohne
frisches `frontend/dist`, bleibt die Oberfläche nach dem Annehmen alt, bis am PC
gebaut wird. Lucy-Karten brauchen die Warnung nicht — dort baut IMMER der PC.
- Ablehnen löscht nur den Remote-Branch; die Historie auf Gitea bleibt.
Diese Datei kam übrigens selbst über das Auftragsbuch ins Repo — als erste echte
End-to-End-Annahme (Probe-Branch `wartung/annahme-probe`, 08.07.2026).
-1
View File
@@ -8,7 +8,6 @@
## Die Bereiche (Sidebar, Stand 09.07.2026)
- **Cockpit** — deine Box auf einen Blick (Status, Leistung, Morgenlage, Erinnerungen).
- **Auftragsbuch** — Vorschläge der Box (Werkstatt/Orchestrator/Traum) per Ein-Klick annehmen/ablehnen.
- **Modelle** — Speicherleiste, installierte Modelle + neue finden/laden, Rollen (hermes/fast/heavy/…).
- **Gedächtnis** — geteilte Fakten/Regeln, die ALLE Tools (Hermes, Desktop, PC) via MCP lesen/schreiben.
- **Wissen** — Lucys Wissens-Vault (Traum-Notizen, `[[verlinkt]]` navigierbar).
+4 -5
View File
@@ -8,7 +8,7 @@ lies das hier KOMPLETT, bevor du irgendetwas empfiehlst oder änderst._
**Notfall + Außen-Review. KEIN Großbau-Partner.**
Den Alltag (Features bauen, Wartung, Ideen umsetzen) macht die Box selbst über ihre
Queue→Karte→Klick-Pipeline. Du wirst gerufen, wenn:
Ideen-Queue und Branch→Ampel→Merge-Pipeline. Du wirst gerufen, wenn:
1. **Notfall** — etwas ist kaputt und die Box kann sich nicht selbst heilen.
2. **Außen-Review** — ein Fremd-Blick auf Code/Architektur ist gewünscht
@@ -34,8 +34,7 @@ Eine 100 % lokale KI-Appliance („die Box", AMD Strix Halo, 128 GB) mit llama.c
llama-swap als Engine, Hermes-Agent als Runtime (Persona „Lucy"), MC2 (dieses Repo: FastAPI
+ React) als Web-Admin-Konsole, Mem0 als Gedächtnis, und einer Electron-Voice-Begleiterin
„Lucy" auf dem Windows-PC (EIGENES Repo `F:\Coding Stuff\lucy`). Die Box wartet sich selbst
(nächtliche Crons: Traum, Chef-Gutachter, Radar; Backups in 3 Schichten; Auftragsbuch mit
Ein-Klick-Annahme). Der Mensch ist das Gate für jede Code-Zeile.
(nächtliche Crons: Traum, Chef-Gutachter, Radar; Backups in 3 Schichten). Der Mensch ist das Gate für jede Code-Zeile.
## Harte Leitplanken (nicht verhandelbar)
@@ -44,8 +43,8 @@ Ein-Klick-Annahme). Der Mensch ist das Gate für jede Code-Zeile.
ohne Bench, Hermes-Quellcode nie forken.
- **Security-Config (approvals/Tokens/ufw/sudoers) nur MELDEN, nie ändern.**
- **`frontend/dist` ist ABSICHT im Git** (Box hat kein Node) — kein Fehler.
- **Propose-only:** Änderungen als Branch/Diff vorschlagen; Merge+Deploy läuft über das
Auftragsbuch (User-Klick) oder ein explizites User-Ja.
- **Propose-only:** Änderungen als Branch/Diff vorschlagen; Merge+Deploy macht nur der User
(Ampel grün, dann `deploy.sh`).
- **Verifizieren vor Behaupten:** Behauptungen über Live-Verhalten nur nach echter Messung
auf der Box. Am 07.07. hat ein Gemini-Umbau Lucys STT still von Box-Parakeet auf
einkernigen PC-Whisper umgestellt und das Gegenteil behauptet — die Messung entlarvte es.
+1 -1
View File
@@ -11,7 +11,7 @@ höchstens „mach". Dieses Blatt ist NUR für den Fall, dass etwas klemmt.
| „… zurückgerollt … GEPINNT" | Update war schlecht, alte Version läuft wieder | keiner (läuft stabil weiter) |
| „KRITISCH: …" | Update UND Rollback kaputt | siehe „Box tot?" unten |
| „🛰️ Evolution-Radar …" | monatlicher Chancen-Report | lesen; bei Interesse „mach" antworten |
| „[Werkstatt] … Vorschlag liegt bereit" | Box hat einen Fix vorbereitet | im Web-UI → **Auftragsbuch** → Karte ansehen → „Annehmen" oder „Ablehnen" klicken |
| „[Werkstatt] … Vorschlag liegt bereit" | Box hat einen Fix vorbereitet | Branch auf Gitea ansehen → Ampel grün? → selbst auf main mergen → `deploy.sh` |
## Box tot / Weboberfläche weg?
+1 -1
View File
@@ -11,7 +11,7 @@ Der Skill beschreibt einen alternativen Git-Workflow für Fälle, in denen der d
3. Änderungen im Clone gemacht, committet, gepusht.
4. Ein Pull Request via Gitea-REST-API auf `main` erstellt.
Das Ergebnis landet als Vorschlags-Branch auf Gitea und geht über das Auftragsbuch ins Live-System — der Commander muss es annehmen.
Das Ergebnis landet als Vorschlags-Branch auf Gitea — der Commander merged es selbst, wenn die Ampel grün ist.
## Wann er zum Einsatz kommt
+2 -2
View File
@@ -27,7 +27,7 @@ _Kuratiert 10.07.2026. Gilt für JEDEN Agenten, der an diesem System arbeitet
2. **Scope challengen.** Ein Tool-/Technik-Vorschlag (auch vom User) ist kein
Umsetzungsbefehl: erst Use-Case klären, bei erkennbarer Fehlannahme kurz stoppen und
warnen. Lehrbuch-Best-Practice ≠ unser Use-Case (Beispiele: LobeChat, Tool Search).
3. **Propose-only + Mensch-Gate.** Jede Code-Zeile ist Klick-pflichtig (Auftragsbuch-Karte).
3. **Propose-only + Mensch-Gate.** Jede Code-Zeile geht als Branch nach Gitea; gemergt wird nur vom User (Ampel grün).
Merge/Deploy NIE ohne grünes Gate. Bagatell-Klassen ohne Klick sind ausschließlich:
Doku/Wiki/Vault/Chronik/tote Dateien — nichts mit Code-Logik, immer Morgenlage-Bericht.
4. **Security-Config NIE ohne explizites User-Ja** (approvals, Tokens, ufw, sudoers,
@@ -49,7 +49,7 @@ _Kuratiert 10.07.2026. Gilt für JEDEN Agenten, der an diesem System arbeitet
- **Hermes Desktop** = Arbeit: Projekte, Coding, Review, lange Threads (cli-Lane).
Faustregel: „Repo oder >2 Minuten → Desktop".
- **Telegram** = mobil + Melde-/Freigabe-Kanal (Alarmkette, Cron-Delivery).
- **MC2-Web-UI** = Verwaltung, Auftragsbuch (Karten-Klicks), Chronik/Zeitmaschine, Wissen.
- **MC2-Web-UI** = Verwaltung, Ideen, Chronik/Zeitmaschine, Wissen.
**Steuerpult, kein Chat** — Gesprächs-/Voice-Funktionen gehören zu Lucy/Telegram, nicht in MC2.
- **Gemini/Antigravity** = Notfall + Außen-Review, siehe [GEMINI_BRIEFING](../GEMINI_BRIEFING.md).
+4 -7
View File
@@ -30,13 +30,10 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
Worker `git init`/Shallow statt voll zu klonen, entsteht ein Branch OHNE gemeinsamen
Vorfahren — Erkennungsmuster auf der Karte: **Diff „0 Dateien" + „hinter main" ≈ ganze
Historie** (z. B. 386). Merge ist NIE möglich; oft ist der Inhalt obendrein Müll
(~/.hermes-Dateien statt Repo-Dateien). Seit 12.07.2026 markiert das Auftragsbuch solche
Karten („kein gemeinsamer Ursprung", Annehmen-Knopf weg) → ablehnen mit Grund, Idee neu
in die Queue. Prävention steht in der werkstatt-SOUL (voll klonen + merge-base-Selbstcheck).
- **Ehrlich-veraltete Branches (echter Merge-Konflikt):** die Annahme versucht seit
12.07.2026 automatisch einen Rebase auf main (isolierter Worktree, Gates laufen danach
normal). Nur wenn AUCH der Rebase kollidiert, fällt die Karte durch — dann ablehnen und
die Idee frisch einqueuen statt „am PC auflösen".
(~/.hermes-Dateien statt Repo-Dateien). (Historisch: das Auftragsbuch markierte solche Branches; es ist seit 04.09.2026
ausgebaut.) Erkennung heute: `git merge-base main <branch>` leer → Branch verwerfen. Prävention steht in der werkstatt-SOUL (voll klonen + merge-base-Selbstcheck).
- **Ehrlich-veraltete Branches (echter Merge-Konflikt):** vor dem Merge `git rebase main`
auf dem Branch; kollidiert auch das, Branch verwerfen und die Idee frisch aufsetzen.
## Box / systemd / llama-swap
+3 -3
View File
@@ -12,15 +12,15 @@ Funktion an der falschen Fläche ist Murks, auch wenn sie „funktioniert"._
| Fläche | Code/Ort | ✅ Gehört hierhin | ❌ Gehört NICHT hierhin |
|---|---|---|---|
| **MC2 Steuerpult** | `~/mission-control-v2` (backend Py + frontend React) `:9001` | Verwaltung, Auftragsbuch/Karten-Klicks, Chronik/Zeitmaschine, Wissens-Tab, Modelle/Rollen, System-Status/Logs, Ideen-Queue-**Eingabe**, Wartung/Updates | **Ein Chat-/Gesprächsfenster** oder ein Konversations-Assistent — dafür ist Lucy/Telegram/Hermes-Desktop da. MC2 wird angeschaut und geklickt, nicht „zugetextet". |
| **Lucy Chat & Stimme** | eigenes Repo `F:\Coding Stuff\lucy` (Electron, `lucy-desktop/`+`lucy-tts/`) | Voice/Zuruf, Gespräch, Avatar/Animation, proaktive Meldungen, Wake-Word/Barge-in, Zettelkasten | **Verwaltungs-/Wartungs-Oberfläche** (Update-Knöpfe, Karten-Verwaltung, System-Logs) — das ist MC2. Lucy bleibt schlank (~1-s-Sprech-Latenz schützen). |
| **MC2 Steuerpult** | `~/mission-control-v2` (backend Py + frontend React) `:9001` | Verwaltung, Chronik/Zeitmaschine, Wissens-Tab, Modelle/Rollen, System-Status/Logs, Ideen-Queue-**Eingabe**, Wartung/Updates | **Ein Chat-/Gesprächsfenster** oder ein Konversations-Assistent — dafür ist Lucy/Telegram/Hermes-Desktop da. MC2 wird angeschaut und geklickt, nicht „zugetextet". |
| **Lucy Chat & Stimme** | eigenes Repo `F:\Coding Stuff\lucy` (Electron, `lucy-desktop/`+`lucy-tts/`) | Voice/Zuruf, Gespräch (innere Stimme, kein Avatar seit 04.09.2026), proaktive Meldungen, Wake-Word/Barge-in, Zettelkasten | **Verwaltungs-/Wartungs-Oberfläche** (Update-Knöpfe, System-Logs) — das ist MC2. Lucy bleibt schlank (~1-s-Sprech-Latenz schützen). |
| **Hermes Agenten-Fundament** | `~/.hermes/hermes-agent` (Framework), `~/.hermes/config.yaml`, `SOUL.md`, `skills/`, `agent-hooks/`, `mcp/` | Agenten-VERHALTEN ändern: über **Config, SOUL, Skills, Hooks, MCP-Server, Plugins** | **Quellcode patchen/forken — NIEMALS.** Kein Edit in `~/.hermes/hermes-agent/`. Fehlt ein Feature nativ → Config/Skill/Hook/MCP drumherum, nicht die Engine ändern. |
| **Telegram** | Hermes-Platform-Lane | mobil: Meldungen empfangen, Freigaben/Zurufe, Alarmkette, Cron-Delivery | Kein Bau-Ziel für UIs. Ist ein Kanal, keine App. |
| **Hermes Desktop** | PC-Hermes, Profil `pc`, Hirn = Box `:9001/v1` | Arbeit am PC: Projekte, Coding, Review, lange Threads. Faustregel „Repo oder >2 Min → Desktop". | Nicht mit Lucy verwechseln (Zuruf/Voice) — Desktop ist die Werkbank. |
## „Ich will X bauen — wohin?" (Schnell-Entscheid)
- **Etwas verwalten/anzeigen/klicken (Status, Karten, Logs, Modelle, Backups, Updates)****MC2** (backend + frontend).
- **Etwas verwalten/anzeigen/klicken (Status, Logs, Modelle, Backups, Updates)****MC2** (backend + frontend).
- **Reden/hören/begleiten (Sprache, Gespräch, Avatar, proaktiv melden)****Lucy** (eigenes Repo).
- **Wie der Agent SICH VERHÄLT (Routing, Prompts, neue Fähigkeit, Werkzeug, Guardrail)**
**Hermes drumherum**: `config.yaml` / `SOUL.md` / ein **Skill** / ein **Hook** (`agent-hooks/`) /
+6 -4
View File
@@ -70,10 +70,12 @@ keine zweite Liste anlegen. Verdikte werden NICHT als „offen" geführt → [VE
`openai` + `base_url http://127.0.0.1:8021/v1` (Telegram spricht dann mit Lucys Stimme),
SOUL.md-Ton auf Raphael (nur mit User-Ja). Ohr-Test Box-Stimme vs. lokal mit `lucy_perf=1`.
Alles in [RAPHAEL.md](RAPHAEL.md).
- **Auftragsbuch = Leiche im Code (Fund 04.09.2026):** Router/View/Annahme-Skripte liegen noch in
main, die Box listet Branches als Karten, aber letzte Annahme 02.08. (revertiert), Kanban-Tools
seit 21.08. aus, PC-Executor-IP in der Box veraltet (`.98` statt `.22`), STACK.md nennt den Weg
trotzdem „Standard". Entscheid nötig: ausbauen + STACK.md/GRENZEN.md auf den manuellen Weg.
- **Auftragsbuch ausgebaut (04.09.2026, Branch `wartung/auftragsbuch-ausbau`):** Router, Service,
View, Annahme-Skripte, Bagatell-Annahme, `docs/AUFTRAGSBUCH.md` weg; Cockpit zeigt Ideen statt
Karten; STACK/GRENZEN/ARBEITSWEISE/README auf den Branch→Ampel→Merge-Weg. Reste, die bewusst
blieben: `deploy/werkstatt-SOUL.md` + `projektstart-SOUL.md` (Werkstatt-Persona, seit Kanban-Aus
ohnehin tot — eigener Aufräum-Faden), Chronik-Kategorie „auftragsbuch" (historische Einträge).
Auf der Box: `mc2-bagatell.timer` deaktiviert, PC-Executor-URL in `~/.hermes/config.yaml` auf .22.
- **Nach dem Ohr-Test:** VERDIKTE.md-Einträge „Electron, nicht Tauri" (Begründung Overlay
entfällt) und „STT läuft auf der BOX / Stimme lokal" ersetzen; Lucy-Repo-Altlasten
(`mini-stimme/`, Kokoro-Notebooks, `Lucy-Startklar.bat` mit totem Pfad) mit User-Ja räumen.
+3 -3
View File
@@ -84,9 +84,9 @@ Weg ist der aus AGENTS.md/STACK.md „Manuell":
Die 21.08.-Notiz sagt „ganze Abstimmung mitgezogen" — wenn dort etwas Box-spezifisch getunt
wurde, gehört es zurück ins Repo, nicht überschrieben.
**Aufräum-Kandidat (User-Entscheid):** Auftragsbuch-Router + View + Annahme-Skripte + PC-Executor-
Verweis ausbauen und STACK.md „Deploy & Pipeline" auf den manuellen Weg umschreiben — sonst
behauptet die Doku weiter einen Standard-Weg, den niemand mehr geht.
**Erledigt (04.09.2026, `wartung/auftragsbuch-ausbau`, baut auf diesem Branch auf):** Auftragsbuch-
Router, Service, View, Annahme-Skripte und Bagatell-Annahme sind raus; STACK.md „Deploy & Pipeline"
beschreibt nur noch den manuellen Weg. Wer `wartung/auftragsbuch-ausbau` merged, hat beide Branches.
## Mobil: Telegram ist die Tür, die Stimme ist dieselbe
+1 -1
View File
@@ -34,6 +34,6 @@ Dazu im Repo-Wurzelverzeichnis bzw. docs/: `AGENTS.md` (verbindliche Projekt-Reg
User-Entscheid) — dann in VERDIKTE.md den alten Eintrag ERSETZEN, nicht löschen.
3. **Verifizieren vor Behaupten:** Stand-Angaben tragen ein Datum; wer STACK.md ändert,
hat live auf der Box gemessen/gelesen, nicht vermutet.
4. Änderungen laufen wie alles über die Pipeline: Branch → Karte im Auftragsbuch → Klick.
4. Änderungen laufen wie alles über die Pipeline: Branch → Ampel grün → Merge durch den User → deploy.sh.
Reine Doku hier gehört zu den "Bagatellen ohne Klick"-Klassen (siehe ZIELBILD.md),
erscheint aber immer in Morgenlage/Chronik.
+5 -4
View File
@@ -114,10 +114,11 @@ Box-sudo braucht Passwort; NOPASSWD nur für update-engine/swap (`/etc/sudoers.d
## Deploy & Pipeline
- **Standard-Weg für Box-Arbeit = Auftragsbuch:** Vorschlags-Branch auf Gitea
(`wartung/*`, `orchestrator/*`, `doku/*`, `feature/*`) → Karte in der UI → User-Klick =
Merge + Deploy + Health + Auto-Revert (detached Runner).
- Manuell: main auf Gitea pushen → `ssh … 'bash ~/mission-control-v2/deploy/deploy.sh'`
- **Standard-Weg für Box-Arbeit (seit 04.09.2026 der EINZIGE):** Vorschlags-Branch auf Gitea
(`wartung/*`, `doku/*`, `feature/*`) → Ampel-CI grün → **der User merged auf main**
`deploy.sh` auf der Box. Das Auftragsbuch (Karten-Klick, detached Annahme-Runner, Bagatell-
Annahme, Lucy-Annahme über den PC-Executor) ist ausgebaut — es war seit 02.08. ungenutzt.
- Deploy: main auf Gitea pushen → `ssh … 'bash ~/mission-control-v2/deploy/deploy.sh'`
ODER `POST :9001/api/system/self-update` (restart meldet ok:false — harmlos, /api/health prüfen).
- deploy.sh macht `git reset --hard origin/main` (main MUSS vorher auf Gitea liegen) und baut
KEIN Frontend → dist lokal bauen + committen. Fallen: [FALLEN.md](FALLEN.md).
+3
View File
@@ -36,6 +36,9 @@ planen, **jede Session hinterlässt einen sauberen Stand**.
Loft, Overlay, Mimik, Dazwischenrufe raus; ein HUD am PC, Telegram unterwegs, **eine Stimme**
für beide (`lucy-stimme.service` auf der Box). Kein zweites Hirn auf dem PC. Details, Reihenfolge
der Annahme und der SOUL.md-Vorschlag: [RAPHAEL.md](RAPHAEL.md).
9. **Auftragsbuch ausgebaut (04.09.2026).** Die Queue→Karte→Klick-Mechanik aus Punkt 2/3/5 war
seit 02.08. ungenutzt (Kanban-Tools seit 21.08. aus). Gate ist jetzt: Ampel grün + Merge durch
den User + `deploy.sh`. Bagatell-Annahme ohne Klick (Punkt 5) entfällt damit ebenfalls.
## Recherche-Fundament (10.07., Web)
@@ -1,4 +1,4 @@
import{c as B,M as T,j as e,N as p,W as g,b as n,J as D,T as G,z as L,O as $,f as R,k as F,u as I,r as q,P as y,Q as v,t as w,X as U,C as k,n as _,q as u}from"./index-l6kyKCL9.js";import{S}from"./SectionLabel-zEOM9wM3.js";import{C as K}from"./clock-QfyF1ur_.js";import{E as O}from"./external-link-ClFd8iN1.js";import{S as Q}from"./shield-CVm2T_f5.js";/**
import{c as B,M as T,j as e,N as p,W as g,b as n,J as D,T as G,z as L,O as $,f as R,k as F,u as I,r as q,P as y,Q as v,t as w,X as U,C as k,n as _,q as u}from"./index-DMY9lUCb.js";import{S}from"./SectionLabel-B7ySrBs7.js";import{C as K}from"./clock-Bth-s5Y3.js";import{E as O}from"./external-link-DcJao8h4.js";import{S as Q}from"./shield-BtE9Fxv6.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as It,r as a,j as E,at as be,W as In,au as An,$ as _n,av as Mn,aw as ht,z as Tn,n as Ln,T as Fn,b as jn}from"./index-l6kyKCL9.js";import{r as At}from"./index-GVsa8kqq.js";import{C as $n,a as Wn}from"./cloud-download-YEWUSwEY.js";import{R as Un}from"./refresh-cw-BV1qZMSR.js";import{S as Bn}from"./search-C1Zs6OOf.js";import{F as Vn}from"./file-text-nO0jfgun.js";/**
import{c as It,r as a,j as E,ar as be,W as In,as as An,$ as _n,at as Mn,au as ht,z as Tn,n as Ln,T as Fn,b as jn}from"./index-DMY9lUCb.js";import{r as At}from"./index-CVfMhtws.js";import{C as $n,a as Wn}from"./cloud-download-BGirmHxu.js";import{R as Un}from"./refresh-cw-hAcZthO8.js";import{S as Bn}from"./search-D_8DnBvS.js";import{F as Vn}from"./file-text-BSQuFPZM.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-l6kyKCL9.js";import{A as u}from"./arrow-right-DSSXd-8y.js";import{C as K}from"./chevron-down-C5keVSfm.js";/**
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-DMY9lUCb.js";import{A as u}from"./arrow-right-CNrCtUoh.js";import{C as K}from"./chevron-down-1Vx9W9jM.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{u,ag as m,k as x,j as s,y as n,ah as b,b as p,n as f,q as h}from"./index-l6kyKCL9.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
import{u,ae as m,k as x,j as s,y as n,af as b,b as p,n as f,q as h}from"./index-DMY9lUCb.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
@@ -1 +1 @@
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-l6kyKCL9.js";import{E as k}from"./external-link-ClFd8iN1.js";import{R as y}from"./refresh-cw-BV1qZMSR.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[n===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(j,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:b,disabled:o,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{K as KonsoleView};
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-DMY9lUCb.js";import{E as k}from"./external-link-DcJao8h4.js";import{R as y}from"./refresh-cw-hAcZthO8.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[n===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(j,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:b,disabled:o,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{K as KonsoleView};
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-l6kyKCL9.js";import{J as et}from"./JobsBar-HflSaqor.js";import{C as tt}from"./code-xml-QQtsFWSM.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-CnZftvep.js";import{S as ge}from"./search-C1Zs6OOf.js";import{P as nt}from"./power-DcJoPDeQ.js";import{C as $e}from"./chevron-down-C5keVSfm.js";import{L as at}from"./layers-BFKjGswj.js";/**
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-DMY9lUCb.js";import{J as et}from"./JobsBar-95gsUJZs.js";import{C as tt}from"./code-xml-Bf-y5tB9.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-2yha4rs4.js";import{S as ge}from"./search-D_8DnBvS.js";import{P as nt}from"./power-DsvD9qMX.js";import{C as $e}from"./chevron-down-1Vx9W9jM.js";import{L as at}from"./layers-CcL78b5Q.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{j as e}from"./index-l6kyKCL9.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
import{j as e}from"./index-DMY9lUCb.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
@@ -1,4 +1,4 @@
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a8 as Q,a9 as A,aa as M,ab as B,ac as P,u as J,r as f,ad as F,ae as I,c as T,af as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-l6kyKCL9.js";import{J as z}from"./JobsBar-HflSaqor.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a6 as Q,a7 as A,a8 as M,a9 as B,aa as P,u as J,r as f,ab as F,ac as I,c as T,ad as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-DMY9lUCb.js";import{J as z}from"./JobsBar-95gsUJZs.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as v,j as e,b as c,r as l,T as W,L as ae,E as ve,n as b,Q as le,Y as oe,X as de,J as Se,u as _e,k as Ce,x as ze,as as Fe,ag as Le,am as Ue,V as P,t as Ae,y as q,q as N}from"./index-l6kyKCL9.js";import{R as F}from"./refresh-cw-BV1qZMSR.js";import{F as Pe}from"./file-text-nO0jfgun.js";import{S as Be}from"./search-C1Zs6OOf.js";import{S as ce}from"./shield-CVm2T_f5.js";import{A as ne}from"./arrow-right-DSSXd-8y.js";import{C as Re}from"./clock-QfyF1ur_.js";import{E as $e}from"./external-link-ClFd8iN1.js";import{C as De,a as Ee}from"./cloud-download-YEWUSwEY.js";import{P as Oe}from"./power-DcJoPDeQ.js";/**
import{c as v,j as e,b as c,r as l,T as W,L as ae,E as ve,n as b,Q as le,Y as oe,X as de,J as Se,u as _e,k as Ce,x as ze,aq as Fe,ae as Le,ak as Ue,V as P,t as Ae,y as q,q as N}from"./index-DMY9lUCb.js";import{R as F}from"./refresh-cw-hAcZthO8.js";import{F as Pe}from"./file-text-BSQuFPZM.js";import{S as Be}from"./search-D_8DnBvS.js";import{S as ce}from"./shield-BtE9Fxv6.js";import{A as ne}from"./arrow-right-CNrCtUoh.js";import{C as Re}from"./clock-Bth-s5Y3.js";import{E as $e}from"./external-link-DcJao8h4.js";import{C as De,a as Ee}from"./cloud-download-BGirmHxu.js";import{P as Oe}from"./power-DsvD9qMX.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
var to=Object.defineProperty;var eo=(e,n,r)=>n in e?to(e,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[n]=r;var Bn=(e,n,r)=>eo(e,typeof n!="symbol"?n+"":n,r);import{c as no,r as W,an as ro,ao as io,j as $,ap as oo,z as ao,A as so,aq as uo,n as lo,b as Ae,L as Ke,a1 as co,ar as fo}from"./index-l6kyKCL9.js";import{i as Ft,c as Xn,a as Yn,b as ho,o as po,m as Zn,d as Kn}from"./string-DoZi9Vij.js";import{R as go}from"./refresh-cw-BV1qZMSR.js";import{S as yo}from"./search-C1Zs6OOf.js";import{F as _o}from"./file-text-nO0jfgun.js";/**
var to=Object.defineProperty;var eo=(e,n,r)=>n in e?to(e,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[n]=r;var Bn=(e,n,r)=>eo(e,typeof n!="symbol"?n+"":n,r);import{c as no,r as W,al as ro,am as io,j as $,an as oo,z as ao,A as so,ao as uo,n as lo,b as Ae,L as Ke,a1 as co,ap as fo}from"./index-DMY9lUCb.js";import{i as Ft,c as Xn,a as Yn,b as ho,o as po,m as Zn,d as Kn}from"./string-DoZi9Vij.js";import{R as go}from"./refresh-cw-hAcZthO8.js";import{S as yo}from"./search-D_8DnBvS.js";import{F as _o}from"./file-text-BSQuFPZM.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as r}from"./index-l6kyKCL9.js";/**
import{c as r}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as o}from"./index-l6kyKCL9.js";/**
import{c as o}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-l6kyKCL9.js";/**
import{c}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-l6kyKCL9.js";/**
import{c as a}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./index-l6kyKCL9.js";/**
import{c as e}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-l6kyKCL9.js";/**
import{c as a}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./index-l6kyKCL9.js";/**
import{c as e}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
-6
View File
@@ -1,6 +0,0 @@
import{c as a}from"./index-l6kyKCL9.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const m=a("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);export{m as H};
+1
View File
@@ -0,0 +1 @@
import{ay as r}from"./index-DMY9lUCb.js";var o=r();export{o as r};
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{aA as r}from"./index-l6kyKCL9.js";var o=r();export{o as r};
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as a}from"./index-l6kyKCL9.js";/**
import{c as a}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as o}from"./index-l6kyKCL9.js";/**
import{c as o}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./index-l6kyKCL9.js";/**
import{c as e}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-l6kyKCL9.js";/**
import{c}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-l6kyKCL9.js";/**
import{c as a}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-l6kyKCL9.js";/**
import{c as a}from"./index-DMY9lUCb.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-l6kyKCL9.js"></script>
<script type="module" crossorigin src="/assets/index-DMY9lUCb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-d4K0juid.css">
</head>
<body>
-3
View File
@@ -25,7 +25,6 @@ const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ defa
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
const IdeenView = lazy(() => import("@/features/ideen/IdeenAnsicht").then((m) => ({ default: m.IdeenView })))
const SkillsView = lazy(() => import("@/views/SkillsView").then((m) => ({ default: m.SkillsView })))
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
@@ -99,7 +98,6 @@ function CockpitSeite() {
}
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
const skillsRoute = ansicht("/skills", () => <SkillsView />)
/** Chronik: `?q=` filtert den Verlauf — Ziel der „Finden"-Einträge der Palette (P6). */
export interface ChronikSuche {
@@ -178,7 +176,6 @@ export const routeTree = rootRoute.addChildren([
indexRoute,
cockpitRoute,
ideenRoute,
auftraegeRoute,
skillsRoute,
chronikRoute,
wissenRoute,
+1 -1
View File
@@ -481,7 +481,7 @@ function IdeenSection() {
<Wahl aktiv={modus === "box"}
onWahl={() => setModus("box")}
k={{ id: "box", icon: Hammer, titel: "Die Box, allein",
text: "Für dein Heimlabor. Das Ergebnis kommt als Patch-Vorschlag ins Auftragsbuch." }} />
text: "Für dein Heimlabor. Das Ergebnis kommt als Branch nach Gitea — mergen tust du." }} />
</div>
</div>
-40
View File
@@ -490,46 +490,6 @@ export interface ModelsResp {
running?: string[]
}
// ── Auftragsbuch (Vorschlags-Inbox) ──────────────────────────────────────────
export interface AuftragStatus {
state: "laeuft" | "eingespielt" | "fehlgeschlagen" | "rollback" | "zurueckgerollt" | "kritisch" | "abgelehnt"
detail: string
ts: number
}
export interface AuftragItem {
repo?: "mc2" | "lucy" // Quelle: Box-Stack (mc2) oder Lucy-Desktop (Annahme baut am PC)
branch: string
kind: string // wartung | orchestrator | doku | feature
subject: string
body: string
author: string
ts: number | null
ahead: number
behind: number
shortstat: string
files: { status: string; path: string }[]
files_truncated: boolean
frontend_ohne_build: boolean // Frontend-Quelltext ohne dist → Box kann nicht bauen
verwaist?: boolean // kein gemeinsamer Ursprung mit main (kaputt aufgesetzt) → Merge unmöglich
leer?: boolean // Diff gegen main ist leer → bringt nichts (schon in main / am Repo vorbei)
status: AuftragStatus | null
ci_status?: "success" | "failure" | "pending" | "skipped" | null
// Empfehlungs-Stempel des nächtlichen Karten-Gutachters (Meinung, kein Gate).
empfehlung: { empfehlung: "ANNEHMEN" | "ABLEHNEN" | "UNKLAR"; satz: string; richter: string } | null
}
export interface SkillKandidat {
file: string
title: string
preview: string
mtime: number
}
export interface AuftragsbuchResp {
available: boolean
items: AuftragItem[]
skill_kandidaten: SkillKandidat[]
open_count: number
}
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
export interface IdeenItem {
id: string
+1 -12
View File
@@ -8,7 +8,6 @@ import {
api,
type AgentStatus,
type AktivitaetResp,
type AuftragsbuchResp,
type ChronikResp,
type ConnectResp,
type ConnectHealth,
@@ -56,7 +55,6 @@ export const qk = {
connectHealth: ["connect-health"] as const,
memoryGraph: ["memory-graph"] as const,
voiceTrace: ["voice-trace"] as const,
auftragsbuch: ["auftragsbuch"] as const,
ideen: ["ideen"] as const,
chronik: ["chronik"] as const,
wissen: ["wissen"] as const,
@@ -71,7 +69,7 @@ export const qk = {
// Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt).
const TAKT = {
graph: 3_000, // System + Token: seit P4 nur noch Rueckfall/Randdaten (siehe unten)
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace
normal: 10_000, // Dienste, Routing, Agent-Status, Health
gemuetlich: 30_000, // Chronik, Ideen
traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen
@@ -96,15 +94,6 @@ const relax = (ms: number) => () => (sseVerbunden() ? ms * 5 : ms)
// 3 s → 60 s ist eine Zwanzigstel-Last; abschalten hätte diese Felder einfrieren lassen.
const langsamWennStrom = (ms: number) => () => (sseVerbunden() ? 60_000 : ms)
// Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende
// Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden.
export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) =>
useQuery({
queryKey: qk.auftragsbuch,
queryFn: () => api<AuftragsbuchResp>("/api/auftragsbuch"),
refetchInterval: relax(refetchInterval),
})
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) =>
useQuery({
+1 -3
View File
@@ -1,7 +1,6 @@
import {
LayoutDashboard,
Boxes,
Inbox,
History,
Library,
Plug,
@@ -12,7 +11,7 @@ import {
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "ideen" | "auftraege" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type ViewId = "dashboard" | "ideen" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
@@ -35,7 +34,6 @@ export const NAV: NavItem[] = [
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
{ id: "ideen", pfad: "/ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
{ id: "auftraege", pfad: "/auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
{ id: "skills", pfad: "/skills", label: "Skills & Jobs", hint: "Autonome Skills manuell auslösen", icon: Bot, group: "Operativ" },
{ id: "chronik", pfad: "/chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
{ id: "wissen", pfad: "/wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
-425
View File
@@ -1,425 +0,0 @@
import { useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, ArrowRight,
} from "lucide-react"
import { useNavigate } from "@tanstack/react-router"
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
import { fmtWhen } from "@/lib/format"
import { SectionLabel } from "@/components/ui/SectionLabel"
// Auftragsbuch = die Vorschlags-Inbox der Box. Werkstatt/Orchestrator liefern propose-only-
// Branches, der Traum liefert Skill-Kandidaten — hier fällt die Entscheidung per Klick.
// Annehmen (Branch) = Merge→Push→Deploy→Health mit Auto-Rollback; alles detached auf der Box.
const KIND_LABEL: Record<string, string> = {
wartung: "Werkstatt",
orchestrator: "Orchestrator",
doku: "Doku",
feature: "Feature",
}
const STATE_UI: Record<string, { label: string; cls: string; spin?: boolean }> = {
laeuft: { label: "wird eingespielt …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
rollback: { label: "Rollback läuft …", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10", spin: true },
eingespielt: { label: "eingespielt ✓", cls: "text-emerald-300 border-emerald-500/40 bg-emerald-500/10" },
fehlgeschlagen: { label: "fehlgeschlagen", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
zurueckgerollt: { label: "zurückgerollt", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
kritisch: { label: "KRITISCH — Box prüfen", cls: "text-red-200 border-red-500/60 bg-red-500/20" },
abgelehnt: { label: "abgelehnt", cls: "text-muted-foreground border-border/60 bg-background/30" },
}
export function AuftragsbuchView() {
const navigate = useNavigate()
const { data, isLoading, refetch, isFetching } = useAuftragsbuch()
// Geteilter Cache mit der IdeenSection — hier nur fürs ehrliche "Nichts zu entscheiden"-
// Banner (eine hängende Karte direkt darunter widerspräche dem grünen Häkchen).
const { data: ideenData } = useIdeen()
const qc = useQueryClient()
const { showAlert, showConfirm, dialogElement } = useDialog()
const [busy, setBusy] = useState<Record<string, boolean>>({})
const reload = () => qc.invalidateQueries({ queryKey: qk.auftragsbuch })
async function act(key: string, path: string, body: object, done?: string) {
setBusy((b) => ({ ...b, [key]: true }))
try {
await api(path, { method: "POST", body: JSON.stringify(body) })
if (done) showAlert("Erledigt", done)
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, [key]: false }))
}
}
const items = data?.items ?? []
const kandidaten = data?.skill_kandidaten ?? []
// Hängende Ideen leben seit 25.07. auf der eigenen Ideen-Seite. Das grüne „Nichts zu
// entscheiden" darf trotzdem nicht behaupten, alles sei erledigt — es zeigt jetzt einen
// Querverweis statt die Karte selbst (vorher stand sie direkt darunter).
const blockedIdeen = ideenData?.available ? ideenData.items.filter((i) => i.status === "blocked").length : 0
const empty = items.length === 0 && kandidaten.length === 0
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-3">
<div>
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
Auftragsbuch
</h1>
<p className="text-sm text-muted-foreground">
Was die Box gebaut oder sich ausgedacht hat du entscheidest. Annehmen spielt es mit Fangnetz ein (Health-Check, automatischer Rollback).
</p>
</div>
<button onClick={() => refetch()}
className="flex h-9 items-center gap-1.5 rounded-lg border border-border/60 bg-card/45 px-3 text-xs font-semibold text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all cursor-pointer">
<RefreshCw className={cn("h-3.5 w-3.5", isFetching && "animate-spin")} /> Aktualisieren
</button>
</div>
{data && !data.available && (
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300">
Das Auftragsbuch lebt auf der Box (liest die Gitea-Branches dort). Im lokalen Dev-Modus ist hier nichts zu sehen.
</div>
)}
{isLoading && (
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> Vorschläge werden geladen
</div>
)}
{data?.available && empty && (
<div className="flex items-center gap-3 rounded-2xl border border-emerald-500/25 bg-emerald-500/5 p-5">
<Check className="h-5 w-5 shrink-0 text-emerald-400" />
<div>
<p className="text-sm font-semibold text-foreground">Nichts zu entscheiden</p>
<p className="text-xs text-muted-foreground">Keine offenen Vorschläge. Werkstatt, Orchestrator und Traum melden sich hier, sobald es etwas gibt.</p>
</div>
</div>
)}
{blockedIdeen > 0 && (
<button
onClick={() => navigate({ to: "/ideen" })}
className="flex w-full items-center gap-3 rounded-2xl border border-red-500/30 bg-red-500/5 p-4 text-left transition-all hover:border-red-500/50 cursor-pointer">
<Lightbulb className="h-5 w-5 shrink-0 text-red-300" />
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">
{blockedIdeen === 1 ? "Eine Idee hängt und braucht dich" : `${blockedIdeen} Ideen hängen und brauchen dich`}
</p>
<p className="text-xs text-muted-foreground">Hier ist nichts zu tun aber drüben in den Ideen wartet eine Frage auf deine Antwort.</p>
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-red-300/70" />
</button>
)}
{items.length > 0 && (
<section className="space-y-3">
<SectionLabel icon={GitBranch}>Patch-Vorschläge ({items.length})</SectionLabel>
{items.map((it) => (
<ProposalCard key={`${it.repo ?? "mc2"}:${it.branch}`} item={it} busy={busy} onAct={act} showConfirm={showConfirm} />
))}
</section>
)}
{kandidaten.length > 0 && (
<section className="space-y-3">
<SectionLabel icon={Sparkles}>Skill-Kandidaten aus dem Traum ({kandidaten.length})</SectionLabel>
{kandidaten.map((k) => (
<KandidatCard key={k.file} k={k} busy={busy} onAct={act} showConfirm={showConfirm} />
))}
</section>
)}
{dialogElement}
</div>
)
}
function StateBadge({ item }: { item: AuftragItem }) {
const st = item.status
if (!st) return null
const ui = STATE_UI[st.state]
if (!ui) return null
return (
<span className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-0.5 text-[10px] font-bold", ui.cls)} title={st.detail}>
{ui.spin && <Loader2 className="h-3 w-3 animate-spin" />}
{ui.label}
</span>
)
}
function CIAmpelBadge({ item }: { item: AuftragItem }) {
const ci = item.ci_status
if (!ci) return null
if (ci === "success") {
return <span className="flex items-center gap-1 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold text-emerald-400" title="CI-Ampel: Grün (Baut & läuft)"><Check className="h-3 w-3" /> CI Grün</span>
}
if (ci === "failure") {
return <span className="flex items-center gap-1 rounded-lg border border-red-500/30 bg-red-500/10 px-1.5 py-0.5 text-[10px] font-bold text-red-400" title="CI-Ampel: Rot (Fehler)"><X className="h-3 w-3" /> CI Rot</span>
}
if (ci === "pending") {
return <span className="flex items-center gap-1 rounded-lg border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-bold text-amber-400" title="CI-Ampel: Läuft"><Loader2 className="h-3 w-3 animate-spin" /> CI Läuft</span>
}
return null
}
function ProposalCard({
item, busy, onAct, showConfirm,
}: {
item: AuftragItem
busy: Record<string, boolean>
onAct: (key: string, path: string, body: object, done?: string) => void
showConfirm: (title: string, msg: string, ok: () => void) => void
}) {
const [open, setOpen] = useState(false)
const [diff, setDiff] = useState<string | null>(null)
const [diffLoading, setDiffLoading] = useState(false)
// Ablehnen mit Grund: der Grund ist das Lern-Gedächtnis des Kreislaufs (Radar/Specifier
// bekommen ihn als „nie wieder vorschlagen"-Material) — deshalb ein kleines Inline-Feld.
const [rejecting, setRejecting] = useState(false)
const [grund, setGrund] = useState("")
const running = item.status?.state === "laeuft" || item.status?.state === "rollback"
const decided = item.status?.state === "eingespielt"
// Lucy-Karten (Desktop-App): Annehmen baut am PC (dist) statt auf der Box zu deployen.
const repo = item.repo ?? "mc2"
const isLucy = repo === "lucy"
const k = `${repo}:${item.branch}`
async function loadDiff() {
const next = !open
setOpen(next)
if (next && diff === null) {
setDiffLoading(true)
try {
const r = await api<{ diff: string; truncated: boolean }>(`/api/auftragsbuch/diff?branch=${encodeURIComponent(item.branch)}&repo=${repo}`)
setDiff(r.diff + (r.truncated ? "\n… (gekürzt)" : ""))
} catch (e: any) {
setDiff(`(Diff nicht ladbar: ${e?.message || e})`)
} finally {
setDiffLoading(false)
}
}
}
return (
<div className="mc-card p-4 space-y-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-primary/30 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-primary">
{KIND_LABEL[item.kind] ?? item.kind}
</span>
{isLucy && (
<span className="rounded-md border border-pink-500/40 bg-pink-500/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-pink-300"
title="Vorschlag fürs Lucy-Repo — Annehmen baut die App am PC neu und startet sie neu, wenn sie läuft.">
Lucy
</span>
)}
<span className="font-mono text-[11px] text-muted-foreground truncate" title={item.branch}>{item.branch}</span>
<span className="flex items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(item.ts)}</span>
<CIAmpelBadge item={item} />
<StateBadge item={item} />
</div>
<p className="mt-1.5 text-sm font-semibold text-foreground">{item.subject || "(ohne Titel)"}</p>
{item.body && <p className="mt-1 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground line-clamp-6">{item.body}</p>}
{item.empfehlung && (
<p className={cn("mt-1.5 flex items-start gap-1.5 rounded-lg border px-2 py-1 text-[11px]",
item.empfehlung.empfehlung === "ANNEHMEN" && "border-emerald-500/30 bg-emerald-500/5 text-emerald-300",
item.empfehlung.empfehlung === "ABLEHNEN" && "border-red-500/30 bg-red-500/5 text-red-300",
item.empfehlung.empfehlung === "UNKLAR" && "border-amber-500/30 bg-amber-500/5 text-amber-300")}
title={`Nächtliches Kurz-Gutachten (${item.empfehlung.richter}) — eine Meinung, kein Muss. Die Entscheidung bleibt bei dir.`}>
<Sparkles className="mt-0.5 h-3 w-3 shrink-0" />
<span><b>Empfehlung: {item.empfehlung.empfehlung === "ANNEHMEN" ? "Annehmen" : item.empfehlung.empfehlung === "ABLEHNEN" ? "Ablehnen" : "Unklar"}.</b> {item.empfehlung.satz}</span>
</p>
)}
</div>
{!running && !decided && (
<div className="flex shrink-0 items-center gap-2">
{!item.verwaist && (
<button
disabled={!!busy[k]}
onClick={() =>
showConfirm("Vorschlag annehmen?",
isLucy
? `'${item.branch}' wird im Lucy-Repo auf main gemergt, dann baut dein PC die App neu (dist) und startet Lucy neu, falls sie gerade läuft. Der PC muss dafür an sein. Schlägt Build oder Start fehl, dreht die Box den Merge automatisch zurück — die laufende Lucy bleibt die alte.`
: `'${item.branch}' wird auf main gemergt und live eingespielt. Die Zentrale startet dabei kurz neu. Geht der Health-Check danach rot, rollt die Box automatisch zurück.${item.frontend_ohne_build ? "\n\nAchtung: Enthält Frontend-Quelltext ohne gebautes Bundle — die Oberfläche bleibt alt, bis am PC gebaut wird." : ""}`,
() => onAct(k, "/api/auftragsbuch/annehmen", { branch: item.branch, repo }))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-emerald-300 transition-all hover:bg-emerald-500/20 cursor-pointer disabled:opacity-50">
{busy[k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />} Annehmen
</button>
)}
<button
disabled={!!busy["ab:" + k]}
onClick={() => setRejecting((v) => !v)}
className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50">
{busy["ab:" + k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Ablehnen
</button>
</div>
)}
</div>
{rejecting && !running && !decided && (
<div className="space-y-2 rounded-xl border border-red-500/25 bg-red-500/5 p-3">
<p className="text-[11px] text-muted-foreground">
Der Branch '{item.branch}' wird auf Gitea gelöscht (die Arbeit bleibt in der Historie).
Sag der Box kurz <b>warum</b> dann schlägt sie dir so etwas nicht wieder vor.
</p>
<input
value={grund}
onChange={(e) => setGrund(e.target.value)}
placeholder={"Grund (optional) — z. B. „gibt es schon“ oder „will ich nicht“"}
maxLength={400}
className="w-full rounded-lg border border-border/60 bg-background/60 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
/>
<div className="flex items-center gap-2">
<button
disabled={!!busy["ab:" + k]}
onClick={() => {
setRejecting(false)
onAct("ab:" + k, "/api/auftragsbuch/ablehnen", { branch: item.branch, repo, grund: grund.trim() })
}}
className="flex h-8 items-center gap-1.5 rounded-lg border border-red-500/40 bg-red-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300 transition-all hover:bg-red-500/20 cursor-pointer disabled:opacity-50">
<X className="h-3.5 w-3.5" /> Wirklich ablehnen
</button>
<button
onClick={() => setRejecting(false)}
className="flex h-8 items-center rounded-lg border border-border/60 bg-background/40 px-3 text-[11px] font-semibold text-muted-foreground transition-all hover:text-foreground cursor-pointer">
Abbrechen
</button>
</div>
</div>
)}
{item.status && (running || !decided) && item.status.detail && (
<p className="text-[11px] text-muted-foreground">{item.status.detail}</p>
)}
{item.verwaist && (
<p className="rounded-lg border border-red-500/30 bg-red-500/5 px-2 py-1.5 text-[11px] text-red-300/90">
<b>Kaputt aufgesetzt:</b> Dieser Branch hat keinen gemeinsamen Ursprung mit main (der Werkstatt-Worker
hat nicht richtig geklont) Annehmen ist technisch unmöglich, deshalb fehlt der Knopf.
Am besten <b>ablehnen mit Grund</b>; die Idee kann danach frisch in die Queue.
</p>
)}
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span className="rounded bg-background/40 px-1.5 py-0.5 font-mono">{item.ahead} Commit{item.ahead === 1 ? "" : "s"}</span>
{item.shortstat && <span className="rounded bg-background/40 px-1.5 py-0.5 font-mono">{item.shortstat.trim()}</span>}
{item.verwaist && (
<span className="flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-red-300" title="Kein gemeinsamer Vorfahre mit main — git kann diesen Branch niemals mergen.">
<AlertTriangle className="h-3 w-3" /> kein gemeinsamer Ursprung
</span>
)}
{item.leer && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="Der Diff gegen main ist leer — der Branch würde nichts verändern (Inhalt schon in main oder am Repo vorbei gearbeitet).">
<AlertTriangle className="h-3 w-3" /> Diff leer bringt nichts
</span>
)}
{item.behind > 0 && !item.verwaist && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="main ist seit diesem Vorschlag weitergelaufen — bei Konflikten versucht die Annahme automatisch einen Rebase.">
<AlertTriangle className="h-3 w-3" /> {item.behind} hinter main
</span>
)}
{item.frontend_ohne_build && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="Die Box kann kein Frontend bauen (kein Node) — nach dem Annehmen bleibt die Oberfläche alt, bis am PC gebaut wird.">
<AlertTriangle className="h-3 w-3" /> Frontend ohne Build
</span>
)}
</div>
<div>
<button onClick={loadDiff}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<FileDiff className="h-3.5 w-3.5" /> {item.files.length} Datei{item.files.length === 1 ? "" : "en"} Änderungen ansehen
</button>
{open && (
<div className="mt-2 space-y-2">
<div className="flex flex-wrap gap-1.5">
{item.files.map((f) => (
<span key={f.path} className="rounded bg-background/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<b className={cn(f.status.startsWith("A") && "text-emerald-400", f.status.startsWith("D") && "text-red-400", f.status.startsWith("M") && "text-sky-400")}>{f.status}</b> {f.path}
</span>
))}
{item.files_truncated && <span className="text-[10px] text-muted-foreground/60"></span>}
</div>
<div className="max-h-96 overflow-auto rounded-xl border border-border/50 bg-background/50 p-3">
{diffLoading
? <span className="text-[11px] text-muted-foreground">Diff wird geladen </span>
: <pre className="text-[10px] leading-relaxed text-foreground/80 whitespace-pre">{
(diff ?? "").split("\n").map((ln, i) => (
<span key={i} className={cn("block", ln.startsWith("+") && !ln.startsWith("+++") && "text-emerald-300/90 bg-emerald-500/5", ln.startsWith("-") && !ln.startsWith("---") && "text-red-300/90 bg-red-500/5", (ln.startsWith("@@") || ln.startsWith("diff ")) && "text-sky-300/90")}>{ln || " "}</span>
))
}</pre>}
</div>
</div>
)}
</div>
</div>
)
}
function KandidatCard({
k, busy, onAct, showConfirm,
}: {
k: SkillKandidat
busy: Record<string, boolean>
onAct: (key: string, path: string, body: object, done?: string) => void
showConfirm: (title: string, msg: string, ok: () => void) => void
}) {
const [open, setOpen] = useState(false)
return (
<div className="rounded-2xl border border-violet-500/25 bg-violet-500/[0.04] p-4 shadow-lg shadow-black/15 backdrop-blur-md space-y-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-violet-500/30 bg-violet-500/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-violet-300">Traum-Idee</span>
<span className="font-mono text-[11px] text-muted-foreground truncate">{k.file}</span>
<span className="flex items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(k.mtime)}</span>
</div>
<p className="mt-1.5 text-sm font-semibold text-foreground">{k.title}</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<button
disabled={!!busy["sk:" + k.file]}
onClick={() =>
showConfirm("Werkstatt beauftragen?",
`Der Kandidat '${k.title}' geht als Auftrag an die Werkstatt (Orchestrator, propose-only). Das Ergebnis erscheint später als neuer Patch-Vorschlag hier im Auftragsbuch — nichts geht ungefragt live.`,
() => onAct("sk:" + k.file, "/api/auftragsbuch/skill/annehmen", { file: k.file },
"Die Werkstatt arbeitet. Das Ergebnis taucht als neuer Vorschlag im Auftragsbuch auf (das kann einige Minuten dauern)."))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-violet-300 transition-all hover:bg-violet-500/20 cursor-pointer disabled:opacity-50">
{busy["sk:" + k.file] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Hammer className="h-3.5 w-3.5" />} Beauftragen
</button>
<button
disabled={!!busy["skab:" + k.file]}
onClick={() =>
showConfirm("Kandidat verwerfen?",
`'${k.title}' wandert ins Archiv des Wissens-Vaults (verworfen/). Der Traum schlägt ihn nicht erneut vor.`,
() => onAct("skab:" + k.file, "/api/auftragsbuch/skill/ablehnen", { file: k.file }))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50">
{busy["skab:" + k.file] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Verwerfen
</button>
</div>
</div>
<button onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<Inbox className="h-3.5 w-3.5" /> Vorschlag lesen
</button>
{open && (
<pre className="max-h-72 overflow-auto rounded-xl border border-border/50 bg-background/50 p-3 text-[11px] leading-relaxed text-foreground/85 whitespace-pre-wrap">{k.preview}</pre>
)}
</div>
)
}
+6 -6
View File
@@ -168,9 +168,9 @@ export function GuideView() {
<strong className="text-foreground"> Hermes Desktop</strong> für richtige Projekte und
<strong className="text-foreground"> Telegram</strong> für unterwegs. In der Box denken die Modelle, das
Gedächtnis lernt automatisch mit und nachts arbeitet sie allein: Sie träumt, inventarisiert sich selbst,
entwickelt Ideen zu fertigen Vorschlägen und legt sie dir als Karten ins Auftragsbuch.
<strong className="text-primary"> Nichts geht live ohne deinen Klick</strong> und wenn nach einer Annahme
etwas rot wird, baut die Box es selbst zurück.
entwickelt Ideen zu Konzepten. Gebaut wird am PC in der IDE, jede Änderung geht als Branch nach Gitea.
<strong className="text-primary"> Nichts geht live ohne deinen Merge</strong> die Ampel-CI muss grün sein,
danach spielt <code>deploy.sh</code> den Stand ein.
</p>
<SystemSchaubild />
</section>
@@ -379,8 +379,8 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
Reden tust du mit ihm über <strong>Hermes Desktop</strong> (am PC, remote via <Pill>/hermes-ui</Pill>),
die <strong>Konsole</strong> (Box-Shell im Browser), <strong>Telegram</strong> oder per Stimme über
<strong> Lucy</strong>. Er hat MCP-Server für Gedächtnis, Stack-Steuerung, Web-Fetch und
<strong> PC-Steuerung</strong> (Executor auf deinem Windows-PC). Nachts betreibt er Werkstatt &amp;
Radar-Crons alles Gebaute landet als Karte im <strong>Auftragsbuch</strong>.
<strong> PC-Steuerung</strong> (Executor auf deinem Windows-PC). Nachts betreibt er Radar-Crons
und meldet Funde in Chronik, Briefkasten und Telegram.
Status &amp; Verdrahtung im <strong>Hermes</strong>-Tab.
</Stack>
</Card>
@@ -445,7 +445,7 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
<ul className="list-disc pl-4 space-y-1">
<li><strong>Backup:</strong> PBS-Backup-Zentrum (Sicherung auf den Proxmox-Server) + tägliche Tarball-Sicherung; Restore echt durchgespielt (<Pill>restore.sh</Pill>, Doku in <Pill>docs/BACKUP.md</Pill>).</li>
<li><strong>Updates:</strong> git-basierter Update-Check; <strong>Update-Buttons</strong> mit anschließendem <strong>Gehirn-Check</strong> (rot, falls ein Update das Gedächtnis zerschießt).</li>
<li><strong>Annahme-Sicherheit:</strong> jede Auftragsbuch-Annahme läuft mit Health-Check wird's rot, baut die Box automatisch zurück (Auto-Revert).</li>
<li><strong>Deploy-Sicherheit:</strong> <code>deploy.sh</code> sichert vorher und prüft danach die Gesundheit; die Zeitmaschine in der Chronik ist der Rückweg.</li>
<li><strong>Dienste &amp; Gefahrenzone:</strong> Restart/Logs pro Dienst, kritische Eingriffe getrennt.</li>
</ul>
<p className="text-[10px] italic">Erreichbar über das System-Icon Tab Wartung".</p>
+14 -36
View File
@@ -2,13 +2,13 @@ import { useState } from "react"
import {
Boxes, Server, ShieldAlert, Plug,
HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight,
Inbox, HelpCircle,
Lightbulb, HelpCircle,
} from "lucide-react"
import { useNavigate } from "@tanstack/react-router"
import { api } from "@/lib/api"
import {
useModels, useServices, useUpdates, useAgentStatus, useConnectHealth,
useAuftragsbuch, useIdeen, useQueryClient, qk,
useIdeen, useQueryClient, qk,
} from "@/lib/queries"
import { useLucyHealth, type Repair } from "@/lib/useLucyHealth"
import { useDialog } from "@/lib/useDialog"
@@ -37,7 +37,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const { data: updates } = useUpdates()
const { data: agent } = useAgentStatus()
const { data: connect } = useConnectHealth()
const { data: auftraege } = useAuftragsbuch()
const { data: ideen } = useIdeen()
const health = useLucyHealth()
@@ -75,9 +74,8 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const pendingCount =
(updates ? (updates.os > 0 ? 1 : 0) + (updates.engine > 0 ? 1 : 0) + (updates.swap > 0 ? 1 : 0) + (updates.models > 0 ? 1 : 0) : 0) +
(updates?.components?.filter((c) => c.update === true).length ?? 0)
const openAuftraege = auftraege?.available ? auftraege.open_count : 0
// Hängende Karten warten auf DEINE Antwort — bisher sah das Cockpit sie gar nicht
// („Nichts zu tun" trotz roter Karte im Auftragsbuch, Review 15.07.).
// Hängende Ideen warten auf DEINE Antwort — das Cockpit muss sie zeigen (Review 15.07.).
const openIdeen = ideen?.available ? ideen.items.filter((i) => i.status !== "done").length : 0
const blockedIdeen = ideen?.available ? ideen.items.filter((i) => i.status === "blocked").length : 0
// Verbinden: die drei Leitungen (Modell-Gateway · Gedächtnis · Desktop-Gateway).
@@ -136,13 +134,13 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<ZoneLabel>Bereiche</ZoneLabel>
<div className="grid gap-4 grid-cols-2 md:grid-cols-3">
<CockpitTile
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
tone={!auftraege ? "loading" : blockedIdeen > 0 || openAuftraege > 0 ? "warn" : "ok"}
icon={Lightbulb} title="Ideen" value={ideen?.available ? openIdeen : "—"}
unit="offen"
tone={!ideen ? "loading" : blockedIdeen > 0 ? "warn" : "ok"}
hint={blockedIdeen > 0
? `${blockedIdeen} Karte${blockedIdeen === 1 ? "" : "n"} wartet auf deine Antwort`
: openAuftraege > 0 ? `${openAuftraege} offene Patches` : "Keine ausstehenden Vorschläge"}
onClick={() => onNavigate("auftraege")}
? `${blockedIdeen} Idee${blockedIdeen === 1 ? "" : "n"} wartet auf deine Antwort`
: openIdeen > 0 ? `${openIdeen} in Arbeit oder wartend` : "Nichts offen"}
onClick={() => onNavigate("ideen")}
/>
<CockpitTile
icon={Boxes} title="Modelle" value={running} unit="warm"
@@ -180,7 +178,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<NeedsYou
problems={health.problems}
pendingCount={pendingCount}
openAuftraege={openAuftraege}
blockedIdeen={blockedIdeen}
busy={busy}
onRepair={runRepair}
@@ -258,13 +255,12 @@ function StatusLine({ verdict, memory, warns, problems }: {
}
// „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates
// + offene Vorschläge im Auftragsbuch.
// + hängende Ideen.
function NeedsYou({
problems, pendingCount, openAuftraege, blockedIdeen, busy, onRepair, onNavigate,
problems, pendingCount, blockedIdeen, busy, onRepair, onNavigate,
}: {
problems: ReturnType<typeof useLucyHealth>["problems"]
pendingCount: number
openAuftraege: number
blockedIdeen: number
busy: Record<string, boolean>
onRepair: (id: string, r: Repair) => void
@@ -272,9 +268,8 @@ function NeedsYou({
}) {
const openDrawer = useSchublade()
const hasUpdates = pendingCount > 0
const hasAuftraege = openAuftraege > 0
const hasBlocked = blockedIdeen > 0
const nothing = problems.length === 0 && !hasUpdates && !hasAuftraege && !hasBlocked
const nothing = problems.length === 0 && !hasUpdates && !hasBlocked
return (
<div className="h-full flex flex-col justify-between">
@@ -313,7 +308,7 @@ function NeedsYou({
))}
{hasBlocked && (
<button
onClick={() => onNavigate("auftraege")}
onClick={() => onNavigate("ideen")}
className="flex w-full items-center justify-between gap-3 rounded-2xl border border-red-500/30 bg-red-500/5 p-4 text-left transition-all hover:bg-red-500/10 cursor-pointer"
>
<div className="flex items-center gap-3">
@@ -328,23 +323,6 @@ function NeedsYou({
<ChevronRight className="h-4 w-4 shrink-0 text-red-300/70" />
</button>
)}
{hasAuftraege && (
<button
onClick={() => onNavigate("auftraege")}
className="flex w-full items-center justify-between gap-3 rounded-2xl border border-primary/30 bg-primary/5 p-4 text-left transition-all hover:bg-primary/10 cursor-pointer"
>
<div className="flex items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-primary/30 bg-background/30 text-primary">
<Inbox className="h-4.5 w-4.5" />
</span>
<div>
<p className="text-sm font-bold text-foreground">{openAuftraege} Vorschl{openAuftraege === 1 ? "ag" : "äge"} im Auftragsbuch</p>
<p className="text-xs text-muted-foreground">Annehmen oder ablehnen.</p>
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-primary/70" />
</button>
)}
{hasUpdates && (
<button
onClick={() => openDrawer("wartung")}
+6 -6
View File
@@ -94,7 +94,7 @@ function Landkarte() {
</text>
<NodeBox x={444} y={54} w={396} h={62} color={T.teal} title="Mission Control (:9001)"
sub="Cockpit · Auftragsbuch (Klick-Gate) · Chronik · Wissen" />
sub="Cockpit · Ideen · Chronik · Wissen" />
<NodeBox x={444} y={126} w={396} h={62} color={T.rose} title="Hermes-Agent"
sub="denkt, nutzt Werkzeuge, betreibt die Werkstatt & Nacht-Crons" />
<NodeBox x={444} y={198} w={396} h={78} color={T.sky} title="Modell-Stall (llama-swap)"
@@ -125,14 +125,14 @@ function Kreislauf() {
const stationen = [
{ x: 14, color: T.amber, title: "Idee", sub: "von dir oder vom", sub2: "Nacht-Radar" },
{ x: 158, color: T.violet, title: "Ideen-Queue", sub: "Kanban sammelt,", sub2: "Specifier plant" },
{ x: 302, color: T.rose, title: "Werkstatt", sub: "Agent baut den", sub2: "Vorschlag (Branch)" },
{ x: 446, color: T.sky, title: "Auftragsbuch", sub: "Karte + Stempel", sub2: "vom Gutachter" },
{ x: 590, color: T.primary, title: "DEIN Klick", sub: "annehmen oder", sub2: "ablehnen (mit Grund)", strong: true },
{ x: 734, color: T.emerald, title: "Live", sub: "Merge + Deploy,", sub2: "Rot = Auto-Rückbau" },
{ x: 302, color: T.rose, title: "IDE am PC", sub: "Agent baut den", sub2: "Branch (Gitea)" },
{ x: 446, color: T.sky, title: "Ampel", sub: "CI prüft Build", sub2: "und Tests" },
{ x: 590, color: T.primary, title: "DEIN Merge", sub: "nur du bringst", sub2: "es auf main", strong: true },
{ x: 734, color: T.emerald, title: "Live", sub: "deploy.sh,", sub2: "Zeitmaschine = Rückweg" },
]
return (
<svg viewBox="0 0 880 246" role="img"
aria-label="Kreislauf: Idee, Ideen-Queue, Werkstatt, Auftragsbuch, dein Klick, live — Chronik und Morgenlage berichten zurück."
aria-label="Kreislauf: Idee, Ideen-Queue, IDE am PC, Ampel, dein Merge, live — Chronik und Morgenlage berichten zurück."
style={{ width: "100%", height: "auto", fontFamily: "var(--font-sans)" }}>
{stationen.map((s) => (
+1 -1
View File
@@ -101,7 +101,7 @@ def idee_notieren(titel: str, notiz: str = "") -> str:
"""Legt eine Idee/einen Auftrag des Commanders in die Ideen-Queue der Box. Nutzen, sobald
der Commander eine Idee für später äußert ('wäre cool wenn …', 'bau mal irgendwann …',
'die Box soll …') NICHT nur versprechen. Die Box arbeitet die Idee selbst aus und baut;
das Ergebnis erscheint als Karte im Auftragsbuch, dort entscheidet der Commander.
das Ergebnis landet als Branch auf Gitea; gemergt wird nur vom Commander (Ampel gruen).
titel = kurze deutsche Überschrift; notiz = seine Worte plus nötiger Kontext."""
try:
r = _post("/api/ideen", {"titel": titel, "notiz": notiz})