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:
@@ -93,6 +93,20 @@ def weiterfuehren(body: WeiterIn) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
class NachschaerfenIn(BaseModel):
|
||||
id: str
|
||||
hinweis: str
|
||||
|
||||
|
||||
@router.post("/ideen/nachschaerfen")
|
||||
def nachschaerfen(body: NachschaerfenIn) -> dict:
|
||||
"""„Punkt X passt nicht" → Überarbeitungs-Runde am bestehenden Konzept."""
|
||||
res = ideen.konzept_ueberarbeiten(body.id, body.hinweis)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Überarbeitung konnte nicht gestartet werden."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/ideen/archivieren")
|
||||
def archive(body: TaskIn) -> dict:
|
||||
res = ideen.archive_task(body.id)
|
||||
|
||||
+136
-27
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user