Konzept nachschaerfen: Rueckmeldung statt nur annehmen-oder-wegwerfen

Nach dem Lesen eines Konzepts gab es bisher nur zwei Wege: "Gefaellt mir" oder
archivieren. Der haeufige Fall fehlte komplett — EIN Punkt passt nicht und der
Commander hat einen besseren Vorschlag.

- POST /api/ideen/nachschaerfen -> konzept_ueberarbeiten(): legt eine neue
  Idee-Karte an (kein Bau), verkettet an die Quell-Karte, mit dem Wortlaut der
  Rueckmeldung. Auftrag: bestehendes Konzept lesen, genannte Punkte UND deren
  Folgewirkungen aendern, Rest stehen lassen, kurzer Haertetest nur auf die
  geaenderten Teile. Schafft der Vorschlag ein Problem: einbauen UND die Folge
  sichtbar in die Risiken schreiben — nicht uebergehen, nicht schoenreden.
  Die neue Fassung geht an DIESELBE Stelle (Repo-Commit bzw. Datei + .bak), so
  zeigt "Konzept" ueberall den frischen Stand. Beliebig oft wiederholbar, weil
  die Ueberarbeitung selbst wieder eine Idee-Karte ist.
- UI: Rueckmeldefeld direkt unter dem gelesenen Konzept, ueber dem
  Gefaellt-mir-Weg (der haeufigere Fall gehoert nach oben und offen sichtbar).
- SOUL: neuer Sonderfall UEBERARBEITUNG (Schritt 2 entfaellt, kein zweites Repo).

Zwei Fehler nebenbei gefunden und behoben:
- _wo_liegt(): der Auftrag verwechselte "Repo bekannt" mit "Konzept kommt aus
  dem Repo". Faellt MC2 auf die lokale Datei zurueck, stand vorher "liegt im
  Repo als <lokaler Dateiname>" im Auftrag — eine Datei, die es dort nie gab.
- SOUL-Regel 4: der Push gilt erst als erledigt, wenn `git ls-remote` ihn
  BEWEIST. Ein Worker hat am 21.07. genau hier abgekuerzt, Erfolg gemeldet, und
  das Repo war nachher nicht auffindbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-21 09:05:34 +02:00
