Ideen-Queue: natives Hermes-Kanban wird DIE Sammelstelle (Abloesung S2)
Entscheid nach Probe (Hermes-first): Dispatcher im Gateway, Specifier (triage -> sauberer Auftrag), Profil-Routing und Artefakt-Tracking sind nativ E2E bewiesen — kein Eigenbau. Auftragsbuch bleibt das Klick-Gate. - backend services/ideen.py + routers/ideen.py: MC2-Tuer zur Queue (GET /api/ideen, POST /api/ideen -> kanban create --triage, Archiv) - AuftragsbuchView: Ideen-Queue-Sektion (Eingabefeld + Status-Liste), dist frisch gebaut - mcp_voice.py: idee_notieren (Lucys Sprech-Leitung, Prompt-Diaet: 1 Tool) - chef-gutachter-feed.sh: Morgenlage bekommt kanban stats + Bewegung - Bagatell-Pfad: bagatell-annahme.sh + mc2-bagatell.timer (04:10) — NUR .md-Anlagen/-AEnderungen, max 2/Nacht, gleicher Annahme-Runner mit Health-Gate + Auto-Revert, Chronik/Telegram-Meldung - werkstatt-SOUL.md: Leitplanken des Kanban-Worker-Profils (propose-only, nie main, nie Live-Checkout) — deploy.sh zieht sie kuenftig nach Box-live heute schon (ausserhalb Repo, mit Backups): toolsets+kanban, platform_toolsets.telegram+kanban, SOUL.md-Ideen-Regel, Profil werkstatt (model.default=coder), Gateway neu gestartet + E2E-Tuer-Test gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Hitonabi
parent
ae4b0f229e
commit
e1ca7f6a5e
@@ -29,6 +29,7 @@ from routers import (
|
|||||||
gateway_proxy,
|
gateway_proxy,
|
||||||
health,
|
health,
|
||||||
hermes_ui,
|
hermes_ui,
|
||||||
|
ideen,
|
||||||
maintenance,
|
maintenance,
|
||||||
memory,
|
memory,
|
||||||
models,
|
models,
|
||||||
@@ -123,6 +124,7 @@ app.include_router(
|
|||||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||||
app.include_router(maintenance.router)
|
app.include_router(maintenance.router)
|
||||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||||
|
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||||
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Ideen-Queue-Endpoints — dünner REST-Layer über services/ideen.py (natives Hermes-Kanban).
|
||||||
|
LAN-only wie alle MC2-Endpoints; das Mensch-Gate bleibt die Karte im Auftragsbuch."""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from services import ideen
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
class IdeeIn(BaseModel):
|
||||||
|
titel: str
|
||||||
|
notiz: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class TaskIn(BaseModel):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ideen")
|
||||||
|
def list_queue() -> dict:
|
||||||
|
return ideen.list_queue()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ideen")
|
||||||
|
def add_idea(body: IdeeIn) -> dict:
|
||||||
|
res = ideen.add_idea(body.titel, body.notiz)
|
||||||
|
if not res.get("ok"):
|
||||||
|
raise HTTPException(400, res.get("error", "Idee konnte nicht angelegt werden."))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ideen/archivieren")
|
||||||
|
def archive(body: TaskIn) -> dict:
|
||||||
|
res = ideen.archive_task(body.id)
|
||||||
|
if not res.get("ok"):
|
||||||
|
raise HTTPException(400, res.get("error", "Archivieren fehlgeschlagen."))
|
||||||
|
return res
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
Ideen-Queue — DIE eine Sammelstelle für Ideen des Commanders (Entscheid 10.07.2026).
|
||||||
|
|
||||||
|
Die Queue selbst ist das NATIVE Hermes-Kanban (SQLite-Board, Dispatcher im Gateway):
|
||||||
|
Idee rein (triage) → der Specifier arbeitet sie aus → ein Worker-Profil setzt sie um →
|
||||||
|
Code-Ergebnisse kommen als Vorschlags-Branch zurück und erscheinen als Karte im
|
||||||
|
Auftragsbuch. Dieser Service ist nur die MC2-Tür dazu: anlegen + anzeigen + aufräumen,
|
||||||
|
alles über die Hermes-CLI (stabile --json-Schnittstelle, kein Griff in die kanban.db).
|
||||||
|
|
||||||
|
Türen insgesamt: Telegram/Desktop (natives kanban_create-Tool), Lucy-Voice
|
||||||
|
(mcp_voice.idee_notieren → POST /api/ideen) und die Zentrale (Auftragsbuch-Tab → hier).
|
||||||
|
|
||||||
|
Lokal (Windows-Dev) ist alles harmlos: available=False, Aktionen geben Fehler statt zu crashen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
|
||||||
|
|
||||||
|
_TASK_ID_RX = re.compile(r"^t_[0-9a-f]{4,16}$")
|
||||||
|
|
||||||
|
# Die CLI kostet pro Aufruf ein paar Sekunden Python-Start — die UI pollt aber.
|
||||||
|
_list_cache: dict = {"ts": 0.0, "data": None}
|
||||||
|
_LIST_EVERY = 15.0 # s
|
||||||
|
|
||||||
|
|
||||||
|
def _available() -> bool:
|
||||||
|
return os.name == "posix" and HERMES.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def _hermes(args: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run([str(HERMES), "kanban", *args],
|
||||||
|
capture_output=True, text=True, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json(stdout: str):
|
||||||
|
"""CLI-Ausgabe kann Log-Zeilen vor dem JSON enthalten — ab der ersten Klammer parsen."""
|
||||||
|
text = stdout or ""
|
||||||
|
for opener in ("[", "{"):
|
||||||
|
idx = text.find(opener)
|
||||||
|
if idx >= 0:
|
||||||
|
try:
|
||||||
|
return json.loads(text[idx:])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_queue() -> dict:
|
||||||
|
"""Alle nicht archivierten Aufgaben des Boards, jüngste zuerst — die Queue-Sicht der UI."""
|
||||||
|
if not _available():
|
||||||
|
return {"available": False, "items": [], "offen": 0}
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if _list_cache["data"] is not None and now - _list_cache["ts"] < _LIST_EVERY:
|
||||||
|
return _list_cache["data"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = _hermes(["list", "--json", "--sort", "created-desc"])
|
||||||
|
raw = _parse_json(r.stdout)
|
||||||
|
except Exception:
|
||||||
|
log.warning("ideen: kanban list fehlgeschlagen", exc_info=True)
|
||||||
|
raw = None
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
# CLI kaputt/Timeout → ehrlich leer melden, aber nicht cachen (nächster Poll versucht's neu)
|
||||||
|
return {"available": True, "items": [], "offen": 0, "fehler": "Queue nicht lesbar"}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for t in raw[:60]:
|
||||||
|
try:
|
||||||
|
items.append({
|
||||||
|
"id": t.get("id", ""),
|
||||||
|
"titel": (t.get("title") or "(ohne Titel)")[:200],
|
||||||
|
"body": (t.get("body") or "")[:600],
|
||||||
|
"status": t.get("status", "?"),
|
||||||
|
"assignee": t.get("assignee"),
|
||||||
|
"erstellt": t.get("created_at"),
|
||||||
|
"fertig": t.get("completed_at"),
|
||||||
|
"von": t.get("created_by"),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
offen = sum(1 for i in items if i["status"] not in ("done",))
|
||||||
|
data = {"available": True, "items": items, "offen": offen}
|
||||||
|
_list_cache.update(ts=now, data=data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui") -> dict:
|
||||||
|
"""Idee in die Queue legen: triage — der Specifier der Box arbeitet sie selbst aus."""
|
||||||
|
if not _available():
|
||||||
|
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
|
||||||
|
titel = (titel or "").strip()
|
||||||
|
if not (3 <= len(titel) <= 200):
|
||||||
|
return {"ok": False, "error": "Titel bitte zwischen 3 und 200 Zeichen."}
|
||||||
|
args = ["create", titel, "--triage", "--created-by", created_by, "--json"]
|
||||||
|
notiz = (notiz or "").strip()
|
||||||
|
if notiz:
|
||||||
|
args[2:2] = ["--body", notiz[:4000]]
|
||||||
|
try:
|
||||||
|
r = _hermes(args)
|
||||||
|
except Exception as exc:
|
||||||
|
return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
|
||||||
|
task = _parse_json(r.stdout)
|
||||||
|
if r.returncode != 0 or not isinstance(task, dict) or not task.get("id"):
|
||||||
|
return {"ok": False, "error": (r.stderr or r.stdout or "kanban create fehlgeschlagen").strip()[:300]}
|
||||||
|
_list_cache["ts"] = 0.0 # nächster Poll zeigt die neue Idee sofort
|
||||||
|
try:
|
||||||
|
from services import announce
|
||||||
|
announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
|
||||||
|
"[Ideen-Queue]", "ideen-queue", "silent")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True, "id": task["id"], "status": task.get("status")}
|
||||||
|
|
||||||
|
|
||||||
|
def archive_task(task_id: str) -> dict:
|
||||||
|
"""Erledigtes/Verworfenes aus der Sicht räumen (Kanban-Archiv, nichts wird gelöscht)."""
|
||||||
|
if not _available():
|
||||||
|
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
|
||||||
|
if not _TASK_ID_RX.match(task_id or ""):
|
||||||
|
return {"ok": False, "error": f"Keine gültige Aufgaben-Nummer: {task_id!r}"}
|
||||||
|
try:
|
||||||
|
r = _hermes(["archive", task_id])
|
||||||
|
except Exception as exc:
|
||||||
|
return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
|
||||||
|
if r.returncode != 0:
|
||||||
|
return {"ok": False, "error": (r.stderr or r.stdout or "Archivieren fehlgeschlagen").strip()[:300]}
|
||||||
|
_list_cache["ts"] = 0.0
|
||||||
|
return {"ok": True}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bagatell-Annahme — „Harmloses darf selbst" (User-Entscheid 10.07.2026, konservativ geschnitten):
|
||||||
|
# Vorschlags-Branches, die AUSSCHLIESSLICH Markdown-Dateien anlegen oder ändern (Doku/Wiki —
|
||||||
|
# keine einzige Code-Zeile, keine Löschungen, keine Umbenennungen, keine Skripte), spielt die
|
||||||
|
# Box nachts OHNE Klick ein. Der Weg ist derselbe wie beim Klick im Auftragsbuch:
|
||||||
|
# auftrag-annehmen.sh (Merge im Worktree → py_compile-Gate → Push main → Deploy → Health →
|
||||||
|
# Auto-Revert). Fangnetz drumherum: Nacht-Sicherung 03:30 (Zeitmaschine) liegt davor, die
|
||||||
|
# Morgenlage 04:30 berichtet danach, jede Annahme steht in Chronik + Telegram.
|
||||||
|
# JEDE Code-Zeile bleibt Klick-pflichtig — dieses Skript weitet die Klasse NIE selbst aus.
|
||||||
|
#
|
||||||
|
# Läuft als mc2-bagatell.timer (04:10) über eine /tmp-Kopie (Selbst-Reset-Falle: die Annahme
|
||||||
|
# deployt und resettet damit das Repo, in dem dieses Skript liegt).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||||
|
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
|
||||||
|
MAX_PRO_NACHT=2 # jede Annahme startet die Zentrale kurz neu — Nachtruhe wahren
|
||||||
|
|
||||||
|
cd "$SRC" || exit 0
|
||||||
|
git fetch -q --prune origin 2>/dev/null || exit 0
|
||||||
|
|
||||||
|
angenommen=0
|
||||||
|
for REF in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin); do
|
||||||
|
[ "$angenommen" -ge "$MAX_PRO_NACHT" ] && break
|
||||||
|
BRANCH="${REF#origin/}"
|
||||||
|
case "$BRANCH" in wartung/*|orchestrator/*|doku/*|feature/*) ;; *) continue ;; esac
|
||||||
|
|
||||||
|
AHEAD="$(git rev-list --count "origin/main..$REF" 2>/dev/null || echo 0)"
|
||||||
|
[ "${AHEAD:-0}" -gt 0 ] || continue
|
||||||
|
|
||||||
|
# Nur unberührte Vorschläge: trägt der Branch schon irgendeinen Annahme-Status
|
||||||
|
# (läuft/fehlgeschlagen/zurückgerollt/…), entscheidet der Commander — nicht wir.
|
||||||
|
STATE="$(python3 - "$STATUS" "$BRANCH" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
print((json.load(open(sys.argv[1], encoding="utf-8")).get("branches", {}).get(sys.argv[2]) or {}).get("state", ""))
|
||||||
|
except Exception:
|
||||||
|
print("")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
[ -z "$STATE" ] || continue
|
||||||
|
|
||||||
|
# Bagatell-Klasse: JEDE geänderte Datei endet auf .md UND ist nur angelegt (A) oder
|
||||||
|
# geändert (M). Alles andere (Löschung, Rename R…, Copy C…, Nicht-Markdown) → Klick-Pflicht.
|
||||||
|
DIFF="$(git diff --name-status "origin/main...$REF" 2>/dev/null)"
|
||||||
|
[ -n "$DIFF" ] || continue
|
||||||
|
OK=1
|
||||||
|
while IFS=$'\t' read -r st path _rest; do
|
||||||
|
[ -n "$st" ] || continue
|
||||||
|
case "$st" in A|M) ;; *) OK=0; break ;; esac
|
||||||
|
case "$path" in *.md) ;; *) OK=0; break ;; esac
|
||||||
|
done <<< "$DIFF"
|
||||||
|
[ "$OK" = 1 ] || continue
|
||||||
|
|
||||||
|
N="$(printf '%s\n' "$DIFF" | wc -l | tr -d ' ')"
|
||||||
|
echo "Bagatelle erkannt: $BRANCH ($N Markdown-Datei(en))"
|
||||||
|
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data "$(jq -n --arg t "Bagatelle (nur Doku, $N Datei(en)): '$BRANCH' wird ohne Klick eingespielt — Fangnetz aktiv, Details in der Morgenlage." \
|
||||||
|
'{text:$t, subject:"[Bagatell-Annahme]", source:"bagatell", priority:"silent"}')" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
RUNNER="/tmp/mc2-bagatell-annehmen-$$.sh"
|
||||||
|
cp "$SRC/deploy/auftrag-annehmen.sh" "$RUNNER" || continue
|
||||||
|
if bash "$RUNNER" "$BRANCH"; then
|
||||||
|
angenommen=$((angenommen+1))
|
||||||
|
else
|
||||||
|
# auftrag-annehmen.sh hat selbst gemeldet (fehlgeschlagen/zurückgerollt) und aufgeräumt;
|
||||||
|
# der Branch trägt jetzt einen Status → nächste Nacht fasst ihn niemand mehr automatisch an.
|
||||||
|
echo "Bagatell-Annahme von $BRANCH nicht grün — Meldung kam vom Annahme-Runner."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Bagatell-Lauf fertig: $angenommen eingespielt."
|
||||||
@@ -34,6 +34,12 @@ EVID="$(
|
|||||||
echo "### Wissens-Vault (neue Traum-Notizen, 24 h):"
|
echo "### Wissens-Vault (neue Traum-Notizen, 24 h):"
|
||||||
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
|
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
|
||||||
echo
|
echo
|
||||||
|
echo "### Ideen-Queue (natives Kanban, Stand jetzt — offene Ideen des Commanders):"
|
||||||
|
bash -lc 'hermes kanban stats' 2>/dev/null | head -12
|
||||||
|
echo
|
||||||
|
echo "### Ideen-Queue: zuletzt bewegte Aufgaben:"
|
||||||
|
bash -lc 'hermes kanban list --sort updated' 2>/dev/null | head -10
|
||||||
|
echo
|
||||||
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
|
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
|
||||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||||
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
|
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ cp "$SRC/deploy/mc2-autoupdate.timer" "$HOME/.config/systemd/user/mc2-autoupdate
|
|||||||
# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d.
|
# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d.
|
||||||
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
|
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
|
||||||
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
|
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
|
||||||
|
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
|
||||||
|
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
|
||||||
|
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
|
||||||
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
|
# 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).
|
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
|
||||||
mkdir -p "$HOME/.hermes/scripts"
|
mkdir -p "$HOME/.hermes/scripts"
|
||||||
@@ -89,6 +92,9 @@ cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-fe
|
|||||||
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
|
# 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").
|
# 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"
|
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
|
||||||
|
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
|
||||||
|
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
|
||||||
|
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
|
||||||
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh"
|
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh"
|
||||||
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
||||||
mkdir -p "$HOME/.hermes/skills/wartung"
|
mkdir -p "$HOME/.hermes/skills/wartung"
|
||||||
@@ -133,6 +139,7 @@ systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
|||||||
# still rückgängig gemacht). Aktueller User-Entscheid (10.07.2026): Timer ist AN —
|
# still rückgängig gemacht). Aktueller User-Entscheid (10.07.2026): Timer ist AN —
|
||||||
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
|
# 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-selfsmoke.timer >/dev/null 2>&1 || true
|
||||||
|
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
|
||||||
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
||||||
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
|
# 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
|
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MC2 Bagatell-Annahme (reine Doku-Vorschläge nachts ohne Klick, mit Fangnetz)
|
||||||
|
Documentation=file:%h/mission-control-v2/docs/AUFTRAGSBUCH.md
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Als /tmp-Kopie laufen lassen: die Annahme deployt (git reset --hard) und würde sonst das
|
||||||
|
# eigene Skript unter dem laufenden bash austauschen (bekannte Selbst-Reset-Falle).
|
||||||
|
ExecStart=/bin/bash -c 'cp "%h/mission-control-v2/deploy/bagatell-annahme.sh" /tmp/mc2-bagatell-run.sh && exec /bin/bash /tmp/mc2-bagatell-run.sh'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Täglich 04:10 — Bagatell-Annahme (nach der Nacht-Sicherung 03:30, vor der Morgenlage 04:30)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 04:10:00
|
||||||
|
Persistent=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Werkstatt-Worker (Profil „werkstatt")
|
||||||
|
|
||||||
|
Du bist der Werkstatt-Coder der AI-Box. Du arbeitest EINE Kanban-Aufgabe ab — meist
|
||||||
|
Code-/Bau-Aufträge an MC2 (mission-control-v2) oder Lucy. Keine Persona, kein Smalltalk —
|
||||||
|
sauberes Handwerk.
|
||||||
|
|
||||||
|
## Eiserne Regeln (nicht verhandelbar)
|
||||||
|
1. Arbeite NUR in deinem Task-Workspace (dein Startverzeichnis). Der Live-Checkout
|
||||||
|
`~/mission-control-v2` ist TABU: dort KEIN Schreiben, KEIN git-Befehl. (Lesen zur
|
||||||
|
Orientierung ist erlaubt.)
|
||||||
|
2. Ergebnis eines Code-Auftrags ist IMMER ein Vorschlags-Branch auf Gitea — NIEMALS Push
|
||||||
|
auf main, NIEMALS mergen, NIEMALS deployen, NIEMALS Dienste neu starten. Der Commander
|
||||||
|
klickt die Karte im Auftragsbuch.
|
||||||
|
3. Frisch klonen statt Live-Checkout nutzen:
|
||||||
|
`git clone https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2`
|
||||||
|
(Credentials liegen in `~/.git-credentials`; Lucy-Repo: `.../Hitonabi/lucy`).
|
||||||
|
Branch-Namen: `wartung/<kurz>`, `feature/<kurz>` oder `doku/<kurz>`.
|
||||||
|
4. Gate vor dem Push: `python3 -m py_compile` für JEDE geänderte .py-Datei. Frontend
|
||||||
|
(`frontend/`) nur anfassen, wenn der Auftrag es verlangt — die Box kann kein
|
||||||
|
`npm build`; das ehrlich in Commit-Text und Summary sagen.
|
||||||
|
5. TABU-Zonen: approvals/Tokens/ufw/Security-Configs, `~/.hermes/config.yaml`,
|
||||||
|
systemd-Units, alles außerhalb deines Workspaces. Braucht der Auftrag so etwas →
|
||||||
|
`kanban_block` mit Begründung, nicht selbst machen.
|
||||||
|
6. Zu groß oder unklar? Lieber `kanban_block` mit ehrlicher Diagnose (was fehlt, wie man
|
||||||
|
kleiner schneidet) als Murks. Für große Umbauten: nur einen PLAN als Kommentar
|
||||||
|
liefern, keinen Code.
|
||||||
|
7. Am Ende `kanban_complete` mit deutscher Summary: was gebaut, der BRANCH-NAME, wie
|
||||||
|
geprüft. Der Branch-Name MUSS in der Summary stehen — daraus wird die Karte gefunden.
|
||||||
|
|
||||||
|
## Stil
|
||||||
|
Deutsch in allem User-Sichtbaren. Kleine, ehrliche Commits. Keine Nebenbaustellen anfassen.
|
||||||
+36
-1
@@ -4,6 +4,29 @@ Das Auftragsbuch ist das Mensch-Gate des propose-only-Kreislaufs als Klick statt
|
|||||||
Git-Handarbeit: alles, was die Box von allein baut oder sich ausdenkt, landet hier
|
Git-Handarbeit: alles, was die Box von allein baut oder sich ausdenkt, landet hier
|
||||||
als Karte — du entscheidest mit **Annehmen** oder **Ablehnen**.
|
als Karte — du entscheidest mit **Annehmen** oder **Ablehnen**.
|
||||||
|
|
||||||
|
## Die Ideen-Queue davor (natives Hermes-Kanban, seit 10.07.2026)
|
||||||
|
|
||||||
|
Vor dem Auftragsbuch liegt DIE eine Ideen-Queue: das native Hermes-Kanban
|
||||||
|
(`hermes kanban`, SQLite-Board, Dispatcher läuft im Gateway). Der Weg einer Idee:
|
||||||
|
|
||||||
|
1. **Idee rein — Tür egal:** Eingabefeld im Auftragsbuch-Tab (`POST /api/ideen`),
|
||||||
|
Telegram/Desktop (natives Tool `kanban_create`, Regel in SOUL.md) oder Lucys
|
||||||
|
Sprech-Leitung (`idee_notieren` in `mcp/mcp_voice.py`). Alles landet als
|
||||||
|
`triage`-Aufgabe auf demselben Board.
|
||||||
|
2. **Die Box arbeitet sie aus:** der Kanban-Specifier macht aus der rohen Idee
|
||||||
|
einen sauberen Auftrag und wählt das Worker-Profil (Routing über die
|
||||||
|
Profil-Beschreibungen: Code → `werkstatt` [Qwen3-Coder-Next, Leitplanken in
|
||||||
|
`deploy/werkstatt-SOUL.md`], Doku/Analyse → `default`).
|
||||||
|
3. **Der Worker baut** im isolierten Workspace; Code-Ergebnisse pusht er als
|
||||||
|
Vorschlags-Branch auf Gitea — der hier als Karte erscheint. **Das Gate bleibt
|
||||||
|
dein Klick.**
|
||||||
|
|
||||||
|
Die Queue-Sicht im Tab (Status, offene Ideen) kommt aus `hermes kanban list --json`
|
||||||
|
(`backend/services/ideen.py`); die Morgenlage bekommt `kanban stats` als
|
||||||
|
Beweismaterial. Entscheid nativ-vs-Eigenbau (Hermes-first): Probe 10.07.2026 —
|
||||||
|
Dispatcher, Specifier, Profil-Modellwahl und Artefakt-Tracking sind nativ E2E
|
||||||
|
bewiesen; ein Eigenbau hätte all das nur nachgebaut.
|
||||||
|
|
||||||
## Woher die Karten kommen
|
## Woher die Karten kommen
|
||||||
|
|
||||||
- **Fertige Patches** — Branches auf Gitea (`wartung/*`, `orchestrator/*`, `doku/*`,
|
- **Fertige Patches** — Branches auf Gitea (`wartung/*`, `orchestrator/*`, `doku/*`,
|
||||||
@@ -27,9 +50,21 @@ Der Lauf ist eine eigene systemd-Unit (detached) — er überlebt den Neustart d
|
|||||||
Backends, das ihn gestartet hat. Fortschritt steht live auf der Karte
|
Backends, das ihn gestartet hat. Fortschritt steht live auf der Karte
|
||||||
(`/srv/models/mc2-auftragsbuch.json`).
|
(`/srv/models/mc2-auftragsbuch.json`).
|
||||||
|
|
||||||
|
## Bagatellen ohne Klick (mc2-bagatell.timer, 04:10)
|
||||||
|
|
||||||
|
Einzige Ausnahme vom Klick-Gate (User-Entscheid 10.07.2026, bewusst eng geschnitten):
|
||||||
|
`deploy/bagatell-annahme.sh` spielt nachts Vorschlags-Branches ein, deren Diff
|
||||||
|
**ausschließlich `.md`-Dateien anlegt oder ändert** — keine Code-Zeile, keine
|
||||||
|
Löschungen, keine Renames, keine Skripte. Der Weg ist derselbe Annahme-Runner wie
|
||||||
|
beim Klick (Merge → Gate → Deploy → Health → Auto-Revert), maximal 2 pro Nacht und
|
||||||
|
nur Branches ohne vorherige Fehl-Läufe. Fangnetz: Nacht-Sicherung 03:30 liegt davor,
|
||||||
|
jede Annahme steht in Chronik + Telegram, die Morgenlage 04:30 berichtet. Die Klasse
|
||||||
|
erweitert nur der Commander, nie das Skript.
|
||||||
|
|
||||||
## Leitplanken
|
## Leitplanken
|
||||||
|
|
||||||
- Propose-only bleibt: **nichts geht ohne Klick live.** Das Gate ist dieses Buch.
|
- Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses
|
||||||
|
Buch; einzige Ausnahme sind die eng geschnittenen Doku-Bagatellen (oben).
|
||||||
- „Frontend ohne Build"-Warnung: die Box kann kein Frontend bauen (kein Node) —
|
- „Frontend ohne Build"-Warnung: die Box kann kein Frontend bauen (kein Node) —
|
||||||
enthält ein Branch `frontend/src` ohne frisches `frontend/dist`, bleibt die
|
enthält ein Branch `frontend/src` ohne frisches `frontend/dist`, bleibt die
|
||||||
Oberfläche nach dem Annehmen alt, bis am PC gebaut wird.
|
Oberfläche nach dem Annehmen alt, bis am PC gebaut wird.
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+154
-154
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BfahO8B0.js"></script>
|
<script type="module" crossorigin src="/assets/index-BRolD2Rb.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BSNo2FSN.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BSNo2FSN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -475,6 +475,24 @@ export interface AuftragsbuchResp {
|
|||||||
open_count: number
|
open_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
|
||||||
|
export interface IdeenItem {
|
||||||
|
id: string
|
||||||
|
titel: string
|
||||||
|
body: string
|
||||||
|
status: string // triage | todo | ready | running | blocked | review | done
|
||||||
|
assignee: string | null
|
||||||
|
erstellt: number | null
|
||||||
|
fertig: number | null
|
||||||
|
von: string | null
|
||||||
|
}
|
||||||
|
export interface IdeenResp {
|
||||||
|
available: boolean
|
||||||
|
items: IdeenItem[]
|
||||||
|
offen: number
|
||||||
|
fehler?: string
|
||||||
|
}
|
||||||
|
|
||||||
// ── Chronik (Timeline aus dem Melde-Briefkasten) ─────────────────────────────
|
// ── Chronik (Timeline aus dem Melde-Briefkasten) ─────────────────────────────
|
||||||
export interface ChronikItem {
|
export interface ChronikItem {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type GroupsResp,
|
type GroupsResp,
|
||||||
type HermesBrainResp,
|
type HermesBrainResp,
|
||||||
type Health,
|
type Health,
|
||||||
|
type IdeenResp,
|
||||||
type Job,
|
type Job,
|
||||||
type Memory,
|
type Memory,
|
||||||
type MemoryGraph,
|
type MemoryGraph,
|
||||||
@@ -53,6 +54,7 @@ export const qk = {
|
|||||||
memoryGraph: ["memory-graph"] as const,
|
memoryGraph: ["memory-graph"] as const,
|
||||||
voiceTrace: ["voice-trace"] as const,
|
voiceTrace: ["voice-trace"] as const,
|
||||||
auftragsbuch: ["auftragsbuch"] as const,
|
auftragsbuch: ["auftragsbuch"] as const,
|
||||||
|
ideen: ["ideen"] as const,
|
||||||
chronik: ["chronik"] as const,
|
chronik: ["chronik"] as const,
|
||||||
wissen: ["wissen"] as const,
|
wissen: ["wissen"] as const,
|
||||||
zeitmaschine: ["zeitmaschine"] as const,
|
zeitmaschine: ["zeitmaschine"] as const,
|
||||||
@@ -68,6 +70,14 @@ export const useAuftragsbuch = (refetchInterval = 8_000) =>
|
|||||||
refetchInterval,
|
refetchInterval,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
|
||||||
|
export const useIdeen = (refetchInterval = 20_000) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: qk.ideen,
|
||||||
|
queryFn: () => api<IdeenResp>("/api/ideen"),
|
||||||
|
refetchInterval,
|
||||||
|
})
|
||||||
|
|
||||||
export const useChronik = (limit = 150, refetchInterval = 15_000) =>
|
export const useChronik = (limit = 150, refetchInterval = 15_000) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.chronik,
|
queryKey: qk.chronik,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
|
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
|
||||||
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw,
|
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, Send,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
|
import { api, type AuftragItem, type IdeenItem, type SkillKandidat } from "@/lib/api"
|
||||||
import { useAuftragsbuch, useQueryClient, qk } from "@/lib/queries"
|
import { useAuftragsbuch, useIdeen, useQueryClient, qk } from "@/lib/queries"
|
||||||
import { useDialog } from "@/lib/useDialog"
|
import { useDialog } from "@/lib/useDialog"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -98,6 +98,8 @@ export function AuftragsbuchView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<IdeenSection />
|
||||||
|
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<SectionLabel icon={GitBranch}>Fertige Patches ({items.length})</SectionLabel>
|
<SectionLabel icon={GitBranch}>Fertige Patches ({items.length})</SectionLabel>
|
||||||
@@ -129,6 +131,134 @@ function SectionLabel({ icon: Icon, children }: { icon: any; children: React.Rea
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||||
|
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" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function IdeenSection() {
|
||||||
|
const { data, isLoading } = useIdeen()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const { showAlert, dialogElement } = useDialog()
|
||||||
|
const [text, setText] = useState("")
|
||||||
|
const [sending, setSending] = useState(false)
|
||||||
|
const [busy, setBusy] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
const items = data?.items ?? []
|
||||||
|
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
|
||||||
|
|
||||||
|
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 : "" }) })
|
||||||
|
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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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="rounded-2xl border border-border/60 bg-card/45 p-4 shadow-lg shadow-black/15 backdrop-blur-md 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="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-3 text-[11px] font-bold uppercase tracking-wide text-primary transition-all hover:bg-primary/20 cursor-pointer disabled:opacity-50">
|
||||||
|
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} In die Queue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
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 „Fertige Patches", 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 && (
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{items.map((it: IdeenItem) => {
|
||||||
|
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
|
||||||
|
return (
|
||||||
|
<li key={it.id} className="flex items-center gap-2 rounded-xl border border-border/40 bg-background/30 px-3 py-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>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-xs text-foreground" title={it.body || 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>
|
||||||
|
{(it.status === "done" || it.status === "blocked") && (
|
||||||
|
<button
|
||||||
|
disabled={!!busy[it.id]}
|
||||||
|
onClick={() => archive(it.id)}
|
||||||
|
title="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>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function StateBadge({ item }: { item: AuftragItem }) {
|
function StateBadge({ item }: { item: AuftragItem }) {
|
||||||
const st = item.status
|
const st = item.status
|
||||||
if (!st) return null
|
if (!st) return null
|
||||||
|
|||||||
@@ -96,5 +96,25 @@ def box_status() -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def idee_notieren(titel: str, notiz: str = "") -> str:
|
||||||
|
"""Legt eine Idee/einen Auftrag des Commanders in die Ideen-Queue der Box. Nutzen, sobald
|
||||||
|
der Commander eine Idee für später äußert ('wäre cool wenn …', 'bau mal irgendwann …',
|
||||||
|
'die Box soll …') — NICHT nur versprechen. Die Box arbeitet die Idee selbst aus und baut;
|
||||||
|
das Ergebnis erscheint als Karte im Auftragsbuch, dort entscheidet der Commander.
|
||||||
|
titel = kurze deutsche Überschrift; notiz = seine Worte plus nötiger Kontext."""
|
||||||
|
try:
|
||||||
|
r = _post("/api/ideen", {"titel": titel, "notiz": notiz})
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
try:
|
||||||
|
detail = e.response.json().get("detail", "")
|
||||||
|
except Exception:
|
||||||
|
detail = e.response.text[:200]
|
||||||
|
return f"Fehlgeschlagen: {detail}"
|
||||||
|
except Exception as exc:
|
||||||
|
return f"Queue nicht erreichbar ({exc})."
|
||||||
|
return f"Idee ist in der Queue ({r.get('id', '?')}). Die Box arbeitet sie von allein ab."
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run()
|
mcp.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user