hugin/scripts/stage0.sh
2026-10-27 08:33:21 +01:00

91 lines
5.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# Stufe 0 (→ KONZEPT.md 20): Verhalten von OpenCode prüfen, ohne Modellaufruf.
# Nutzt nur `opencode debug …` und kostet keine Tokens. Ergebnisse → STUFE0.md.
#
# scripts/stage0.sh # mit dem installierten opencode
# OPENCODE=opencode2 scripts/stage0.sh
set -euo pipefail
OC="${OPENCODE:-opencode}"
T="$(mktemp -d)"
trap 'rm -rf "$T"' EXIT
cd "$T" && git init -q
mkdir -p .claude/skills/s0-unbekannt .claude/skills/s0-doppelt .opencode/skills/s0-doppelt \
.claude/skills/s0-slash-ohne .claude/skills/s0-slash-false \
.claude/commands .claude/agents .opencode/commands .opencode/agents .hugin/rules
printf -- '---\nname: s0-unbekannt\ndescription: Claude-Felder\nallowed-tools: [Read]\npaths: ["**/*.go"]\neffort: high\n---\nx\n' \
> .claude/skills/s0-unbekannt/SKILL.md
printf -- '---\nname: s0-doppelt\ndescription: aus .claude/skills\n---\nA\n' > .claude/skills/s0-doppelt/SKILL.md
printf -- '---\nname: s0-doppelt\ndescription: aus .opencode/skills\n---\nB\n' > .opencode/skills/s0-doppelt/SKILL.md
printf -- '---\nname: s0-slash-ohne\ndescription: ohne metadata\n---\nx\n' > .claude/skills/s0-slash-ohne/SKILL.md
printf -- '---\nname: s0-slash-false\ndescription: slash false\nmetadata:\n opencode/slash: "false"\n---\nx\n' \
> .claude/skills/s0-slash-false/SKILL.md
printf -- '---\ndescription: aus .claude/commands\n---\nx\n' > .claude/commands/s0-claude-befehl.md
printf -- '---\nname: s0-claude-agent\ndescription: aus .claude/agents\n---\nx\n' > .claude/agents/s0-claude-agent.md
printf -- '---\ndescription: aus .opencode/commands\n---\nTu $1\n' > .opencode/commands/s0-befehl.md
printf -- '---\ndescription: aus .opencode/agents\nmode: subagent\npermission:\n edit: deny\n bash: deny\n webfetch: deny\n---\nx\n' \
> .opencode/agents/s0-agent.md
printf '# Regel\n' > .hugin/rules/s0.md
cat > opencode.json <<'EOF'
{
// JSONC: Kommentar und Komma am Ende
"instructions": [".hugin/rules/*.md"],
"mcp": {
"s0": { "type": "remote", "url": "http://127.0.0.1:9/mcp", "enabled": true,
"headers": { "Authorization": "Bearer {env:S0_KEY}" } }
},
}
EOF
# Über eine Pipe schneidet opencode lange Ausgaben ab; deshalb erst in Dateien
json() { python3 -c 'import json,sys; r=open(sys.argv[1]).read(); print(json.dumps(json.loads(r[min(i for i in (r.find("{"), r.find("[")) if i >= 0):])))' "$1"; }
echo "OpenCode: $($OC --version)"
$OC debug skill --pure >"$T/.skills" 2>/dev/null
S0_KEY=geheim $OC debug config --pure >"$T/.config" 2>/dev/null
SKILLS=$(json "$T/.skills")
CONFIG=$(json "$T/.config")
python3 - "$SKILLS" "$CONFIG" <<'EOF'
import json, sys
skills = [s for s in json.loads(sys.argv[1]) if s["name"].startswith("s0-")]
config = json.loads(sys.argv[2])
names = [s["name"] for s in skills]
def report(question, ok, text): print(f"{'✓' if ok else '✗'} {question}: {text}")
report("1 unbekannte Felder", "s0-unbekannt" in names, "Skill geladen" if "s0-unbekannt" in names else "Skill fehlt")
duplicates = [s for s in skills if s["name"] == "s0-doppelt"]
report("2 gleicher Name in .claude und .opencode", len(duplicates) == 1,
f"{len(duplicates)} Fassung(en): " + ", ".join(s["description"] for s in duplicates))
commands = list((config.get("command") or {}))
agents = list((config.get("agent") or {}))
report("3 liest .claude/commands nicht", "s0-claude-befehl" not in commands, f"Befehle: {commands}")
report("3 liest .claude/agents nicht", "s0-claude-agent" not in agents, f"Agenten: {[a for a in agents if a.startswith('s0')]}")
report("6 JSONC erlaubt", config.get("instructions") == [".hugin/rules/*.md"], f"instructions: {config.get('instructions')}")
mcp = (config.get("mcp") or {}).get("s0") or (config.get("mcp") or {}).get("servers", {}).get("s0")
# Ab 1.19 zeigt debug config Header nur maskiert; dann belegt der Rauchtest {env:VAR}
masked = bool(mcp) and "***" in json.dumps(mcp)
report("10 MCP alte Form, {env:VAR}", bool(mcp) and ("geheim" in json.dumps(mcp) or masked),
json.dumps(mcp) + (" (Header maskiert, {env:VAR} prüft tool-smoke-test.sh)" if masked else ""))
EOF
# Slash-Befehle: dieselbe Liste, die das TUI bei „/“ anbietet, liefert der Server unter /command
PORT="${PORT:-47123}"
$OC serve --pure --port "$PORT" >"$T/.serve" 2>&1 &
SERVE=$!
trap 'kill $SERVE 2>/dev/null; wait $SERVE 2>/dev/null; rm -rf "$T"' EXIT
for _ in $(seq 60); do curl -fsS "http://127.0.0.1:$PORT/command" >"$T/.commands" 2>/dev/null && break; sleep 0.25; done
python3 - "$T/.commands" <<'EOF2'
import json, sys
try:
commands = {c["name"]: c.get("source") for c in json.load(open(sys.argv[1]))}
except Exception as e:
sys.exit(f"✗ 8 Slash-Befehle: /command nicht lesbar ({e})")
def report(question, ok, text): print(f"{'✓' if ok else '✗'} {question}: {text}")
report("8 Skill ohne metadata ist Slash-Befehl", "s0-slash-ohne" in commands, commands.get("s0-slash-ohne", "fehlt"))
# Erwartet (1.19.3): opencode/slash wirkt nicht, auch „false“ blendet nicht aus
report("8 opencode/slash: \"false\" wirkt nicht", "s0-slash-false" in commands,
"weiter in der Liste" if "s0-slash-false" in commands else "ausgeblendet: opencode/slash wirkt")
EOF2
echo
echo "Mit Modellaufruf in scripts/tool-smoke-test.sh: 4, 5, 8 (benannte Argumente), opencode/autoinvoke"