parent 94dc3fba9b
commit 0cea01c699
29 changed files with 286 additions and 118 deletions
+136 -27
View File
@@ -885,6 +885,52 @@ _AUS_IDEE_FUSS = (
"· Uebergabe wie ueblich: Repo-URL nennen, damit der Commander in Zed loslegen kann.")
def _wo_liegt(konz: dict) -> str:
"""Eine Zeile: WO das Konzept wirklich liegt — für den Auftrag an den Worker.
Wichtig ist der Unterschied zwischen „Repo bekannt" und „Konzept KOMMT aus dem Repo":
findet die Repo-Datei nicht statt (Push verpufft, Datei anders benannt), liest MC2 die
lokale Fassung. Wer dann trotzdem „liegt im Repo als <lokaler Dateiname>" in den Auftrag
schreibt, schickt den Worker auf eine Datei, die es dort nie gab.
"""
repo, pfad = konz.get("repo"), konz.get("pfad")
url = konz.get("clone_url") or (f"https://{_GITEA_HOST}/{repo}.git" if repo else "")
if konz.get("quelle") == "repo":
return (f"Das gepruefte Konzept liegt im Gitea-Repo `{repo}` ({url}) "
f"als `{konz.get('datei') or 'KONZEPT.md'}`.")
if repo:
return (f"Das gepruefte Konzept liegt als Datei `{pfad}` auf dieser Box. Zur Idee gehoert "
f"bereits das Gitea-Repo `{repo}` ({url}) — dort ist das Konzept aber NICHT (mehr) "
"abrufbar. Pruefe das Repo, bevor du etwas anlegst.")
return f"Das gepruefte Konzept liegt als Datei `{pfad}` auf dieser Box."
def _konzept_und_name(task_id: str) -> tuple:
"""(Konzept-Daten, kurzer Anzeige-Name, Fehlertext) einer Idee-Karte.
Der Kartentitel ist der ROHE Zuruf des Commanders („Recherchiere zu … Lass ma…") —
als Name unbrauchbar. Die Ueberschrift des Konzepts ist die aufgeraeumte Fassung
derselben Sache; nur wenn es keine gibt, den Zuruf kappen.
"""
konz = konzept_of(task_id)
if not konz.get("konzept"):
return {}, "", (konz.get("error")
or "Zu dieser Idee gibt es noch kein Konzept — erst durchdenken lassen.")
try:
r = _hermes(["show", task_id, "--json"], timeout=30)
d = _parse_json(r.stdout)
except Exception as exc:
return {}, "", f"Karte nicht lesbar: {exc}"
t = (d.get("task") if isinstance(d, dict) and isinstance(d.get("task"), dict)
else d if isinstance(d, dict) else {})
quell_titel = _TITEL_PRAEFIX_RX.sub("", str(t.get("title") or "").strip())
if not quell_titel:
return {}, "", "Quell-Karte hat keinen Titel."
h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.M)
kurz = h1.group(1).strip() if h1 else re.split(r"(?<=[.!?])\s", quell_titel)[0]
return konz, _kurz(kurz, 85), ""
def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
"""Aus einer fertig durchdachten Idee eine IDE-Projekt-Karte machen.
@@ -897,39 +943,20 @@ def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
if not _TASK_ID_RX.match(task_id or ""):
return {"ok": False, "error": f"Keine gültige Aufgaben-Nummer: {task_id!r}"}
konz = konzept_of(task_id)
if not konz.get("konzept"):
return {"ok": False, "error": konz.get("error")
or "Zu dieser Idee gibt es noch kein Konzept — erst durchdenken lassen."}
try:
r = _hermes(["show", task_id, "--json"], timeout=30)
d = _parse_json(r.stdout)
except Exception as exc:
return {"ok": False, "error": f"Karte nicht lesbar: {exc}"}
t = (d.get("task") if isinstance(d, dict) and isinstance(d.get("task"), dict)
else d if isinstance(d, dict) else {})
quell_titel = re.sub(r"^\s*Idee\s*[:\-]\s*", "", str(t.get("title") or "").strip())
if not quell_titel:
return {"ok": False, "error": "Quell-Karte hat keinen Titel."}
# Der Kartentitel ist der ROHE Zuruf des Commanders („Recherchiere zu … Lass ma…") —
# als Projektname unbrauchbar. Die Ueberschrift des Konzepts ist die aufgeraeumte
# Fassung derselben Sache; nur wenn es keine gibt, den Zuruf kappen.
h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.M)
kurz = h1.group(1).strip() if h1 else re.split(r"(?<=[.!?])\s", quell_titel)[0]
kurz = (kurz[:85].rstrip() + "") if len(kurz) > 86 else kurz
konz, kurz, fehler = _konzept_und_name(task_id)
if fehler:
return {"ok": False, "error": fehler}
titel = f"Projekt: {kurz}"[:200]
if konz.get("repo"):
# Liegt das Konzept schon in einem Gitea-Repo, ist DAS das Projekt-Repo —
# ein zweites anzulegen wuerde den Commander nur mit Dubletten beschenken.
quelle = (f"· Das gepruefte Konzept liegt bereits im Gitea-Repo `{konz['repo']}` "
f"({konz.get('clone_url') or ''}) als `{konz.get('datei') or 'KONZEPT.md'}`. "
"NUTZE GENAU DIESES REPO als Projekt-Repo — lege KEIN neues an (Schritt 2 "
quelle = (f"· {_wo_liegt(konz)}\n"
f"· NUTZE GENAU DIESES REPO als Projekt-Repo — lege KEIN neues an (Schritt 2 "
"entfaellt). Klone es voll, ergaenze die fehlenden Doku-Dateien und pushe auf main.")
else:
quelle = (f"· Das gepruefte Konzept liegt als Datei `{konz.get('pfad')}` auf dieser Box. "
"Lies sie vollstaendig, lege dann wie in Schritt 2 ein frisches Repo an und "
quelle = (f"· {_wo_liegt(konz)}\n"
"· Lies es vollstaendig, lege dann wie in Schritt 2 ein frisches Repo an und "
"uebernimm den Inhalt als `KONZEPT.md`.")
zielwahl = _ZIEL.get(str(ziel).lower())
@@ -955,7 +982,7 @@ def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
_list_cache["ts"] = 0.0
try:
from services import announce
announce.add(f"Aus der Idee „{_kurz(quell_titel, 60)}“ wird ein Projekt: {task['id']} — die Box "
announce.add(f"Aus der Idee „{_kurz(kurz, 60)}“ wird ein Projekt: {task['id']} — die Box "
"bereitet das Repo vor, gebaut wird in Zed.",
"[Ideen-Queue]", "ideen-queue", "silent")
except Exception:
@@ -963,6 +990,88 @@ def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
return {"ok": True, "id": task["id"], "titel": titel}
# ── „So nicht" — Konzept mit Rueckmeldung ueberarbeiten lassen (21.07.) ──────
# Vorher gab es nach dem Lesen nur annehmen oder wegwerfen. Ein Konzept wird aber
# selten beim ersten Wurf richtig: der Commander sieht EINEN Punkt, der nicht passt.
# Die Ueberarbeitung ist wieder eine Idee-Karte (kein Bau) und schreibt die neue
# Fassung an DIESELBE Stelle — so zeigt „Konzept" ueberall den frischen Stand.
_UEBERARBEITEN_KOPF = (
"AUFTRAGS-ART: IDEE PRUEFEN — UEBERARBEITUNG. Es gibt bereits ein durchdachtes, "
"gehaertetes Konzept. Der Commander hat es GELESEN und will gezielte Aenderungen. "
"KEIN Projektstart, KEIN Bau, KEIN Geruest.")
_UEBERARBEITEN_FUSS = (
"· Arbeite die Rueckmeldung EIN: lies das bestehende Konzept vollstaendig, aendere die "
"genannten Punkte und alles, was dadurch nicht mehr stimmt (Folgewirkungen!). Denk das "
"Ganze NICHT von vorn — was er nicht angesprochen hat, bleibt.\n"
"· Nimm seinen Vorschlag ernst, aber sei ehrlich: schafft er ein Problem, dann bau ihn ein "
"UND schreib die Folge sichtbar ins Konzept (Abschnitt Risiken/offene Punkte). Setz dich "
"nicht stillschweigend darueber hinweg und rede es auch nicht schoen.\n"
"· Ein KURZER Haertetest (Advocatus Diaboli) NUR auf die geaenderten Teile reicht — die "
"volle Kaskade ist Verschwendung, der Rest ist bereits geprueft.\n"
"· Uebergabe: was du geaendert hast, was daraus folgte und was du bewusst gelassen hast.")
def konzept_ueberarbeiten(task_id: str, hinweis: str) -> dict:
"""Rueckmeldung zum Konzept → neue Idee-Karte, die das Konzept ueberarbeitet.
Haengt als Kind an der Quell-Karte (Kette im Auftragsbuch) und schreibt die neue
Fassung an dieselbe Stelle wie die alte (Repo bzw. Datei).
"""
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}"}
hinweis = (hinweis or "").strip()
if not (3 <= len(hinweis) <= 2000):
return {"ok": False, "error": "Bitte schreib zwischen 3 und 2000 Zeichen, was anders werden soll."}
konz, kurz, fehler = _konzept_und_name(task_id)
if fehler:
return {"ok": False, "error": fehler}
if konz.get("quelle") == "repo":
quelle = (f"· {_wo_liegt(konz)}\n"
"· Die NEUE FASSUNG gehoert an dieselbe Stelle: Repo voll klonen, die Datei "
"ueberschreiben, committen und auf main pushen (Push mit `git ls-remote origin main` "
"BEWEISEN, nicht annehmen). KEIN neues Repo anlegen — der Commander vergleicht die "
"Fassungen ueber die Git-Historie.")
else:
quelle = (f"· {_wo_liegt(konz)}\n"
"· Die NEUE FASSUNG gehoert an dieselbe Stelle. Sichere die alte Fassung vorher "
"als `<datei>.bak`, dann schreib die ueberarbeitete Fassung in die Originaldatei."
+ (" Gibt es das genannte Repo wirklich, gehoert die neue Fassung ZUSAETZLICH "
"als `KONZEPT.md` hinein (klonen, schreiben, pushen, Push beweisen)."
if konz.get("repo") else ""))
teile = [f"{_UEBERARBEITEN_KOPF}\n{quelle}\n"
f"· SO SOLL ES ANDERS WERDEN — Wortlaut des Commanders:\n{hinweis}\n"
f"{_UEBERARBEITEN_FUSS}",
_INFRA_LXC]
titel = f"Konzept nachschärfen: {kurz}"[:200]
args = ["create", titel, "--body", "\n\n".join(teile)[:4000], "--assignee", "projektstart",
"--parent", task_id, "--created-by", "mc2-ui", "--json"]
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]}
# Die alte Fassung wird ueberschrieben → gecachtes Konzept der Quell-Karte verwerfen,
# sonst zeigt die alte Karte nach der Ueberarbeitung noch tagelang den alten Stand.
_konzept_cache.pop(task_id, None)
_list_cache["ts"] = 0.0
try:
from services import announce
announce.add(f"Konzept „{_kurz(kurz, 60)}“ wird nachgeschärft ({task['id']}): "
f"{_kurz(hinweis, 120)}",
"[Ideen-Queue]", "ideen-queue", "silent")
except Exception:
pass
return {"ok": True, "id": task["id"], "titel": titel}
def log_of(task_id: str) -> dict:
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
if not _available():