update
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user