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,
|
||||
health,
|
||||
hermes_ui,
|
||||
ideen,
|
||||
maintenance,
|
||||
memory,
|
||||
models,
|
||||
@@ -123,6 +124,7 @@ app.include_router(
|
||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||
app.include_router(maintenance.router)
|
||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||
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):"
|
||||
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
|
||||
echo
|
||||
echo "### Ideen-Queue (natives Kanban, Stand jetzt — offene Ideen des Commanders):"
|
||||
bash -lc 'hermes kanban stats' 2>/dev/null | head -12
|
||||
echo
|
||||
echo "### Ideen-Queue: zuletzt bewegte Aufgaben:"
|
||||
bash -lc 'hermes kanban list --sort updated' 2>/dev/null | head -10
|
||||
echo
|
||||
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
|
||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
|
||||
|
||||
@@ -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.
|
||||
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
|
||||
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
|
||||
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
|
||||
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
|
||||
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
|
||||
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
|
||||
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
|
||||
mkdir -p "$HOME/.hermes/scripts"
|
||||
@@ -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
|
||||
# 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"
|
||||
# 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"
|
||||
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
||||
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 —
|
||||
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
|
||||
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
|
||||
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
||||
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- **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
|
||||
(`/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
|
||||
|
||||
- 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) —
|
||||
enthält ein Branch `frontend/src` ohne frisches `frontend/dist`, bleibt die
|
||||
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="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -475,6 +475,24 @@ export interface AuftragsbuchResp {
|
||||
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) ─────────────────────────────
|
||||
export interface ChronikItem {
|
||||
id: number
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type GroupsResp,
|
||||
type HermesBrainResp,
|
||||
type Health,
|
||||
type IdeenResp,
|
||||
type Job,
|
||||
type Memory,
|
||||
type MemoryGraph,
|
||||
@@ -53,6 +54,7 @@ export const qk = {
|
||||
memoryGraph: ["memory-graph"] as const,
|
||||
voiceTrace: ["voice-trace"] as const,
|
||||
auftragsbuch: ["auftragsbuch"] as const,
|
||||
ideen: ["ideen"] as const,
|
||||
chronik: ["chronik"] as const,
|
||||
wissen: ["wissen"] as const,
|
||||
zeitmaschine: ["zeitmaschine"] as const,
|
||||
@@ -68,6 +70,14 @@ export const useAuftragsbuch = (refetchInterval = 8_000) =>
|
||||
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) =>
|
||||
useQuery({
|
||||
queryKey: qk.chronik,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
|
||||
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw,
|
||||
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, Send,
|
||||
} from "lucide-react"
|
||||
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
|
||||
import { useAuftragsbuch, useQueryClient, qk } from "@/lib/queries"
|
||||
import { api, type AuftragItem, type IdeenItem, type SkillKandidat } from "@/lib/api"
|
||||
import { useAuftragsbuch, useIdeen, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -98,6 +98,8 @@ export function AuftragsbuchView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IdeenSection />
|
||||
|
||||
{items.length > 0 && (
|
||||
<section className="space-y-3">
|
||||
<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 }) {
|
||||
const st = item.status
|
||||
if (!st) return null
|
||||
|
||||
@@ -96,5 +96,25 @@ def box_status() -> str:
|
||||
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__":
|
||||
mcp.run()
|
||||
|
||||
Reference in New Issue
Block a user