Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76d2d7c74b | |||
| 7638f0244d | |||
| 4985977492 | |||
| f65ff48599 | |||
| 87f446c5ae | |||
| f524ef4375 | |||
| 605c9d7dd1 | |||
| 9791f44644 | |||
| 1e3e0066af | |||
| 2365f7aac6 | |||
| 56b374a0aa | |||
| eef7fdb4df | |||
| cd60974ff9 | |||
| 20d4f40ed9 | |||
| cb649aac34 | |||
| d18aac0e1b | |||
| 6c14705798 | |||
| 991c9a2f82 | |||
| ad49dd65ca | |||
| c28aa4a6a2 | |||
| 8b76c0f749 | |||
| a934deee41 | |||
| 543eee95d0 | |||
| 00f2b71a63 | |||
| 2cecd06c5b | |||
| 080eaa5ff5 | |||
| 7dd44a3630 | |||
| 554db10a6f | |||
| ce030d6817 | |||
| 8996c0d012 | |||
| a1de1ec2ac |
@@ -1,72 +1,150 @@
|
||||
# Mission Control 2.0
|
||||
|
||||
Komponierbarer Local-AI-Stack für den Bosgame M5 (Strix Halo). Läuft **live auf der Box**
|
||||
als sudo-freier systemd-User-Dienst (`:9001`) und ist als **Final-Version eingefroren** —
|
||||
Pflege übernehmen ab jetzt die selbst-wartenden Box-KIs (Auto-Update mit Fangnetz + die
|
||||
„Werkstatt"), nicht mehr manuelle Releases.
|
||||
Steuerzentrale des lokalen KI-Stacks auf dem **Bosgame M5** (AMD Ryzen AI MAX+ 395 „Strix Halo",
|
||||
122 GB Unified Memory, **keine dedizierte GPU** — llama.cpp über **Vulkan/RADV**). Läuft live als
|
||||
sudo-freier systemd-User-Dienst und ist auf **Autonomie** ausgelegt: Die Box wartet sich selbst,
|
||||
prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
|
||||
|
||||
**Schichten:** Engine (llama.cpp **Vulkan/RADV** auf Strix Halo) · Router (**llama-swap**,
|
||||
Ko-Residenz-Warm-Set) · **Builtin-Routing-Gateway** in MC2 (`model: auto`, kein externer
|
||||
LiteLLM — scheitert auf Python 3.14) · Mission Control 2.0 (FastAPI + React/shadcn) ·
|
||||
**Hermes Agent** (api_server-Gateway :8642, Tools/MCP/Telegram/cron) · auto-lernendes
|
||||
**Gedächtnis** (Mem0-Sidecar, semantisch). Jede Schicht hinter stabilem Vertrag austauschbar.
|
||||
> **100 % lokal.** Kein Cloud-Modell, kein externer Dienst im Datenpfad.
|
||||
|
||||
> **Sprachassistentin Lucy** (Voice + 3D-Avatar) ist in ein **eigenes Repo** ausgelagert
|
||||
> (`Hitonabi/lucy`) und spricht über den Hermes-Gateway. Aus MC2 selbst sind die Voice-/
|
||||
> Web-Reste entfernt — MC2 ist die Steuerzentrale, kein Sprach-Frontend.
|
||||
## Die drei Bahnen
|
||||
|
||||
## Status: **live + eingefroren (MC2 Final)**
|
||||
| Bahn | Was sie tut | Wo |
|
||||
|---|---|---|
|
||||
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
|
||||
| **Coding** | Zed + OpenCode am Tag, `opencode-lauf.sh` in der Nacht — **ein Regelwerk, zwei Auslöser** | Governor `:8100` |
|
||||
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
|
||||
|
||||
Der ganze Stack ist auf der Box in Betrieb und auf **Autonomie** ausgelegt (Ziel: läuft auch
|
||||
ohne Betreuung monatelang weiter):
|
||||
Lucy holt ihr Hirn über MC2, spricht aber nie durch den Governor — ein Gespräch ist keine
|
||||
Bau-Sitzung und wird nie unterbrochen.
|
||||
|
||||
- **Modelle & Routing** — Discover (live HF + Fit/Caps), Engine-Write (register, `groups`/
|
||||
Ko-Residenz, vocab-geprüfte Spec-Drafts), Gateway `model: auto` + Fallbacks.
|
||||
- **System & Wartung** — Status (CPU/RAM/GPU/Disk), **ehrliche Speicher-Zahlen** (echte KV-Bytes
|
||||
aus GGUF-Architektur), Logs mit Fehler-Filter, Dienst-Neustart (sudo-frei).
|
||||
- **Updates mit Fangnetz** — OS/Engine/Router/Hermes per Timer: Backup → Update → Postcheck →
|
||||
bei Fehler **Auto-Rollback + Pin**; verständliche Zusammenfassung („Musst du etwas tun?") in
|
||||
Lucys Stimme. Eigene Software wird eingefroren, nicht geupdatet.
|
||||
- **Gedächtnis** — Mem0-Sidecar (auto-lernend, semantisch), 4 Kategorien, Auto-Dedupe.
|
||||
- **Selbsterhaltung** — Health-Wächter (`sentry`), Warm-Set-Wächter (`warmer`), tägliche
|
||||
Self-Smokes, Lucy-Watchdog + Alarmkette; **Werkstatt** (Hermes wartet den Stack via Gitea-
|
||||
Branch → Gate → Deploy selbst).
|
||||
## Ports & Dienste
|
||||
|
||||
API: `health · models · discover · fit · models/register · groups · routing · system/* ·
|
||||
maintenance/* · connect · memory/* · agent/* · voice/announce · reminders/*`. MCP: `mcp/`.
|
||||
Box-Runbooks: [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md), [`docs/RUNBOOK.md`](docs/RUNBOOK.md),
|
||||
[`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) + `deploy/` (Units, `deploy.sh`, `backup.sh`, `warmup.sh`).
|
||||
| Port | Dienst | Erreichbar |
|
||||
|---|---|---|
|
||||
| `9001` | **MC2** (FastAPI + React) — Cockpit, API, Konsole | LAN |
|
||||
| `8100` | **Governor** — Token-Wächter vor dem Gateway | LAN |
|
||||
| `8080` | **llama-swap** — Modell-Router | LAN |
|
||||
| `9010` | `mc2-gateway` — `/v1`-Datenpfad (`model: auto`, Bild-Weiche) | loopback |
|
||||
| `8642` · `9119` | Hermes-Gateway · Hermes-Web-UI | loopback |
|
||||
| `8765` · `8650` | Mem0-Sidecar · Voice-Sidecar (STT/TTS) | loopback |
|
||||
| `7682` | ttyd — Box-Konsole, same-origin unter `/console/` | loopback |
|
||||
|
||||
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` · `governor` ·
|
||||
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
|
||||
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
|
||||
|
||||
## Der Weg einer Idee
|
||||
|
||||
```
|
||||
Idee in MC2 → Gitea-Repo (mit CI-Ampel + VERIFY) → in Zed bauen → Ampel → main
|
||||
└── oder nachts: Hermes-Cron → opencode-lauf.sh → dieselben Regeln
|
||||
```
|
||||
|
||||
Jedes neue Repo wird von [`deploy/gitea-repo-create.sh`](deploy/gitea-repo-create.sh) **mit beiden
|
||||
Wächtern geboren**:
|
||||
|
||||
- **`.gitea/workflows/ci.yml`** — die Außen-Prüfung nach dem Push (Gitea Actions)
|
||||
- **`VERIFY`** — die Innen-Prüfung: eine Zeile, wie man das Projekt testet. Das OpenCode-Plugin
|
||||
führt sie nach jeder Etappe aus und gibt rote Tests dem Agenten **sofort** zurück, statt sie
|
||||
erst der CI zu zeigen. Keine `VERIFY`-Datei = Prüf-Tor aus.
|
||||
|
||||
## Modelle & Rollen
|
||||
|
||||
Gemessen auf der Box (25.07.2026, Vulkan, Q4):
|
||||
|
||||
| Rolle | Modell | Tempo | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| `coder` | Qwen3-Coder-Next (80B-A3B) | **51,5 t/s** · Prefill **754 t/s** | Plant und baut — Kopf der Coding-Mannschaft |
|
||||
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6 t/s** | Lucys Hirn **und** der Sucher-Subagent. Immer warm |
|
||||
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265–318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen) |
|
||||
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen |
|
||||
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30). **Nie tagsüber** — verdrängt das warme Set |
|
||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
||||
|
||||
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
||||
auf dieser bandbreitenbegrenzten Box bricht sein Prefill mit wachsendem Kontext ein (bei 32k
|
||||
gemessene 63 t/s ≈ **9 Minuten nur zum Lesen**). Mit dem 16k-Deckel bleibt der schlimmste Fall
|
||||
je Review unter einer Minute. Deshalb ist er der **Gegenleser für Etappen**, nicht der Coder —
|
||||
als Coder ist er auf dieser Box disqualifiziert (siehe VERDIKTE).
|
||||
|
||||
‼️ **Speicher-Regel:** `Warm-Set + größtes On-Demand-Modell ≤ ~115 GB`. Die ko-residente Gruppe
|
||||
(`groups.brains` in der llama-swap-Config) muss **`persistent: false`** sein — sonst räumt
|
||||
llama-swap vor einem großen Modell nicht ab und der Kernel schießt Prozesse ab. Details und
|
||||
Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
|
||||
|
||||
## Qualitäts-Tore
|
||||
|
||||
1. **Werkzeug-Zaun** ([`deploy/opencode/plugin/`](deploy/opencode/plugin/)) — blockt `git push`,
|
||||
`rm -rf`, `sudo`, `curl | sh` und Fremd-Hosts im Agentenlauf.
|
||||
2. **Prüf-Tor** — `VERIFY` nach jeder Etappe; rot → der Agent repariert selbst weiter (max. 3 Runden).
|
||||
3. **Governor** ([`deploy/governor/`](deploy/governor/)) — zählt Tokens ehrlich (aus den echten
|
||||
`usage.prompt_tokens` jeder Antwort, selbstkalibrierend). Bei ~45.000: Savepoint schreiben und
|
||||
Sitzung sauber beenden. Bei ~50.000: harter Riegel. Sichtbar als Kachel im Cockpit.
|
||||
4. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft bei jedem Push.
|
||||
|
||||
Gemeinsame Lehre dahinter: **Wissen lebt in Datei + git, nicht im schrumpfenden Chat-Kontext.**
|
||||
Kontext-Komprimierung löscht still die Regeln mit.
|
||||
|
||||
## Selbsterhaltung
|
||||
|
||||
- **Health-Wächter** (`sentry`) + **Warm-Set-Wächter** (`warmer`, per Env abschaltbar)
|
||||
- **Updates mit Fangnetz** — Backup → Update → Postcheck → bei Fehler **Auto-Rollback + Pin**
|
||||
- **Melde-Briefkasten** (`/api/voice/announce`) — alles, was die Box von sich aus sagen will;
|
||||
Lucy pollt ihn und spricht es. Absender `loop` = Coding-Ereignisse
|
||||
- **Gedächtnis** — Mem0-Sidecar, auto-lernend, semantisch, mit Auto-Dedupe
|
||||
- Tägliche Self-Smokes, Zeitmaschine (Snapshot + Ein-Klick-Restore), Chronik der autonomen Taten
|
||||
|
||||
## API (Auswahl)
|
||||
|
||||
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
|
||||
memory/* · agent/* · **governor** · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
|
||||
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
|
||||
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
|
||||
|
||||
## Entwickeln
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
python -m venv .venv && .venv/Scripts/python -m pip install -r requirements.txt # Windows
|
||||
.venv/Scripts/python -m uvicorn app:app --port 9000
|
||||
|
||||
# Frontend (Dev, proxyt /api → :9000)
|
||||
cd frontend && npm install && npm run dev # http://localhost:5173
|
||||
|
||||
# Frontend (Build → wird vom Backend ausgeliefert)
|
||||
cd frontend && npm run build # → frontend/dist
|
||||
```
|
||||
|
||||
**Frontend (Dev, proxyt /api → :9000):**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
**Frontend (Build → wird vom Backend ausgeliefert):**
|
||||
```bash
|
||||
cd frontend && npm run build # → frontend/dist
|
||||
```
|
||||
`frontend/dist` **wird mitcommittet** — die Box hat kein Node; Deploy ist ein `git pull` plus
|
||||
Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `npm run build`.
|
||||
|
||||
## Env-Vars (Auswahl)
|
||||
|
||||
| Variable | Default | Zweck |
|
||||
|---|---|---|
|
||||
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine |
|
||||
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap Config |
|
||||
| `MC_GATEWAY_URL` | `http://127.0.0.1:$MC_PORT` | Builtin-Gateway (Teil von MC2, kein externer Dienst) |
|
||||
| `MC_PORT` | `9000` | MC-Backend-Port |
|
||||
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | Aktive Engine-Binary (Vulkan-Build) |
|
||||
| `MC_ENGINE_REPO` | `ggml-org/llama.cpp` | Quelle für Engine-Update-Check |
|
||||
| `MC_DRAFTS_DIR` | `$MODELS/drafts` | Spec-Draft-Modelle (vocab-geprüft) |
|
||||
| `MC_SPEC_TYPE` | `draft-simple` | Speculative-Decoding-Typ (llama.cpp) |
|
||||
| `MC_PORT` | `9000` | MC-Backend-Port (**die Unit auf der Box setzt `9001`**) |
|
||||
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine/Router |
|
||||
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap-Config |
|
||||
| `MC_V1_UPSTREAM` | – | gesetzt → `/v1` geht an `mc2-gateway` (`:9010`) statt in-process |
|
||||
| `MC_GOVERNOR_URL` | `http://127.0.0.1:8100` | Token-Wächter für die Cockpit-Kachel |
|
||||
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | aktive Engine (Vulkan-Build) |
|
||||
| `MC_REWARM_ENABLED` | `1` | Warm-Set-Wächter (auf der Box bewusst `0`) |
|
||||
| `MC_DRAFTS_DIR` · `MC_SPEC_TYPE` | `$MODELS/drafts` · `draft-simple` | Spekulatives Dekodieren |
|
||||
|
||||
Governor-eigene Schalter (`GOV_THRESHOLD`, `GOV_HARD_CEILING`, `GOV_EXEMPT_MODELS`, …):
|
||||
[`deploy/governor/README.md`](deploy/governor/README.md).
|
||||
|
||||
## Weiterlesen
|
||||
|
||||
| Dokument | Inhalt |
|
||||
|---|---|
|
||||
| [`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) | Wie man MC2 benutzt |
|
||||
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) · [`docs/DISASTER_RECOVERY.md`](docs/DISASTER_RECOVERY.md) | Betrieb, Störungen, Wiederherstellung |
|
||||
| [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md) | Hermes-Agent, Profile, Crons |
|
||||
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
|
||||
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
|
||||
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
|
||||
| [`deploy/opencode/README.md`](deploy/opencode/README.md) | Coding-Bahn: Mannschaft, Verteilung, Fallen |
|
||||
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
|
||||
|
||||
@@ -26,6 +26,15 @@ class AntwortIn(BaseModel):
|
||||
antwort: str
|
||||
|
||||
|
||||
class ArchivAlleIn(BaseModel):
|
||||
# Leer = alle fertigen Karten der Queue; gesetzt = nur die dieses Projekts.
|
||||
projekt: str = ""
|
||||
|
||||
|
||||
class EinordnenIn(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@router.get("/ideen")
|
||||
def list_queue() -> dict:
|
||||
return ideen.list_queue()
|
||||
@@ -114,6 +123,22 @@ def archive(body: TaskIn) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/einordnen")
|
||||
def einordnen(body: EinordnenIn) -> dict:
|
||||
"""Einordnungs-VORSCHLAG beim Tippen. Wirft nie — scheitert es, kommt `ok: false`
|
||||
und die Oberfläche behält ihre Voreinstellung."""
|
||||
return ideen.einordnen(body.text)
|
||||
|
||||
|
||||
@router.post("/ideen/archivieren-alle")
|
||||
def archive_alle(body: ArchivAlleIn) -> dict:
|
||||
"""Fertige Karten sammelweise wegräumen. Fasst ausschließlich `done` an."""
|
||||
res = ideen.archive_done(body.projekt)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Archivieren fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/log")
|
||||
def get_log(id: str) -> dict:
|
||||
return ideen.log_of(id)
|
||||
|
||||
@@ -22,6 +22,9 @@ import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from config import GATEWAY_URL, V1_UPSTREAM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
|
||||
@@ -509,6 +512,101 @@ def archive_task(task_id: str) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_EINORDNEN_SYS = (
|
||||
"Du ordnest eine frisch getippte Projekt-Idee ein. Antworte AUSSCHLIESSLICH mit einem "
|
||||
"JSON-Objekt, ohne Prosa, ohne Codezaun:\n"
|
||||
'{"wer":"box"|"zed","tiefe":"schlank"|"gruendlich","nur_denken":true|false}\n\n'
|
||||
"wer=box: Aufgabe am eigenen Heimlabor des Nutzers — seine Server, seine bestehenden "
|
||||
"Dienste (Mission Control, Lucy, Rippy), Wartung, Doku, Automatisierung. Die Box baut selbst.\n"
|
||||
"wer=zed: ein NEUES eigenstaendiges Programm/Werkzeug/App, das der Nutzer danach selbst "
|
||||
"programmieren will. Er bekommt nur ein vorbereitetes Repo.\n"
|
||||
"tiefe=gruendlich: gross, vage oder mit vielen offenen Entwurfsfragen. "
|
||||
"tiefe=schlank: klein und klar umrissen.\n"
|
||||
"nur_denken=true: der Text stellt eine FRAGE oder waegt ab (\"lohnt sich\", \"waere es sinnvoll\", "
|
||||
"\"brauche ich\") statt einen Auftrag zu geben."
|
||||
)
|
||||
|
||||
|
||||
def einordnen(text: str) -> dict:
|
||||
"""Grober Einordnungs-VORSCHLAG für eine frisch getippte Idee.
|
||||
|
||||
Nur eine Vorauswahl in der Oberfläche — der Mensch sieht und überschreibt sie. Deshalb
|
||||
bewusst anspruchslos: ein einziger kurzer Aufruf ans dauerwarme Hirn (69,6 t/s gemessen),
|
||||
Schnellspur-Marker gegen das Nachdenken, hartes Timeout. Scheitert irgendetwas, kommt
|
||||
`{"ok": False}` zurück und die Oberfläche behält schlicht ihre Voreinstellung — eine
|
||||
Einordnung darf das Tippen NIE blockieren oder eine Fehlermeldung erzeugen.
|
||||
"""
|
||||
text = (text or "").strip()
|
||||
if len(text) < 12:
|
||||
return {"ok": False}
|
||||
ziel_url = (V1_UPSTREAM or GATEWAY_URL).rstrip("/")
|
||||
marker = os.environ.get("MC_NO_THINK_MARKER", "[[MC:NO_THINK]]")
|
||||
try:
|
||||
r = httpx.post(f"{ziel_url}/v1/chat/completions", timeout=12.0, json={
|
||||
"model": "hermes", "temperature": 0, "max_tokens": 60, "stream": False,
|
||||
"messages": [
|
||||
{"role": "system", "content": _EINORDNEN_SYS},
|
||||
{"role": "user", "content": f"{marker} {text[:600]}"},
|
||||
],
|
||||
})
|
||||
r.raise_for_status()
|
||||
inhalt = (r.json()["choices"][0]["message"]["content"] or "").strip()
|
||||
except Exception:
|
||||
log.debug("ideen: einordnen fehlgeschlagen", exc_info=True)
|
||||
return {"ok": False}
|
||||
|
||||
# Modelle packen JSON gern in einen Codezaun oder schreiben einen Satz davor.
|
||||
m = re.search(r"\{.*\}", inhalt, re.DOTALL)
|
||||
if not m:
|
||||
return {"ok": False}
|
||||
try:
|
||||
d = json.loads(m.group(0))
|
||||
except Exception:
|
||||
return {"ok": False}
|
||||
|
||||
wer = d.get("wer") if d.get("wer") in ("box", "zed") else None
|
||||
tiefe = d.get("tiefe") if d.get("tiefe") in ("schlank", "gruendlich") else None
|
||||
if wer is None and tiefe is None:
|
||||
return {"ok": False}
|
||||
return {"ok": True, "wer": wer or "zed", "tiefe": tiefe or "schlank",
|
||||
"nur_denken": bool(d.get("nur_denken"))}
|
||||
|
||||
|
||||
def archive_done(projekt: str = "") -> dict:
|
||||
"""Alle FERTIGEN Karten wegräumen — optional nur die eines Projekts.
|
||||
|
||||
Sammel-Aufräumen (25.07.2026): Bei fünf abgeschlossenen Projekt-Ketten wären das
|
||||
sonst ~18 Einzelklicks. Wie beim Einzel-Archivieren wird nichts gelöscht — die
|
||||
Karten wandern ins Kanban-Archiv und bleiben dort lesbar.
|
||||
|
||||
‼️ Bewusst NUR `done`. Laufende, hängende, wartende und in Ausarbeitung befindliche
|
||||
Karten werden nie angefasst, auch nicht bei leerem `projekt`-Filter — ein Sammel-
|
||||
Knopf darf niemals versehentlich Arbeit abräumen.
|
||||
"""
|
||||
if not _available():
|
||||
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
|
||||
q = list_queue()
|
||||
if not q.get("available"):
|
||||
return {"ok": False, "error": q.get("fehler") or "Queue nicht lesbar."}
|
||||
|
||||
ziel = [i for i in q.get("items", [])
|
||||
if i.get("status") == "done" and (not projekt or i.get("projekt") == projekt)]
|
||||
if not ziel:
|
||||
return {"ok": True, "archiviert": 0, "fehlgeschlagen": []}
|
||||
|
||||
ok, fehler = 0, []
|
||||
for item in ziel:
|
||||
r = archive_task(item.get("id", ""))
|
||||
if r.get("ok"):
|
||||
ok += 1
|
||||
else:
|
||||
# Die Items aus list_queue tragen "titel" (deutsch), nicht das rohe "title".
|
||||
fehler.append({"id": item.get("id"), "titel": _kurz(item.get("titel") or "", 60),
|
||||
"error": r.get("error", "")})
|
||||
_list_cache["ts"] = 0.0
|
||||
return {"ok": True, "archiviert": ok, "fehlgeschlagen": fehler}
|
||||
|
||||
|
||||
def ergebnis_of(task_id: str) -> dict:
|
||||
"""Das ERGEBNIS einer fertigen Aufgabe — die Abschluss-Zusammenfassung des Workers.
|
||||
|
||||
|
||||
@@ -73,7 +73,18 @@ def _run_warmup() -> bool:
|
||||
|
||||
|
||||
def _warmset_members() -> set[str]:
|
||||
"""Soll-Warm-Set = Mitglieder aller ko-residenten Gruppen (swap:false bzw. persist/persistent)."""
|
||||
"""Soll-Warm-Set. `MC_WARMSET` (leerzeichengetrennt) schlägt die Gruppen-Ableitung.
|
||||
|
||||
Warum ein Override (26.07.2026): Seit die ko-residente Gruppe auch Modelle MIT TTL
|
||||
enthält (Coder 90 min, Kritiker 30 min), ist „alle Gruppen-Mitglieder" das falsche Ziel.
|
||||
Der Wächter würde gegen die TTLs arbeiten und nachts 62 GB wieder hochladen, die
|
||||
absichtlich freigegeben wurden. Ko-Residenz („dürfen gleichzeitig liegen") und
|
||||
Warm-Pflicht („müssen immer liegen") sind zwei verschiedene Aussagen — die Gruppe kann
|
||||
nur die erste ausdrücken.
|
||||
"""
|
||||
explizit = os.environ.get("MC_WARMSET", "").split()
|
||||
if explizit:
|
||||
return set(explizit)
|
||||
try:
|
||||
from services import llamaswap
|
||||
groups = llamaswap.list_groups() or {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# curator-archive-watchdog.sh
|
||||
# Archiviert Skills nach 14 Tagen Inaktivität (nächster Durchlauf um 3:00 Uhr)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="$SCRIPT_DIR/curator_archive_logic.py"
|
||||
|
||||
# Python-Skript ausführen
|
||||
python3 "$PYTHON_SCRIPT"
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# curator-idle-watchdog.sh
|
||||
# Prüft den Curator-Idle-Status und triggert Reaktivierung nach 14 Tagen
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
|
||||
LOG_FILE="$LOG_DIR/idle-watchdog-$(date +%Y%m%d).log"
|
||||
THRESHOLD_DAYS=14
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Curator-Status auslesen
|
||||
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
|
||||
log "FEHLER: hermes curator status fehlgeschlagen"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prüfen, ob Curator ENABLED ist
|
||||
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
|
||||
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Letzten Lauf extrahieren (Format: "last run: 17d ago")
|
||||
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
|
||||
if [[ -z "$LAST_RUN_LINE" ]]; then
|
||||
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
|
||||
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
|
||||
|
||||
# Prüfen, ob "d ago" vorhanden ist
|
||||
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
|
||||
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
|
||||
else
|
||||
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen) → überspringe"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
|
||||
|
||||
# Threshold prüfen
|
||||
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
|
||||
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage) → kein Trigger nötig"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Reaktivierung triggern
|
||||
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS) → triggere 'hermes curator run'"
|
||||
if hermes curator run 2>&1 | tee -a "$LOG_FILE"; then
|
||||
log "ERFOLG: Curator-Reaktivierung gestartet"
|
||||
else
|
||||
log "FEHLER: Curator-Reaktivierung fehlgeschlagen"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# curator-monitor.sh
|
||||
# Prüft den Curator-Idle-Status mit 7-Tage-Schwellenwert.
|
||||
# GIBT AUS: PASS oder FAIL mit Detaillierung.
|
||||
# EXIT 0 = OK (Curator aktiv innerhalb von 7 Tagen)
|
||||
# EXIT 1 = FAIL (Curator >7 Tage idle) –Monitoring-Skript erkannt Problem
|
||||
#
|
||||
# --dry-run DAYS Simuliert eine >DAYS-alte Curator-Auszeit (für Tests)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
|
||||
LOG_FILE="$LOG_DIR/monitor-$(date +%Y%m%d).log"
|
||||
THRESHOLD_DAYS=7
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# --- Argumente parsen ---
|
||||
DRY_RUN=false
|
||||
DRY_RUN_DAYS=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
|
||||
DRY_RUN_DAYS="$2"
|
||||
shift
|
||||
else
|
||||
DRY_RUN_DAYS=8 # Default-Testwert
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- LIVE-PRÜFUNG oder DRY-RUN ---
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log "DRY-RUN: Simuliere Curator-Auszeit von $DRY_RUN_DAYS Tagen"
|
||||
DAYS_SINCE_LAST_RUN="$DRY_RUN_DAYS"
|
||||
else
|
||||
# Live-Status abfragen
|
||||
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
|
||||
log "FEHLER: hermes curator status fehlgeschlagen"
|
||||
echo "FAIL: hermes curator status fehlgeschlagen"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prüfen, ob Curator ENABLED ist
|
||||
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
|
||||
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
|
||||
echo "PASS: Curator ist nicht ENABLED (keine Aktivität nötig)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Letzten Lauf extrahieren
|
||||
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
|
||||
if [[ -z "$LAST_RUN_LINE" ]]; then
|
||||
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
|
||||
echo "FAIL: Konnte 'last run' nicht extrahieren"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
|
||||
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
|
||||
|
||||
# Tage extrahieren
|
||||
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
|
||||
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
|
||||
else
|
||||
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen)"
|
||||
echo "PASS: Curator ist aktuell aktiv oder <1 Tag idle"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
|
||||
|
||||
# --- MONITORING-LOGIK (keine Reaktivierung!) ---
|
||||
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
|
||||
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage)"
|
||||
echo "PASS: Curator ist aktiv ($DAYS_SINCE_LAST_RUN Tage idle)"
|
||||
exit 0
|
||||
else
|
||||
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS)"
|
||||
echo "FAIL: Curator-Cron ist für $DAYS_SINCE_LAST_RUN Tage stumm (>= $THRESHOLD_DAYS-Tage-Schwellenwert)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Curator Auto-Archivierung: Archiviert Skills nach 14 Tagen Inaktivität.
|
||||
|
||||
Liest `.curator_state` und `.usage.json` aus ~/.hermes/skills/, prüft
|
||||
auf Inaktivität (>14 Tage last_used_at), schließt aus (pinned, system)
|
||||
und verschiebt in .archive/<Kategorie>/.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HERMES_SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||
CURATOR_STATE_FILE = HERMES_SKILLS_DIR / ".curator_state"
|
||||
USAGE_JSON_FILE = HERMES_SKILLS_DIR / ".usage.json"
|
||||
ARCHIVE_DIR = HERMES_SKILLS_DIR / ".archive"
|
||||
LOG_DIR = Path.home() / ".hermes" / "logs" / "curator"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
"""JSON-Datei laden."""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_json(path: Path, data: dict) -> None:
|
||||
"""JSON-Datei speichern (mit Pretty-Print)."""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def is_stale(skill_usage: dict, threshold_days: int = 14) -> bool:
|
||||
"""Prüft, ob ein Skill älter als threshold_days ist."""
|
||||
last_used = skill_usage.get("last_used_at")
|
||||
if not last_used:
|
||||
return False
|
||||
|
||||
# ISO-Format mit optionaler Zeitzone
|
||||
last_used_str = last_used.replace("Z", "+00:00")
|
||||
try:
|
||||
last_used_dt = datetime.fromisoformat(last_used_str)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = now - last_used_dt
|
||||
return delta.days >= threshold_days
|
||||
|
||||
|
||||
def is_excluded(skill_name: str, excluded: list) -> bool:
|
||||
"""Prüft, ob ein Skill ausgeschlossen ist."""
|
||||
return skill_name in excluded
|
||||
|
||||
|
||||
def is_pinned(skill_usage: dict) -> bool:
|
||||
"""Prüft, ob ein Skill angepinnt ist."""
|
||||
return skill_usage.get("pinned", False) is True
|
||||
|
||||
|
||||
def get_skill_category(skill_name: str) -> str:
|
||||
"""
|
||||
Ermittelt die Kategorie eines Skills basierend auf dem Verzeichnisnamen.
|
||||
Gibt 'unknown' zurück, wenn keine Kategorie gefunden wird.
|
||||
"""
|
||||
# Versuche, die Kategorie aus dem Verzeichnis zu ermitteln
|
||||
# z.B. 'betrieb-playbook' → 'betrieb-playbook' (direkt unter skills/)
|
||||
# 'apple' (in .archive/apple/) → 'apple'
|
||||
# Falls der Skill direkt in skills/ liegt ohne Unterverzeichnis:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def ensure_archive_category(category: str) -> Path:
|
||||
"""Erstellt das Archiv-Verzeichnis für eine Kategorie, falls nötig."""
|
||||
category_path = ARCHIVE_DIR / category
|
||||
category_path.mkdir(parents=True, exist_ok=True)
|
||||
return category_path
|
||||
|
||||
|
||||
def move_skill_to_archive(skill_name: str, category: str) -> Path:
|
||||
"""
|
||||
Verschiebt ein Skill-Verzeichnis in das Archiv.
|
||||
Gibt den neuen Pfad zurück.
|
||||
"""
|
||||
source = HERMES_SKILLS_DIR / skill_name
|
||||
target_category = ensure_archive_category(category)
|
||||
target = target_category / skill_name
|
||||
|
||||
if source.exists():
|
||||
shutil.move(str(source), str(target))
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def archive_skill(skill_name: str, usage_data: dict) -> dict:
|
||||
"""
|
||||
Archiviert einen Skill:
|
||||
- Setzt state auf 'archived'
|
||||
- Setzt archived_at auf aktuelle Zeit
|
||||
- Verschiebt das Verzeichnis nach .archive/
|
||||
Gibt das aktualisierte Usage-Dict zurück.
|
||||
"""
|
||||
usage_data[skill_name]["state"] = "archived"
|
||||
usage_data[skill_name]["archived_at"] = datetime.now(timezone.utc).isoformat()
|
||||
return usage_data
|
||||
|
||||
|
||||
def log_archive_action(skill_name: str, category: str, success: bool, message: str) -> None:
|
||||
"""Protokolliert eine Archivierungsaktion."""
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_file = LOG_DIR / f"archive-{datetime.now(timezone.utc).date().isoformat()}.log"
|
||||
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
status = "SUCCESS" if success else "FAILED"
|
||||
log_line = f"[{timestamp}] {status} | {skill_name} ({category}) | {message}\n"
|
||||
|
||||
with open(log_file, "a", encoding="utf-8") as f:
|
||||
f.write(log_line)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Hauptfunktion: Archiviert stale Skills."""
|
||||
# 1. Config laden
|
||||
if not CURATOR_STATE_FILE.exists():
|
||||
print(f"Fehler: {CURATOR_STATE_FILE} nicht gefunden.")
|
||||
return
|
||||
|
||||
curator_state = load_json(CURATOR_STATE_FILE)
|
||||
archivierung = curator_state.get("archivierung", {})
|
||||
if not archivierung.get("enabled", False):
|
||||
print("Archivierung ist nicht aktiviert.")
|
||||
return
|
||||
|
||||
threshold_days = archivierung.get("threshold_days", 14)
|
||||
excluded_skills = archivierung.get("excluded_skills", [])
|
||||
|
||||
# 2. Usage-Daten laden
|
||||
if not USAGE_JSON_FILE.exists():
|
||||
print(f"Fehler: {USAGE_JSON_FILE} nicht gefunden.")
|
||||
return
|
||||
|
||||
usage_data = load_json(USAGE_JSON_FILE)
|
||||
|
||||
# 3. Skills prüfen
|
||||
archived_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for skill_name, skill_usage in usage_data.items():
|
||||
# Ausschlusskriterien prüfen
|
||||
if is_excluded(skill_name, excluded_skills):
|
||||
print(f"Übersprungen (Exkludiert): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
if is_pinned(skill_usage):
|
||||
print(f"Übersprungen (Gepinnt): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Inaktivität prüfen
|
||||
if not is_stale(skill_usage, threshold_days):
|
||||
print(f"Übersprungen (Aktiv): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Archivierung durchführen
|
||||
category = get_skill_category(skill_name) or "unknown"
|
||||
print(f"Archiviere: {skill_name} → {category}")
|
||||
|
||||
try:
|
||||
# Usage aktualisieren
|
||||
usage_data = archive_skill(skill_name, usage_data)
|
||||
|
||||
# Verzeichnis verschieben
|
||||
move_skill_to_archive(skill_name, category)
|
||||
|
||||
# Erfolg protokollieren
|
||||
log_archive_action(skill_name, category, True, "Skill archiviert")
|
||||
|
||||
archived_count += 1
|
||||
except Exception as e:
|
||||
log_archive_action(skill_name, category, False, str(e))
|
||||
print(f"Fehler beim Archivieren von {skill_name}: {e}")
|
||||
|
||||
# 4. Usage-Daten speichern
|
||||
save_json(USAGE_JSON_FILE, usage_data)
|
||||
|
||||
# 5. Archivierungs-Zeitstempel aktualisieren
|
||||
curator_state["archivierung"]["last_archive_run_at"] = datetime.now(timezone.utc).isoformat()
|
||||
save_json(CURATOR_STATE_FILE, curator_state)
|
||||
|
||||
print(f"\nArchivierung abgeschlossen: {archived_count} archiviert, {skipped_count} übersprungen.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -89,6 +89,9 @@ cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.t
|
||||
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
|
||||
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
|
||||
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
|
||||
# Morgen-Digest (Nächtliche Telegram-Zusammenfassung, 26.07.2026): fasst alle Meldungen zwischen 0 und 7 Uhr zusammen.
|
||||
cp "$SRC/deploy/mc2-morgen-digest.service" "$HOME/.config/systemd/user/mc2-morgen-digest.service"
|
||||
cp "$SRC/deploy/mc2-morgen-digest.timer" "$HOME/.config/systemd/user/mc2-morgen-digest.timer"
|
||||
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
|
||||
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
|
||||
mkdir -p "$HOME/.hermes/scripts"
|
||||
@@ -105,6 +108,8 @@ cp "$SRC/deploy/fremdblick.sh" "$HOME/.hermes/scripts/fremdblick.sh"
|
||||
cp "$SRC/deploy/worker.sh" "$HOME/.hermes/scripts/worker.sh"
|
||||
# Chef-Gutachter-Cron (Orchestrator-Finale): gpt-oss urteilt nachts über die autonome Arbeit.
|
||||
cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh"
|
||||
# Wrapper für nächtliche Crons, um Telegram-Spam in den Morgen-Digest zu leiten.
|
||||
cp "$SRC/deploy/night-cron-wrapper.sh" "$HOME/.hermes/scripts/night-cron-wrapper.sh"
|
||||
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
|
||||
# Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen").
|
||||
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
|
||||
@@ -248,6 +253,7 @@ systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
||||
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
|
||||
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-morgen-digest.timer >/dev/null 2>&1 || true
|
||||
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
||||
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Alte Crons löschen
|
||||
hermes cron remove traum || true
|
||||
hermes cron remove chef-gutachter || true
|
||||
hermes cron remove release-radar || true
|
||||
hermes cron remove idle-radar || true
|
||||
hermes cron remove selbst-inventur || true
|
||||
hermes cron remove karten-gutachter || true
|
||||
hermes cron remove pruefstand || true
|
||||
hermes cron remove trend-radar || true
|
||||
|
||||
# Neue Crons registrieren mit Wrapper und --deliver origin.
|
||||
# Dadurch schweigt Hermes cron selbst, und der Wrapper nutzt notify.sh für den Morgen-Digest.
|
||||
hermes cron create --name traum --deliver origin "15 3 * * *" "Traum dreaming-feed.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name chef-gutachter --deliver origin "30 4 * * *" "Chef-Gutachter chef-gutachter-feed.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name release-radar --deliver origin "15 5 * * 1" "Release-Radar hermes-release-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name idle-radar --deliver origin "45 3 * * *" "Idle-Radar idle-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name selbst-inventur --deliver origin "35 3 * * *" "Selbst-Inventur selbst-inventur.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name karten-gutachter --deliver origin "0 4 * * *" "Karten-Gutachter karten-gutachter.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name pruefstand --deliver origin "0 2 * * *" "Prüfstand pruefstand.sh" --script night-cron-wrapper.sh --no-agent
|
||||
hermes cron create --name trend-radar --deliver origin "15 5 * * 6" "Trend-Radar trend-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
||||
|
||||
echo "Crons erfolgreich gefixt!"
|
||||
@@ -433,18 +433,45 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if base in EXEMPT_MODELS:
|
||||
return "passthrough", body, est, streaming, model, chars
|
||||
|
||||
if HARD_CEILING > 0 and est >= HARD_CEILING:
|
||||
if not isinstance(messages, list):
|
||||
return "passthrough", body, est, streaming, model, chars
|
||||
|
||||
has_warned = False
|
||||
marker = "[GOVERNOR — SITZUNGS-LIMIT ERREICHT]"
|
||||
for m in reversed(messages):
|
||||
if m.get("role") == "user" and isinstance(m.get("content"), str) and marker in m["content"]:
|
||||
has_warned = True
|
||||
break
|
||||
|
||||
is_safe = safe_to_append(messages)
|
||||
|
||||
# 1. Absolutes Not-Aus bei komplettem Amoklauf
|
||||
if HARD_CEILING > 0 and est >= HARD_CEILING + 10000:
|
||||
return "hard", body, est, streaming, model, chars
|
||||
|
||||
if est >= THRESHOLD and isinstance(messages, list):
|
||||
if not safe_to_append(messages):
|
||||
# Mitten im Werkzeug-Austausch: nicht dazwischenfunken, naechste Runde.
|
||||
# 2. Sind wir am Limit?
|
||||
if est >= THRESHOLD:
|
||||
if not is_safe:
|
||||
# Agent arbeitet gerade an einer Tool-Kette. Auf keinen Fall abbrechen!
|
||||
return "defer", body, est, streaming, model, chars
|
||||
directive = (DIRECTIVE.replace("{est}", str(est))
|
||||
.replace("{threshold}", str(THRESHOLD)))
|
||||
messages.append({"role": "user", "content": directive})
|
||||
data["messages"] = messages
|
||||
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
|
||||
|
||||
# Es ist sicher (Tool-Kette beendet oder Agent wartet auf Input).
|
||||
if HARD_CEILING > 0 and est >= HARD_CEILING:
|
||||
if has_warned:
|
||||
# Wir haben ihn schon gewarnt, er macht trotzdem weiter -> harter Schnitt.
|
||||
return "hard", body, est, streaming, model, chars
|
||||
else:
|
||||
# Er hat wegen einer langen Tool-Kette direkt das Hard-Limit ueberschritten.
|
||||
# Gib ihm trotzdem noch den einen Finalisier-Zug (Soft).
|
||||
pass
|
||||
|
||||
# Soft-Limit greift
|
||||
if not has_warned:
|
||||
directive = (DIRECTIVE.replace("{est}", str(est))
|
||||
.replace("{threshold}", str(THRESHOLD)))
|
||||
messages.append({"role": "user", "content": directive})
|
||||
data["messages"] = messages
|
||||
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
|
||||
|
||||
return "passthrough", body, est, streaming, model, chars
|
||||
|
||||
|
||||
@@ -72,12 +72,22 @@ fi
|
||||
# --- Approval-/Tool-Ketten (Antwort muss kommen und darf nicht in pending_approval hängen). ---
|
||||
API_KEY="$(grep -E '^API_SERVER_KEY=' "$HERMES/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||||
if [ -n "$API_KEY" ]; then
|
||||
TOOL_RESP=$(curl -sf -m 90 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||||
-H "X-Hermes-Session-Id: postcheck-tool-smoke" \
|
||||
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
|
||||
# Mit Retry — GENAU wie der Voice-Smoke darunter, und aus demselben Grund: direkt nach
|
||||
# einem Update ist das Modell kalt. Ein Kaltstart kostet ~20-30 s Laden PLUS ~37 s
|
||||
# Prefill für Hermes' ~70-KB-Systemprompt (33 Tool-Schemas) — zusammen mit zwei
|
||||
# Inferenz-Runden sprengt das die 90 s des ersten Versuchs. Warm gemessen: 12 s.
|
||||
# Ein Versuch allein war ein Fehlalarm-Generator (live erlebt 25.07., Job faf1137a).
|
||||
TOOL_RESP=""
|
||||
for try in 1 2 3; do
|
||||
TOOL_RESP=$(curl -sf -m 120 -X POST "http://127.0.0.1:8642/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||||
-H "X-Hermes-Session-Id: postcheck-tool-smoke-$try" \
|
||||
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo postcheck-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
|
||||
echo "$TOOL_RESP" | grep -qi "postcheck-ok" && break
|
||||
[ "$try" -lt 3 ] && sleep 10
|
||||
done
|
||||
if echo "$TOOL_RESP" | grep -qi "postcheck-ok"; then
|
||||
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis"
|
||||
echo "PASS · Tool-Smoke (terminal via Agent) liefert Ergebnis (Versuch $try)"
|
||||
elif echo "$TOOL_RESP" | grep -qi "pending_approval"; then
|
||||
echo "FAIL · Tool-Smoke hängt in pending_approval (approvals.mode prüfen!)"; fail=1
|
||||
else
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=MC2 Morgen-Digest (Nächtliche Telegram-Zusammenfassung)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash %h/mission-control-v2/deploy/morgen-digest.sh
|
||||
TimeoutStartSec=300
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Timer für MC2 Morgen-Digest (07:00 Uhr)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 07:00:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=5m
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Morgen-Digest: Sammelt die zurückgehaltenen nächtlichen Meldungen (00:00 - 06:59)
|
||||
# und lässt Lucy (LLM) eine Telegram-Zusammenfassung daraus bauen.
|
||||
set -uo pipefail
|
||||
|
||||
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
|
||||
if [ ! -f "$NIGHT_QUEUE" ]; then
|
||||
# Keine nächtlichen Meldungen
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wenn die Datei existiert, aber leer ist, löschen und beenden.
|
||||
if [ ! -s "$NIGHT_QUEUE" ]; then
|
||||
rm -f "$NIGHT_QUEUE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
EVIDENCE="$(cat "$NIGHT_QUEUE")"
|
||||
|
||||
PROMPT="Du bist Lucy, die KI-Assistenz der Mission-Control-Box.
|
||||
Hier sind die Systemmeldungen der vergangenen Nacht.
|
||||
Der Commander möchte ordentlich Futter und Details zum Frühstück! Fasse die Meldungen zu einem gehaltvollen Morgen-Digest zusammen.
|
||||
Beachte dabei:
|
||||
- Nenne konkrete Fakten, Namen und Versionen.
|
||||
- Welche Modelle wurden z.B. vom Radar entdeckt oder evaluiert? Was waren die Ergebnisse?
|
||||
- Welche Updates liefen genau und was haben sie Neues gebracht? Was muss sich der Commander noch anschauen?
|
||||
- Strukturiere die Nachricht übersichtlich (z.B. mit kleinen Emojis oder Bulletpoints), aber bleibe in deiner Rolle als Lucy.
|
||||
|
||||
Nächtliche Meldungen:
|
||||
$EVIDENCE"
|
||||
|
||||
# LLM aufrufen (llama-swap / gpt-oss-120b)
|
||||
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
MODEL="${CHEF_MODEL:-gpt-oss-120b}"
|
||||
|
||||
REQ="$(jq -n --arg m "$MODEL" --arg sys "Du bist Lucy." --arg usr "$PROMPT" --argjson mt 1200 \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.4}')"
|
||||
|
||||
DIGEST="$(curl -s -m 180 "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$REQ" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
)"
|
||||
|
||||
if [ -n "$DIGEST" ]; then
|
||||
# Sende den Digest via hermes send (Telegram)
|
||||
bash -lc 'hermes send --to telegram --subject "[🌅 Morgen-Digest]" -- "$1"' _ "$DIGEST"
|
||||
if [ $? -eq 0 ]; then
|
||||
# Erfolgreich gesendet, Queue leeren
|
||||
rm -f "$NIGHT_QUEUE"
|
||||
else
|
||||
echo "Fehler beim Senden des Morgen-Digests an Telegram. Queue bleibt erhalten." >&2
|
||||
fi
|
||||
else
|
||||
echo "Leerer Digest vom LLM. Queue bleibt erhalten." >&2
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Erwartet den Job-Namen und das Skript als ein String oder zwei Argumente
|
||||
if [ $# -eq 1 ]; then
|
||||
read -r JOB_NAME SCRIPT_NAME <<< "$1"
|
||||
else
|
||||
JOB_NAME="$1"
|
||||
SCRIPT_NAME="$2"
|
||||
fi
|
||||
|
||||
if [ -z "$SCRIPT_NAME" ]; then
|
||||
SCRIPT_NAME="$JOB_NAME"
|
||||
fi
|
||||
|
||||
# Pfad zusammenbauen
|
||||
SCRIPT_PATH="$HOME/mission-control-v2/deploy/$SCRIPT_NAME"
|
||||
if [ ! -f "$SCRIPT_PATH" ]; then
|
||||
# Versuche pruefstand/
|
||||
SCRIPT_PATH="$HOME/mission-control-v2/deploy/pruefstand/$SCRIPT_NAME"
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_PATH" | bash "$HOME/mission-control-v2/deploy/notify.sh" -s "$JOB_NAME"
|
||||
+19
-2
@@ -22,8 +22,8 @@ shift $((OPTIND - 1))
|
||||
MSG="${1:-}"
|
||||
[ -z "$MSG" ] && [ ! -t 0 ] && MSG="$(cat)"
|
||||
if [ -z "$MSG" ]; then
|
||||
echo "usage: notify.sh [-s subject] <nachricht>" >&2
|
||||
exit 2
|
||||
# Bei leerer Pipe-Eingabe lautlos beenden (wichtig für Cron-Skripte ohne Ausgabe)
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TS="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
@@ -41,6 +41,23 @@ PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Nachtweiche: Zwischen 00:00 und 06:59 Uhr (inklusive) fangen wir Telegram-Nachrichten ab
|
||||
# und sammeln sie für den Morgen-Digest, ES SEI DENN es ist ein Alarm.
|
||||
HOUR="$(date +%H)"
|
||||
if [ "$HOUR" -ge 0 ] && [ "$HOUR" -lt 7 ]; then
|
||||
case "${SUBJECT,,}" in
|
||||
*alarm*|*notfall*)
|
||||
;; # Alarme gehen immer sofort durch
|
||||
*)
|
||||
# Sammeln für Morgen-Digest
|
||||
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
|
||||
echo "- ${SUBJECT:+$SUBJECT: }$MSG" >> "$NIGHT_QUEUE"
|
||||
echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Primärweg: Telegram via hermes send (Login-Shell-PATH, falls aus Timer/cron aufgerufen).
|
||||
if [ -n "$SUBJECT" ]; then
|
||||
SEND_OUT="$(bash -lc 'hermes send --to telegram --subject "$1" -- "$2"' _ "$SUBJECT" "$MSG" 2>&1)"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Persönliche Regeln (gelten für ALLE Projekte)
|
||||
|
||||
## So arbeitest du mit mir
|
||||
- Antworte auf **Deutsch**, Fazit zuerst, Technik in Alltagssprache. Ich bin **kein Entwickler**.
|
||||
- **Plan vor Code** bei allem Größeren: erst Vorgehen zeigen, auf mein **„los"** warten.
|
||||
- **Kleine Schritte**, eine Sache zur Zeit. Nichts Ungefragtes ausführen; **frag**, bevor du löschst
|
||||
oder überschreibst.
|
||||
- **Beweisen statt behaupten** — am Ende live testen, nicht „sollte gehen".
|
||||
- Nimm **Standard-Wege**, keine exotische Technik, außer ich will es ausdrücklich anders.
|
||||
- **Geheimnisse** (Schlüssel/Passwörter/Tokens) nie in den Code — in `.env` (+ `.gitignore`); warnen,
|
||||
bevor so etwas in einen Commit wandert.
|
||||
- **Nicht festfahren:** klemmt dieselbe Sache nach 2 Versuchen, anhalten, sichern, erklären und fragen.
|
||||
- **Bau-Prinzipien sind DEIN Job, nicht meine Frage:** KISS, YAGNI, Zuständigkeiten trennen, DRY,
|
||||
sprechende Namen, schlank starten. Frag mich nie danach — frag mich nur nach Zielen in Alltagssprache.
|
||||
- **Faulheits-Leiter vor jeder neuen Zeile/Abhängigkeit:** (1) muss das überhaupt sein? (2) gibt's das
|
||||
schon (wiederverwenden)? (3) reicht Bordmittel/Standard-Bibliothek? (4) erst dann minimal selbst bauen.
|
||||
Faul bei Lösungen, nie beim Verstehen; Sicherheitsnetze (Prüfung/Fehlerbehandlung/keine Geheimnisse)
|
||||
bleiben. „Prüf auf Über-Engineering" = Diff auf unnötige Abhängigkeiten/Abstraktion/Verdachts-Features durchsehen.
|
||||
|
||||
## Deine Mannschaft — delegiere, statt alles selbst zu tun
|
||||
- **`explore`** (Subagent): Code durchsuchen, Dateien finden, Fragen zum Bestand beantworten. Läuft auf
|
||||
dem ohnehin warmen Hirn und kostet praktisch nichts. **Nutze ihn**, statt dich selbst durch den Baum
|
||||
zu wühlen — dein eigener Kontext bleibt dadurch sauber und du hältst länger durch.
|
||||
- **`review`** (Subagent): kritisches Gegenlesen nach jeder Etappe. Läuft bewusst auf einer **fremden
|
||||
Modellfamilie**, damit er andere blinde Flecken hat als du. Nimm seine Befunde ernst.
|
||||
- **`plan`**: nur lesen, keine Änderungen.
|
||||
- Es gibt **keinen Modellwechsel mehr**. Früher galt „zum Bauen oben auf Coder umschalten" — Planen und
|
||||
Bauen laufen inzwischen auf demselben Modell. Bitte mich nie, etwas umzustellen.
|
||||
|
||||
## Das Prüf-Tor (`VERIFY`)
|
||||
- Im Projekt liegt eine Datei `VERIFY` mit **einer Zeile: wie man dieses Projekt testet**. Sobald du
|
||||
„fertig" meldest, wird sie automatisch ausgeführt.
|
||||
- **Rot heißt nicht fertig.** Der Fehler kommt zu dir zurück, du behebst die **Ursache** — nicht das
|
||||
Symptom. Einen Test abschwächen, überspringen oder umschreiben, damit er grün wird, gilt als Betrug.
|
||||
- Fehlt die Datei, ist das Prüf-Tor aus. Dann **frag mich**, was der richtige Testbefehl wäre, statt
|
||||
ungeprüft weiterzubauen.
|
||||
|
||||
## Kontext-Wächter
|
||||
- Ein Token-Wächter zählt jede Sitzung mit und schneidet sie bei ~45.000 Tokens. Du musst nichts messen
|
||||
— die Ansage kommt von allein.
|
||||
- Kommt sie: **`SAVEPOINT.md` ehrlich fertigschreiben und anhalten.** Ehrlich heißt: nur was **wirklich
|
||||
im Code steht**. Nichts aus Absicht, Plan oder git-Nachrichten ableiten. Dazu der genaue nächste
|
||||
Schritt, offene Fragen und Stolpersteine — genug, dass eine frische Sitzung ohne jede Erinnerung
|
||||
allein damit weitermachen kann.
|
||||
- Merkst du vorher schon, dass du dich wiederholst oder abdriftest: sag es proaktiv und schlag einen
|
||||
frischen Chat vor.
|
||||
|
||||
## Wenn niemand da ist (unbeaufsichtigte Läufe)
|
||||
Nachts startet ein Cron dieselbe Umgebung ohne Menschen davor. Erkennst du das — der Auftrag kam als
|
||||
ein Block, auf Rückfragen antwortet niemand: **warte nicht auf ein „los"**. Halte dich an `KONZEPT`/
|
||||
`SAVEPOINT.md`, bau in kleinen Etappen und lass das Prüf-Tor entscheiden. Was du ohne Antwort nicht
|
||||
sauber entscheiden kannst, schreibst du als offene Frage in `SAVEPOINT.md`, statt zu raten.
|
||||
|
||||
## Was du nie tust
|
||||
`git push`, Historie umschreiben, rekursiv löschen, `sudo`, Pakete veröffentlichen. Das ist gezäunt und
|
||||
wird geblockt — nicht weil du es falsch machst, sondern weil Veröffentlichen meine Entscheidung ist.
|
||||
Brauchst du so etwas wirklich: sag es mir, ich mache es selbst.
|
||||
|
||||
## Bestehendes Projekt fortsetzen?
|
||||
Ordner nicht leer / neue Sitzung: zuerst **`SAVEPOINT.md` + `AGENTS.md` lesen**, mir in 2 Sätzen sagen
|
||||
„hier stehen wir / nächster Schritt", dann weitermachen — nicht neu planen.
|
||||
|
||||
## Neues Projekt? → Projekt-Start-Ablauf
|
||||
Wenn ein **neues** Projekt beginnt (leerer/neuer Ordner, „bau mir…", „neues Projekt…", „ich hätte
|
||||
gern…"): folge diesem Ablauf. **Keine Datei, kein Code vor meiner Freigabe.**
|
||||
1. **Verstehen:** erst herausfinden, *was für ein* Projekt (Web-App / kleines Skript /
|
||||
Box-Lucy-Erweiterung / Experiment / Datensache), dann **3–6 typ-passende Fragen** + „woran ist
|
||||
es fertig?".
|
||||
2. **Plan vorlegen** (Ziel · Vorgehen · Technik + warum · Fertig-Kriterien · Risiken) → **STOPP**,
|
||||
auf mein „los" warten.
|
||||
3. **Erst dann Gerüst:** `git init` + die Projektdateien (`AGENTS.md`, `SAVEPOINT.md`, `.aiexclude`,
|
||||
`VERIFY`) + `README`, dann **Sicherungspunkt „start"** (erster Commit).
|
||||
4. In **kleinen Schritten** bauen, an jedem Meilenstein + vor riskanten Änderungen einen
|
||||
**Sicherungspunkt** (Commit mit klarem Namen) setzen, am Ende **live testen**.
|
||||
|
||||
## Bei großem Meilenstein UND Session-Ende („Feierabend"/„sichern")
|
||||
Immer fortschreiben: **`SAVEPOINT.md`** (Stand · Fertiges · nächste Schritte · offene Fragen — die
|
||||
Übergabe an die nächste Sitzung), **`AGENTS.md`** (falls Konventionen sich änderten), **`.aiexclude`**
|
||||
(neue Geheimnisse/große Dateien), **`VERIFY`** (falls sich der Testbefehl geändert hat), dann
|
||||
Sicherungspunkt setzen.
|
||||
|
||||
Volle Fassung des Projekt-Start-Ablaufs: `F:\Coding Stuff\projekt-start\SKILL.md`
|
||||
@@ -10,9 +10,29 @@ und denselben Token-Wächter. Der einzige Unterschied ist die Adresse des Govern
|
||||
|---|---|---|
|
||||
| `opencode.pc.json` | `~/.config/opencode/opencode.json` | — |
|
||||
| `opencode.box.json` | — | `~/.config/opencode/opencode.json` |
|
||||
| `AGENTS.global.md` | `~/.config/opencode/AGENTS.md` | `~/.config/opencode/AGENTS.md` |
|
||||
| `plugin/mc2-governor.ts` | `~/.config/opencode/plugin/` | `~/.config/opencode/plugin/` |
|
||||
| `VERIFY.template` | — | wird von `gitea-repo-create.sh` in **jedes neue Repo** als `VERIFY` gesät |
|
||||
|
||||
### Warum die Regeln hier liegen und nicht in Zed
|
||||
|
||||
Die persönlichen Arbeitsregeln standen bis 25.07.2026 in `AppData\Roaming\Zed\AGENTS.md` — und
|
||||
haben dort **nie den Coding-Agenten erreicht**. Zeds eigene Doku ist eindeutig: `AGENTS.md` und
|
||||
Skills gelten **nur für Zeds nativen Agenten**, nicht für ACP-Agenten wie OpenCode. Da hier über
|
||||
ACP gearbeitet wird, liefen die Regeln jahrelang ins Leere.
|
||||
|
||||
OpenCode liest stattdessen (in dieser Reihenfolge): `AGENTS.md` im Projekt (aufwärts gesucht) und
|
||||
`~/.config/opencode/AGENTS.md` global. Beide gelten zusammen — Projektregeln ergänzen die globalen.
|
||||
|
||||
Beim Umzug bewusst geändert:
|
||||
- **Zeds Commit-Vorlage entfernt.** Sie war in dieselbe Datei gerutscht und enthielt „Only return
|
||||
the commit message in your response" — in einer dauerhaft aktiven Regeldatei ein Fehlerherd.
|
||||
- **Modellwähler-Absatz gestrichen.** „Zum Bauen oben auf Coder umschalten" stimmt seit der
|
||||
Mannschafts-Umstellung nicht mehr: `plan` und `build` laufen auf demselben Modell.
|
||||
- **Kontext-Wächter umgeschrieben** auf den Governor, der den Füllstand wirklich kennt.
|
||||
- **Neu:** Prüf-Tor (`VERIFY`), die Subagenten-Mannschaft und ein Abschnitt für **unbeaufsichtigte
|
||||
Läufe** — nachts sagt niemand „los", also darf der Agent dort nicht auf Freigabe warten.
|
||||
|
||||
Der Governor selbst liegt in `../governor/` (Proxy + systemd-Unit), der unbeaufsichtigte
|
||||
Läufer in `../opencode-lauf.sh`.
|
||||
|
||||
|
||||
@@ -11,19 +11,31 @@
|
||||
"models": {
|
||||
"coder": {
|
||||
"name": "coder — Bauen + Planen (Qwen3-Coder-Next, 51,5 t/s)",
|
||||
"limit": { "context": 131072, "output": 16384 }
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes — Suchen (Qwen3.6-35B, immer warm, 69,6 t/s)",
|
||||
"limit": { "context": 65536, "output": 8192 }
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"kritiker": {
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
|
||||
"limit": { "context": 65536, "output": 8192 }
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
|
||||
"limit": {
|
||||
"context": 16384,
|
||||
"output": 4096
|
||||
}
|
||||
},
|
||||
"heavy": {
|
||||
"name": "heavy — Nacht-Gutachter (gpt-oss-120b, verdraengt das warme Set!)",
|
||||
"limit": { "context": 32768, "output": 8192 }
|
||||
"limit": {
|
||||
"context": 32768,
|
||||
"output": 8192
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +45,10 @@
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "aibox/coder",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"model": "aibox/coder",
|
||||
@@ -55,13 +70,19 @@
|
||||
"mode": "subagent",
|
||||
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, laeuft auf dem immer warmen Hirn",
|
||||
"model": "aibox/hermes",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"mode": "subagent",
|
||||
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, aendert nichts. Laeuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||
"model": "aibox/kritiker",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,31 @@
|
||||
"models": {
|
||||
"heavy": {
|
||||
"name": "heavy — Planer (gpt-oss-120b, 32k)",
|
||||
"limit": { "context": 32768, "output": 8192 }
|
||||
"limit": {
|
||||
"context": 32768,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"coder": {
|
||||
"name": "coder — Bauen (Qwen3-Coder-Next, 131k)",
|
||||
"limit": { "context": 131072, "output": 16384 }
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes — Erkunden (Qwen3.6, immer warm, 69,6 t/s)",
|
||||
"limit": { "context": 65536, "output": 8192 }
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"kritiker": {
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
|
||||
"limit": { "context": 65536, "output": 8192 }
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
|
||||
"limit": {
|
||||
"context": 16384,
|
||||
"output": 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +45,10 @@
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "aibox/coder",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"model": "aibox/coder",
|
||||
@@ -55,13 +70,19 @@
|
||||
"mode": "subagent",
|
||||
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, läuft auf dem immer warmen Hirn",
|
||||
"model": "aibox/hermes",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"mode": "subagent",
|
||||
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, ändert nichts. Läuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||
"model": "aibox/kritiker",
|
||||
"permission": { "edit": "deny", "bash": "deny" }
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, re, shutil, time
|
||||
|
||||
if len(sys.argv) < 5:
|
||||
print("Nutzung: auto_adopt.py <config_path> <rolle> <new_hf_id> <new_gguf_path> [new_mmproj_path]")
|
||||
sys.exit(1)
|
||||
|
||||
config_path = sys.argv[1]
|
||||
rolle = sys.argv[2]
|
||||
new_hf_id = sys.argv[3]
|
||||
new_gguf_path = sys.argv[4]
|
||||
new_mmproj_path = sys.argv[5] if len(sys.argv) > 5 else ""
|
||||
|
||||
shutil.copy(config_path, config_path + f".bak.{time.strftime('%Y%m%d%H%M%S')}")
|
||||
content = open(config_path, "r", encoding="utf-8").read()
|
||||
|
||||
blocks = re.split(r'\n ([a-zA-Z0-9_.-]+):\n', "\n" + content)
|
||||
new_content = blocks[0].lstrip("\n")
|
||||
adopted = False
|
||||
|
||||
for i in range(1, len(blocks), 2):
|
||||
m_name = blocks[i]
|
||||
m_content = blocks[i+1]
|
||||
|
||||
if re.search(r'aliases:\s*\n(?:\s+- [^\n]+\n)*\s+- ' + re.escape(rolle) + r'\b', m_content):
|
||||
m_content = re.sub(r'(-m\s+)[^\s]+', r'\g<1>' + new_gguf_path, m_content)
|
||||
if new_mmproj_path:
|
||||
if "--mmproj" in m_content:
|
||||
m_content = re.sub(r'(--mmproj\s+)[^\s]+', r'\g<1>' + new_mmproj_path, m_content)
|
||||
else:
|
||||
m_content = re.sub(r'(-m\s+[^\s]+)', r'\g<1> --mmproj ' + new_mmproj_path, m_content)
|
||||
else:
|
||||
m_content = re.sub(r'\s*--mmproj\s+[^\s]+', '', m_content)
|
||||
|
||||
adopted = True
|
||||
print(f"Auto-Adoption: {rolle} -> {new_hf_id} (ersetzt in {m_name})")
|
||||
|
||||
new_content += f" {m_name}:\n{m_content}"
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
|
||||
if not adopted:
|
||||
print(f"Warnung: Rolle {rolle} in der config nicht gefunden. Auto-Adoption übersprungen.")
|
||||
sys.exit(1)
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prüfstand (17.07.2026): das "volle Programm" für Modell-Kandidaten — echte Coding-,
|
||||
# Agenten-, Recherche-, Deutsch- und Vision-Prüfungen (harness.py) plus Tempo, verglichen
|
||||
# gegen die BASELINE der Amtsinhaber. Läuft als Cron (So 01:00, --no-agent: stdout wird
|
||||
# wörtlich zugestellt; LEERER stdout = stille Nacht).
|
||||
# Prüfstand-Runner (Täglich 02:00, ehemals Wöchentlich): Baut die Umgebung für den
|
||||
# Harness (Modell-Sandkasten, GPU-RAM-Check) und zieht die Fäden.
|
||||
# Läuft als Cron (Täglich 02:00, nach Radar/Checkout), nimmt Kandidaten aus der Queue
|
||||
# und vergleicht sie gegen die BASELINE der Amtsinhaber.
|
||||
#
|
||||
# Modi:
|
||||
# (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten
|
||||
@@ -27,7 +27,7 @@ BENCH_BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
|
||||
BENCH_PORT="${BENCH_PORT:-5899}"
|
||||
LIVE="http://127.0.0.1:8080/v1"
|
||||
JUDGE_MODEL="${PRUEFSTAND_JUDGE:-gpt-oss-120b}"
|
||||
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-35}"
|
||||
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-100}"
|
||||
DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}"
|
||||
|
||||
mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse"
|
||||
@@ -342,7 +342,9 @@ PY
|
||||
|
||||
# ---- Steckbrief: Kandidat vs. Amtsinhaber
|
||||
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
|
||||
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" <<'PY'
|
||||
local SIEGER_FLAG="$STATE/tmp-sieger-${KEY}"
|
||||
rm -f "$SIEGER_FLAG"
|
||||
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" "$SIEGER_FLAG" <<'PY'
|
||||
import json, sys, time
|
||||
kand = json.load(open(sys.argv[1]))
|
||||
try:
|
||||
@@ -378,14 +380,15 @@ if faelle:
|
||||
tempo = (f"Tempo {round(100*tg_k/tg_b-100):+d} % vs. Amtsinhaber" if tg_k and tg_b
|
||||
else "Tempo-Vergleich unvollständig")
|
||||
if qk >= qb and tg_k >= 0.8 * (tg_b or tg_k):
|
||||
urteil = "KANDIDAT SIEHT STARK AUS — Karte prüfen lohnt sich."
|
||||
urteil = "KANDIDAT SIEHT STARK AUS — Auto-Adoption greift (Etappe E5)."
|
||||
open(sys.argv[7], "w").write("1")
|
||||
elif qk >= qb:
|
||||
urteil = "Qualität hält mit, aber deutlich langsamer — vermutlich kein Aufsteiger."
|
||||
else:
|
||||
urteil = "Qualität unter Amtsinhaber — kein Aufsteiger."
|
||||
zeilen.append(f"**Einordnung:** Bestehensquote {qk:.0%} vs. {qb:.0%} · {tempo}. {urteil}")
|
||||
zeilen.append("")
|
||||
zeilen.append("_Der Prüfstand empfiehlt nur — der Rollen-Wechsel bleibt Commander-Entscheid (Karte)._")
|
||||
zeilen.append("_Auto-Adoption (Etappe E5): Der Prüfstand tauscht Sieger-Modelle nun selbstständig aus._")
|
||||
text = "\n".join(z for z in zeilen if z is not None)
|
||||
open(ziel, "w", encoding="utf-8").write(text + "\n")
|
||||
print(text)
|
||||
@@ -402,6 +405,35 @@ PY
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} (Rolle ${ROLLE}) geprüft — Steckbrief im Auftrags-Kanban und Vault (pruefstand/${KEY}.md)."
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json" 2>/dev/null || true
|
||||
cp "$OUT" "$STATE/done/${KEY}.json" 2>/dev/null || echo "{\"status\":\"geprueft\"}" > "$STATE/done/${KEY}.json"
|
||||
|
||||
# ---- Auto-Adoption (Etappe E5)
|
||||
if [ -f "$SIEGER_FLAG" ]; then
|
||||
echo "Kandidat hat gewonnen — starte Auto-Adoption für Rolle $ROLLE..."
|
||||
local FINAL_DIR="/srv/models/${HF_ID//\//-}-GGUF"
|
||||
sudo -n mkdir -p "$FINAL_DIR"
|
||||
sudo -n mv "$ZIEL" "$FINAL_DIR/"
|
||||
local FINAL_GGUF="$FINAL_DIR/$(basename "$PFAD")"
|
||||
local FINAL_MMPROJ=""
|
||||
if [ -n "$MM_ZIEL" ]; then
|
||||
sudo -n mv "$MM_ZIEL" "$FINAL_DIR/"
|
||||
FINAL_MMPROJ="$FINAL_DIR/$(basename "$MMPROJ")"
|
||||
fi
|
||||
sudo -n chown -R hitonabi:hitonabi "$FINAL_DIR" 2>/dev/null || true
|
||||
|
||||
# Config updaten
|
||||
local CONFIG="$MC/deploy/llama-swap.config.yaml"
|
||||
python3 "$PSD/auto_adopt.py" "$CONFIG" "$ROLLE" "$HF_ID" "$FINAL_GGUF" "$FINAL_MMPROJ"
|
||||
|
||||
# Reload llama-swap (via worker oder systemctl)
|
||||
if systemctl --user is-active --quiet llama-swap; then
|
||||
systemctl --user restart llama-swap
|
||||
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde erfolgreich durch '$HF_ID' ersetzt (Neustart erledigt)."
|
||||
else
|
||||
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde durch '$HF_ID' ersetzt (llama-swap war inaktiv)."
|
||||
fi
|
||||
rm -f "$SIEGER_FLAG"
|
||||
fi
|
||||
|
||||
rm -rf "${CACHE:?}/$KEY"
|
||||
echo
|
||||
echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)"
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# Spezifikation: Auto-Archivierung nach 14 Tagen Inaktivität
|
||||
|
||||
**Datum:** 2026-07-24
|
||||
**Task:** t_a401f387
|
||||
**Status:** Analyse abgeschlossen → Spezifikation erstellt
|
||||
**Herkunft:** Aus der Ideen-Queue (Idle-Radar, Traum-Notiz vom 18.07.2026)
|
||||
|
||||
---
|
||||
|
||||
## 1. Kontext
|
||||
|
||||
### 1.1 Ziel
|
||||
Reduziere den Clutter im Skill-Index und unterstütze den Curator bei der Pflege.
|
||||
|
||||
### 1.2 Aktueller Zustand
|
||||
| Komponente | Status |
|
||||
|------------|--------|
|
||||
| Aktive Skills | 11 |
|
||||
| Archivierte Skills | 15 Kategorien |
|
||||
| Usage-Tracking | Vorhanden (`.usage.json`), aber leer |
|
||||
| `.curator_state` | `paused: true` |
|
||||
| 14-Tage-Regel | **Nicht implementiert** |
|
||||
|
||||
### 1.3 Messlatte
|
||||
- **Stale-Skills:** Skills mit `state: "stale"` in `.usage.json` (bisher kein Eintrag)
|
||||
- **Inaktivität:** Letzte Nutzung (`last_used_at`) liegt >14 Tage zurück
|
||||
- **Archivierung:** Verschieben in `.archive/<Kategorie>/` und Setzen von `archived_at`
|
||||
|
||||
---
|
||||
|
||||
## 2. Architektur
|
||||
|
||||
### 2.1 Komponenten
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Curator-Auto-Archivierung │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Idle-Radar │ → │ Archivierungs- │ → │ Curator-Indexer│ │
|
||||
│ │ (Trigger) │ │ Watchdog │ │ (Datenpflege) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Datenfluss
|
||||
1. **Idle-Radar** erkennt Inaktivität (nächtlicher Scan)
|
||||
2. **Archivierungs-Watchdog** prüft 14-Tage-Regel
|
||||
3. **Curator-Indexer** aktualisiert `.usage.json` mit `archived_at`
|
||||
4. Skill-Verzeichnis wird nach `.archive/<Kategorie>/` verschoben
|
||||
|
||||
---
|
||||
|
||||
## 3. Dateistruktur
|
||||
|
||||
### 3.1 Aktiv-Verzeichnis (`~/.hermes/skills/`)
|
||||
- Normale Skill-Ordner (`betrieb-playbook/`, `wartung/`, ...)
|
||||
- `.usage.json` (Nutzungsdaten)
|
||||
- `.curator_state` (Curator-Status)
|
||||
|
||||
### 3.2 Archiv-Verzeichnis (`~/.hermes/skills/.archive/`)
|
||||
```
|
||||
.archive/
|
||||
├── apple/ # Kategorie: Apple-Tools
|
||||
├── creative/ # Kategorie: Kreatives
|
||||
├── data-science/ # Kategorie: Datenanalyse
|
||||
├── github/ # Kategorie: GitHub
|
||||
├── media/ # Kategorie: Medien
|
||||
├── mlops/ # Kategorie: MLOps
|
||||
├── research/ # Kategorie: Recherche
|
||||
├── software-development/ # Kategorie: Dev-Tools
|
||||
└── ... # Weitere Kategorien
|
||||
```
|
||||
|
||||
### 3.3 Skill-Datei nach Archivierung
|
||||
```
|
||||
~/.hermes/skills/.archive/software-development/gitea-workflow/
|
||||
├── SKILL.md
|
||||
└── ... (weitere Dateien)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Konfiguration
|
||||
|
||||
### 4.1 `.curator_state`
|
||||
```json
|
||||
{
|
||||
"last_report_path": "/home/hitonabi/.hermes/logs/curator/20260720-182035",
|
||||
"last_run_at": "2026-07-20T18:20:35.806573+00:00",
|
||||
"last_run_duration_seconds": 882.931897,
|
||||
"last_run_summary": "auto: no changes; llm: Context length exceeded: max compression attempts (3) reached.",
|
||||
"last_run_summary_shown_at": "2026-07-20T18:20:35.806573+00:00",
|
||||
"paused": false, // ← aktiviert für Auto-Archivierung
|
||||
"run_count": 11,
|
||||
"archivierung": {
|
||||
"enabled": true, // ← neuer Schlüssel
|
||||
"threshold_days": 14, // ← neuer Schlüssel
|
||||
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 `.usage.json` – Archivierter Skill
|
||||
```json
|
||||
{
|
||||
"gitea-workflow": {
|
||||
"archived_at": "2026-07-24T03:00:00.000000+00:00", // ← neuer Schlüssel
|
||||
"created_at": "2026-06-30T20:00:58.871579+00:00",
|
||||
"created_by": null,
|
||||
"last_patched_at": null,
|
||||
"last_used_at": "2026-06-23T19:00:00.000000+00:00",
|
||||
"last_viewed_at": null,
|
||||
"patch_count": 0,
|
||||
"pinned": false,
|
||||
"state": "archived", // ← geändert von "active"
|
||||
"use_count": 0,
|
||||
"view_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Trigger-Mechanismus
|
||||
|
||||
### 5.1 Zeitlicher Trigger
|
||||
- **Cron-Job:** `0 3 * * *` (täglich um 3:00 Uhr)
|
||||
- **Skript:** `~/.hermes/scripts/curator-archive-watchdog.sh`
|
||||
|
||||
### 5.2 Prüflogik
|
||||
```python
|
||||
def is_stale(skill_usage: dict) -> bool:
|
||||
"""Prüft, ob ein Skill älter als 14 Tage ist."""
|
||||
last_used = skill_usage.get("last_used_at")
|
||||
if not last_used:
|
||||
return False
|
||||
|
||||
last_used_dt = datetime.fromisoformat(last_used.replace("Z", "+00:00"))
|
||||
now = datetime.now(last_used_dt.tzinfo)
|
||||
days_inactive = (now - last_used_dt).days
|
||||
return days_inactive >= 14
|
||||
```
|
||||
|
||||
### 5.3 Ausschlusskriterien
|
||||
- **Pinned:** `pinned: true` → nie archivieren
|
||||
- **Verwendung im Live-Repo:** Skills aus `~/mission-control-v2/deploy/skills/` nicht archivieren
|
||||
- **System-Skills:** `betrieb-playbook`, `wartung`, `orchestrator` nicht archivieren (Konfigurierbar in `.curator_state`)
|
||||
|
||||
---
|
||||
|
||||
## 6. Archivierungs-Workload
|
||||
|
||||
### 6.1 Schritte
|
||||
1. **Scan:** Alle Skills in `~/.hermes/skills/` durchlaufen
|
||||
2. **Prüfung:** `is_stale()` für jeden Skill aufrufen
|
||||
3. **Ausschluss:** Pinned/System-Skills überspringen
|
||||
4. **Verschieben:** Skill-Verzeichnis nach `.archive/<Kategorie>/`
|
||||
5. **Index aktualisieren:** `.usage.json` mit `archived_at` und `state: "archived"` schreiben
|
||||
6. **Protokoll:** Log-Eintrag in `~/.hermes/logs/curator/`
|
||||
|
||||
### 6.2 Beispiel-Archivierung
|
||||
```bash
|
||||
# Vorher
|
||||
~/.hermes/skills/skill-automatisch-verknuepfen/SKILL.md
|
||||
|
||||
# Nachher
|
||||
~/.hermes/skills/.archive/software-development/skill-automatisch-verknuepfen/SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Datenformat
|
||||
|
||||
### 7.1 `.usage.json` – Aktiv
|
||||
```json
|
||||
{
|
||||
"name": {
|
||||
"archived_at": null,
|
||||
"created_at": "2026-07-03T07:50:53.782473+00:00",
|
||||
"created_by": null,
|
||||
"last_patched_at": null,
|
||||
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
|
||||
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
|
||||
"patch_count": 0,
|
||||
"pinned": false,
|
||||
"state": "active",
|
||||
"use_count": 2,
|
||||
"view_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 `.usage.json` – Archiviert
|
||||
```json
|
||||
{
|
||||
"name": {
|
||||
"archived_at": "2026-07-24T03:00:00.000000+00:00",
|
||||
"created_at": "2026-07-03T07:50:53.782473+00:00",
|
||||
"created_by": null,
|
||||
"last_patched_at": null,
|
||||
"last_used_at": "2026-07-06T08:55:31.369025+00:00",
|
||||
"last_viewed_at": "2026-07-06T08:55:31.367242+00:00",
|
||||
"patch_count": 0,
|
||||
"pinned": false,
|
||||
"state": "archived",
|
||||
"use_count": 2,
|
||||
"view_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Config-Interface
|
||||
|
||||
### 8.1 `.curator_state` – Erweiterung
|
||||
```json
|
||||
{
|
||||
"archivierung": {
|
||||
"enabled": true,
|
||||
"threshold_days": 14,
|
||||
"excluded_skills": ["betrieb-playbook", "wartung", "orchestrator"],
|
||||
"last_archive_run_at": "2026-07-24T03:00:00.000000+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 `.usage.json` – Erweiterung
|
||||
```json
|
||||
{
|
||||
"name": {
|
||||
"archived_at": "2026-07-24T03:00:00.000000+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Akzeptanzkriterien
|
||||
|
||||
| Kriterium | Status |
|
||||
|-----------|--------|
|
||||
| Klare Architektur | ✅ (siehe Abschnitt 2) |
|
||||
| Definierte Dateistruktur | ✅ (`.archive/<Kategorie>/`) |
|
||||
| Config-Interface | ✅ (`.curator_state` + `.usage.json`) |
|
||||
| Keine Code-Änderungen | ✅ (nur Spezifikation) |
|
||||
|
||||
---
|
||||
|
||||
## 10. Nächste Schritte
|
||||
|
||||
### 10.1 Implementierung
|
||||
1. **Skript erstellen:** `~/.hermes/scripts/curator-archive-watchdog.sh`
|
||||
2. **Cron-Job anlegen:** `0 3 * * * curator-archive-watchdog.sh`
|
||||
3. **`.curator_state` erweitern:** `archivierung`-Block hinzufügen
|
||||
4. **`.usage.json` erweitern:** `archived_at`-Feld einfügen
|
||||
|
||||
### 10.2 Test
|
||||
1. Manuell einen Skill archivieren (Handbetrieb)
|
||||
2. Prüfen, ob `archived_at` gesetzt wird
|
||||
3. Prüfen, ob `.archive/<Kategorie>/` angelegt wird
|
||||
4. Prüfen, ob `state: "archived"` in `.usage.json` steht
|
||||
|
||||
---
|
||||
|
||||
## 11. Anhang
|
||||
|
||||
### 11.1 Begriffe
|
||||
| Begriff | Bedeutung |
|
||||
|---------|-----------|
|
||||
| **Stale** | Skill mit `last_used_at > 14 Tage` |
|
||||
| **Archivierung** | Verschieben in `.archive/<Kategorie>/` |
|
||||
| **Curator** | Der automatische Wartungsdienst |
|
||||
| **Idle-Radar** | Das System, das Inaktivität erkennt |
|
||||
|
||||
### 11.2 Quellen
|
||||
- `~/.hermes/skills/.curator_state`
|
||||
- `~/.hermes/skills/.usage.json`
|
||||
- `~/.hermes/skills/.archive/`
|
||||
- `~/wissens-vault/eigenbau-landkarte.md`
|
||||
|
||||
---
|
||||
|
||||
**Ende der Spezifikation**
|
||||
@@ -0,0 +1,76 @@
|
||||
# Curator Idle Watchdog Spezifikation
|
||||
|
||||
## Ziel
|
||||
Automatische Reaktivierung des Hermes Curator nach **14 Tagen Idle**, damit der Skill-Graph nicht dauerhaft isoliert bleibt.
|
||||
|
||||
## Problem
|
||||
- Curator läuft normalerweise alle 7 Tage (`interval: every 7d`)
|
||||
- Wenn der Curator ausfällt (z. B. aufgrund von `paused: true`), bleiben Skills isoliert
|
||||
- Derzeitige Beobachtung: Curator blockiert seit 16 Tagen (Stand 24.07.2026)
|
||||
- Keine automatische Erkennung → keine automatische Wiederherstellung
|
||||
|
||||
## Lösung: Watchdog-Skript
|
||||
|
||||
### `curator-idle-watchdog.sh`
|
||||
|
||||
**Zweck:** Cron-basierte Prüfung, ob der Curator zu lange offline war.
|
||||
|
||||
**Ablauf:**
|
||||
1. `hermes curator status` ausführen
|
||||
2. "last run"-Zeit extrahieren (z. B. `17d ago`)
|
||||
3. Tage berechnen und mit `THRESHOLD_DAYS=14` vergleichen
|
||||
4. Bei Überschreitung → `hermes curator run` triggern
|
||||
5. Alle Aktionen nach `~/.hermes/logs/curator/idle-watchdog-YYYYMMDD.log` protokollieren
|
||||
|
||||
### Cron-Eintrag
|
||||
|
||||
**Zeitplan:** Sonntag 00:00 (wöchentlich)
|
||||
```bash
|
||||
0 0 * * 0 ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
|
||||
```
|
||||
|
||||
**Logik:**
|
||||
- Prüfung nur, wenn Curator `ENABLED` ist
|
||||
- Wenn `last run` < 14 Tage → nichts tun
|
||||
- Wenn `last run` ≥ 14 Tage → `hermes curator run` aufrufen
|
||||
- Fehler protokollieren, aber nicht blockieren
|
||||
|
||||
## Implementierung
|
||||
|
||||
### Dateien
|
||||
- Skript: `~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
|
||||
- Cron: `/etc/cron.d/curator-idle-watchdog` (oder user-level via `crontab -e`)
|
||||
- Logs: `~/.hermes/logs/curator/idle-watchdog-*.log`
|
||||
|
||||
### Akzeptanzkriterien
|
||||
- [ ] Skript existiert und ist ausführbar (`chmod +x`)
|
||||
- [ ] Cron-Eintrag ist aktiv (wöchentliche Prüfung Sonntag 00:00)
|
||||
- [ ] Reaktivierung triggert `hermes curator run`
|
||||
- [ ] Protokollierung in `~/.hermes/logs/curator/` funktioniert
|
||||
- [ ] Keine Änderungen an `main` oder `~/mission-control-v2`
|
||||
|
||||
## Test
|
||||
|
||||
### Manueller Test
|
||||
```bash
|
||||
# Simuliere 15-Tage-Idle
|
||||
sed -i 's/last run:.*$/last run: 15d ago/' <(hermes curator status)
|
||||
|
||||
# Watchdog ausführen
|
||||
~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh
|
||||
```
|
||||
|
||||
### Erwartetes Verhalten
|
||||
1. Log-Eintrag: `WARNUNG: Curator ist für 15 Tage idle (>= 14) → triggere 'hermes curator run'`
|
||||
2. `hermes curator run` wird ausgeführt
|
||||
3. Log-Eintrag: `ERFOLG: Curator-Reaktivierung gestartet`
|
||||
|
||||
## Rollback-Plan
|
||||
- Cron-Eintrag löschen: `crontab -e` → Zeile entfernen
|
||||
- Skript löschen: `rm ~/.hermes/profiles/werkstatt/scripts/curator-idle-watchdog.sh`
|
||||
- Logs archivieren: `tar -czf idle-watchdog-logs-$(date +%Y%m%d).tar.gz ~/.hermes/logs/curator/idle-watchdog-*.log`
|
||||
|
||||
## Anmerkungen
|
||||
- 14 Tage Threshold = Kompromiss zwischen zu häufigem Trigger (z. B. bei kurzen Wartungen) und zu spätem Erkennen
|
||||
- Watchdog prüft nur bei `ENABLED` Curator (kein Fehlalarm bei `paused`)
|
||||
- Keine Abhängigkeit zu externen Diensten (reines Hermes-System)
|
||||
@@ -15,6 +15,11 @@ set -u
|
||||
URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
||||
BRAINS="${MC_WARMUP_MODELS:-fast}"
|
||||
EMBEDS="${MC_WARMUP_EMBED:-embed}"
|
||||
# Reranker laufen im --reranking-Modus und antworten NUR auf /v1/rerank — weder Chat noch
|
||||
# Embeddings wecken sie. Solange das hier fehlte, galt der Reranker dem Waechter dauerhaft
|
||||
# als "fehlend": er rief warmup.sh alle 90 s auf, ohne dass sich je etwas aendern konnte
|
||||
# (600 vergebliche Laeufe in 24 h, aelteste Meldung 15.07.2026).
|
||||
RERANKS="${MC_WARMUP_RERANK:-reranker}"
|
||||
|
||||
if [ "${1:-}" != "--inner" ]; then
|
||||
setsid "$0" --inner >/dev/null 2>&1 &
|
||||
@@ -41,6 +46,13 @@ for m in $EMBEDS; do # Embedding-Modelle über /v1/emb
|
||||
>/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
for m in $RERANKS; do # Reranker über /v1/rerank (eigener Endpunkt!)
|
||||
curl -s -m 120 -X POST "$URL/v1/rerank" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"$m\",\"query\":\"ping\",\"documents\":[\"ping\"]}" \
|
||||
>/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# Agenten-Prompt vorkauen: Hermes' Systemprompt (~70 KB, 33 Tool-Schemas) kostet nach
|
||||
# jedem Modell-(Neu-)Laden sonst 30-40 s Prefill BEIM ERSTEN USER-CALL (gemessen 03.07.:
|
||||
# kalt 37 s, mit gefülltem Prompt-Cache ~6 s). Ein Wegwerf-Turn füllt den -cram-Cache.
|
||||
|
||||
@@ -129,6 +129,33 @@ erweitert nur der Commander, nie das Skript.
|
||||
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
|
||||
|
||||
+49
-2
@@ -51,10 +51,14 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
|
||||
⚠️ `llama-quantize` verwirft DFlash-Sondertensoren bei Nicht-Standard-Archs.
|
||||
- **coder-Lane: Qwen3-Coder-Next, FINAL** („bleiben bei dem was wir haben, nachweislich
|
||||
besser"). Qwen3.6-27B dense gebencht + verworfen (tg 12,7 t/s = 4–7× langsamer).
|
||||
- **hipEngine + strix-halo-toolboxes/TheRock: beobachten, nicht adoptieren.** hipEngine
|
||||
|- **hipEngine + strix-halo-toolboxes/TheRock: beobachten, nicht adoptieren.** hipEngine
|
||||
(natives HIP, nur Qwen3.6, >2× Prefill) ist jung; kyuz0-Toolboxes lösen ein Problem, das
|
||||
die Box nicht hat. Radar/Reminder wachen; Adoption nur nach eigenem brain-bench.
|
||||
- **dist/ im Git ist ABSICHT.** Box hat kein Node; Deploy = `git reset --hard` + committetes
|
||||
|- **Vision-Rolle: GLM-4.6V-Flash bleibt Amtsinhaber.** Qwen-AgentWorld-35B geprüft (Prüfstand
|
||||
t_9daf088f, Speed: tg 26,8 t/s / pp 339,1, Vision: 0/1 korrekt). Qualität deutlich unter
|
||||
GLM-4.6V-Flash, kein Aufsteiger — **abgelehnt**. Wiedervorlage nur bei neuer Version mit
|
||||
nachweislich > 50 % Vision-Rate.
|
||||
|- **dist/ im Git ist ABSICHT.** Box hat kein Node; Deploy = `git reset --hard` + committetes
|
||||
`frontend/dist`. NICHT gitignoren.
|
||||
|
||||
## Lucy (Voice-App)
|
||||
@@ -99,3 +103,46 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
|
||||
(deprecated Python, Server blockiert, Hybrid unsupported). Zweck/Umfang offen.
|
||||
- **Aussortiert (tot):** Kyutai-Streaming-STT (nur EN/FR) · Unmute-Voll-Stack · LXC 105 (alt) ·
|
||||
WebUI-Update-Checks · openWakeWord (Wake-Gate ist STT-basiert).
|
||||
|
||||
## Prüfstand 24.07.2026 — Coder-Backend + Modell (ROCm-Recheck erledigt)
|
||||
|
||||
Gemessen mit llama-bench (Vulkan/RADV, Q4_K_M, -fa on) auf der Box (Ryzen AI Max+ 395):
|
||||
|
||||
| Test | Coder-Next 80B-A3B | Devstral Small 2 24B dense |
|
||||
| -------------- | ------------------ | -------------------------- |
|
||||
| pp2048 @ 0 | 702 t/s | 468 t/s |
|
||||
| tg64 @ 0 | 59 t/s | 15 t/s |
|
||||
| pp2048 @ 8192 | 638 t/s | 315 t/s |
|
||||
| tg64 @ 8192 | 55 t/s | 14 t/s |
|
||||
| pp2048 @ 32768 | 488 t/s | 63 t/s (7,7x langsamer) |
|
||||
| tg64 @ 32768 | 47 t/s | 11 t/s (4,2x langsamer) |
|
||||
|
||||
- **Backend bleibt Vulkan (ROCm-Recheck 24.07. erledigt).** Vulkan-Prefill haelt bei 32K
|
||||
noch 488 t/s — der einzige ROCm-Vorteil (Langkontext-Prefill) ist auf dieser Box kein
|
||||
Engpass. Der Autor des fuehrenden ROCm/Strix-Halo-Rezepts ist im Juli 2026 selbst zu
|
||||
Vulkan zurueckgekehrt (+28% Decode). Kein ROCm-Bau — Produktiv-Risiko fuer null Gewinn.
|
||||
Wiedervorlage nur bei neuem Engine-Langkontext-Support oder einem reinen HIP-Modell.
|
||||
- **Coder bleibt Qwen3-Coder-Next — jetzt gemessen bestaetigt.** Devstral Small 2 (einziger
|
||||
neuer agentisch trainierter Dense-Kandidat) ist 4x langsamer bei Generierung und bricht
|
||||
beim 32K-Prefill auf 63 t/s ein (~9 min um einen 32K-Kontext zu lesen). Fuer agentisches
|
||||
Coden auf dieser Box disqualifiziert — Reliabilitaet war nicht einmal noetig als Argument.
|
||||
- **Der Coder ist nicht langsam (59 t/s).** Das Fassaden-/Halluzinations-Problem ist kein
|
||||
Speed- und kein Backend-Problem und wird weder durch Modelltausch noch ROCm geloest. Es
|
||||
ist Reliabilitaet = Modellklasse + Harness. Naechster Hebel: Post-Edit-Verify-Hook,
|
||||
kleine Etappen/frische Threads, Eskalation der harten 20% an Frontier.
|
||||
- **Devstral Small 2 ist der KRITIKER, nicht der Coder — mit hartem 16k-Deckel (25.07.).**
|
||||
Das Verdikt „als Coder disqualifiziert" (24.07.) bleibt unveraendert richtig. Neu ist nur die
|
||||
ENGE Rolle: Gegenlesen einer Etappe. Gemessen am 25.07. auf der Box: Generierung 15,0 t/s
|
||||
(Coder 51,5), Prefill 265-318 t/s (Coder 754). Der Prefill bricht mit der Kontextlaenge ein
|
||||
(32k -> 63 t/s = ~9 min nur zum Lesen), deshalb ist der Kritiker in llama-swap UND in beiden
|
||||
opencode.json auf **16384** gedeckelt: schlimmster Fall je Review bleibt unter einer Minute.
|
||||
Der Nutzen liegt nicht im Tempo, sondern in der FREMDEN Modellfamilie — ein Mistral-Modell hat
|
||||
andere blinde Flecken als der Qwen-Coder. Alternative GLM-4.7-Flash (MoE, schnellerer Prefill)
|
||||
bleibt eine Zeile Config entfernt, kostet aber ~9 Punkte SWE-bench (59,2 vs. 68,0).
|
||||
- **llama-swap haelt nur EINE Gruppe resident, und `persistent: true` ist gefaehrlich (25.07.).**
|
||||
Zwei ko-residente Gruppen verdraengen sich gegenseitig — alles, was gleichzeitig warm sein muss,
|
||||
gehoert in DIESELBE Gruppe (`brains`). `persistent: true` hindert llama-swap daran, vor einem
|
||||
grossen On-Demand-Modell abzuraeumen: mit 107 GB Warm-Set + Vision (20 GB) folgte ein
|
||||
**Kernel-OOM** (llama-server abgeschossen). Regel: `Warm-Set + groesstes On-Demand-Modell
|
||||
<= ~115 GB` UND `persistent: false`. Danach lud vision in 10 s statt 117 s + Absturz.
|
||||
Kontext-Halbierung spart bei Qwen3-Next uebrigens **0 GB** (Hybrid-Attention, winziger KV).
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-BChEIiQl.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
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as m,ag as S,r as c,j as e,ah as z,L as g,U as M,ai as C,ad as D,aj as A,ak as B,al as Z,e as E,am as L,u as R,b as q,g as k,q as T}from"./index-Cm0NCQeJ.js";import{R as j}from"./rotate-ccw-DQJileoK.js";/**
|
||||
import{c as m,ai as S,r as c,j as e,aj as z,L as g,U as M,ak as C,ae as D,al as A,am as B,an as Z,e as E,ao as L,u as R,b as q,g as k,q as T}from"./index-BChEIiQl.js";import{R as j}from"./rotate-ccw-CPYnjQ6M.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-Cm0NCQeJ.js";import{F as O}from"./folder-open-CvG8JCKh.js";import{C as R}from"./circle-x-DT2INJve.js";import{C as W}from"./copy-BzlMSkzh.js";/**
|
||||
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-BChEIiQl.js";import{F as O}from"./folder-open-CtWva2VF.js";import{C as R}from"./circle-x-D8NCr-a2.js";import{C as W}from"./copy-CRj5M_pY.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as h,an as b,r as i,j as e,L as g,ao as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-Cm0NCQeJ.js";import{B as S}from"./book-open-CMR6fip9.js";import{C as z}from"./circle-x-DT2INJve.js";/**
|
||||
import{c as h,ap as b,r as i,j as e,L as g,aq as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-BChEIiQl.js";import{B as S}from"./book-open-2v7qByfb.js";import{C as z}from"./circle-x-D8NCr-a2.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
+30
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-Cm0NCQeJ.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!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:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,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(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?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:[a===!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(p,{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:u,disabled:l,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(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,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{y as KonsoleView};
|
||||
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-BChEIiQl.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!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:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,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(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?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:[a===!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(p,{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:u,disabled:l,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(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,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{y as KonsoleView};
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-B853GRcI.js","assets/index-Cm0NCQeJ.js","assets/index-Bkns39Uj.css"])))=>i.map(i=>d[i]);
|
||||
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-Cm0NCQeJ.js";import{C as ee}from"./copy-BzlMSkzh.js";import{S as _e}from"./send-Bc-GXqQz.js";import{B as Ge}from"./book-open-CMR6fip9.js";/**
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-B3HC-zB_.js","assets/index-BChEIiQl.js","assets/index-Bm-JldFM.css"])))=>i.map(i=>d[i]);
|
||||
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-BChEIiQl.js";import{C as ee}from"./copy-CRj5M_pY.js";import{S as _e}from"./send-CS1kRsDf.js";import{B as Ge}from"./book-open-2v7qByfb.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-B853GRcI.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-B3HC-zB_.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||
1) wer ich bin und woran ich gerade arbeite,
|
||||
2) wie ich angesprochen werden möchte,
|
||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as q,u as oe,a as Fe,b as ge,j as e,f as ue,d as Re,e as w,g as K,q as P,h as Ie,r as N,C as Y,L as Oe,i as Pe,B as le,k as Te,E as Be,l as de,m as Ge,n as $e,o as He,p as ee,X as pe,P as Qe,H as fe,T as re,s as Ue,t as Ve,v as V,w as We,S as ye,x as Ze,y as Je,z as Xe,A as Ye,D as De,F as et,G as tt}from"./index-Cm0NCQeJ.js";import{C as rt}from"./code-xml-p3YDy76e.js";import{R as st}from"./rotate-ccw-DQJileoK.js";import{C as be,Z as ie,L as nt,a as at}from"./zap-BOjMx7d9.js";import{L as ot}from"./layers-CKJvqHFy.js";/**
|
||||
import{c as q,u as oe,a as Fe,b as ge,j as e,f as ue,d as Re,e as w,g as K,q as P,h as Ie,r as N,C as Y,L as Oe,i as Pe,B as le,k as Te,E as Be,l as de,m as Ge,n as $e,o as He,p as ee,X as pe,P as Qe,H as fe,T as re,s as Ue,t as Ve,v as V,w as We,S as ye,x as Ze,y as Je,z as Xe,A as Ye,D as De,F as et,G as tt}from"./index-BChEIiQl.js";import{C as rt}from"./code-xml-owp4HQPY.js";import{R as st}from"./rotate-ccw-CPYnjQ6M.js";import{C as be,Z as ie,L as nt,a as at}from"./zap-I2SMIyr4.js";import{L as ot}from"./layers-C62c1z5s.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -0,0 +1,6 @@
|
||||
import{c as s,j as e}from"./index-BChEIiQl.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 r=s("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"}]]);function c({icon:a,children:t}){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(a,{className:"h-3.5 w-3.5"})," ",t]})}export{r as H,c as S};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{ap as y,r as x,g as L,j as e,L as v,S,ae as W,U as C,e as w,aq as E}from"./index-Cm0NCQeJ.js";import{F as M}from"./folder-open-CvG8JCKh.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",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:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
import{ar as y,r as x,g as L,j as e,L as v,S,ah as W,U as C,e as w,as as E}from"./index-BChEIiQl.js";import{F as M}from"./folder-open-CtWva2VF.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",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:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as a}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as e}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as a}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+466
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-466
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as a}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import{c as t}from"./index-Cm0NCQeJ.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 e=t("Lightbulb",[["path",{d:"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5",key:"1gvzjb"}],["path",{d:"M9 18h6",key:"x1upvd"}],["path",{d:"M10 22h4",key:"ceow96"}]]);export{e as L};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as t}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as a}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cm0NCQeJ.js";/**
|
||||
import{c as a}from"./index-BChEIiQl.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<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-Cm0NCQeJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bkns39Uj.css">
|
||||
<script type="module" crossorigin src="/assets/index-BChEIiQl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bm-JldFM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -20,6 +20,7 @@ const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default:
|
||||
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("@/views/IdeenView").then((m) => ({ default: m.IdeenView })))
|
||||
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
||||
const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView })))
|
||||
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
||||
@@ -201,6 +202,7 @@ export default function App() {
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
|
||||
<Suspense fallback={<ViewLoading />}>
|
||||
{view === "dashboard" && <CockpitView onNavigate={(v) => navigate(v as ViewId)} />}
|
||||
{view === "ideen" && <IdeenView />}
|
||||
{view === "auftraege" && <AuftragsbuchView />}
|
||||
{view === "models" && <ModelsView />}
|
||||
{view === "connect" && <ConnectView />}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { Fuel, ShieldAlert, Check } from "lucide-react"
|
||||
import { useGovernor } from "@/lib/queries"
|
||||
import type { GovernorRun } from "@/lib/api"
|
||||
|
||||
// ── Token-Wächter („Tankuhr mit Handbremse") ────────────────────────────────
|
||||
// Zeigt in Klartext, wie voll die laufende Coding-Sitzung ist. Zwei Marken:
|
||||
// Soft = ab hier bittet der Governor um einen Savepoint (Sitzung sauber beenden)
|
||||
// Hart = ab hier antwortet er selbst und lässt das Modell gar nicht mehr ran
|
||||
// Die Zeile „Schätzung vs. Wahrheit" ist der Ehrlichkeits-Nachweis: bis 25.07. lag der
|
||||
// Governor bei werkzeugdichten Agenten um Faktor 3 daneben, weil er Werkzeug-Schemata
|
||||
// nicht mitzählte. Jetzt lernt er aus den echten prompt_tokens jeder Antwort.
|
||||
|
||||
const fmtNum = (n: number | null | undefined) =>
|
||||
n == null ? "–" : n.toLocaleString("de-DE")
|
||||
|
||||
function ago(ts: number): string {
|
||||
const s = Math.max(0, Date.now() / 1000 - ts)
|
||||
if (s < 60) return `vor ${Math.round(s)} s`
|
||||
if (s < 3600) return `vor ${Math.round(s / 60)} min`
|
||||
return `vor ${Math.round(s / 3600)} h`
|
||||
}
|
||||
|
||||
const ACTION_META: Record<string, { label: string; tone: string }> = {
|
||||
passthrough: { label: "frei", tone: "text-emerald-400" },
|
||||
defer: { label: "wartet", tone: "text-amber-400" },
|
||||
soft: { label: "Savepoint", tone: "text-amber-400" },
|
||||
hard: { label: "STOPP", tone: "text-rose-400" },
|
||||
}
|
||||
|
||||
/** Abweichung Schätzung↔Wahrheit in Prozent — der Ehrlichkeits-Messwert. */
|
||||
function drift(run: GovernorRun | undefined): number | null {
|
||||
if (!run?.est || !run?.exact) return null
|
||||
return ((run.est - run.exact) / run.exact) * 100
|
||||
}
|
||||
|
||||
export function GovernorCard() {
|
||||
const { data } = useGovernor()
|
||||
|
||||
if (!data?.reachable) {
|
||||
return (
|
||||
<div className="mc-card p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Fuel className="h-4.5 w-4.5 text-muted-foreground" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-muted-foreground">
|
||||
Nicht erreichbar. Die Coding-Sitzungen laufen dann ohne Sitzungs-Bremse.
|
||||
<div className="mt-1 font-mono text-[10px] text-muted-foreground/60">
|
||||
{data?.error ?? "keine Antwort von :8100"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const soft = data.soft ?? 0
|
||||
const hard = data.hard ?? null
|
||||
const runs = data.recent ?? []
|
||||
// Nur echte Bau-Läufe füllen die Tankuhr; Lucys Gespräche sind ausgenommen.
|
||||
const lastBuild = runs.find((r) => !(data.exempt_models ?? []).includes(r.model))
|
||||
const füllung = lastBuild?.exact ?? lastBuild?.est ?? 0
|
||||
const skala = Math.max(hard ?? soft * 1.2, soft * 1.2, füllung)
|
||||
const pct = Math.min(100, (füllung / skala) * 100)
|
||||
const abw = drift(runs.find((r) => r.est != null && r.exact != null))
|
||||
const c = data.counters
|
||||
|
||||
const tone =
|
||||
hard && füllung >= hard ? "bg-rose-500"
|
||||
: füllung >= soft ? "bg-amber-500"
|
||||
: "bg-emerald-500"
|
||||
|
||||
return (
|
||||
<div className="mc-card p-5">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Fuel className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
|
||||
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="font-space text-3xl font-bold tracking-tight tabular-nums text-foreground">
|
||||
{fmtNum(füllung)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Tokens in der letzten Bau-Anfrage
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 pt-1 text-right">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-amber-400">
|
||||
Savepoint ab {fmtNum(soft)}
|
||||
</div>
|
||||
{hard != null && (
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-rose-400">
|
||||
Stopp ab {fmtNum(hard)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tankuhr */}
|
||||
<div className="relative h-3 overflow-hidden rounded-sm bg-muted/25">
|
||||
<div className={`h-full ${tone} transition-all`} style={{ width: `${pct}%` }} />
|
||||
<div
|
||||
className="absolute inset-y-0 w-px bg-amber-400/80"
|
||||
style={{ left: `${Math.min(100, (soft / skala) * 100)}%` }}
|
||||
title={`Savepoint-Marke ${fmtNum(soft)}`}
|
||||
/>
|
||||
{hard != null && (
|
||||
<div
|
||||
className="absolute inset-y-0 w-px bg-rose-400/80"
|
||||
style={{ left: `${Math.min(100, (hard / skala) * 100)}%` }}
|
||||
title={`Harter Stopp ${fmtNum(hard)}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Zählerstand */}
|
||||
<div className="mt-3 grid grid-cols-4 gap-2 text-center">
|
||||
{[
|
||||
{ k: "Anfragen", v: fmtNum(c?.chat) },
|
||||
{ k: "Savepoints", v: fmtNum(c?.soft) },
|
||||
{ k: "Stopps", v: fmtNum(c?.hard) },
|
||||
{ k: "Tokens ges.", v: fmtNum(c?.tokens_prompt) },
|
||||
].map((x) => (
|
||||
<div key={x.k} className="rounded-sm bg-muted/20 py-1.5">
|
||||
<div className="font-mono text-sm font-semibold tabular-nums text-foreground">{x.v}</div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground/70">{x.k}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Ehrlichkeits-Nachweis */}
|
||||
{abw != null && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-sm bg-muted/15 px-2.5 py-1.5">
|
||||
{Math.abs(abw) < 15
|
||||
? <Check className="h-3.5 w-3.5 shrink-0 text-emerald-400" />
|
||||
: <ShieldAlert className="h-3.5 w-3.5 shrink-0 text-amber-400" />}
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
Schätzung lag{" "}
|
||||
<span className={Math.abs(abw) < 15 ? "font-semibold text-emerald-400" : "font-semibold text-amber-400"}>
|
||||
{abw > 0 ? "+" : ""}{abw.toFixed(1)} %
|
||||
</span>{" "}
|
||||
neben der Wahrheit — er kalibriert sich aus jeder Antwort selbst nach.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Letzte Läufe */}
|
||||
{runs.length > 0 && (
|
||||
<div className="mt-3 space-y-1">
|
||||
{runs.slice(0, 5).map((r, i) => {
|
||||
const m = ACTION_META[r.action] ?? { label: r.action, tone: "text-muted-foreground" }
|
||||
return (
|
||||
<div key={`${r.t}-${i}`} className="flex items-center gap-2 text-[11px]">
|
||||
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(r.t)}</span>
|
||||
<span className="w-20 shrink-0 truncate font-medium text-foreground">{r.model || "?"}</span>
|
||||
<span className="flex-1 font-mono tabular-nums text-muted-foreground">
|
||||
{fmtNum(r.exact ?? r.est)}
|
||||
{r.exact != null && r.est != null && (
|
||||
<span className="text-muted-foreground/50"> (geschätzt {fmtNum(r.est)})</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`shrink-0 font-semibold ${m.tone}`}>{m.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
|
||||
Alle Coding-Agenten laufen durch den Wächter (:8100). Lucys Alltagsmodelle sind
|
||||
ausgenommen — ein Gespräch ist keine Bau-Sitzung und wird nie unterbrochen.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
|
||||
// Kleine Abschnitts-Überschrift innerhalb einer Seite. Lag vorher in AuftragsbuchView;
|
||||
// seit der Ideen-Queue eine eigene Seite hat (25.07.2026) brauchen beide sie — also
|
||||
// einmal hier statt zweimal dupliziert.
|
||||
export function SectionLabel({ icon: Icon, children }: { icon: LucideIcon; children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
||||
<Icon className="h-3.5 w-3.5" /> {children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -30,3 +30,9 @@ export function fmtEta(s?: number): string {
|
||||
export function fmtCtx(c: number | null): string {
|
||||
return c ? `${Math.round(c / 1024)}k` : "—"
|
||||
}
|
||||
|
||||
/** Unix-Sekunden → "25.07., 21:04"; "—" bei 0/null/undefined. */
|
||||
export function fmtWhen(ts: number | null | undefined): string {
|
||||
if (!ts) return "—"
|
||||
return new Date(ts * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
+7
-1
@@ -10,10 +10,11 @@ import {
|
||||
Sparkles,
|
||||
SquareTerminal,
|
||||
HelpCircle,
|
||||
Lightbulb,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
export type ViewId = "dashboard" | "auftraege" | "eigenleben" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
export type ViewId = "dashboard" | "ideen" | "auftraege" | "eigenleben" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
|
||||
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
||||
|
||||
@@ -28,6 +29,11 @@ export interface NavItem {
|
||||
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
|
||||
export const NAV: NavItem[] = [
|
||||
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
||||
// Der Haupteingang: Idee → Konzept → Repo → Zed. Steht bewusst direkt unter dem
|
||||
// Cockpit — hier fängt jede Arbeit an (eigene Seite seit 25.07.2026).
|
||||
// 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", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
|
||||
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
||||
{ id: "eigenleben", label: "Skills", hint: "Fähigkeiten der Box — aktiv/inaktiv, Herkunft & Vorschlags-Bilanz", icon: Sparkles, group: "Operativ" },
|
||||
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
|
||||
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Layers, Lightbulb, Send, HelpCircle,
|
||||
RotateCcw, Square, ArrowUp, ArrowDown, Code2, FileText, GitFork,
|
||||
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, ArrowRight,
|
||||
} from "lucide-react"
|
||||
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type IdeenKonzept, type IdeenProjekt, type SkillKandidat } from "@/lib/api"
|
||||
import { Markdown } from "@/lib/markdown"
|
||||
import { useAuftragsbuch, useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
|
||||
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.
|
||||
@@ -31,10 +31,6 @@ const STATE_UI: Record<string, { label: string; cls: string; spin?: boolean }> =
|
||||
abgelehnt: { label: "abgelehnt", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||
}
|
||||
|
||||
function fmtWhen(ts: number | null | undefined): string {
|
||||
if (!ts) return "—"
|
||||
return new Date(ts * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
export function AuftragsbuchView() {
|
||||
const { data, isLoading, refetch, isFetching } = useAuftragsbuch()
|
||||
@@ -62,8 +58,11 @@ export function AuftragsbuchView() {
|
||||
|
||||
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 && blockedIdeen === 0
|
||||
const empty = items.length === 0 && kandidaten.length === 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -104,7 +103,20 @@ export function AuftragsbuchView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IdeenSection />
|
||||
{blockedIdeen > 0 && (
|
||||
<button
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "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">
|
||||
@@ -129,759 +141,6 @@ export function AuftragsbuchView() {
|
||||
)
|
||||
}
|
||||
|
||||
function SectionLabel({ icon: Icon, children }: { icon: any; children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
||||
<Icon className="h-3.5 w-3.5" /> {children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
|
||||
// Eine Queue, drei Türen (Zentrale/Telegram/Lucy). Idee rein → die Box arbeitet sie
|
||||
// selbst aus (Specifier) und baut (Werkstatt-Profil) → Code kommt als Karte zurück.
|
||||
|
||||
const IDEEN_STATE: Record<string, { label: string; cls: string; spin?: boolean }> = {
|
||||
triage: { label: "wird ausgearbeitet …", cls: "text-violet-300 border-violet-500/40 bg-violet-500/10", spin: true },
|
||||
todo: { label: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||
scheduled: { label: "geplant", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||
ready: { label: "gleich dran", cls: "text-sky-300/80 border-sky-500/30 bg-sky-500/5" },
|
||||
running: { label: "in Arbeit …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
|
||||
review: { label: "im Review", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
|
||||
blocked: { label: "hängt — braucht dich", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
|
||||
done: { label: "fertig ✓", cls: "text-emerald-300 border-emerald-500/40 bg-emerald-500/10" },
|
||||
}
|
||||
|
||||
// Welche Welt hat die Karte durchlaufen? Vorher stand auf Idee UND IDE-Projekt dasselbe
|
||||
// „IDE" — man sah der fertigen Karte nicht an, ob sie nur durchdacht oder als Repo
|
||||
// vorbereitet wurde (User 21.07.). Jetzt drei ehrliche Marken.
|
||||
const ART_UI: Record<string, { label: string; cls: string; icon: typeof Hammer; title: string }> = {
|
||||
box: {
|
||||
label: "BOX", icon: Hammer,
|
||||
cls: "border-border/60 bg-background/40 text-muted-foreground",
|
||||
title: "Box-Welt: die Werkstatt baut das selbst. Das Ergebnis kommt als Patch-Vorschlag zurück — nichts geht ohne deinen Klick live.",
|
||||
},
|
||||
idee: {
|
||||
label: "IDEE", icon: Lightbulb,
|
||||
cls: "border-sky-500/40 bg-sky-500/10 text-sky-300",
|
||||
title: "Nur durchdacht: die Box hat die Produkt-Idee mit voller Denk-Kaskade zu einem Konzept ausgearbeitet. Kein Gerüst, kein Bau — du liest es und entscheidest, ob ein Projekt draus wird.",
|
||||
},
|
||||
projekt: {
|
||||
label: "IDE", icon: Code2,
|
||||
cls: "border-violet-500/40 bg-violet-500/10 text-violet-300",
|
||||
title: "IDE-Welt: die Box bereitet nur vor (Konzept + Repo + Doku, kein Code). Gebaut wird von dir in Zed.",
|
||||
},
|
||||
}
|
||||
|
||||
// Wie die Frage einer hängenden Karte eingeleitet wird — je nach Block-Grund des Workers.
|
||||
const FRAGE_HINT: Record<string, string> = {
|
||||
needs_input: "Die Box hat eine Frage:",
|
||||
capability: "Die Box kam hier nicht weiter:",
|
||||
transient: "Die Aufgabe hakt:",
|
||||
}
|
||||
|
||||
function IdeenSection() {
|
||||
const { data, isLoading } = useIdeen()
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [text, setText] = useState("")
|
||||
// Intake-Modus: Box baut · Idee (nur durchdenken) · IDE simpel/gründlich (Projekt vorbereiten).
|
||||
const [modus, setModus] = useState<"box" | "idee" | "simpel" | "gruendlich">("box")
|
||||
const isIdee = modus === "idee" // Produkt-Idee prüfen
|
||||
const isProjekt = modus === "simpel" || modus === "gruendlich" // Projekt für Zed
|
||||
const isIde = isIdee || isProjekt // beides → projektstart
|
||||
// Ziel-Plattform des Projekts (nur IDE-Welt): läuft es als LXC-Dienst oder in Docker?
|
||||
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
|
||||
const [sending, setSending] = useState(false)
|
||||
const [busy, setBusy] = useState<Record<string, boolean>>({})
|
||||
const [openLog, setOpenLog] = useState<string | null>(null)
|
||||
const [openKonzept, setOpenKonzept] = useState<string | null>(null)
|
||||
const [antworten, setAntworten] = useState<Record<string, string>>({})
|
||||
|
||||
const items = data?.items ?? []
|
||||
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
|
||||
|
||||
// Polling-Log nur aktivieren, wenn Idee triage/running und geöffnet
|
||||
const logEnabled = openLog !== null && (data?.items.find((i) => i.id === openLog)?.status === "triage" || data?.items.find((i) => i.id === openLog)?.status === "running")
|
||||
const logData = useIdeenLog(openLog ?? "", logEnabled)
|
||||
|
||||
async function submit() {
|
||||
const t = text.trim()
|
||||
if (t.length < 3 || sending) return
|
||||
setSending(true)
|
||||
try {
|
||||
// Lange Eingaben: Kurzfassung als Titel, alles als Notiz — der Specifier der Box
|
||||
// macht daraus ohnehin einen sauberen Auftrag.
|
||||
const titel = t.length > 160 ? t.slice(0, 157) + "…" : t
|
||||
await api("/api/ideen", { method: "POST", body: JSON.stringify({
|
||||
titel, notiz: t.length > 160 ? t : "",
|
||||
welt: isIde ? "ide" : "box",
|
||||
tiefe: isProjekt ? modus : "",
|
||||
ziel: isProjekt ? ziel : "", // eine Idee braucht noch keine Plattform
|
||||
art: isIdee ? "idee" : "projekt",
|
||||
}) })
|
||||
setText("")
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function archive(id: string) {
|
||||
setBusy((b) => ({ ...b, [id]: true }))
|
||||
try {
|
||||
await api("/api/ideen/archivieren", { method: "POST", body: JSON.stringify({ id }) })
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, [id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Karten-Steuerung (Stopp / Neuer Versuch / Prio) — ein Helfer für alle drei Endpunkte.
|
||||
async function steuer(prefix: string, id: string, pfad: string, body: object) {
|
||||
setBusy((b) => ({ ...b, [prefix + id]: true }))
|
||||
try {
|
||||
await api(pfad, { method: "POST", body: JSON.stringify(body) })
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, [prefix + id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Hängende Aufgabe beantworten: die Antwort geht als Kommentar an die Karte, der Worker
|
||||
// macht damit weiter (Ausweg aus der Sackgasse „fragt, aber niemand kann antworten").
|
||||
async function answer(id: string) {
|
||||
const t = (antworten[id] || "").trim()
|
||||
if (t.length < 1 || busy["ans:" + id]) return
|
||||
setBusy((b) => ({ ...b, ["ans:" + id]: true }))
|
||||
try {
|
||||
await api("/api/ideen/antwort", { method: "POST", body: JSON.stringify({ id, antwort: t }) })
|
||||
setAntworten((a) => ({ ...a, [id]: "" }))
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, ["ans:" + id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// EINE Zeilen-Darstellung für alle Sichten (Projekt-Gruppen + Einzelkarten) — damit
|
||||
// Antwortfeld, Live-Log und Ergebnis-Panel überall identisch funktionieren.
|
||||
const zeile = (it: IdeenItem) => {
|
||||
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
|
||||
const isOpen = openLog === it.id
|
||||
// Aufklappbar: laufende Karten zeigen das Live-Log, FERTIGE das Ergebnis
|
||||
// (User-Wunsch 15.07.: „Dinge werden getestet, ich sehe das Ergebnis nicht").
|
||||
const canOpen = it.status === "triage" || it.status === "running" || it.status === "done"
|
||||
const blocked = it.status === "blocked"
|
||||
const answering = !!busy["ans:" + it.id]
|
||||
// Hänger-Verdacht: läuft, aber seit >10 min kein Lebenszeichen (Heartbeat) — der
|
||||
// Prozess kann trotzdem „leben" (wartet z. B. auf einen selbst gestarteten Server).
|
||||
const haengt = it.status === "running" && (it.puls_alter ?? 0) > 600
|
||||
const wartend = it.status === "todo" || it.status === "ready" || it.status === "scheduled"
|
||||
return (
|
||||
<li key={it.id} className={cn("flex flex-col gap-2 rounded-xl border bg-background/30 px-3 py-2", blocked ? "border-red-500/30" : haengt ? "border-amber-500/40" : "border-border/40")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("flex shrink-0 items-center gap-1.5 rounded-lg border px-2 py-0.5 text-[10px] font-bold", ui.cls)}>
|
||||
{ui.spin && <Loader2 className="h-3 w-3 animate-spin" />}
|
||||
{ui.label}
|
||||
</span>
|
||||
{haengt && (
|
||||
<span className="flex shrink-0 items-center gap-1 rounded-lg border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-bold text-amber-300"
|
||||
title="Der Worker lebt, gibt aber kein Lebenszeichen mehr — vermutlich wartet er auf etwas, das nie kommt. „Neuer Versuch“ startet die Karte frisch.">
|
||||
<AlertTriangle className="h-3 w-3" /> {Math.round((it.puls_alter ?? 0) / 60)} min ohne Regung
|
||||
</span>
|
||||
)}
|
||||
{(it.prio ?? 0) !== 0 && (
|
||||
<span className="shrink-0 rounded bg-background/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
title="Priorität — höher kommt zuerst dran (bei gleicher Prio: Erstellzeit)">P{it.prio}</span>
|
||||
)}
|
||||
{(() => {
|
||||
const a = ART_UI[it.art || (it.welt === "ide" ? "projekt" : "box")]
|
||||
if (!a) return null
|
||||
const Icon = a.icon
|
||||
return (
|
||||
<span className={cn("flex shrink-0 items-center gap-1 rounded-lg border px-2 py-0.5 text-[10px] font-bold", a.cls)} title={a.title}>
|
||||
<Icon className="h-3 w-3" /> {a.label}
|
||||
</span>
|
||||
)
|
||||
})()}
|
||||
{/* Tooltip = der volle TITEL (der ist abgeschnitten), nicht der Auftragstext —
|
||||
der beginnt mit Vorspann-Textbausteinen und half beim Lesen nie. */}
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-foreground" title={it.titel}>{it.titel}</span>
|
||||
<span className="flex shrink-0 items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(it.erstellt)}</span>
|
||||
{wartend && (
|
||||
<span className="flex shrink-0 items-center">
|
||||
<button
|
||||
disabled={!!busy["pr:" + it.id]}
|
||||
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "hoch" })}
|
||||
title="Vorziehen (Prio +1)"
|
||||
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!!busy["pr:" + it.id]}
|
||||
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "runter" })}
|
||||
title="Zurückstellen (Prio −1)"
|
||||
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{(it.status === "running" || blocked) && (
|
||||
<button
|
||||
disabled={!!busy["rt:" + it.id]}
|
||||
onClick={() =>
|
||||
showConfirm("Neuer Versuch?",
|
||||
it.status === "running"
|
||||
? `Der laufende Worker von '${it.titel.slice(0, 60)}' wird beendet (samt gestarteter Test-Prozesse) und die Karte frisch gestartet. Bisheriger Zwischenstand im Workspace bleibt liegen.`
|
||||
: `Die Karte '${it.titel.slice(0, 60)}' wird entsperrt und läuft weiter, sobald ein Platz frei ist.`,
|
||||
() => steuer("rt:", it.id, "/api/ideen/retry", { id: it.id }))}
|
||||
title="Neuer Versuch — Karte frisch starten"
|
||||
className={cn("shrink-0 rounded-md p-1 transition-colors cursor-pointer disabled:opacity-50",
|
||||
haengt ? "text-amber-300 hover:text-amber-200" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
{busy["rt:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RotateCcw className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{(it.status === "running" || wartend || it.status === "triage") && (
|
||||
<button
|
||||
disabled={!!busy["st:" + it.id]}
|
||||
onClick={() =>
|
||||
showConfirm("Karte stoppen?",
|
||||
`'${it.titel.slice(0, 60)}' wird angehalten${it.status === "running" ? " und der laufende Worker beendet" : ""}. Sie bleibt als gestoppt im Board und kann jederzeit per „Neuer Versuch“ weiterlaufen.`,
|
||||
() => steuer("st:", it.id, "/api/ideen/stopp", { id: it.id }))}
|
||||
title="Stoppen — Karte anhalten"
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-red-300 cursor-pointer disabled:opacity-50">
|
||||
{busy["st:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Square className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{(it.status === "done" || blocked) && (
|
||||
<button
|
||||
disabled={!!busy[it.id]}
|
||||
onClick={() => archive(it.id)}
|
||||
title={blocked ? "Aufgeben und aus der Liste räumen (wandert ins Kanban-Archiv)" : "Aus der Liste räumen (wandert ins Kanban-Archiv)"}
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
{busy[it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{it.welt === "ide" && (it.status === "running" || it.status === "done") && (
|
||||
<button
|
||||
onClick={() => setOpenKonzept(openKonzept === it.id ? null : it.id)}
|
||||
title="Konzept ansehen (das ausgearbeitete KONZEPT.md)"
|
||||
className={cn("flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-bold transition-colors cursor-pointer",
|
||||
openKonzept === it.id ? "text-violet-300" : "text-violet-300/70 hover:text-violet-200")}>
|
||||
<FileText className="h-3.5 w-3.5" /> Konzept
|
||||
</button>
|
||||
)}
|
||||
{canOpen && (
|
||||
<button
|
||||
onClick={() => setOpenLog(isOpen ? null : it.id)}
|
||||
title={it.status === "done" ? "Ergebnis anzeigen" : "Worker-Log anzeigen"}
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer">
|
||||
{isOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openKonzept === it.id && (
|
||||
<KonzeptPanel
|
||||
id={it.id}
|
||||
istIdee={it.art === "idee" && it.status === "done"}
|
||||
onWeiter={reload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{it.status === "running" && it.notiz && (
|
||||
<p className="-mt-1 ml-1 text-[11px] italic leading-relaxed text-sky-200/80" title="Jüngste Zwischenmeldung des Workers (Heartbeat)">
|
||||
„{it.notiz}“
|
||||
</p>
|
||||
)}
|
||||
{(it.status === "todo" || it.status === "scheduled" || it.status === "ready") && (it.wartet_auf?.length ?? 0) > 0 && (
|
||||
<p className="-mt-1 ml-1 text-[10px] text-muted-foreground/70">
|
||||
→ wartet auf: {it.wartet_auf!.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{blocked && (
|
||||
<div className="space-y-2 border-t border-red-500/20 pt-2">
|
||||
<p className="flex items-start gap-1.5 text-[11px] leading-relaxed text-red-200/90">
|
||||
<HelpCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-300" />
|
||||
<span>
|
||||
<b>{FRAGE_HINT[it.frage_kind || ""] ?? "Die Aufgabe hängt und wartet auf dich:"}</b>{" "}
|
||||
{it.frage
|
||||
? <span className="text-foreground/90">{it.frage}</span>
|
||||
: <span className="text-muted-foreground italic">kein genauer Grund hinterlegt — antworte einfach mit einer Entscheidung, dann läuft sie weiter.</span>}
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={antworten[it.id] || ""}
|
||||
onChange={(e) => setAntworten((a) => ({ ...a, [it.id]: e.target.value }))}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") answer(it.id) }}
|
||||
placeholder="Deine Antwort — die Box macht damit weiter"
|
||||
maxLength={2000}
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => answer(it.id)}
|
||||
disabled={answering || (antworten[it.id] || "").trim().length < 1}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-emerald-300 transition-all hover:bg-emerald-500/20 cursor-pointer disabled:opacity-50">
|
||||
{answering ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} Antworten & weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && canOpen && it.status !== "done" && (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-border/50 bg-background/50 p-2">
|
||||
{logEnabled && logData.isLoading ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Log wird geladen …</p>
|
||||
) : logData.error ? (
|
||||
<p className="text-[10px] text-red-400">Log nicht ladbar</p>
|
||||
) : logData.data?.available && logData.data.lines.length > 0 ? (
|
||||
<pre className="max-h-64 overflow-auto font-mono text-[10px] leading-relaxed text-foreground/80 whitespace-pre-wrap">
|
||||
{logData.data.lines.join("\n")}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground">Kein Log verfügbar</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && it.status === "done" && <ErgebnisPanel id={it.id} />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
// Windows-Dev / Queue nicht verfügbar → Sektion still weglassen (wie der Rest der Seite).
|
||||
if (data && !data.available) return null
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<SectionLabel icon={Lightbulb}>
|
||||
Ideen-Queue{data ? ` — ${data.offen} offen` : ""}
|
||||
</SectionLabel>
|
||||
<div className="mc-card p-4 space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
|
||||
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
|
||||
className="h-9 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={sending || text.trim().length < 3}
|
||||
className={cn("flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-3 text-[11px] font-bold uppercase tracking-wide transition-all cursor-pointer disabled:opacity-50",
|
||||
modus === "gruendlich" ? "border-amber-500/40 bg-amber-500/10 text-amber-300 hover:bg-amber-500/20"
|
||||
: modus === "simpel" ? "border-violet-500/40 bg-violet-500/10 text-violet-300 hover:bg-violet-500/20"
|
||||
: isIdee ? "border-sky-500/40 bg-sky-500/10 text-sky-300 hover:bg-sky-500/20"
|
||||
: "border-primary/40 bg-primary/10 text-primary hover:bg-primary/20")}>
|
||||
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : isIdee ? <Lightbulb className="h-3.5 w-3.5" /> : isProjekt ? <Code2 className="h-3.5 w-3.5" /> : <Send className="h-3.5 w-3.5" />}
|
||||
{isIdee ? "Durchdenken" : isProjekt ? "Vorbereiten" : "In die Queue"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-muted-foreground/70">Modus</span>
|
||||
<div className="flex rounded-lg border border-border/60 p-0.5">
|
||||
<button
|
||||
onClick={() => setModus("box")}
|
||||
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
modus === "box" ? "bg-primary/15 text-primary" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
<Hammer className="h-3 w-3" /> Box
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModus("idee")}
|
||||
title="Produkt-Idee erst einmal durchdenken lassen: Was ist es, für wen, was gibt es schon, was spricht dagegen? Ergebnis ist ein Konzept — kein Projekt, kein Bau-Druck."
|
||||
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
modus === "idee" ? "bg-sky-500/15 text-sky-300" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
<Lightbulb className="h-3 w-3" /> Idee
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModus("simpel")}
|
||||
title="IDE-Projekt, schnell vorbereitet: knappes Konzept, wenige Denk-Runden. Für kleine, klar umrissene Sachen."
|
||||
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
modus === "simpel" ? "bg-violet-500/15 text-violet-300" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
<Code2 className="h-3 w-3" /> IDE: simpel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModus("gruendlich")}
|
||||
title="IDE-Projekt, gründlich durchdacht: volle Kaskade mit mehreren Rollen und Härtetests. Für große/ambitionierte Projekte."
|
||||
className={cn("flex items-center gap-1 rounded px-2 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
modus === "gruendlich" ? "bg-amber-500/15 text-amber-300" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
<Code2 className="h-3 w-3" /> IDE: gründlich
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Immer sichtbare Orientierung — der Text darunter erklärt nur den GEWÄHLTEN
|
||||
Modus, die Wahl selbst muss man aber vorher treffen können (User 21.07.). */}
|
||||
<p className="text-[10px] leading-relaxed text-muted-foreground/60">
|
||||
<b className="text-primary/80">Box</b>, wenn die Box es selbst bauen soll (dein Heimlabor,
|
||||
MC2, Lucy — Ergebnis kommt als Patch-Vorschlag zurück). ·{" "}
|
||||
<b className="text-sky-300/80">Idee</b>, wenn du noch nicht weißt, ob es das überhaupt wert
|
||||
ist — erst durchdenken, entscheiden kannst du danach. ·{" "}
|
||||
<b className="text-violet-300/80">IDE</b>, wenn du es selbst in Zed bauen willst und dafür
|
||||
ein startklares Repo brauchst.
|
||||
</p>
|
||||
{isProjekt && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
<button
|
||||
onClick={() => setZiel("lxc")}
|
||||
title="Läuft als Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker). Wird vom Backup-Server automatisch mitgesichert."
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
ziel === "lxc" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
LXC
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setZiel("docker")}
|
||||
title="Natives Docker nach Standard-Praxis: Dockerfile + compose, portabel, über Gitea-/GitLab-Pipelines baubar. Für Projekte, die bewusst außerhalb der LXC-Welt leben."
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
ziel === "docker" ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
Docker
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground/50">
|
||||
{ziel === "lxc" ? "Dienst im Proxmox-Container (deine Heimat)" : "natives Docker, portabel & pipeline-tauglich"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{isIdee ? (
|
||||
<>
|
||||
<b className="text-sky-300/90">Idee durchdenken:</b> Die Box arbeitet deine Produkt-Idee
|
||||
mit Produkt-Blick aus — was ist es genau, für wen, was gibt es dafür schon, was wäre der
|
||||
kleinste sinnvolle Wurf und was spricht dagegen. Volle Denk-Kaskade mit Kritiker, Ergebnis
|
||||
ist <b>nur ein Konzept</b> (kein Gerüst, keine Roadmap, kein Bau-Druck) — du liest es hier
|
||||
und entscheidest danach, ob ein Projekt draus wird.
|
||||
</>
|
||||
) : isProjekt ? (
|
||||
<>
|
||||
<b className={modus === "gruendlich" ? "text-amber-300/90" : "text-violet-300/90"}>IDE-Welt{modus === "gruendlich" ? " · gründlich" : " · simpel"}:</b>{" "}
|
||||
Die Box denkt das Konzept durch{modus === "gruendlich"
|
||||
? " — volle Kaskade: mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Kritiker-Härtetest (für große/ambitionierte Projekte, dauert entsprechend)"
|
||||
: " — schlank: 2 Rollen, 1 Denk-Runde (für kleine, klar umrissene Projekte, ein paar Minuten)"}, legt
|
||||
ein leeres Gitea-Repo an und füllt es nur mit den wichtigen Dokumenten (Konzept, Roadmap, AGENTS.md,
|
||||
.aiexclude) — <b>kein Code</b>. Danach öffnest du das Repo in Zed und baust selbst.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
|
||||
aus und baut — Code-Ergebnisse erscheinen als Karte unter „Patch-Vorschläge“, nichts davon geht ohne
|
||||
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{isLoading && (
|
||||
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen …</p>
|
||||
)}
|
||||
{data?.fehler && (
|
||||
<p className="text-xs text-amber-300">Queue gerade nicht lesbar — neuer Versuch läuft.</p>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{(data?.projekte ?? []).map((p) => (
|
||||
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)} zeile={zeile} />
|
||||
))}
|
||||
{(() => {
|
||||
// Einzelkarten (keine Kette) — die klassische flache Liste wie bisher.
|
||||
const keys = new Set((data?.projekte ?? []).map((p) => p.key))
|
||||
const einzeln = items.filter((i) => !i.projekt || !keys.has(i.projekt))
|
||||
return einzeln.length > 0 ? <ul className="space-y-1.5">{einzeln.map((it) => zeile(it))}</ul> : null
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{data?.available && !isLoading && items.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground/70">Queue ist leer — wirf der Box eine Idee zu.</p>
|
||||
)}
|
||||
</div>
|
||||
{dialogElement}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// Karten-Familie (verkettete Karten = ein Projekt) als Block: Fortschritt oben, dann die
|
||||
// AKTIVEN Karten in Ausführungs-Reihenfolge, Wartende und Fertige zusammengeklappt.
|
||||
// Ersetzt für Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
|
||||
const AKTIV_RANG: Record<string, number> = { blocked: 0, running: 1, review: 2, ready: 3, triage: 4 }
|
||||
|
||||
function ProjektGruppe({ projekt, karten, zeile }: {
|
||||
projekt: IdeenProjekt
|
||||
karten: IdeenItem[]
|
||||
zeile: (it: IdeenItem) => React.ReactNode
|
||||
}) {
|
||||
const [zeigWartende, setZeigWartende] = useState(false)
|
||||
const [zeigFertige, setZeigFertige] = useState(false)
|
||||
const aktiv = karten
|
||||
.filter((i) => AKTIV_RANG[i.status] !== undefined)
|
||||
.sort((a, b) => (AKTIV_RANG[a.status] - AKTIV_RANG[b.status]) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
|
||||
const wartend = karten
|
||||
.filter((i) => i.status === "todo" || i.status === "scheduled")
|
||||
.sort((a, b) => ((a.wartet_auf?.length ? 1 : 0) - (b.wartet_auf?.length ? 1 : 0)) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
|
||||
const fertig = karten
|
||||
.filter((i) => i.status === "done")
|
||||
.sort((a, b) => (b.fertig ?? 0) - (a.fertig ?? 0))
|
||||
const anteil = projekt.gesamt > 0 ? Math.round((projekt.fertig / projekt.gesamt) * 100) : 0
|
||||
return (
|
||||
<div className="space-y-2 rounded-xl border border-border/50 bg-background/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 shrink-0 text-primary/70" />
|
||||
<p className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground" title={projekt.titel}>{projekt.titel}</p>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">{projekt.fertig} von {projekt.gesamt} fertig</span>
|
||||
</div>
|
||||
<div className="h-1 overflow-hidden rounded-full bg-background/60" title={`${anteil}%`}>
|
||||
<div className="h-1 rounded-full bg-primary/70 transition-all" style={{ width: `${anteil}%` }} />
|
||||
</div>
|
||||
{aktiv.length > 0 && <ul className="space-y-1.5">{aktiv.map((it) => zeile(it))}</ul>}
|
||||
{wartend.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setZeigWartende((v) => !v)}
|
||||
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
{zeigWartende ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
{wartend.length} Karte{wartend.length === 1 ? "" : "n"} in der Kette — laufen automatisch der Reihe nach
|
||||
</button>
|
||||
{zeigWartende && <ul className="mt-1.5 space-y-1.5">{wartend.map((it) => zeile(it))}</ul>}
|
||||
</div>
|
||||
)}
|
||||
{fertig.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setZeigFertige((v) => !v)}
|
||||
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
|
||||
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
{fertig.length} fertig
|
||||
</button>
|
||||
{zeigFertige && <ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Konzept einer IDE-Karte: holt es aus dem Gitea-Repo — oder, wenn der Worker keins
|
||||
// angelegt hat, aus seiner Datei in ~/konzepte (Bug-Fund 21.07.: dann stand hier vorher
|
||||
// „noch kein Repo hinterlegt", obwohl das Konzept längst fertig war).
|
||||
// Bei einer fertig durchdachten IDEE hängt unten der Weg nach vorn: „Gefällt mir".
|
||||
function KonzeptPanel({ id, istIdee, onWeiter }: {
|
||||
id: string
|
||||
istIdee?: boolean
|
||||
onWeiter?: () => void
|
||||
}) {
|
||||
const [data, setData] = useState<IdeenKonzept | null>(null)
|
||||
const [fehler, setFehler] = useState(false)
|
||||
const [versuch, setVersuch] = useState(0)
|
||||
useEffect(() => {
|
||||
let weg = false
|
||||
setData(null); setFehler(false)
|
||||
api<IdeenKonzept>(`/api/ideen/${id}/konzept`)
|
||||
.then((r) => { if (!weg) setData(r) })
|
||||
.catch(() => { if (!weg) setFehler(true) })
|
||||
return () => { weg = true }
|
||||
}, [id, versuch])
|
||||
return (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-violet-500/25 bg-violet-500/[0.04] p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-violet-300">
|
||||
<FileText className="h-3 w-3" /> Konzept
|
||||
</p>
|
||||
{data?.repo && (
|
||||
<a href={`https://git.tobisniceshomelab.ddnsfree.com/${data.repo}`}
|
||||
className="flex items-center gap-1 text-[10px] text-violet-300/70 hover:text-violet-200">
|
||||
<GitFork className="h-3 w-3" /> {data.repo}
|
||||
</a>
|
||||
)}
|
||||
{!data?.repo && data?.quelle === "datei" && (
|
||||
<span className="truncate font-mono text-[10px] text-muted-foreground/70"
|
||||
title="Der Worker hat kein Repo angelegt — das Konzept liegt als Datei auf der Box.">
|
||||
{data.pfad}
|
||||
</span>
|
||||
)}
|
||||
<button onClick={() => setVersuch((v) => v + 1)} title="Neu laden"
|
||||
className="ml-auto rounded p-0.5 text-muted-foreground/50 hover:text-foreground cursor-pointer">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
{fehler ? (
|
||||
<p className="text-[11px] text-red-400">Konzept nicht ladbar — später nochmal versuchen.</p>
|
||||
) : data === null ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Konzept wird geladen …</p>
|
||||
) : data.konzept ? (
|
||||
<div className="max-h-[28rem] overflow-y-auto pr-1">
|
||||
<Markdown text={data.konzept} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground italic">
|
||||
{data.error || "Noch kein Konzept — das Projekt wird gerade vorbereitet."}
|
||||
</p>
|
||||
)}
|
||||
{istIdee && data?.konzept && <WeiterLeiste id={id} onFertig={onWeiter} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Was nach dem Lesen passieren kann. Zwei Wege, bewusst nebeneinander statt hintereinander:
|
||||
// nachschärfen (ein Punkt passt nicht — häufiger Fall, darum oben und offen) oder annehmen.
|
||||
function WeiterLeiste({ id, onFertig }: { id: string; onFertig?: () => void }) {
|
||||
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
|
||||
const [tiefe, setTiefe] = useState<"simpel" | "gruendlich">("simpel")
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [neu, setNeu] = useState<{ id: string; art: "projekt" | "runde" } | null>(null)
|
||||
const [fehler, setFehler] = useState("")
|
||||
const [hinweis, setHinweis] = useState("")
|
||||
const ruf = async (pfad: string, rumpf: object, art: "projekt" | "runde") => {
|
||||
setBusy(true); setFehler("")
|
||||
try {
|
||||
const r = await api<{ id: string }>(pfad, { method: "POST", body: JSON.stringify(rumpf) })
|
||||
setNeu({ id: r.id, art })
|
||||
onFertig?.()
|
||||
} catch (e: any) {
|
||||
setFehler(String(e?.message || e))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
if (neu) {
|
||||
return (
|
||||
<p className="mt-3 border-t border-violet-500/20 pt-2 text-[11px] text-emerald-300">
|
||||
{neu.art === "projekt" ? (
|
||||
<>Projekt-Karte <span className="font-mono">{neu.id}</span> ist angelegt — die Box bereitet
|
||||
das Repo vor (Konzept übernimmt sie, sie denkt es nicht neu). Danach öffnest du es in Zed.</>
|
||||
) : (
|
||||
<>Überarbeitung <span className="font-mono">{neu.id}</span> läuft — die Box arbeitet deine
|
||||
Rückmeldung ein und schreibt die neue Fassung an dieselbe Stelle. Danach hier wieder
|
||||
„Konzept" öffnen.</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="mt-3 space-y-3 border-t border-violet-500/20 pt-2.5">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<b className="text-sky-300/90">Passt etwas nicht?</b> Schreib hin, was anders werden soll —
|
||||
die Box arbeitet es ein und lässt den Rest stehen. Beliebig oft wiederholbar.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={hinweis}
|
||||
onChange={(e) => setHinweis(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && hinweis.trim().length >= 3 && !busy) ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde") }}
|
||||
placeholder="z. B. „Punkt 3: kein WebSocket, das ist mir zu fragil — nimm Polling“"
|
||||
maxLength={2000}
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-sky-400/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde")}
|
||||
disabled={busy || hinweis.trim().length < 3}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-sky-500/40 bg-sky-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-sky-300 transition-all hover:bg-sky-500/20 cursor-pointer disabled:opacity-50">
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
|
||||
Nachschärfen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<b className="text-violet-300/90">Passt es so?</b> Dann macht die Box daraus ein startklares
|
||||
Repo — Konzept wird übernommen (nicht neu gedacht), dazu Roadmap, AGENTS.md & Co.
|
||||
Gebaut wird danach von dir in Zed.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
{(["lxc", "docker"] as const).map((z) => (
|
||||
<button key={z} onClick={() => setZiel(z)}
|
||||
title={z === "lxc" ? "Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker) — wird automatisch mitgesichert."
|
||||
: "Natives Docker: Dockerfile + compose, portabel und pipeline-tauglich."}
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
ziel === z ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{z === "lxc" ? "LXC" : "Docker"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground/70">Aufwand</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
{(["simpel", "gruendlich"] as const).map((t) => (
|
||||
<button key={t} onClick={() => setTiefe(t)}
|
||||
title={t === "simpel" ? "Konzept ist schon gehärtet — nur kurz auf die Bau-Sicht nachschärfen (empfohlen)."
|
||||
: "Nochmal volle Kaskade über die Bau-Sicht: mehrere Rollen, Härtetests. Dauert entsprechend."}
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
tiefe === t ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{t === "simpel" ? "schlank" : "gründlich"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => ruf("/api/ideen/weiterfuehren", { id, ziel, tiefe }, "projekt")}
|
||||
disabled={busy}
|
||||
className="ml-auto flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-violet-300 transition-all hover:bg-violet-500/20 cursor-pointer disabled:opacity-50">
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Code2 className="h-3.5 w-3.5" />}
|
||||
Gefällt mir — Projekt daraus machen
|
||||
</button>
|
||||
</div>
|
||||
{fehler && <p className="text-[11px] text-red-400">{fehler}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Ergebnis einer FERTIGEN Queue-Aufgabe: die Abschluss-Zusammenfassung des Workers
|
||||
// (kanban_complete). Lazy — wird erst beim Aufklappen geladen, ändert sich nie mehr.
|
||||
function ErgebnisPanel({ id }: { id: string }) {
|
||||
const [erg, setErg] = useState<IdeenErgebnis | null>(null)
|
||||
const [fehler, setFehler] = useState(false)
|
||||
// Fehler darf nicht kleben: fällt der erste Versuch in ein Deploy-Fenster
|
||||
// (Zentrale startet gerade neu), muss ein Klick neu laden können (Vorfall 16.07.).
|
||||
const [versuch, setVersuch] = useState(0)
|
||||
useEffect(() => {
|
||||
let weg = false
|
||||
setFehler(false)
|
||||
setErg(null)
|
||||
api<IdeenErgebnis>(`/api/ideen/${id}/ergebnis`)
|
||||
.then((r) => { if (!weg) setErg(r) })
|
||||
.catch(() => { if (!weg) setFehler(true) })
|
||||
return () => { weg = true }
|
||||
}, [id, versuch])
|
||||
const text = erg ? (erg.summary || erg.result) : ""
|
||||
return (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/5 p-2.5">
|
||||
<p className="mb-1 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-emerald-300">
|
||||
<ClipboardCheck className="h-3 w-3" /> Ergebnis des Workers
|
||||
</p>
|
||||
{fehler ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVersuch((v) => v + 1)}
|
||||
className="text-[10px] text-red-400 underline decoration-dotted underline-offset-2 hover:text-red-300"
|
||||
>
|
||||
Ergebnis nicht ladbar — nochmal versuchen
|
||||
</button>
|
||||
) : erg === null ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Wird geladen …</p>
|
||||
) : text ? (
|
||||
<p className="max-h-64 overflow-y-auto whitespace-pre-wrap text-xs leading-relaxed text-foreground/85">{text}</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground italic">
|
||||
Der Worker hat keine Abschluss-Zusammenfassung hinterlassen (ältere Karte oder Protokollverstoß).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StateBadge({ item }: { item: AuftragItem }) {
|
||||
const st = item.status
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ import { ErinnerungenCard } from "./ErinnerungenCard"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
||||
import { GovernorCard } from "@/components/dashboard/GovernorCard"
|
||||
|
||||
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
|
||||
const openDrawer = (tab: "maintenance" | "logs") =>
|
||||
@@ -129,10 +128,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
<div className="mt-4">
|
||||
<LatencyCard />
|
||||
</div>
|
||||
{/* Token-Wächter: wie voll ist die laufende Coding-Sitzung, und wie ehrlich zählt er */}
|
||||
<div className="mt-4">
|
||||
<GovernorCard />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
|
||||
|
||||
Reference in New Issue
Block a user