Auftragsbuch: 'wartung/idle-radar' angenommen (Ein-Klick-Gate)

This commit is contained in:
Auftragsbuch
2026-07-12 20:10:33 +02:00
20 changed files with 909 additions and 79 deletions
+2 -1
View File
@@ -12,6 +12,7 @@ router = APIRouter(prefix="/api")
class BranchIn(BaseModel): class BranchIn(BaseModel):
branch: str branch: str
repo: str = "mc2" # "mc2" (Box-Stack) oder "lucy" (Desktop-App, Annahme baut am PC) repo: str = "mc2" # "mc2" (Box-Stack) oder "lucy" (Desktop-App, Annahme baut am PC)
grund: str = "" # nur beim Ablehnen: optionaler Grund → Lern-Gedächtnis des Kreislaufs
class KandidatIn(BaseModel): class KandidatIn(BaseModel):
@@ -41,7 +42,7 @@ def accept(body: BranchIn) -> dict:
@router.post("/auftragsbuch/ablehnen") @router.post("/auftragsbuch/ablehnen")
def reject(body: BranchIn) -> dict: def reject(body: BranchIn) -> dict:
res = auftragsbuch.reject(body.branch, body.repo) res = auftragsbuch.reject(body.branch, body.repo, body.grund)
if not res.get("ok"): if not res.get("ok"):
raise HTTPException(400, res.get("error", "Ablehnen fehlgeschlagen.")) raise HTTPException(400, res.get("error", "Ablehnen fehlgeschlagen."))
return res return res
+43 -7
View File
@@ -36,6 +36,10 @@ REPO = Path(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2")).expanduser
LUCY = Path(os.environ.get("MC2_LUCY_DIR", "~/lucy")).expanduser() LUCY = Path(os.environ.get("MC2_LUCY_DIR", "~/lucy")).expanduser()
VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser() VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
STATUS_PATH = Path(os.environ.get("MC2_AUFTRAG_STATUS", str(MODELS_DIR / "mc2-auftragsbuch.json"))) STATUS_PATH = Path(os.environ.get("MC2_AUFTRAG_STATUS", str(MODELS_DIR / "mc2-auftragsbuch.json")))
# Empfehlungs-Stempel des nächtlichen Karten-Gutachters (deploy/karten-gutachter.sh schreibt,
# die API liest nur) und das Lern-Gedächtnis der Ablehnungen (Radar/Specifier lesen es).
GUTACHTEN_PATH = Path(os.environ.get("MC2_KARTEN_GUTACHTEN", str(MODELS_DIR / "mc2-karten-gutachten.json")))
ABLEHNUNGEN_PATH = Path(os.environ.get("MC2_ABLEHNUNGEN", str(MODELS_DIR / "mc2-ablehnungen.jsonl")))
# Karten-Quellen. Die Runner-Skripte liegen IMMER im MC2-Checkout (deploy/) — auch der # Karten-Quellen. Die Runner-Skripte liegen IMMER im MC2-Checkout (deploy/) — auch der
# Lucy-Runner, denn er läuft auf der Box; nur der Build passiert am PC. # Lucy-Runner, denn er läuft auf der Box; nur der Build passiert am PC.
@@ -104,6 +108,15 @@ def _set_status(key: str, state: str, detail: str) -> None:
log.warning("auftragsbuch: Status %s nicht schreibbar", STATUS_PATH, exc_info=True) log.warning("auftragsbuch: Status %s nicht schreibbar", STATUS_PATH, exc_info=True)
def _gutachten() -> dict:
"""Alle Karten-Stempel ("<repo>:<branch>"{empfehlung, satz, richter, commit_ts, ts})."""
try:
data = json.loads(GUTACHTEN_PATH.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _remote_branches(repo: str) -> list[str]: def _remote_branches(repo: str) -> list[str]:
r = _git(repo, ["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"]) r = _git(repo, ["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"])
out = [] out = []
@@ -118,7 +131,7 @@ def _valid_branch(branch: str) -> bool:
return bool(_BRANCH_RX.match(branch)) and ".." not in branch and branch.startswith(PREFIXES) return bool(_BRANCH_RX.match(branch)) and ".." not in branch and branch.startswith(PREFIXES)
def _repo_items(repo: str, statuses: dict) -> list[dict]: def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
items = [] items = []
for branch in _remote_branches(repo): for branch in _remote_branches(repo):
ref = f"origin/{branch}" ref = f"origin/{branch}"
@@ -130,6 +143,12 @@ def _repo_items(repo: str, statuses: dict) -> list[dict]:
behind = int((_git(repo, ["rev-list", "--count", f"{ref}..origin/main"]).stdout or "0").strip() or 0) behind = int((_git(repo, ["rev-list", "--count", f"{ref}..origin/main"]).stdout or "0").strip() or 0)
show = _git(repo, ["show", "-s", "--format=%s%x1f%b%x1f%ct%x1f%an", ref]) show = _git(repo, ["show", "-s", "--format=%s%x1f%b%x1f%ct%x1f%an", ref])
subject, body, cts, author = ((show.stdout or "").split("\x1f") + ["", "", "", ""])[:4] subject, body, cts, author = ((show.stdout or "").split("\x1f") + ["", "", "", ""])[:4]
ts_val = int(cts) if cts.strip().isdigit() else None
# Stempel nur zeigen, wenn er zum AKTUELLEN Commit gehört (nachgeschobener
# Commit macht das alte Gutachten ungültig — der Nacht-Lauf stempelt neu).
stempel = gutachten.get(f"{repo}:{branch}")
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
stempel = None
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip() stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines() files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
files = [] files = []
@@ -147,7 +166,8 @@ def _repo_items(repo: str, statuses: dict) -> list[dict]:
"subject": subject.strip(), "subject": subject.strip(),
"body": body.strip()[:2000], "body": body.strip()[:2000],
"author": author.strip(), "author": author.strip(),
"ts": int(cts) if cts.strip().isdigit() else None, "ts": ts_val,
"empfehlung": stempel,
"ahead": ahead, "ahead": ahead,
"behind": behind, "behind": behind,
"shortstat": stat, "shortstat": stat,
@@ -170,12 +190,13 @@ def list_proposals() -> dict:
return {"available": False, "items": [], "skill_kandidaten": [], "open_count": 0} return {"available": False, "items": [], "skill_kandidaten": [], "open_count": 0}
statuses = _statuses() statuses = _statuses()
gutachten = _gutachten()
items = [] items = []
for repo in REPOS: for repo in REPOS:
if not _repo_ok(repo): if not _repo_ok(repo):
continue # z. B. ~/lucy (noch) nicht geklont → Karten dieses Repos einfach weglassen continue # z. B. ~/lucy (noch) nicht geklont → Karten dieses Repos einfach weglassen
_fetch_throttled(repo) _fetch_throttled(repo)
items.extend(_repo_items(repo, statuses)) items.extend(_repo_items(repo, statuses, gutachten))
items.sort(key=lambda i: i.get("ts") or 0, reverse=True) items.sort(key=lambda i: i.get("ts") or 0, reverse=True)
kandidaten = list_skill_kandidaten() kandidaten = list_skill_kandidaten()
@@ -230,22 +251,37 @@ def accept(branch: str, repo: str = "mc2") -> dict:
return {"ok": True, "unit": unit} return {"ok": True, "unit": unit}
def reject(branch: str, repo: str = "mc2") -> dict: def reject(branch: str, repo: str = "mc2", grund: str = "") -> dict:
"""Ablehnen: Remote-Branch löschen (die Arbeit bleibt in der Gitea-Historie referenzierbar, """Ablehnen: Remote-Branch löschen (die Arbeit bleibt in der Gitea-Historie referenzierbar,
aber die Entscheidung ist gefallen). Meldung in den Briefkasten — ehrlich und sichtbar.""" aber die Entscheidung ist gefallen). Der optionale GRUND ist das Lern-Gedächtnis des
Kreislaufs: er landet in ABLEHNUNGEN_PATH (jsonl), und Idle-Radar/Analysten bekommen
ihn als „NIE wieder vorschlagen"-Material vorgelegt. Meldung in den Briefkasten."""
if not _repo_ok(repo): if not _repo_ok(repo):
return {"ok": False, "error": "Ablehnen geht nur auf der Box."} return {"ok": False, "error": "Ablehnen geht nur auf der Box."}
if not _valid_branch(branch) or branch not in _remote_branches(repo): if not _valid_branch(branch) or branch not in _remote_branches(repo):
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"} return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
grund = (grund or "").strip()[:400]
# Betreff VOR dem Löschen sichern — danach ist der Ref weg.
subject = (_git(repo, ["show", "-s", "--format=%s", f"origin/{branch}"]).stdout or "").strip()[:200]
r = _git(repo, ["push", "origin", "--delete", branch], timeout=30) r = _git(repo, ["push", "origin", "--delete", branch], timeout=30)
if r.returncode != 0: if r.returncode != 0:
return {"ok": False, "error": f"Löschen fehlgeschlagen: {(r.stderr or '').strip()[:300]}"} return {"ok": False, "error": f"Löschen fehlgeschlagen: {(r.stderr or '').strip()[:300]}"}
_git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30) _git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30)
_set_status(_status_key(repo, branch), "abgelehnt", "Vom Commander abgelehnt — Branch gelöscht") detail = "Vom Commander abgelehnt — Branch gelöscht"
if grund:
detail += f" (Grund: {grund})"
_set_status(_status_key(repo, branch), "abgelehnt", detail)
try:
with ABLEHNUNGEN_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps({"ts": int(time.time()), "repo": repo, "branch": branch,
"subject": subject, "grund": grund}, ensure_ascii=False) + "\n")
except OSError:
log.warning("auftragsbuch: Ablehn-Gedächtnis %s nicht schreibbar", ABLEHNUNGEN_PATH, exc_info=True)
try: try:
from services import announce from services import announce
wo = "im Lucy-Repo " if repo == "lucy" else "" wo = "im Lucy-Repo " if repo == "lucy" else ""
announce.add(f"Vorschlag '{branch}' {wo}wurde abgelehnt und der Branch gelöscht.", warum = f" Grund: {grund}" if grund else ""
announce.add(f"Vorschlag '{branch}' {wo}wurde abgelehnt und der Branch gelöscht.{warum}",
"[Auftragsbuch]", "auftragsbuch", "silent") "[Auftragsbuch]", "auftragsbuch", "silent")
except Exception: except Exception:
pass pass
+8 -1
View File
@@ -92,10 +92,17 @@ cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-fe
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die # Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
# Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen"). # Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen").
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
# Idle-Radar (S4 "Zuendung", 12.07.2026): die Box gibt sich nachts selbst Arbeit —
# Journal-Fehlermuster + Traum-Funde -> belegte rohe Ideen (triage) im nativen Kanban.
cp "$SRC/deploy/idle-radar-feed.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh"
# "Lucy kennt sich" (12.07.2026): Selbst-Inventur generiert den Steckbrief + bemerkt
# eigene Aenderungen; der Karten-Gutachter stempelt jede Karte mit einer Empfehlung.
cp "$SRC/deploy/selbst-inventur.sh" "$HOME/.hermes/scripts/selbst-inventur.sh"
cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen # Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt). # (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md" [ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes. # Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
mkdir -p "$HOME/.hermes/skills/wartung" mkdir -p "$HOME/.hermes/skills/wartung"
cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md" cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
+28
View File
@@ -186,6 +186,34 @@ if [ "${TREFFER:-1}" != "0" ] && [ -x "$FREMDBLICK" ]; then
echo echo
fi fi
# Treffer fuettern die Ideen-Queue ("Lucy kennt sich", 12.07.2026): aus einem echten
# Radar-Treffer wird EINE rohe Idee (triage) — Specifier + Commander entscheiden, ob eine
# Probe gebaut wird. Idempotent je Release-Tag; der Radar selbst bleibt reiner Melder.
if [ -n "${TREFFER:-}" ] && [ "$TREFFER" != "0" ] && [ -x "$HOME/.local/bin/hermes" ]; then
IDEE_ID="$("$HOME/.local/bin/hermes" kanban create \
"Hermes ${NEWEST}: Release-Radar-Treffer pruefen (Probe bauen?)" \
--body "Automatisch vom Release-Radar am $(date '+%d.%m.%Y'). ABGLEICH (Analyst: ${RICHTER}):
${ANALYSE}
FREMDBLICK:
${GEGEN:-(keiner)}
Rohe Idee: nur pruefen bzw. eine risikofreie Probe vorschlagen — Updates bleiben manuell (Entscheid 04.07.)." \
--triage --created-by release-radar --idempotency-key "release-radar-${NEWEST}" --json 2>/dev/null \
| python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("{")
try:
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
except Exception:
print("")
' 2>/dev/null || true)"
if [ -n "${IDEE_ID// /}" ]; then
echo "### IN DIE IDEEN-QUEUE GELEGT: ${IDEE_ID} — Probe-Pruefung fuer ${NEWEST} (der Specifier uebernimmt)."
echo
fi
fi
printf '%s\n' "$NEWEST" > "$STATE" printf '%s\n' "$NEWEST" > "$STATE"
briefkasten "Release-Radar" "Neue hermes-agent-Releases bis $NEWEST (Box: ${INSTALLED_TAG:-?}). briefkasten "Release-Radar" "Neue hermes-agent-Releases bis $NEWEST (Box: ${INSTALLED_TAG:-?}).
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env bash
# Idle-Radar (naechtlich 03:45 — S4 "Zuendung", 12.07.2026): die Box gibt sich selbst Arbeit.
# Destilliert aus JOURNAL-Fehlermustern (24 h) und der juengsten TRAUM-Notiz 0-2 KLEINE,
# belegte Wartungs-Kandidaten und legt sie als ROHE IDEEN (triage) ins native Hermes-Kanban.
# Danach laeuft der bewiesene S2-Kreislauf von allein: Specifier arbeitet aus -> Werkstatt
# baut einen Vorschlags-Branch -> Karte im Auftragsbuch -> der KLICK des Commanders bleibt
# das einzige Gate. Muster wie hermes-release-radar-feed.sh: liegt in ~/.hermes/scripts/
# (deploy.sh kopiert ihn), stdout wird dem Cron-Agenten als Prompt eingespeist; der
# eigentliche ABGLEICH laeuft als Ein-Schuss-Completion gegen llama-swap (:8080).
# Grundsaetze:
# - Kandidat NUR mit woertlichem Beleg-Zitat; das Skript prueft das Zitat MECHANISCH
# gegen das Material (reproduziert-oder-abgelehnt, Verdikt 04.07.) — haerter als
# ein zweites LLM-Urteil und gratis.
# - Max. 2 neue Ideen je Nacht + STAU-BREMSE: liegt schon genug Arbeit (volle Queue
# oder volles Auftragsbuch), legt der Radar NICHTS nach — die Box soll Arbeit
# abarbeiten, nicht anhaeufen.
# - Dedup dreifach: State-Datei (schon gemeldet) + Queue/Karten-Liste im Analysten-
# Kontext + --idempotency-key beim Anlegen.
# - Der Radar LEGT NUR IDEEN AN — er baut nichts, merged nichts, konfiguriert nichts.
# State: ~/.hermes/state/idle-radar-gemeldet.txt (ein KEY je bereits gemeldetem Kandidaten).
set -uo pipefail
MC="$HOME/mission-control-v2"
VAULT="$HOME/wissens-vault"
STATE_DIR="$HOME/.hermes/state"
STATE="$STATE_DIR/idle-radar-gemeldet.txt"
ENDPOINT="${IDLE_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
ANALYST="${IDLE_MODEL:-gpt-oss-120b}"
VERTRETUNG="${IDLE_FALLBACK:-GLM-4.7-Flash}"
HERMES="$HOME/.local/bin/hermes"
MAX_NEU=2 # harte Obergrenze neuer Ideen je Nacht (wie Bagatell-Pfad)
STAU_QUEUE=4 # ab so vielen offenen Queue-Aufgaben: Bremse
STAU_KARTEN=3 # ab so vielen offenen Auftragsbuch-Karten: Bremse
mkdir -p "$STATE_DIR"; touch "$STATE"
# Briefkasten-Spiegel (still, wie Morgenlage/Release-Radar): Chronik liest source=idle-radar.
briefkasten() { # $1=Betreff $2=Text
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "idle-radar", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
cat "$MC/deploy/idle-radar-prompt.md" 2>/dev/null || cat <<'EOF'
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des naechtlichen Idle-Radars.
Berichte den Befund unten in Lucys Stimme in hoechstens 3 Saetzen: wie viele neue
Queue-Ideen angelegt wurden (Titel nennen) oder warum keine. Nichts selbst umsetzen,
keine Kanban-Aktionen — das Skript hat alles Noetige bereits getan.
EOF
echo
# ---------- Stau-Bremse (vor dem teuren Analysten) ----------
OFFEN_QUEUE="$("$HERMES" kanban list --json 2>/dev/null | python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("[")
try:
d = json.loads(t[i:]) if i >= 0 else []
except Exception:
d = []
print(sum(1 for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")))
' 2>/dev/null || echo 0)"
OFFEN_KARTEN="$(curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
import json, sys
try:
print(int(json.load(sys.stdin).get("open_count", 0)))
except Exception:
print(0)
' 2>/dev/null || echo 0)"
if [ "${OFFEN_QUEUE:-0}" -ge "$STAU_QUEUE" ] || [ "${OFFEN_KARTEN:-0}" -ge "$STAU_KARTEN" ]; then
echo "## IDLE-RADAR-BEFUND: STAU-BREMSE (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "Offene Queue-Aufgaben: ${OFFEN_QUEUE} (Bremse ab ${STAU_QUEUE}) · offene Karten: ${OFFEN_KARTEN} (Bremse ab ${STAU_KARTEN})."
echo "Es liegt genug Arbeit — heute Nacht keine neuen Ideen. Dem Commander EINEN kurzen Satz melden."
briefkasten "Idle-Radar" "Stau-Bremse: ${OFFEN_QUEUE} offene Queue-Aufgaben, ${OFFEN_KARTEN} offene Karten — keine neuen Ideen angelegt."
exit 0
fi
# ---------- Material sammeln (erst sammeln, dann kuerzen — SIGPIPE-Falle) ----------
TRAUM_DATEI="$(ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -1 || true)"
EVID="$(
echo "### JOURNAL-FEHLERMUSTER (user-Dienste, letzte 24 h; Anzahl · Muster, lange Zahlen/IDs normalisiert):"
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
| sed -E 's/^[A-Z][a-z]{2} [0-9 ]{2} [0-9:]{8} [^ ]+ //; s/\[[0-9]+\]//; s/[0-9]{3,}/N/g; s/t_[0-9a-f]+/t_X/g' \
| sort | uniq -c | sort -rn | head -18
echo
echo "### JUENGSTE TRAUM-NOTIZ ($(basename "${TRAUM_DATEI:-keine}") — Funde des naechtlichen Lern-Crons):"
if [ -n "$TRAUM_DATEI" ]; then head -70 "$TRAUM_DATEI"; else echo "(keine Traum-Notiz gefunden)"; fi
echo
echo "### OFFENE SKILL-KANDIDATEN im Vault (nur Titelzeilen):"
ls -1 "$VAULT/skill-kandidaten/"*.md 2>/dev/null | head -8 | while read -r f; do
echo "- $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
done
echo
echo "### SELBST-STECKBRIEF (auto-generiert von selbst-inventur.sh — das EXISTIERT schon; NICHTS davon neu vorschlagen):"
head -80 "$HOME/.hermes/state/selbst-steckbrief.md" 2>/dev/null || echo "(noch keine Selbst-Inventur gelaufen)"
echo
echo "### SEIT GESTERN AN MIR GEAENDERT (Selbst-Inventur-Diff — gute Kandidaten-Quelle, wenn eine kleine Massnahme erkennbar ist):"
head -40 "$HOME/.hermes/state/selbst-steckbrief.diff" 2>/dev/null || echo "(keine Aenderung erkannt)"
echo
echo "### VOM COMMANDER ABGELEHNT (mit Grund — sinngemaess NIE wieder vorschlagen):"
tail -12 "${MC2_ABLEHNUNGEN:-/srv/models/mc2-ablehnungen.jsonl}" 2>/dev/null | python3 -c '
import json, sys
n = 0
for line in sys.stdin:
try:
d = json.loads(line)
except Exception:
continue
n += 1
print("- [{}] {} — Grund: {}".format(d.get("branch", "?"), (d.get("subject") or "")[:80],
d.get("grund") or "(keiner angegeben)"))
if not n:
print("(keine)")
' 2>/dev/null || echo "(keine)"
echo
echo "### SCHON IN DER IDEEN-QUEUE (inkl. archiviert = vom Commander verworfen; NICHT erneut vorschlagen):"
"$HERMES" kanban list --archived --json 2>/dev/null | python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("[")
try:
d = json.loads(t[i:]) if i >= 0 else []
except Exception:
d = []
for x in d[:40]:
if isinstance(x, dict):
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:110]))
' 2>/dev/null || echo "(Queue nicht lesbar)"
echo
echo "### OFFENE KARTEN IM AUFTRAGSBUCH (warten auf Klick; NICHT erneut vorschlagen):"
curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
import json, sys
try:
items = json.load(sys.stdin).get("items") or []
except Exception:
items = []
if not items:
print("(keine)")
for it in items[:15]:
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
echo
echo "### VOM IDLE-RADAR BEREITS GEMELDET (KEYs; NICHT erneut vorschlagen):"
tail -40 "$STATE" 2>/dev/null || true
)"
RASTER='Du bist der IDLE-RADAR einer lokalen KI-Box (Hermes-Agent, MC2, Lucy). Du bekommst naechtliches Material: Journal-Fehlermuster, die juengste Traum-Notiz, offene Skill-Kandidaten sowie Listen dessen, was schon in Arbeit ist. Deine Aufgabe: destilliere daraus 0 bis 2 KLEINE, konkrete Wartungs-Kandidaten, die die Box-Werkstatt selbst umsetzen kann (Patch in 1-2 Dateien, Skript- oder Doku-Aenderung im MC2- oder Lucy-Repo). REGELN: Jeder Kandidat MUSS eine Zeile aus dem Material WOERTLICH zitieren (BELEG) — das Zitat wird mechanisch gegengeprueft, erfundene Zitate fliegen raus. Nichts vorschlagen, was sinngemaess schon in der IDEEN-QUEUE, den OFFENEN KARTEN oder der BEREITS-GEMELDET-Liste steht. Der SELBST-STECKBRIEF sagt dir, was die Box schon HAT und KANN — schlage NIE etwas vor, das dort schon steht (der Fehler vom 12.07.: ein Gateway-Neustart wurde vorgeschlagen, der laengst konfiguriert war). Was der Commander mit Grund ABGELEHNT hat, ist entschieden — nicht neu verpacken. Aenderungen aus dem Inventur-Diff sind gute Kandidaten-Quellen, aber nur mit konkreter kleiner Massnahme. TABU (niemals vorschlagen): Security (approvals/Tokens/ufw/sudoers/ssh), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode, Updates/Upgrades, alles was den PC oder einen Menschen braucht. Ein wiederkehrendes Fehlermuster ist NUR dann ein Kandidat, wenn eine konkrete kleine Ursachen-Behebung erkennbar ist — "beobachten", "untersuchen" oder "Monitoring bauen" ist KEIN Auftrag. Qualitaet vor Quote: KANDIDATEN: 0 ist der Normalfall und voellig ok. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "KANDIDATEN: <0|1|2>". Danach je Kandidat ein Block aus genau vier Zeilen: "TITEL: <max 90 Zeichen, Imperativ, deutsch>" / "KEY: <kurzer-kebab-slug>" / "BELEG: <woertliches Zitat aus dem Material, EINE Zeile>" / "WARUM: <ein Satz>". Bei 0 stattdessen EIN Satz, warum nichts ansteht.'
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh).
judge() {
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
| python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
msg = d["choices"][0]["message"]
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
except Exception:
pass'
}
RICHTER="$ANALYST"
ANALYSE="$(judge "$ANALYST" 420 2600)"
if [ -z "${ANALYSE// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
fi
echo "## IDLE-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo "Offene Queue-Aufgaben vorher: ${OFFEN_QUEUE} · offene Karten: ${OFFEN_KARTEN} · Deckel: ${MAX_NEU}/Nacht"
echo
if [ -z "${ANALYSE// /}" ]; then
echo "### ANALYSE AUSGEFALLEN"
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet — dem Commander EINEN ehrlichen Satz melden; morgen neuer Versuch."
briefkasten "Idle-Radar" "Analyse ausgefallen (kein Analyst erreichbar) — keine neuen Ideen."
exit 0
fi
echo "### ANALYSE (Analyst: $RICHTER)"
echo "$ANALYSE"
echo
# ---------- Ehrlichkeits-Gate + Dedup + Anlegen (deterministisch im Skript) ----------
# Temp-Dateien statt Pipes: Heredoc + Pipe gleichzeitig frisst stdin (Release-Radar-Lehre 10.07.).
TMPD="$(mktemp -d /tmp/idle-radar.XXXXXX)"
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
printf '%s' "$EVID" > "$TMPD/material.txt"
PLAN="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$MAX_NEU" <<'PY'
import re, sys, unicodedata
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
try:
state = {l.strip() for l in open(sys.argv[3], encoding="utf-8", errors="replace") if l.strip()}
except Exception:
state = set()
max_neu = int(sys.argv[4])
def norm(s):
s = unicodedata.normalize("NFKC", s)
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
return " ".join(s.lower().split())
nev = norm(evid)
# Zeilenweise einsammeln (tolerant gegen Markdown-Deko und Leerzeilen zwischen Feldern).
cands, cur = [], {}
for line in ana.splitlines():
line = line.strip().lstrip("*->#• ").strip()
m = re.match(r"(TITEL|KEY|BELEG|WARUM)\s*:\s*(.+)$", line)
if not m:
continue
k, v = m.group(1), m.group(2).strip()
if k == "TITEL" and cur:
cands.append(cur); cur = {}
cur[k] = v
if cur:
cands.append(cur)
neu = 0
SEP = "\x1f"
for c in cands:
titel = (c.get("TITEL") or "").strip()[:120]
warum = (c.get("WARUM") or "").strip()[:300]
beleg = (c.get("BELEG") or "").strip().strip('"„“»«>`').strip()
key = re.sub(r"[^a-z0-9-]+", "-", (c.get("KEY") or "").strip().lower())[:48].strip("-")
if not (titel and key and beleg):
continue
if key in state or ("idle-radar-" + key) in state:
print("SKIP" + SEP + key + SEP + "schon frueher gemeldet (State)")
continue
if not norm(beleg) or norm(beleg) not in nev:
print("SKIP" + SEP + key + SEP + "BELEG nicht woertlich im Material — verworfen (Ehrlichkeits-Gate)")
continue
if neu >= max_neu:
print("SKIP" + SEP + key + SEP + "Deckel erreicht")
continue
neu += 1
print("NEU" + SEP + key + SEP + titel + SEP + beleg + SEP + warum)
PY
)"
ANGELEGT=""; VERWORFEN=""
while IFS=$'\x1f' read -r art key titel beleg warum; do
[ -n "${art:-}" ] || continue
if [ "$art" = "SKIP" ]; then
VERWORFEN="${VERWORFEN}- ${key}: ${titel}"$'\n' # bei SKIP traegt das 3. Feld den Grund
continue
fi
[ "$art" = "NEU" ] || continue
BODY="Automatisch vom Idle-Radar erhoben am $(date '+%d.%m.%Y') (Quelle: Journal-Muster/Traum-Notiz der Nacht).
BELEG (woertlich aus dem Material): ${beleg}
WARUM: ${warum}
Rohe Idee — bitte pruefen, klein schneiden und nur wenn tragfaehig promoten; sonst archivieren."
TASK_ID="$("$HERMES" kanban create "$titel" --body "$BODY" --triage \
--created-by idle-radar --idempotency-key "idle-radar-${key}" --json 2>/dev/null \
| python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("{")
try:
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
except Exception:
print("")
' 2>/dev/null || true)"
if [ -n "${TASK_ID// /}" ]; then
echo "$key" >> "$STATE"
ANGELEGT="${ANGELEGT}- ${titel} (${TASK_ID}, Key ${key})"$'\n'
else
VERWORFEN="${VERWORFEN}- ${key}: kanban create fehlgeschlagen"$'\n'
fi
done <<< "$PLAN"
rm -rf "$TMPD"
echo "### ANGELEGT (rohe Ideen, triage — der Specifier uebernimmt):"
echo "${ANGELEGT:-(keine)}"
echo "### NICHT ANGELEGT (Gate/Dedup/Deckel):"
echo "${VERWORFEN:-(keine)}"
N_NEU="$(printf '%s' "$ANGELEGT" | grep -c '^-' || true)"
if [ "${N_NEU:-0}" -gt 0 ]; then
briefkasten "Idle-Radar" "Idle-Radar hat ${N_NEU} neue Idee(n) in die Queue gelegt:
${ANGELEGT}Der Specifier arbeitet sie aus; die Karten-Annahme bleibt beim Commander."
else
briefkasten "Idle-Radar" "Idle-Radar: keine neuen Ideen (Analyst: ${RICHTER})."
fi
+26
View File
@@ -0,0 +1,26 @@
# Idle-Radar — Auftrag für den Boten-Agenten
Du bist der Bote des nächtlichen Idle-Radars. Unten steht ein automatisch erhobener
BEFUND: aus Journal-Fehlermustern und der jüngsten Traum-Notiz destilliert ein
Analysten-Modell kleine Wartungs-Kandidaten; das Skript hat die tragfähigen davon
BEREITS als rohe Ideen (triage) in die Ideen-Queue gelegt — den Rest erledigt der
bewiesene Kreislauf (Specifier → Werkstatt → Karte im Auftragsbuch → Klick des
Commanders).
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
Fazit zuerst), in höchstens 3 Sätzen:
1. Bei „ANGELEGT: (keine)" oder „KANDIDATEN: 0": GENAU EIN kurzer Satz
(z. B. „Idle-Radar: nichts Belegbares heute Nacht — Queue bleibt wie sie ist.").
2. Bei neuen Ideen: nenne Anzahl und die Titel; sage dazu, dass die Box sie jetzt
selbst ausarbeitet und die Annahme wie immer per Karten-Klick beim Commander liegt.
3. Bei „STAU-BREMSE" oder „AUSGEFALLEN": ein ehrlicher Satz, warum der Radar heute
Nacht nichts angelegt hat.
Harte Regeln:
- NICHTS selbst umsetzen: keine Kanban-Aktionen, nichts bauen, nichts konfigurieren —
das Anlegen hat das Skript schon deterministisch erledigt.
- Erfinde keine Kandidaten über den Befund hinaus; im Ehrlichkeits-Gate verworfene
Vorschläge (BELEG nicht im Material) nicht als Erfolg verkaufen.
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur
die kurze Telegram-Meldung.
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Karten-Gutachter (naechtlich 04:00 — "Lucy kennt sich", 12.07.2026): stempelt jede neue
# Auftragsbuch-Karte mit einer Empfehlung (ANNEHMEN/ABLEHNEN/UNKLAR + EIN ehrlicher Satz),
# damit der Commander nie blind klickt. Ein-Schuss-Richter gegen llama-swap (:8080,
# fremdblick-Architektur, Muster chef-gutachter-feed.sh) — bekommt Karte (Commit-Text,
# Dateien, Diff) UND den Selbst-Steckbrief (was schon existiert), prueft also genau die
# Falle vom 12.07. (redundanter Gateway-Restart-Vorschlag). Der Stempel ist eine MEINUNG,
# kein Gate: die Karte bleibt klickbar, die Entscheidung bleibt beim Commander.
# Laeuft als Cron mit --no-agent: stdout leer = still; gestempelte Karten = kurze Zeilen.
# Ablage: /srv/models/mc2-karten-gutachten.json (Backend mischt es in die Karten-API).
set -uo pipefail
API="${MC_API:-http://127.0.0.1:9001}"
OUT="${MC2_KARTEN_GUTACHTEN:-/srv/models/mc2-karten-gutachten.json}"
ENDPOINT="${GUTACHTER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
CHEF="${GUTACHTER_MODEL:-gpt-oss-120b}"
VERTRETUNG="${GUTACHTER_FALLBACK:-GLM-4.7-Flash}"
STECK="$HOME/.hermes/state/selbst-steckbrief.md"
MAX_JE_LAUF=3 # Nacht-GPU-Budget: Rest kommt morgen dran
briefkasten() { # $1=Betreff $2=Text
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "karten-gutachter", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
TMPD="$(mktemp -d /tmp/karten-gutachter.XXXXXX)"
trap 'rm -rf "$TMPD"' EXIT
if ! curl -sf -m 20 "$API/api/auftragsbuch" -o "$TMPD/items.json" 2>/dev/null; then
echo "Karten-Gutachter: Auftragsbuch nicht erreichbar — kein Lauf."
exit 0
fi
# Offene, noch ungestempelte Karten ermitteln + verwaiste Stempel aufraeumen (Karte weg
# oder neuer Stand = Stempel weg). Schluessel: "<repo>:<branch>@<ts>" haengt am Commit-
# Zeitstempel — ein nachgeschobener Commit macht den alten Stempel automatisch ungueltig.
python3 - "$TMPD/items.json" "$OUT" > "$TMPD/plan.txt" <<'PY'
import json, sys, os
items = (json.load(open(sys.argv[1], encoding="utf-8")) or {}).get("items") or []
out_path = sys.argv[2]
try:
alt = json.load(open(out_path, encoding="utf-8"))
if not isinstance(alt, dict):
alt = {}
except Exception:
alt = {}
offen = {}
for it in items:
state = (it.get("status") or {}).get("state")
if state in ("eingespielt", "laeuft", "rollback"):
continue # entschieden oder gerade in Arbeit — kein Stempel noetig
key = "{}:{}".format(it.get("repo", "mc2"), it.get("branch", "?"))
offen[key] = it
# Verwaiste/veraltete Stempel raus (atomar zurueckschreiben).
neu = {}
for key, g in alt.items():
it = offen.get(key)
if it and g.get("commit_ts") == it.get("ts"):
neu[key] = g
tmp = out_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(neu, f, ensure_ascii=False)
os.replace(tmp, out_path)
for key, it in offen.items():
if key in neu:
continue
repo, branch = key.split(":", 1)
print("\x1f".join([repo, branch, str(it.get("ts") or 0),
(it.get("subject") or "")[:200],
(it.get("shortstat") or "")[:200]]))
PY
RASTER='Du bist der KARTEN-GUTACHTER einer lokalen KI-Box. Du bekommst EINE Vorschlags-Karte aus dem Auftragsbuch (Commit-Text, Datei-Liste, Diff) und den SELBST-STECKBRIEF der Box (was schon existiert und laeuft). Der Commander ist kein Entwickler — dein Urteil ist seine einzige fachliche Zweitmeinung vor dem Klick. Pruefe NUR anhand des Materials, erfinde nichts: (1) REDUNDANZ — existiert das laut Steckbrief oder Diff schon? Ein Vorschlag, der Bestehendes nur nachbaut oder verschlechtert (z. B. funktionierende Einstellungen ersetzt), ist abzulehnen. (2) RISIKO — beruehrt der Diff Security (Tokens/approvals/ufw/sudoers), loescht er Daten oder aendert er Verhalten, das der Commander nicht bestellt hat? (3) NUTZEN — loest er ein echtes, benanntes Problem? Antworte auf DEUTSCH in exakt zwei Zeilen: "URTEIL: ANNEHMEN" oder "URTEIL: ABLEHNEN" oder "URTEIL: UNKLAR", danach "SATZ: <ein ehrlicher Satz Begruendung in Alltagssprache>". Kein weiterer Text.'
judge() { # $1=Modell $2=Timeout $3=max_tokens (liest $EVID)
local req
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
max_tokens:$mt, temperature:0.2}')"
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
| python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
msg = d["choices"][0]["message"]
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
except Exception:
pass'
}
N=0
while IFS=$'\x1f' read -r repo branch cts subject stat; do
[ -n "${repo:-}" ] || continue
[ "$N" -ge "$MAX_JE_LAUF" ] && { echo "Karten-Gutachter: Tages-Deckel ($MAX_JE_LAUF) erreicht — Rest morgen."; break; }
DIFF="$(curl -sfG -m 30 "$API/api/auftragsbuch/diff" \
--data-urlencode "branch=$branch" --data-urlencode "repo=$repo" 2>/dev/null \
| python3 -c '
import json, sys
try:
print((json.load(sys.stdin).get("diff") or "")[:20000])
except Exception:
pass' || true)"
EVID="KARTE ${repo}:${branch}
COMMIT: ${subject}
UMFANG: ${stat}
DIFF (ggf. gekuerzt):
${DIFF:-(Diff nicht lesbar)}
SELBST-STECKBRIEF DER BOX (was schon existiert):
$(head -90 "$STECK" 2>/dev/null || echo '(noch keine Selbst-Inventur gelaufen)')"
RICHTER="$CHEF"
URTEIL_RAW="$(judge "$CHEF" 420 1600)"
if [ -z "${URTEIL_RAW// /}" ]; then
RICHTER="$VERTRETUNG (Vertretung)"
URTEIL_RAW="$(judge "$VERTRETUNG" 240 1200)"
fi
[ -z "${URTEIL_RAW// /}" ] && { echo "Karte ${branch}: Gutachter ausgefallen (kein Richter erreichbar)."; continue; }
printf '%s' "$URTEIL_RAW" > "$TMPD/urteil.txt"
STAMP="$(python3 - "$TMPD/urteil.txt" "$OUT" "$repo" "$branch" "$cts" "$RICHTER" <<'PY'
import json, os, re, sys, time
raw = open(sys.argv[1], encoding="utf-8", errors="replace").read()
m_u = re.search(r"URTEIL:\s*(ANNEHMEN|ABLEHNEN|UNKLAR)", raw, re.I)
m_s = re.search(r"SATZ:\s*(.+)", raw)
urteil = (m_u.group(1).upper() if m_u else "UNKLAR")
satz = (m_s.group(1).strip() if m_s else "Analyse unklar — Urteil nicht lesbar.")[:300]
out_path = sys.argv[2]
try:
data = json.load(open(out_path, encoding="utf-8"))
if not isinstance(data, dict):
data = {}
except Exception:
data = {}
key = "{}:{}".format(sys.argv[3], sys.argv[4])
data[key] = {"empfehlung": urteil, "satz": satz, "richter": sys.argv[6],
"commit_ts": int(sys.argv[5]) if sys.argv[5].isdigit() else None,
"ts": int(time.time())}
tmp = out_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp, out_path)
print(urteil + "\x1f" + satz)
PY
)"
URTEIL="${STAMP%%$'\x1f'*}"; SATZ="${STAMP#*$'\x1f'}"
N=$((N + 1))
echo "Karte ${branch}: Empfehlung ${URTEIL}${SATZ}"
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung ${URTEIL}${SATZ} (Richter: ${RICHTER})"
done < "$TMPD/plan.txt"
# stdout bleibt LEER, wenn nichts zu stempeln war (--no-agent => stille Nacht).
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Selbst-Inventur (naechtlich 03:35 — "Lucy kennt sich", 12.07.2026): erhebt den
# SELBST-STECKBRIEF der Box LIVE aus der Realitaet (Versionen, Dienste, Modelle, Crons,
# Queue, Karten) statt ihn von Hand zu pflegen — und BEMERKT SELBST, wenn sich etwas
# geaendert hat (Diff gegen gestern -> stille Chronik-Karte + Einzeiler an den Commander).
# Der Steckbrief ist das "was es schon gibt"-Gedaechtnis fuer alle Vorschlags-Macher:
# idle-radar-feed.sh legt ihn dem Analysten vor, die Werkstatt liest ihn beim
# Realitaets-Check. Laeuft als Cron mit --no-agent: stdout LEER = still, nur bei
# Aenderungen gibt es eine kurze deutsche Meldung (klassisches Watchdog-Muster).
# Dateien: ~/.hermes/state/selbst-steckbrief.md (+ .prev, .diff)
set -uo pipefail
STATE_DIR="$HOME/.hermes/state"
STECK="$STATE_DIR/selbst-steckbrief.md"
PREV="$STATE_DIR/selbst-steckbrief.prev"
DIFFOUT="$STATE_DIR/selbst-steckbrief.diff"
API="${MC_API:-http://127.0.0.1:9001}"
HERMES="$HOME/.local/bin/hermes"
mkdir -p "$STATE_DIR"
briefkasten() { # $1=Betreff $2=Text (still, Chronik liest source=selbst-inventur)
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
-H 'Content-Type: application/json' \
--data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
"source": "selbst-inventur", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}
# ---------- Steckbrief live erheben (nur LESEN, keine Seiteneffekte) ----------
# Fluechtiges (Uhrzeiten, Zaehler, PIDs) bleibt DRAUSSEN, damit der Diff nur echte
# Aenderungen zeigt. Reihenfolgen werden sortiert (stabiler Diff).
{
echo "# SELBST-STECKBRIEF der Box (auto-generiert von selbst-inventur.sh — NICHT von Hand pflegen)"
echo
echo "Das bin ich, das habe ich, das laeuft bei mir. Feste Entscheide und Verbote stehen in"
echo "~/mission-control-v2/docs/wissen/ (VERDIKTE.md, FALLEN.md, STACK.md) und der"
echo "Eigenbau-Landkarte ~/wissens-vault/eigenbau-landkarte.md — dort nachlesen, nie neu erfinden."
echo
echo "## Software-Versionen"
echo "- hermes: $(bash -lc 'hermes --version' 2>/dev/null | head -1 || echo unbekannt)"
echo "- OS: $(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unbekannt}") · Kernel $(uname -r)"
echo "- python3: $(python3 --version 2>/dev/null || echo unbekannt)"
echo
echo "## Laufende Dienste (systemd --user)"
systemctl --user list-units --type=service --state=running --no-legend --plain 2>/dev/null \
| awk '{print "- " $1}' | sort
echo
echo "## Timer (systemd --user)"
systemctl --user list-timers --all --no-legend --plain 2>/dev/null \
| awk '{for(i=1;i<=NF;i++) if ($i ~ /\.timer$/) print "- " $i}' | sort -u
echo
echo "## Modelle (llama-swap :8080)"
curl -sf -m 10 http://127.0.0.1:8080/v1/models 2>/dev/null | python3 -c '
import json, sys
try:
data = json.load(sys.stdin).get("data") or []
except Exception:
data = []
for m in sorted(x.get("id", "?") for x in data):
print("- " + m)
' 2>/dev/null || echo "(llama-swap nicht erreichbar)"
echo
echo "## Hermes-Crons (Name · Zeitplan · Skript)"
bash -lc 'hermes cron list' 2>/dev/null | python3 -c '
import re, sys
jobs, cur = [], {}
for line in sys.stdin:
line = line.strip()
m = re.match(r"(Name|Schedule|Script):\s+(.+)$", line)
if not m:
continue
k, v = m.groups()
if k == "Name" and cur:
jobs.append(cur); cur = {}
cur[k] = v.strip()
if cur:
jobs.append(cur)
for j in sorted(jobs, key=lambda x: x.get("Name", "")):
print("- {} · {} · {}".format(j.get("Name", "?"), j.get("Schedule", "?"), j.get("Script", "-")))
' 2>/dev/null || echo "(cron list nicht lesbar)"
echo
echo "## Ideen-Queue (natives Kanban, offene Aufgaben)"
"$HERMES" kanban list --json 2>/dev/null | python3 -c '
import json, sys
t = sys.stdin.read(); i = t.find("[")
try:
d = json.loads(t[i:]) if i >= 0 else []
except Exception:
d = []
off = [x for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")]
if not off:
print("- (leer)")
for x in sorted(off, key=lambda x: x.get("title") or ""):
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:100]))
' 2>/dev/null || echo "(Queue nicht lesbar)"
echo
echo "## Offene Karten im Auftragsbuch"
curl -sf -m 10 "$API/api/auftragsbuch" 2>/dev/null | python3 -c '
import json, sys
try:
items = json.load(sys.stdin).get("items") or []
except Exception:
items = []
if not items:
print("- (keine)")
for it in sorted(items, key=lambda x: x.get("branch") or ""):
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
echo
echo "## Feste Leitplanken (Kurzfassung — Details docs/wissen/)"
echo "- Jede Code-Zeile geht NUR ueber eine Auftragsbuch-Karte live (Klick des Commanders)."
echo "- TABU ohne explizites User-Ja: Security (approvals/Tokens/ufw/sudoers), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode."
echo "- Updates macht der Auto-Update-Timer mit Fangnetz — niemand sonst."
} > "$STECK.tmp" 2>/dev/null
mv "$STECK.tmp" "$STECK"
# ---------- Aenderung selbst bemerken (Diff gegen gestern) ----------
if [ -f "$PREV" ]; then
if ! diff -u "$PREV" "$STECK" > "$DIFFOUT.tmp" 2>/dev/null; then
# Nur echte Inhaltszeilen behalten (kein +++/---/@@-Rauschen fuer die Meldung).
mv "$DIFFOUT.tmp" "$DIFFOUT"
KURZ="$(grep -E '^[+-][^+-]' "$DIFFOUT" | head -12)"
N="$(grep -cE '^[+-][^+-]' "$DIFFOUT" || true)"
briefkasten "Selbst-Inventur" "Mein Selbstbild hat sich geaendert (${N:-?} Zeilen):
${KURZ}
(Vollstaendiger Steckbrief: ~/.hermes/state/selbst-steckbrief.md)"
echo "Selbst-Inventur: Ich habe ${N:-?} Aenderungen an mir bemerkt (Details in der Chronik). Beispiele:"
printf '%s\n' "$KURZ" | head -5
else
rm -f "$DIFFOUT.tmp" "$DIFFOUT"
: # keine Aenderung -> stdout leer -> Cron bleibt still
fi
else
briefkasten "Selbst-Inventur" "Erster Selbst-Steckbrief erhoben (~/.hermes/state/selbst-steckbrief.md)."
echo "Selbst-Inventur: erster Steckbrief erhoben — ab morgen melde ich nur noch Aenderungen."
fi
cp "$STECK" "$PREV"
+8 -2
View File
@@ -29,8 +29,14 @@ Langsamer (Swap-Latenz + einer nach dem anderen: Minuten), aber bei Hintergrund-
Passt: ein Auftrag, der in **24 abgegrenzte Teil-Tasks** zerfaellt, jeder ein 12-Datei-Bau-Schritt Passt: ein Auftrag, der in **24 abgegrenzte Teil-Tasks** zerfaellt, jeder ein 12-Datei-Bau-Schritt
(neues kleines Modul + Test, kleine neue Funktion + Verdrahtung, zusammenhaengender Refactor in (neues kleines Modul + Test, kleine neue Funktion + Verdrahtung, zusammenhaengender Refactor in
wenigen Dateien). Zu gross (viele Module, echte Architektur-Entscheidungen, breite API-Aenderung): wenigen Dateien). Zu gross (viele Module, echte Architektur-Entscheidungen, breite API-Aenderung):
dann NUR einen **Plan** als Text im Telegram-Vorschlag liefern, KEINEN Code — der Commander schneidet **Etappen-Weg statt Ablehnung** (Grossbau-Entscheid 10.07.2026) — zerlege das Projekt in Etappen,
dann kleiner. von denen JEDE fuer sich lauffaehig, gate-gruen und einzeln annehmbar ist. Baue in DIESEM Lauf nur
Etappe 1 (als normalen Orchestrator-Durchlauf mit Workern + Zwei-Kritiker-Gate); den Etappen-Plan
in den Telegram-Vorschlag schreiben und die Folge-Etappen als neue Aufgaben in die Ideen-Queue
legen (`hermes kanban create "<Projekt> — Etappe <n>: <was>" --triage`, im Body: Plan + Branch der
Vor-Etappe). Eine Folge-Etappe wird erst gebaut, wenn der Branch der Vor-Etappe in main ist —
NIE auf unangenommene Branches aufbauen. Rauere Qualitaet ist bei Grossbau ok, aber ehrlich im
Vorschlag ausweisen, was fehlt oder wacklig ist.
## Leitplanken (nicht verhandelbar) ## Leitplanken (nicht verhandelbar)
- **Du delegierst, du baust NICHT selbst.** Jedes Bau-Artefakt MUSS aus einem `worker.sh`-Aufruf - **Du delegierst, du baust NICHT selbst.** Jedes Bau-Artefakt MUSS aus einem `worker.sh`-Aufruf
+9 -1
View File
@@ -16,12 +16,20 @@ Nutze diesen Skill, wenn ein kleiner, klar umrissener Wartungsauftrag für das M
**Scope-Check zuerst:** Klein = Config-Schlüssel-Migration, Dependency-Bump (Lockfile), **Scope-Check zuerst:** Klein = Config-Schlüssel-Migration, Dependency-Bump (Lockfile),
Patch in 12 Dateien. Alles Größere (Architektur, mehrere Module, neue Features): Patch in 12 Dateien. Alles Größere (Architektur, mehrere Module, neue Features):
NUR einen Plan liefern (Text im Telegram-Vorschlag), KEINEN Code. NICHT pauschal ablehnen, sondern den **Etappen-Weg** gehen (Großbau-Entscheid 10.07.2026):
einen Etappen-Plan in den Vorschlag schreiben (jede Etappe für sich lauffähig + annehmbar)
und NUR Etappe 1 als eigenen Vorschlags-Branch bauen; Folge-Etappen als neue Aufgaben in
die Ideen-Queue (`hermes kanban create "<Projekt> — Etappe <n>: <was>" --triage`). NIE auf
unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt die nächste.
## Leitplanken (nicht verhandelbar) ## Leitplanken (nicht verhandelbar)
- NIEMALS auf `main` committen. NIEMALS mergen. NIEMALS deployen oder Dienste neu starten. - NIEMALS auf `main` committen. NIEMALS mergen. NIEMALS deployen oder Dienste neu starten.
- Security-Config ist TABU: keine Tokens, approvals, ufw, sudoers anfassen. - Security-Config ist TABU: keine Tokens, approvals, ufw, sudoers anfassen.
- **Realitäts-Check zuerst (12.07.2026):** Existiert das schon? Selbst-Steckbrief
(`~/.hermes/state/selbst-steckbrief.md`) und Live-System LESEN, bevor du baust. Stellt
sich der Auftrag als schon erledigt/überflüssig heraus → ehrlich melden und abbrechen,
KEINEN Branch liefern.
- **Hermes-first (10.07.2026):** Bevor du NEUE Infrastruktur baust oder vorschlägst (Skript, - **Hermes-first (10.07.2026):** Bevor du NEUE Infrastruktur baust oder vorschlägst (Skript,
Dienst, Tool, Cron), prüfe ob hermes-agent das nativ kann — erst in die Eigenbau-Landkarte Dienst, Tool, Cron), prüfe ob hermes-agent das nativ kann — erst in die Eigenbau-Landkarte
schauen (`~/wissens-vault/eigenbau-landkarte.md`: Eigenbauten ↔ nativ, Verdikte), notfalls schauen (`~/wissens-vault/eigenbau-landkarte.md`: Eigenbauten ↔ nativ, Verdikte), notfalls
+24 -4
View File
@@ -24,10 +24,30 @@ sauberes Handwerk.
5. TABU-Zonen: approvals/Tokens/ufw/Security-Configs, `~/.hermes/config.yaml`, 5. TABU-Zonen: approvals/Tokens/ufw/Security-Configs, `~/.hermes/config.yaml`,
systemd-Units, alles außerhalb deines Workspaces. Braucht der Auftrag so etwas → systemd-Units, alles außerhalb deines Workspaces. Braucht der Auftrag so etwas →
`kanban_block` mit Begründung, nicht selbst machen. `kanban_block` mit Begründung, nicht selbst machen.
6. Zu groß oder unklar? Lieber `kanban_block` mit ehrlicher Diagnose (was fehlt, wie man 6. Unklar? Lieber `kanban_block` mit ehrlicher Diagnose (was fehlt, wie man kleiner
kleiner schneidet) als Murks. Für große Umbauten: nur einen PLAN als Kommentar schneidet) als Murks. GROSSE Aufträge sind erlaubt — aber NUR in Etappen
liefern, keinen Code. (Großbau-Entscheid 10.07.2026):
7. Am Ende `kanban_complete` mit deutscher Summary: was gebaut, der BRANCH-NAME, wie - Zuerst einen Etappen-Plan als Task-Kommentar: welche Etappen, was ist am Ende
JEDER Etappe fertig und für sich lauffähig.
- Dann NUR Etappe 1 bauen: eigener Vorschlags-Branch, Gates grün, für sich
annehmbar — eine halbe Baustelle kommt NIE auf eine Karte.
- Folge-Etappen als NEUE Kanban-Aufgaben anlegen (`kanban_create`, Titel
„<Projekt> — Etappe <n>: <was>"; im Body: der Plan + Branch der Vor-Etappe).
- Bearbeitest du eine Etappen-Aufgabe ab Etappe 2: prüfe ZUERST, ob der Branch der
Vor-Etappe schon in main ist (`git log origin/main`). Wenn nein →
`kanban_block("wartet auf Annahme Etappe <n-1>")` — NIE auf unangenommene
Branches aufbauen.
- Rauere Qualität ist bei Großbau ok, aber ehrlich: was fehlt oder wacklig ist,
steht in der Summary. Scheitert eine Etappe: `kanban_block` + Diagnose als
Kommentar (was gelernt, wie kleiner schneiden) — Fehlversuche sind Lernmaterial.
7. REALITÄTS-CHECK vor jedem Bau: Existiert das schon? Lies den Selbst-Steckbrief
(`~/.hermes/state/selbst-steckbrief.md`, auto-generiert) und prüfe das Live-System
(Config/Code LESEN ist erlaubt). Stellt sich der Auftrag als schon erledigt oder
überflüssig heraus: KEINEN Branch liefern — `kanban_block` mit ehrlicher Diagnose
(„schon vorhanden, weil …"). Ein Branch für etwas Überflüssiges ist ein GESCHEITERTER
Lauf (Vorfall 12.07.2026: Gateway-Neustart war längst konfiguriert, der Worker wusste
es — und lieferte trotzdem eine Karte).
8. Am Ende `kanban_complete` mit deutscher Summary: was gebaut, der BRANCH-NAME, wie
geprüft. Der Branch-Name MUSS in der Summary stehen — daraus wird die Karte gefunden. geprüft. Der Branch-Name MUSS in der Summary stehen — daraus wird die Karte gefunden.
## Stil ## Stil
+34
View File
@@ -21,6 +21,18 @@ Vor dem Auftragsbuch liegt DIE eine Ideen-Queue: das native Hermes-Kanban
Vorschlags-Branch auf Gitea — der hier als Karte erscheint. **Das Gate bleibt Vorschlags-Branch auf Gitea — der hier als Karte erscheint. **Das Gate bleibt
dein Klick.** dein Klick.**
**Die Queue füttert sich auch selbst — Idle-Radar (S4 „Zündung", 12.07.2026):** ein
nächtlicher Hermes-Cron (03:45, `deploy/idle-radar-feed.sh`) destilliert aus den
Journal-Fehlermustern der letzten 24 h und der jüngsten Traum-Notiz bis zu **2 belegte**
Wartungs-Kandidaten und legt sie als rohe Ideen (`triage`, `created_by=idle-radar`) auf
dasselbe Board. Leitplanken: jedes Beleg-Zitat wird **mechanisch** gegen das Material
geprüft (erfundene Belege fliegen raus — reproduziert-oder-abgelehnt), **Stau-Bremse**
(ab 4 offenen Queue-Aufgaben oder 3 offenen Karten legt er nichts nach), dreifacher
Dedup (State-Datei `~/.hermes/state/idle-radar-gemeldet.txt`, Board-Abgleich inklusive
archivierter = verworfener Ideen, `--idempotency-key`), Security/Config/Updates tabu.
Der Radar legt nur Ideen an — gebaut wird über denselben Kreislauf, dein Klick bleibt
das Gate.
Die Queue-Sicht im Tab (Status, offene Ideen) kommt aus `hermes kanban list --json` Die Queue-Sicht im Tab (Status, offene Ideen) kommt aus `hermes kanban list --json`
(`backend/services/ideen.py`); die Morgenlage bekommt `kanban stats` als (`backend/services/ideen.py`); die Morgenlage bekommt `kanban stats` als
Beweismaterial. Entscheid nativ-vs-Eigenbau (Hermes-first): Probe 10.07.2026 — Beweismaterial. Entscheid nativ-vs-Eigenbau (Hermes-first): Probe 10.07.2026 —
@@ -83,6 +95,28 @@ nur Branches ohne vorherige Fehl-Läufe. Fangnetz: Nacht-Sicherung 03:30 liegt d
jede Annahme steht in Chronik + Telegram, die Morgenlage 04:30 berichtet. Die Klasse jede Annahme steht in Chronik + Telegram, die Morgenlage 04:30 berichtet. Die Klasse
erweitert nur der Commander, nie das Skript. erweitert nur der Commander, nie das Skript.
## „Lucy kennt sich" (12.07.2026): Selbstbild, Empfehlungs-Stempel, Lern-Gedächtnis
- **Selbst-Inventur (Cron 03:35, `deploy/selbst-inventur.sh`):** erhebt den
**Selbst-Steckbrief** der Box live aus der Realität (Versionen, Dienste, Modelle,
Crons, Queue, Karten → `~/.hermes/state/selbst-steckbrief.md`) und **bemerkt selbst,
wenn sich etwas ändert** (Diff gegen gestern → stille Chronik-Karte + kurze
Telegram-Zeile; keine Änderung = still). Der Steckbrief ist das „was es schon
gibt"-Gedächtnis: Idle-Radar-Analyst und Werkstatt (Realitäts-Check-Regel) lesen ihn,
bevor sie vorschlagen oder bauen.
- **Karten-Gutachter (Cron 04:00, `deploy/karten-gutachter.sh`):** stempelt jede neue
Karte mit **„Empfehlung: Annehmen/Ablehnen/Unklar" + einem ehrlichen Satz** —
geprüft werden Redundanz (existiert das laut Steckbrief schon?), Risiko und Nutzen.
Der Stempel ist eine Meinung, kein Gate; ein nachgeschobener Commit macht ihn
ungültig (wird neu gestempelt). Ablage `/srv/models/mc2-karten-gutachten.json`.
- **Ablehnen mit Grund:** Beim Ablehnen einer Karte kannst du der Box in einem Satz
sagen, *warum* („gibt es schon", „will ich nicht"). Der Grund landet in
`/srv/models/mc2-ablehnungen.jsonl` und wird Radar/Analysten als „NIE wieder
vorschlagen"-Material vorgelegt — dein Klick wird damit zum Lehrer, nicht nur zum Tor.
- **Radar-Treffer → Queue:** Meldet der wöchentliche Release-Radar einen echten Treffer,
legt er zusätzlich EINE rohe Idee in die Ideen-Queue („Probe bauen?", idempotent je
Release-Tag) — der Blick nach draußen füttert damit denselben Kreislauf.
## Leitplanken ## Leitplanken
- Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses - Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses
+19 -2
View File
@@ -6,6 +6,8 @@ keine zweite Liste anlegen. Verdikte werden NICHT als „offen" geführt → [VE
## Termine (auch als Box-Reminder hinterlegt) ## Termine (auch als Box-Reminder hinterlegt)
- **Mo 13.07., 05:15** — erster echter Release-Radar-Cron-Lauf → Morgenlage/Chronik prüfen. - **Mo 13.07., 05:15** — erster echter Release-Radar-Cron-Lauf → Morgenlage/Chronik prüfen.
- **täglich 03:45 (ab Registrierung)** — Idle-Radar legt max. 2 belegte Wartungs-Ideen in
die Queue; erste Läufe in der Morgenlage prüfen.
- **20.25.07. — ROCm-Recheck** (Anlass: Ryzen-AI-Halo-Launch 06.07., ROCm 7.13 Preview). - **20.25.07. — ROCm-Recheck** (Anlass: Ryzen-AI-Halo-Launch 06.07., ROCm 7.13 Preview).
Fragen: neue Halo-Community-Benches? llama.cpp-Issue #21284 (gfx1151-Prefill) gemerged? Fragen: neue Halo-Community-Benches? llama.cpp-Issue #21284 (gfx1151-Prefill) gemerged?
kyuz0-Grid aktualisiert (kyuz0.github.io/amd-strix-halo-toolboxes)? Falls ROCm dann kyuz0-Grid aktualisiert (kyuz0.github.io/amd-strix-halo-toolboxes)? Falls ROCm dann
@@ -29,8 +31,23 @@ keine zweite Liste anlegen. Verdikte werden NICHT als „offen" geführt → [VE
f-String-`\"` auf Python 3.14 → Poll blind; Abschluss manuell nachgezogen, Build war grün) — f-String-`\"` auf Python 3.14 → Poll blind; Abschluss manuell nachgezogen, Build war grün) —
Fixes + Executor-Fensterblitz-Fix in Karte `wartung/lucy-annahme-fixes`. Details FALLEN.md. Fixes + Executor-Fensterblitz-Fix in Karte `wartung/lucy-annahme-fixes`. Details FALLEN.md.
**Der nächste Lucy-Auftrag ist der ehrliche Voll-Automatik-Test.** **Der nächste Lucy-Auftrag ist der ehrliche Voll-Automatik-Test.**
- **S4 „leg los zuendung":** Idle-Radar / Fehler→Auftrag (Traum-Funde + Journal-Muster → - **S4 „leg los zuendung" ✅ gebaut (12.07.):** Idle-Radar (`deploy/idle-radar-feed.sh`,
Queue-Kandidaten) · Großbau-Etappen-Regeln in Skills · Generalprobe ohne Claude. Hermes-Cron nächtlich 03:45): Journal-Fehlermuster + Traum-Funde → max. 2 belegte rohe
Ideen/Nacht ins native Kanban (Ehrlichkeits-Gate prüft Beleg-Zitate mechanisch,
Stau-Bremse bei voller Queue/vollem Auftragsbuch, dreifacher Dedup inkl. archivierter
Ideen) · Großbau-Etappen-Regeln in werkstatt-SOUL + wartung-/orchestrator-Skill (jede
Etappe für sich lauffähig + annehmbar, NIE auf unangenommene Branches bauen,
Folge-Etappen als neue Queue-Aufgaben). **Generalprobe ohne Claude BESTANDEN 12.07.
~19:0019:30:** Radar legte 2 echte Ideen, Specifier zerlegte (deutsch), Werkstatt
lieferte Karte `wartung/hermes-gateway-restart` — deren Inhalt war redundant
(Restart längst konfiguriert) → daraus entstand noch am selben Tag das
**„Lucy kennt sich"-Paket** (User-Auftrag): Selbst-Inventur-Cron 03:35 (Steckbrief
auto-generiert + Änderungs-Erkennung), Karten-Gutachter-Cron 04:00 (Empfehlungs-
Stempel auf jeder Karte), Ablehnen-mit-Grund (Lern-Gedächtnis
`/srv/models/mc2-ablehnungen.jsonl`), Realitäts-Check-Regel (werkstatt-SOUL 7 +
wartung-Skill), Release-Radar-Treffer → Queue-Idee. **Offen: erste Scharf-Nächte in
der Morgenlage beobachten (03:35 Inventur → 03:45 Idle-Radar → 04:00 Gutachter →
04:10 Bagatell → 04:30 Morgenlage → Mo 05:15 Release-Radar).**
## Lucy ## Lucy
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-ZY9WIA27.js"></script> <script type="module" crossorigin src="/assets/index-D-87Dv5s.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Be3zHdDf.css"> <link rel="stylesheet" crossorigin href="/assets/index-Cq3xDDcK.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+2
View File
@@ -462,6 +462,8 @@ export interface AuftragItem {
files_truncated: boolean files_truncated: boolean
frontend_ohne_build: boolean // Frontend-Quelltext ohne dist → Box kann nicht bauen frontend_ohne_build: boolean // Frontend-Quelltext ohne dist → Box kann nicht bauen
status: AuftragStatus | null status: AuftragStatus | null
// Empfehlungs-Stempel des nächtlichen Karten-Gutachters (Meinung, kein Gate).
empfehlung: { empfehlung: "ANNEHMEN" | "ABLEHNEN" | "UNKLAR"; satz: string; richter: string } | null
} }
export interface SkillKandidat { export interface SkillKandidat {
file: string file: string
+47 -4
View File
@@ -283,6 +283,10 @@ function ProposalCard({
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [diff, setDiff] = useState<string | null>(null) const [diff, setDiff] = useState<string | null>(null)
const [diffLoading, setDiffLoading] = useState(false) const [diffLoading, setDiffLoading] = useState(false)
// Ablehnen mit Grund: der Grund ist das Lern-Gedächtnis des Kreislaufs (Radar/Specifier
// bekommen ihn als „nie wieder vorschlagen"-Material) — deshalb ein kleines Inline-Feld.
const [rejecting, setRejecting] = useState(false)
const [grund, setGrund] = useState("")
const running = item.status?.state === "laeuft" || item.status?.state === "rollback" const running = item.status?.state === "laeuft" || item.status?.state === "rollback"
const decided = item.status?.state === "eingespielt" const decided = item.status?.state === "eingespielt"
// Lucy-Karten (Desktop-App): Annehmen baut am PC (dist) statt auf der Box zu deployen. // Lucy-Karten (Desktop-App): Annehmen baut am PC (dist) statt auf der Box zu deployen.
@@ -326,6 +330,16 @@ function ProposalCard({
</div> </div>
<p className="mt-1.5 text-sm font-semibold text-foreground">{item.subject || "(ohne Titel)"}</p> <p className="mt-1.5 text-sm font-semibold text-foreground">{item.subject || "(ohne Titel)"}</p>
{item.body && <p className="mt-1 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground line-clamp-6">{item.body}</p>} {item.body && <p className="mt-1 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground line-clamp-6">{item.body}</p>}
{item.empfehlung && (
<p className={cn("mt-1.5 flex items-start gap-1.5 rounded-lg border px-2 py-1 text-[11px]",
item.empfehlung.empfehlung === "ANNEHMEN" && "border-emerald-500/30 bg-emerald-500/5 text-emerald-300",
item.empfehlung.empfehlung === "ABLEHNEN" && "border-red-500/30 bg-red-500/5 text-red-300",
item.empfehlung.empfehlung === "UNKLAR" && "border-amber-500/30 bg-amber-500/5 text-amber-300")}
title={`Nächtliches Kurz-Gutachten (${item.empfehlung.richter}) — eine Meinung, kein Muss. Die Entscheidung bleibt bei dir.`}>
<Sparkles className="mt-0.5 h-3 w-3 shrink-0" />
<span><b>Empfehlung: {item.empfehlung.empfehlung === "ANNEHMEN" ? "Annehmen" : item.empfehlung.empfehlung === "ABLEHNEN" ? "Ablehnen" : "Unklar"}.</b> {item.empfehlung.satz}</span>
</p>
)}
</div> </div>
{!running && !decided && ( {!running && !decided && (
@@ -343,10 +357,7 @@ function ProposalCard({
</button> </button>
<button <button
disabled={!!busy["ab:" + k]} disabled={!!busy["ab:" + k]}
onClick={() => onClick={() => setRejecting((v) => !v)}
showConfirm("Vorschlag ablehnen?",
`Der Branch '${item.branch}' wird auf Gitea gelöscht. Die Arbeit bleibt in der Historie, aber die Karte verschwindet.`,
() => onAct("ab:" + k, "/api/auftragsbuch/ablehnen", { branch: item.branch, repo }))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50"> className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50">
{busy["ab:" + k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Ablehnen {busy["ab:" + k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Ablehnen
</button> </button>
@@ -354,6 +365,38 @@ function ProposalCard({
)} )}
</div> </div>
{rejecting && !running && !decided && (
<div className="space-y-2 rounded-xl border border-red-500/25 bg-red-500/5 p-3">
<p className="text-[11px] text-muted-foreground">
Der Branch '{item.branch}' wird auf Gitea gelöscht (die Arbeit bleibt in der Historie).
Sag der Box kurz <b>warum</b> dann schlägt sie dir so etwas nicht wieder vor.
</p>
<input
value={grund}
onChange={(e) => setGrund(e.target.value)}
placeholder={"Grund (optional) — z. B. „gibt es schon“ oder „will ich nicht“"}
maxLength={400}
className="w-full rounded-lg border border-border/60 bg-background/60 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
/>
<div className="flex items-center gap-2">
<button
disabled={!!busy["ab:" + k]}
onClick={() => {
setRejecting(false)
onAct("ab:" + k, "/api/auftragsbuch/ablehnen", { branch: item.branch, repo, grund: grund.trim() })
}}
className="flex h-8 items-center gap-1.5 rounded-lg border border-red-500/40 bg-red-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300 transition-all hover:bg-red-500/20 cursor-pointer disabled:opacity-50">
<X className="h-3.5 w-3.5" /> Wirklich ablehnen
</button>
<button
onClick={() => setRejecting(false)}
className="flex h-8 items-center rounded-lg border border-border/60 bg-background/40 px-3 text-[11px] font-semibold text-muted-foreground transition-all hover:text-foreground cursor-pointer">
Abbrechen
</button>
</div>
</div>
)}
{item.status && (running || !decided) && item.status.detail && ( {item.status && (running || !decided) && item.status.detail && (
<p className="text-[11px] text-muted-foreground">{item.status.detail}</p> <p className="text-[11px] text-muted-foreground">{item.status.detail}</p>
)} )}