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}
|
||||
Reference in New Issue
Block a user