Auftragsbuch: Konzept-Ansicht fuer IDE-Projekte (KONZEPT.md inline)

IDE-Vorbereiten-Karten bekommen einen 'Konzept'-Knopf: MC2 holt KONZEPT.md
aus dem vorbereiteten Gitea-Repo (Gitea-Raw-API + Auth aus git-credentials)
und rendert es direkt im Auftragsbuch (leichter MD-Renderer lib/markdown.tsx).
So liest der Commander das ausgearbeitete Konzept in der GUI, ohne das Repo
aufzumachen (User-Wunsch: 'lesen gehoert in MC2, feilen in die IDE').

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-20 16:02:12 +02:00
parent ff8125d2ac
commit bf9c22ab78
31 changed files with 322 additions and 70 deletions
+94
View File
@@ -558,6 +558,100 @@ def prio_task(task_id: str, richtung: str) -> dict:
return {"ok": True}
# ── Konzept-Ansicht (IDE-Welt) — KONZEPT.md aus dem vorbereiteten Repo holen ──
# Der projektstart-Worker legt ein Gitea-Repo an und schreibt das wasserdichte
# Konzept als KONZEPT.md hinein. Hier holen wir es (Gitea-API + Auth aus
# ~/.git-credentials), damit der Commander es DIREKT im Auftragsbuch liest,
# ohne das Repo aufzumachen (User-Wunsch 20.07.).
_GITEA_HOST = "git.tobisniceshomelab.ddnsfree.com"
_GITEA_REPO_RX = re.compile(
re.escape(_GITEA_HOST) + r"/([A-Za-z0-9_.-]+/[A-Za-z0-9_][A-Za-z0-9_.-]*?)(?:\.git)?(?=[\s)\"'<>]|/?$)"
)
_REPO_FALLBACK_RX = re.compile(r"[Rr]epo\S*[:\s]+([A-Za-z0-9_.-]+/[A-Za-z0-9_][A-Za-z0-9_.-]*)")
_KONZEPT_NAMES = ("KONZEPT.md", "Konzept.md", "konzept.md", "CONCEPT.md")
_konzept_cache: dict = {}
def _gitea_creds() -> tuple | None:
"""(user, token) fuer den oeffentlichen Gitea-Host aus ~/.git-credentials."""
try:
cred = Path("~/.git-credentials").expanduser()
for line in cred.read_text(encoding="utf-8").splitlines():
m = re.match(r"https://([^:]+):([^@]+)@" + re.escape(_GITEA_HOST), line.strip())
if m:
return m.group(1), m.group(2)
except Exception:
pass
return None
def _extract_repo(text: str) -> str | None:
"""Ziel-Repo (owner/name) aus dem Ergebnis-/Body-Text der Karte ziehen."""
if not text:
return None
m = _GITEA_REPO_RX.search(text)
if m:
return m.group(1)
m = _REPO_FALLBACK_RX.search(text)
return m.group(1) if m else None
def konzept_of(task_id: str) -> dict:
"""Das KONZEPT.md des vorbereiteten IDE-Projekts holen (aus dem Gitea-Repo)."""
if not _available():
return {"available": False}
if not _TASK_ID_RX.match(task_id or ""):
return {"available": True, "error": "Ungültige Aufgaben-Nummer."}
if task_id in _konzept_cache:
return _konzept_cache[task_id]
# Repo aus Ergebnis + Body der Karte ziehen (show --json trägt beides).
try:
r = _hermes(["show", task_id, "--json"], timeout=30)
d = _parse_json(r.stdout)
except Exception:
return {"available": True, "error": "Karte nicht lesbar."}
haystack = ""
if isinstance(d, dict):
haystack = str(d.get("result") or "") + "\n" + str(d.get("body") or "")
for ev in d.get("events") or []:
if isinstance(ev, dict) and ev.get("kind") == "completed":
haystack += "\n" + str((ev.get("payload") or {}).get("summary") or "")
repo = _extract_repo(haystack)
if not repo:
return {"available": True, "repo": None,
"error": "Noch kein Repo hinterlegt — das Projekt wird gerade vorbereitet."}
creds = _gitea_creds()
auth = creds if creds else None
konzept, used_name = "", None
try:
import httpx
with httpx.Client(timeout=10.0, follow_redirects=True) as c:
for name in _KONZEPT_NAMES:
url = f"https://{_GITEA_HOST}/{repo}/raw/branch/main/{name}"
resp = c.get(url, auth=auth)
if resp.status_code == 200 and resp.text.strip():
konzept, used_name = resp.text, name
break
except Exception:
log.warning("ideen: KONZEPT-Abruf für %s (%s) fehlgeschlagen", task_id, repo, exc_info=True)
return {"available": True, "repo": repo, "error": "Konzept konnte nicht geladen werden."}
if not konzept:
return {"available": True, "repo": repo,
"error": "KONZEPT.md im Repo (noch) nicht gefunden."}
clone_url = f"https://{_GITEA_HOST}/{repo}.git"
data = {"available": True, "repo": repo, "clone_url": clone_url,
"datei": used_name, "konzept": konzept[:60000]}
# Fertige Konzepte ändern sich selten → cachen (aber nur, wenn die Karte done ist).
if isinstance(d, dict) and d.get("status") == "done":
if len(_konzept_cache) >= 20:
_konzept_cache.pop(next(iter(_konzept_cache)))
_konzept_cache[task_id] = data
return data
def log_of(task_id: str) -> dict:
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
if not _available():