This commit is contained in:
team3
2026-07-06 17:32:52 +02:00
parent cc27e53b9e
commit 73ac3e532f
19 changed files with 565 additions and 523 deletions

View File

@@ -1673,14 +1673,22 @@ async def _preload_state(flow: Flow):
async def run_boards(ctx: GenContext, set_p, files: dict, q: dict, folder, instructions: str,
research: bool = True, artefacts: bool = True, qa_force: bool = False) -> bool:
research: bool = True, artefacts: bool = True, qa_force: bool = False,
auto_inventory: bool = True, auto_artefacts: bool = True,
status_out: dict | None = None) -> bool:
"""Run the inventory board (plus board 2 „Artefakte") until quiescence.
research=False = Continue: drain the existing queue, search nothing new.
qa_force=True overrides a failed QA gate (user clicked „Trotzdem fortsetzen")."""
qa_force=True overrides a failed QA gate (user clicked „Trotzdem fortsetzen").
auto_inventory: after the inventory QA, loop repair until 100 %/stall/10×, then open the
board-2 gate; off = pause after the QA. auto_artefacts: same loop after board 2.
status_out (optional): filled with {"weiter": bool, "note_artefakte": float|None} — the
orchestrator uses „weiter" to decide whether the guide level may start next."""
topic = ctx.topic
flow = Flow(topic, files["arbeit"])
flow.state["instructions"] = instructions
flow.state["qa_force"] = qa_force
flow.state["auto_inventory"] = auto_inventory
flow.state["auto_artefacts"] = auto_artefacts
run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M')}-{uuid.uuid4().hex[:4]}"
flow.state["run_id"] = run_id
flow.state["run_started"] = datetime.now(timezone.utc).isoformat()
@@ -1744,11 +1752,21 @@ async def run_boards(ctx: GenContext, set_p, files: dict, q: dict, folder, instr
from blocks import _blocks_errors # lazy: Import-Zyklus vermeiden
_blocks_errors[topic] = flow.state.get("infra_error") \
or "Pausiert: API-Ratelimit (429) — «Fortsetzen», sobald Kontingent zurück"
if status_out is not None:
status_out["weiter"] = False
return True # kein Fehler: Karten warten resümierbar in ihrer Spalte
if flow.state.get("qa_paused"):
if status_out is not None:
status_out["weiter"] = False # Inventar pausiert → Board 2 lief nicht
return True # kein Fehler: Flow hielt am QA-Gate, Karten warten in generate
# Board 2 lief durch → Artefakt-Ebene: QA + (Auto) „Befunde beheben"-Loop.
if artefacts and not ctx.is_cancelled():
await _artefakt_auto_loop(ctx, flow, set_p)
await _write_final(topic, files)
await _write_run_summary(topic, flow)
if status_out is not None:
status_out["weiter"] = not (ctx.is_cancelled() or flow.state.get("artefakt_stopp"))
status_out["note_artefakte"] = flow.state.get("note_artefakte")
return True
finally:
# erst NACH der Abschluss-QA leeren: deren Judge-Events gehören zum Lauf —
@@ -1781,17 +1799,49 @@ async def _qa_gate_watch(ctx: GenContext, flow: Flow, inv_names: list[str], set_
report = await qa.qa_report(topic, llm=QA_GATE_LLM)
note = float(report["note"]) if report else 10.0
flow.state["qa_note"] = note
if report:
try: # Report-Persistenz ist Komfort — ein Schreibfehler darf das Gate nicht öffnen
await qa.write_report(report)
except Exception:
log.exception("[%s] QA-Report schreiben fehlgeschlagen", topic)
if note >= QA_GATE_NOTE:
_log(topic, f"QA-Gate: Note {note}{QA_GATE_NOTE} — Board 2 startet")
if not report:
flow.state["qa_ok"] = True # nichts messbar → fail-open (QA ist Helfer, nicht Jailer)
flow.wake.set()
return
try: # Report-Persistenz ist Komfort — ein Schreibfehler darf das Gate nicht öffnen
await qa.write_report(report)
except Exception:
log.exception("[%s] QA-Report schreiben fehlgeschlagen", topic)
if not flow.state.get("auto_inventory", True):
# Auto aus: nach der QA anhalten (Kontrollpunkt), egal welche Note.
_log(topic, f"Inventar-QA {round(note * 10)} % — Auto aus, pausiert")
set_p(f"Inventar-QA {round(note * 10)} % — Auto aus, «Fortsetzen» startet Board 2")
flow.state["qa_paused"] = True
flow.stop = True
flow.wake.set()
return
# Auto an: „Befunde beheben"-Loop bis 100 % / Stillstand / 10×.
from auto_loop import auto_repair_loop
import repair as _repair
async def _reparieren():
set_p("Befunde beheben (Inventar)…")
r = await _repair.repair_befunde(topic, "inventory") # behebt + misst neu
n = float(r.get("note", 10.0))
flow.state["qa_note"] = n
return n
res = await auto_repair_loop("Inventar", note, _reparieren)
end_note = res["note"]
if res["grund"] == "fertig":
_log(topic, f"Inventar: 100 % nach {res['runden']} Runde(n) — Board 2 startet")
flow.state["qa_ok"] = True
else:
_log(topic, f"QA-Gate: Note {note} < {QA_GATE_NOTE} — pausiert (Continue erzwingt)")
set_p(f"QA-Note {note} < {QA_GATE_NOTE} — pausiert")
# Stillstand/Limit ohne 100 % → Abbruch: Repair hat keinen Pfad für die Restbefunde.
msg = (f"Inventar bleibt bei {round(end_note * 10)} % ({res['grund']}, "
f"{res['runden']} Runden) — «Befunde beheben» hat keinen Pfad für die "
f"Restbefunde (Systemfehler)")
_log(topic, msg)
set_p(msg)
from blocks import _blocks_errors
_blocks_errors[topic] = msg
flow.state["qa_paused"] = True
flow.stop = True
flow.wake.set()
@@ -1803,6 +1853,55 @@ async def _qa_gate_watch(ctx: GenContext, flow: Flow, inv_names: list[str], set_
flow.wake.set()
async def _artefakt_auto_loop(ctx: GenContext, flow: Flow, set_p):
"""Nach Board 2: Artefakt-QA → (Auto) „Befunde beheben"-Loop bis 100 %/Stillstand/10×.
Auto aus = nur messen (Kontrollpunkt). Setzt flow.state[„note_artefakte"] und bei
Stopp flow.state[„artefakt_stopp"] (der Guide startet dann nicht automatisch).
Fail-open: ein QA-Fehler überspringt die Ebene (QA ist Helfer, nicht Jailer)."""
topic = ctx.topic
try:
import qa
rep = await qa.qa_report(topic, llm=QA_GATE_LLM)
na = rep.get("note_artefakte") if rep else None
if na is None: # Board 2 hat (noch) nichts geliefert → nichts zu messen
return
na = float(na)
flow.state["note_artefakte"] = na
try:
await qa.write_report(rep)
except Exception:
log.exception("[%s] QA-Report schreiben fehlgeschlagen", topic)
if na >= 10.0:
return
if not flow.state.get("auto_artefacts", True):
_log(topic, f"Artefakt-QA {round(na * 10)} % — Auto aus, Guide startet nicht automatisch")
set_p(f"Artefakt-QA {round(na * 10)} % — Auto aus")
flow.state["artefakt_stopp"] = True
return
from auto_loop import auto_repair_loop
import repair as _repair
async def _reparieren():
set_p("Befunde beheben (Artefakte)…")
r = await _repair.repair_befunde(topic, "artefacts")
return float(r.get("note_artefakte") or 10.0)
res = await auto_repair_loop("Artefakte", na, _reparieren)
flow.state["note_artefakte"] = res["note"]
if res["grund"] != "fertig":
msg = (f"Artefakte bleiben bei {round(res['note'] * 10)} % ({res['grund']}, "
f"{res['runden']} Runden) — «Befunde beheben» hat keinen Pfad (Systemfehler)")
_log(topic, msg)
set_p(msg)
from blocks import _blocks_errors
_blocks_errors[topic] = msg
flow.state["artefakt_stopp"] = True
else:
_log(topic, f"Artefakte: 100 % nach {res['runden']} Runde(n)")
except Exception:
log.exception("[%s] Artefakt-Auto-Loop fehlgeschlagen — übersprungen (fail-open)", topic)
async def _write_run_summary(topic: str, flow: Flow):
"""lauf-summary.json: the per-run numbers block QA diffs against. Never fatal."""
try:
@@ -2034,23 +2133,6 @@ async def reset_board_from_stage(topic: str, board: str, stage: str, files: dict
return moved
async def restart_artefact_card(topic: str, card_id: str) -> bool:
"""Restart ONE artefacts card from `generate` — wipes only ITS derived DB rows
(per-block work-dir slots overwrite themselves; finalize re-upserts later).
Only call while nothing is generating (the route guards). → False if unknown."""
card = await db.kanban_get_card(topic, "artefacts", card_id)
if card is None or card["kind"] != "ablock":
return False
await db.delete_subblocks(topic, card_id)
await db.delete_question_pattern(topic, card_id)
await db.delete_sub_artefakte(topic, card_id)
p = {k: v for k, v in card["payload"].items() if k in ("title", "description")}
await db.kanban_set_payload(topic, "artefacts", card_id, p)
await db.kanban_advance(topic, "artefacts", card_id, "generate")
await db.add_event(topic, "reset", key=f"artefacts:{card_id}", status="card-restart")
return True
async def requeue_dead(topic: str) -> int:
"""Dead-letter → restart stage by card kind (fresh retries). → requeued count."""
n = 0