#!/usr/bin/env bash # Rauchtest mit den echten Werkzeugen (→ KONZEPT.md 17.2): Server starten, # Testpakete mit Erkennungswörtern veröffentlichen, per `hugin sync` in ein # leeres Repo installieren und Claude Code und OpenCode fragen. Dazu die # Verhaltensfragen 4, 5 und 8 aus Stufe 0 sowie opencode/autoinvoke (→ STUFE0.md). # # Dazu: Installiert ein Agent ein Paket auf Bitte richtig, also über .hugin.yaml und # hugin sync, wie es der MCP-Endpunkt von Hugin vorgibt, statt Dateien selbst zu schreiben? # Und bricht er ab, statt Dateien selbst zu schreiben, wenn hugin nicht im PATH ist? # # Braucht Zugänge zu beiden Werkzeugen und kostet Tokens (rund 30 Modellaufrufe). # Ergebnis: COMPATIBILITY.md im Wurzelordner. # # scripts/tool-smoke-test.sh # TOOLS=opencode scripts/tool-smoke-test.sh # nur ein Werkzeug # CLAUDE_MODEL=sonnet OPENCODE_MODEL=anthropic/claude-haiku-5-5 scripts/tool-smoke-test.sh # OPENCODE=opencode2 OUT=/tmp/c.md scripts/tool-smoke-test.sh # KEEP=1 scripts/tool-smoke-test.sh # Arbeitsordner und Logs bleiben set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" WORK="$(mktemp -d)" PORT="${PORT:-8766}" URL="http://127.0.0.1:$PORT" OUT="${OUT:-$ROOT/COMPATIBILITY.md}" TOOLS="${TOOLS:-claude opencode}" OC="${OPENCODE:-opencode}" CLAUDE_MODEL="${CLAUDE_MODEL:-haiku}" OPENCODE_MODEL="${OPENCODE_MODEL:-}" TIMEOUT="${TIMEOUT:-180}" # Sekunden je Aufruf TRIES="${TRIES:-2}" # Modelle antworten nicht immer gleich export HUGIN_DATABASE_URL="sqlite:///$WORK/hugin.db" export HUGIN_ADMIN_TOKEN="tool-smoke-admin" ADMIN=(-H "Authorization: Bearer $HUGIN_ADMIN_TOKEN") RESULTS="$WORK/results.tsv" mkdir -p "$WORK/logs" && : > "$RESULTS" cleanup() { [[ -n "${SERVER:-}" ]] && kill "$SERVER" 2>/dev/null || true if [[ -z "${KEEP:-}" ]]; then rm -rf "$WORK"; else echo "Arbeitsordner: $WORK"; fi } trap cleanup EXIT uses() { [[ " $TOOLS " == *" $1 "* ]]; } uses claude && ! command -v claude >/dev/null && { echo "claude fehlt (oder TOOLS setzen)"; exit 2; } uses opencode && ! command -v "$OC" >/dev/null && { echo "$OC fehlt (oder TOOLS setzen)"; exit 2; } api() { curl -fsS -H "Content-Type: application/json" "$@"; } key() { python3 -c 'import json,sys; print(json.load(sys.stdin)["key"])'; } echo "== Server" (cd "$ROOT/server" && uv run -q hugin-server migrate) (cd "$ROOT/server" && exec uv run -q hugin-server run --host 127.0.0.1 --port "$PORT") >"$WORK/server.log" 2>&1 & SERVER=$! for _ in $(seq 50); do curl -fsS "$URL/readyz" >/dev/null 2>&1 && break; sleep 0.2; done curl -fsS "$URL/readyz" >/dev/null api "${ADMIN[@]}" "$URL/api/v1/admin/tenants" -d '{"slug":"plattform","name":"Plattform"}' >/dev/null api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/projects" -d '{"slug":"kern","name":"Kern"}' >/dev/null AUTHOR=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"autor","project":"kern","roles":["pub_author"]}' | key) REVIEWER=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"pruefer","project":"kern","roles":["pub_reviewer"]}' | key) READER=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"leser","project":"kern"}' | key) # Lokaler Server hat Risiko „Skripte“: persönlicher Key, zwei Freigaben von Personen # Plugin hat Risiko „Rechte“: eine der zwei Freigaben vom Tenant-Admin (Carla) person() { api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/people" -d "{\"name\":\"$1\",\"mail\":\"$1@smoke.test\",\"project\":\"kern\",\"roles\":$2}" | key; } ANNA=$(person anna '["pub_author"]'); BERT=$(person bert '["pub_reviewer"]'); DORA=$(person dora '["pub_reviewer"]') CARLA=$(person carla '["pub_reviewer","tenant_admin"]') # MCP-Paket zeigt auf diesen Hugin selbst; so lässt sich auch die Verbindung prüfen PACKAGES="$WORK/packages" cp -R "$ROOT/scripts/tool-smoke/packages" "$PACKAGES" mkdir -p "$PACKAGES/pub-smoke-mcp/1.0.0" cat > "$PACKAGES/pub-smoke-mcp/1.0.0/hugin.yaml" </dev/null api -H "Authorization: Bearer $AUTHOR" -X POST "$base/submit" >/dev/null api -H "Authorization: Bearer $REVIEWER" "$base/approve" -d '{"comment":"Rauchtest"}' >/dev/null } # publish_example : Beispielpaket aus contract/ mit Annas persönlichem Key publish_example() { local zip="$WORK/package.zip" id=$1 base="$URL/api/v1/packages/$1/versions/1.0.0"; shift rm -f "$zip"; (cd "$ROOT/contract/examples/packages/$id/1.0.0" && zip -qr "$zip" .) curl -fsS -H "Authorization: Bearer $ANNA" -H "Content-Type: application/zip" --data-binary "@$zip" "$URL/api/v1/drafts" >/dev/null api -H "Authorization: Bearer $ANNA" -X POST "$base/submit" >/dev/null for r in "$@"; do api -H "Authorization: Bearer $r" "$base/approve" -d '{"comment":"Rauchtest"}' >/dev/null; done } echo "== Pakete und Repo" IDS=(pub-smoke-skill pub-smoke-manual pub-smoke-rules pub-smoke-command pub-smoke-agent pub-smoke-mcp) for id in "${IDS[@]}"; do publish "$id"; done # Lokaler MCP-Server (Python, nur Standardbibliothek): hugin sync installiert ihn nach # HUGIN_MCP_HOME, die Werkzeuge starten ihn über hugin mcp run publish_example pub-zeit-lokal "$BERT" "$DORA"; IDS+=(pub-zeit-lokal) # OpenCode-Plugin mit eigenem Werkzeug; Claude Code bekommt davon nichts publish_example pub-specht-plugin "$BERT" "$CARLA"; IDS+=(pub-specht-plugin) export HUGIN_MCP_HOME="$WORK/mcp" ZEIT_ZONE_HINWEIS=smoke (cd "$ROOT/cli" && go build -o "$WORK/bin/hugin" ./cmd/hugin) ln -s "$WORK/bin/hugin" "$WORK/hugin" # PATH ohne hugin (für „Ohne hugin nichts schreiben“): alle Ordner weglassen, in denen eines liegt NO_HUGIN_PATH=$(IFS=:; for d in $PATH; do [[ -x "$d/hugin" ]] || printf '%s:' "$d"; done); NO_HUGIN_PATH=${NO_HUGIN_PATH%:} export PATH="$WORK/bin:$PATH" # für Agenten, die hugin sync selbst aufrufen REPO="$WORK/repo"; mkdir -p "$REPO"; cd "$REPO"; git init -q printf '# Rauchtest\n' > CLAUDE.md export HUGIN_KEY="$READER" HUGIN_SMOKE_KEY="$READER" "$WORK/hugin" init --server "$URL" --project kern >/dev/null # Werkzeuge fest eintragen: im leeren Repo erkennt `auto` nur Claude (wegen CLAUDE.md) sed -i.alt -e "s/^packages: \[\]/packages: [$(IFS=,; echo "${IDS[*]}")]/" \ -e "s/^tools: auto/tools: [$(echo $TOOLS | sed 's/ /, /g')]/" .hugin.yaml && rm -f .hugin.yaml.alt "$WORK/hugin" sync # Ausführen mit Zeitlimit; Modellaufrufe ohne Lesewerkzeuge, damit die Antwort # aus dem geladenen Kontext kommt und nicht aus den Dateien im Repo limit() { perl -e 'alarm shift; exec @ARGV' "$TIMEOUT" "$@" "$log" 2>&1 && grep -q "BEREIT" "$log" && return 0 echo " ! $1 kann kein Modell aufrufen, Modellprüfungen übersprungen:" grep -m1 -i "error" "$log" | sed 's/\x1b\[[0-9;]*m//g; s/^/ /' || tail -n1 "$log" return 1 } # skip : nicht geprüft skip() { printf ' – %-8s %s\n' "$2" "$1"; printf 'smoke\t%s\t%s\tskip\t\n' "$1" "$2" >>"$RESULTS"; } # check : ok, wenn die Ausgabe passt check() { local area=$1 name=$2 tool=$3 pattern=$4; shift 4 local log; log="$WORK/logs/$(printf '%s-%s' "$tool" "$name" | tr -c 'A-Za-z0-9-' '_').log" local status=fail for _ in $(seq "$TRIES"); do "$@" >"$log" 2>&1 || true if grep -Eqi -- "$pattern" "$log"; then status=ok; break; fi done printf ' %s %-8s %s\n' "$([[ $status == ok ]] && echo ✓ || echo ✗)" "$tool" "$name" printf '%s\t%s\t%s\t%s\t%s\n' "$area" "$name" "$tool" "$status" "$(tr '\n\t' ' ' <"$log" | cut -c1-200)" >>"$RESULTS" } # observe : welche der Wörter in der Antwort stehen observe() { local area=$1 name=$2 tool=$3 words=$4; shift 4 local log; log="$WORK/logs/$(printf '%s-%s' "$tool" "$name" | tr -c 'A-Za-z0-9-' '_').log" "$@" >"$log" 2>&1 || true local found="" w for w in $words; do grep -q -- "$w" "$log" && found+="$w "; done printf ' · %-8s %s: %s\n' "$tool" "$name" "${found:-keins}" printf '%s\t%s\t%s\t%s\t%s\n' "$area" "$name" "$tool" "${found% }" "$(tr '\n\t' ' ' <"$log" | cut -c1-200)" >>"$RESULTS" } SKILL_Q="Wie lautet das Skill-Erkennungswort? Antworte nur mit dem Wort." RULES_Q="Wie lautet das Regel-Erkennungswort? Antworte nur mit dem Wort." MANUAL_Q="Wie lautet das Handbuch-Erkennungswort? Antworte nur mit dem Wort, oder mit UNBEKANNT, wenn du es nicht weißt." MCP_Q="Rufe das Werkzeug search_packages des MCP-Servers hugin-smoke mit der Suche „smoke“ auf und nenne nur die gefundenen Paket-IDs." MCP_EXPECT="pub-smoke-(skill|rules|command|agent|mcp)" MODEL_CHECKS=("Skill wird gefunden" "Skill wird geladen" "Regeln wirken" "Befehl mit zwei Argumenten" "Subagent vorhanden" "Subagent wirkt" "MCP verbunden" "Lokaler Server antwortet" "Installieren auf Bitte" "Ohne hugin nichts schreiben") LOCAL_Q="Rufe das Werkzeug zeit des MCP-Servers zeit auf und gib seine Antwort wörtlich wieder." PLUGIN_Q="Rufe das Werkzeug specht_marke auf und gib nur sein Ergebnis wieder." AGENT_Q="Starte den Subagenten pub-smoke-agent mit der Aufgabe „Hallo“ und gib seine Antwort wörtlich wieder." # Installieren auf Bitte: frisches Repo, in dem nur die MCP-Anbindung an Hugin steht. # Die Bitte sagt nicht, wie; den Weg (.hugin.yaml + hugin sync) muss der Agent aus den # Anweisungen des MCP-Endpunkts bzw. dem Feld install von get_package kennen. INSTALL_Q="Ich möchte den Befehl pub-smoke-command aus Hugin dauerhaft in diesem Repo nutzen. Richte das bitte ein." # install_try : ERGEBNIS=INSTALLIERT, wenn hugin sync das Paket geschrieben hat install_try() { local tool=$1 file=$2; shift 2 local repo="$WORK/install-$tool" rm -rf "$repo" && mkdir -p "$repo" && cd "$repo" && git init -q hugin init --server "$URL" --project kern >/dev/null sed -i.alt -e "s/^packages: \[\]/packages: [pub-smoke-mcp]/" -e "s/^tools: auto/tools: [$tool]/" .hugin.yaml && rm -f .hugin.yaml.alt hugin sync >/dev/null if [[ -n "${WITHOUT_HUGIN:-}" ]]; then PATH="$NO_HUGIN_PATH" "$@" || true; else "$@" || true; fi echo "--- .hugin.yaml"; cat .hugin.yaml if grep -q "pub-smoke-command" hugin.lock && [[ -f "$file" ]]; then echo "ERGEBNIS=INSTALLIERT" elif [[ -f "$file" ]]; then echo "ERGEBNIS=SELBST_GESCHRIEBEN (Datei da, aber nicht in hugin.lock)" else echo "ERGEBNIS=NICHT_INSTALLIERT" fi cd "$REPO" } # Ohne hugin im PATH: bestanden, wenn der Agent nichts selbst geschrieben hat without_hugin() { WITHOUT_HUGIN=1 install_try "$@"; } claude_install() { limit "$CLAUDE_BIN" -p "$INSTALL_Q" --model "$CLAUDE_MODEL" --settings '{"enableAllProjectMcpServers":true}' \ --allowedTools "Read,Edit,Write,Bash(hugin *),mcp__hugin-smoke__search_packages,mcp__hugin-smoke__get_package" \ --disallowedTools "WebFetch,WebSearch" } OC_INSTALL='{"permission":{"read":"allow","edit":"allow","bash":"allow","webfetch":"deny","websearch":"deny"}}' OC_BIN=$(command -v "$OC" || true); CLAUDE_BIN=$(command -v claude || true) # fest, weil der PATH wechselt opencode_install() { OPENCODE_CONFIG_CONTENT="$OC_INSTALL" limit "$OC_BIN" run --pure ${OPENCODE_MODEL:+-m "$OPENCODE_MODEL"} "$INSTALL_Q"; } if uses claude; then echo "== Claude Code $(claude --version | cut -d' ' -f1)" check smoke "MCP eingetragen" claude "hugin-smoke" limit claude mcp get hugin-smoke # Projektserver stehen bis zur Zustimmung auf „Pending approval“; die Verbindung zeigt „Lokaler Server antwortet“ check smoke "Lokaler Server eingetragen" claude "Args: mcp run pub-zeit-lokal" limit claude mcp get zeit if model_ok claude claude_ask "Antworte nur mit BEREIT."; then check smoke "Skill wird gefunden" claude "pub-smoke-skill" \ claude_ask "Welche Skills stehen dir zur Verfügung? Nenne nur die Namen, einen pro Zeile." check smoke "Skill wird geladen" claude "KRAKE-7381" claude_ask "$SKILL_Q" check smoke "Regeln wirken" claude "MOEWE-4412" claude_ask "$RULES_Q" check smoke "Befehl mit zwei Argumenten" claude "ERSTES=rot ZWEITES=fuchs" claude_ask "/pub-smoke-command rot fuchs" check smoke "Subagent vorhanden" claude "pub-smoke-agent" \ claude_ask "Welche Subagent-Typen kannst du mit dem Agent-Werkzeug starten? Nenne nur die Namen, einen pro Zeile." check smoke "Subagent wirkt" claude "FALKE-9035" claude_ask "$AGENT_Q" check smoke "MCP verbunden" claude "$MCP_EXPECT" claude_ask "$MCP_Q" check smoke "Lokaler Server antwortet" claude "UHR-4711" claude_ask "$LOCAL_Q" check smoke "Installieren auf Bitte" claude "ERGEBNIS=INSTALLIERT" \ install_try claude .claude/commands/pub-smoke-command.md claude_install check smoke "Ohne hugin nichts schreiben" claude "ERGEBNIS=(NICHT_INSTALLIERT)" \ without_hugin claude .claude/commands/pub-smoke-command.md claude_install # Skill nur von Hand (disable-model-invocation): Modell darf ihn nicht selbst laden observe stage0 manual-auto claude "IGEL-8140" claude_ask "$MANUAL_Q" observe stage0 manual-slash claude "IGEL-8140" claude_ask "/pub-smoke-manual" else for c in "${MODEL_CHECKS[@]}"; do skip "$c" claude; done fi fi if uses opencode; then echo "== OpenCode $("$OC" --version)" check smoke "MCP eingetragen" opencode "hugin-smoke" limit "$OC" mcp list --pure check smoke "Skill wird gefunden" opencode "pub-smoke-skill" limit "$OC" debug skill --pure check smoke "Subagent vorhanden" opencode "pub-smoke-agent" limit "$OC" agent list --pure check smoke "MCP verbunden" opencode "hugin-smoke.*connected" limit "$OC" mcp list --pure check smoke "Lokaler Server verbunden" opencode "zeit.*connected" limit "$OC" mcp list --pure check smoke "Plugin geladen" opencode "plugins/pub-specht-plugin.ts" limit "$OC" debug config if model_ok opencode opencode_run "Antworte nur mit BEREIT."; then check smoke "Skill wird geladen" opencode "KRAKE-7381" opencode_run "$SKILL_Q" check smoke "Regeln wirken" opencode "MOEWE-4412" opencode_run "$RULES_Q" check smoke "Befehl mit zwei Argumenten" opencode "ERSTES=rot ZWEITES=fuchs" opencode_run --command pub-smoke-command rot fuchs check smoke "Subagent wirkt" opencode "FALKE-9035" opencode_run "$AGENT_Q" check smoke "Lokaler Server antwortet" opencode "UHR-4711" opencode_run "$LOCAL_Q" check smoke "Plugin wirkt" opencode "SPECHT-5127" opencode_plugins "$PLUGIN_Q" check smoke "Installieren auf Bitte" opencode "ERGEBNIS=INSTALLIERT" \ install_try opencode .opencode/commands/pub-smoke-command.md opencode_install check smoke "Ohne hugin nichts schreiben" opencode "ERGEBNIS=(NICHT_INSTALLIERT)" \ without_hugin opencode .opencode/commands/pub-smoke-command.md opencode_install # Skill nur von Hand (opencode/autoinvoke: "false") observe stage0 manual-auto opencode "IGEL-8140" opencode_run "$MANUAL_Q" observe stage0 manual-slash opencode "IGEL-8140" opencode_run --command pub-smoke-manual OPENCODE_MODEL_OK=1 else for c in "Skill wird geladen" "Regeln wirken" "Befehl mit zwei Argumenten" "Subagent wirkt" "Lokaler Server antwortet" "Plugin wirkt" "Installieren auf Bitte" "Ohne hugin nichts schreiben"; do skip "$c" opencode; done fi fi # Stufe 0: je ein eigenes Repo ohne Hugin, nur mit den Dateien, um die es geht WORDS_Q="Nenne alle Erkennungswörter (Form WORT-1234), die in deinen Anweisungen aus CLAUDE.md, AGENTS.md oder eingebundenen Dateien stehen. Nur die Wörter, nichts erfinden." echo "== Stufe 0" if uses opencode && [[ -n "${OPENCODE_MODEL_OK:-}" ]]; then Q4="$WORK/q4"; mkdir -p "$Q4/.hugin/rules"; (cd "$Q4" && git init -q) printf '# Projekt\n\nErkennungswort: DACHS-2207\n\n@.hugin/rules/q4.md\n' > "$Q4/CLAUDE.md" printf 'Erkennungswort: OTTER-5590\n' > "$Q4/.hugin/rules/q4.md" (cd "$Q4" && observe stage0 4 opencode "DACHS-2207 OTTER-5590" opencode_run "$WORDS_Q") fi if uses claude; then Q5="$WORK/q5"; mkdir -p "$Q5"; (cd "$Q5" && git init -q) printf '# Projekt\n\nErkennungswort: LUCHS-3318\n' > "$Q5/CLAUDE.md" printf '# Projekt\n\nErkennungswort: ELCH-6624\n' > "$Q5/AGENTS.md" (cd "$Q5" && observe stage0 5 claude "LUCHS-3318 ELCH-6624" claude_ask "$WORDS_Q") fi python3 - "$RESULTS" "$OUT" "$TOOLS" "$(uses claude && claude --version | cut -d' ' -f1)" \ "$(uses opencode && "$OC" --version)" "$CLAUDE_MODEL" "${OPENCODE_MODEL:-Standard}" <<'EOF' import sys, datetime results, out, tools, claude_v, oc_v, claude_m, oc_m = sys.argv[1:] rows = [line.rstrip("\n").split("\t") for line in open(results) if line.strip()] names = {"claude": f"Claude Code {claude_v}", "opencode": f"OpenCode {oc_v}"} used = [t for t in ("claude", "opencode") if t in tools.split()] smoke = {(r[1], r[2]): r[3] for r in rows if r[0] == "smoke"} order = ["Skill wird gefunden", "Skill wird geladen", "Regeln wirken", "Befehl mit zwei Argumenten", "Subagent vorhanden", "Subagent wirkt", "MCP eingetragen", "MCP verbunden", "Lokaler Server eingetragen", "Lokaler Server verbunden", "Lokaler Server antwortet", "Plugin geladen", "Plugin wirkt", "Installieren auf Bitte", "Ohne hugin nichts schreiben"] checks = [c for c in order if any(k[0] == c for k in smoke)] package = {"Skill": "pub-smoke-skill", "Regeln": "pub-smoke-rules", "Befehl": "pub-smoke-command", "Subagent": "pub-smoke-agent", "MCP": "pub-smoke-mcp", "Lokaler": "pub-zeit-lokal", "Plugin": "pub-specht-plugin", "Installieren": "pub-smoke-command", "Ohne": "pub-smoke-command"} mark = {"ok": "✓", "fail": "✗", "skip": "–"} lines = [ "# Kompatibilität", "", f"Erzeugt von `scripts/tool-smoke-test.sh` am {datetime.date.today():%d.%m.%Y} (→ KONZEPT.md 17.2).", f"Modelle: Claude Code `{claude_m}`, OpenCode `{oc_m}`. Installiert mit `hugin sync`.", "", "## Rauchtests", "", "| Paket | Prüfung | " + " | ".join(names[t] for t in used) + " |", "|---|---|" + "---|" * len(used), ] for c in checks: pkg = next(v for k, v in package.items() if c.startswith(k)) lines.append(f"| `{pkg}` | {c} | " + " | ".join(mark.get(smoke.get((c, t), ""), "–") for t in used) + " |") meaning = { "4": ("OpenCode, nur `CLAUDE.md` mit `@`-Verweis, keine `AGENTS.md`, keine `instructions`", { "DACHS-2207 OTTER-5590": "liest `CLAUDE.md` **und** löst `@` auf: Hugin-Regeln kämen doppelt an", "DACHS-2207": "liest `CLAUDE.md`, löst `@` nicht auf: keine Doppel", "": "liest `CLAUDE.md` nicht: keine Doppel"}), "5": ("Claude Code, `CLAUDE.md` und `AGENTS.md`", { "LUCHS-3318 ELCH-6624": "liest beide", "LUCHS-3318": "liest nur `CLAUDE.md`", "": "keins erkannt, Log ansehen"}), } stage0 = [r for r in rows if r[0] == "stage0" and not r[1].startswith("manual-")] manual = {(r[1], r[2]): r[3] for r in rows if r[0] == "stage0" and r[1].startswith("manual-")} if stage0 or manual: lines += ["", "## Stufe 0", "", "| Frage | Aufbau | erkannte Wörter | Folgerung |", "|---|---|---|---|"] for _, q, tool, found, _ in stage0: setup, text = meaning[q] lines.append(f"| {q} | {setup} | {found or '–'} | {text.get(found, 'unerwartet, Log ansehen')} |") field = {"claude": "`disable-model-invocation: true`", "opencode": "`metadata: opencode/autoinvoke: \"false\"`"} for tool in used: if ("manual-auto", tool) not in manual: continue auto, slash = manual[("manual-auto", tool)], manual[("manual-slash", tool)] if not slash: text = "per `/name` nicht aufrufbar, Log ansehen" elif auto: text = "wirkt **nicht**: das Modell lädt den Skill selbst" else: text = "wirkt: Skill nur per `/name`" lines.append(f"| autoinvoke | {names[tool]}, Skill mit {field[tool]} | selbst: {auto or '–'}, per `/`: {slash or '–'} | {text} |") claude_cmd = smoke.get(("Befehl mit zwei Argumenten", "claude")) if claude_cmd in ("ok", "fail"): lines.append(f"| 8 | Claude Code {claude_v}, Befehl mit `arguments` und `$name` | – | " + ("benannte Argumente wirken" if claude_cmd == "ok" else "benannte Argumente wirken **nicht**") + " |") lines += ["", "✓ bestanden · ✗ fehlgeschlagen · – nicht geprüft (kein Modellzugang).", "Modellantworten schwanken; ein einzelnes ✗ mit `KEEP=1` wiederholen und die Logs ansehen.", ""] open(out, "w").write("\n".join(lines)) print(f"\n{out} geschrieben.") sys.exit(1 if any(v == "fail" for v in smoke.values()) else 0) EOF