Lokale Matrix liefert Ergebnisse: 119 Anforderungen aus Gemma und Qwen
Skill 13.0.0/13.1.0, Adapter 2.5.2. Spiegel-Arbeitsverzeichnis: OpenCode laeuft nicht mehr direkt im Codebasis-Root, sondern in einem Verzeichnis aus Junctions auf die Top-Level-Eintraege, mit einem echten Ergebnisverzeichnis, dessen Inhalt nach dem Lauf uebernommen wird. Damit landen relative wie absolute Ausgabepfade am richtigen Ort, ohne dass der Prompt vom Wortlaut der Claude-Laeufe abweichen muss. Der Spiegel liegt in _meta des Laufs; die Codebasis bleibt unberuehrt. Der Spiegel allein genuegte nicht. Sechs Laeufe schrieben mit korrektem absolutem Pfad und wurden dennoch abgewiesen, weil OpenCode Ziele im Repository gegen den worktree-relativen Pfad abgleicht - ein Befund, der seit Skill 10.0.2 dokumentiert war und erst sichtbar wurde, als der Spiegel vom Temp-Verzeichnis ins Projekt wanderte. analyse-anforderungen.py erkennt Kennungen als Markdown-Ueberschrift, auch ohne Feldnamen, sofern die Pflichtfelder folgen. Regressionsprobe an fuenf Claude-Laeufen unveraendert. Ergebnisse der gueltigen Laeufe: Qwen 3.5-9B liefert 107 der 119 Anforderungen, davon 75 im Modus custom mit nur drei Subagenten. Gemma kommt auf 12. Der Modus builtin fiel bei beiden Modellen aus - drei von drei Laeufen endeten nach einem Turn ohne einen Werkzeugaufruf. Die 13 Laeufe mit Adapter 2.3.0 bis 2.5.1 sind Artefakte der Fehlersuche und als adapterbedingte Fehlmessungen gekennzeichnet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
28e927013b
commit
6c5c26a2e4
@@ -27,6 +27,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -36,7 +37,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADAPTER_VERSION = "2.2.0"
|
||||
ADAPTER_VERSION = "2.5.2"
|
||||
DEFAULT_PROVIDER = "tensorx"
|
||||
PROVIDER_ID = DEFAULT_PROVIDER
|
||||
PROVIDERS: dict[str, dict] = {
|
||||
@@ -229,6 +230,114 @@ def readonly_shell_permissions() -> dict[str, str]:
|
||||
return permissions
|
||||
|
||||
|
||||
def _mklink(argument: str, verknuepfung: Path, ziel: Path) -> bool:
|
||||
"""Windows-Verknuepfung anlegen; /J = Verzeichnis-Junction, /H = Hardlink."""
|
||||
fertig = subprocess.run(
|
||||
["cmd", "/c", "mklink", argument, str(verknuepfung), str(ziel)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
return fertig.returncode == 0
|
||||
|
||||
|
||||
def erstelle_spiegel(root: Path, spiegel: Path, output_dir: Path, log) -> Path:
|
||||
"""Arbeitsverzeichnis, das wie die Codebasis aussieht, aber beschreibbar ist.
|
||||
|
||||
Der Agent loest ``Ergebnisse/StRS.md`` relativ zu seinem Arbeitsverzeichnis
|
||||
auf. Steht dort die eingefrorene Codebasis, geht jeder solche Schreibversuch
|
||||
ins Leere - gemessen an sechs Laeufen, die kein einziges Artefakt ablegten,
|
||||
obwohl sie die richtigen Dateinamen erzeugten.
|
||||
|
||||
Statt die Codebasis zu kopieren (ueber zehn Minuten fuer 24.663 Dateien)
|
||||
entsteht ein Spiegel aus Verknuepfungen: je eine Junction auf jeden
|
||||
Top-Level-Ordner, je ein Hardlink auf jede Top-Level-Datei, und ``Ergebnisse``
|
||||
als Junction auf das echte Laufverzeichnis. Der Spiegel ist damit in
|
||||
Sekunden angelegt, sieht fuer den Agenten aus wie die Codebasis, und
|
||||
``Ergebnisse/...`` landet ohne Zutun am richtigen Ort.
|
||||
|
||||
Die Codebasis selbst bleibt unberuehrt - die Verknuepfungen liegen im
|
||||
Spiegel, nicht im Snapshot. Schreibzugriffe *durch* eine Junction bleiben
|
||||
ueber die Editierrechte gesperrt; die belastbare Kontrolle ist wie bisher
|
||||
der Vorher/Nachher-Vergleich per ``git status``.
|
||||
"""
|
||||
if spiegel.exists():
|
||||
entferne_spiegel(spiegel, log)
|
||||
spiegel.mkdir(parents=True)
|
||||
|
||||
ordner = dateien = 0
|
||||
for eintrag in sorted(root.iterdir()):
|
||||
ziel = spiegel / eintrag.name
|
||||
if eintrag.is_dir():
|
||||
if _mklink("/J", ziel, eintrag):
|
||||
ordner += 1
|
||||
elif _mklink("/H", ziel, eintrag):
|
||||
dateien += 1
|
||||
else: # Hardlinks scheitern ueber Laufwerksgrenzen hinweg.
|
||||
shutil.copy2(eintrag, ziel)
|
||||
dateien += 1
|
||||
|
||||
# Bewusst ein **echtes** Verzeichnis, keine Junction: Zeigt `Ergebnisse`
|
||||
# per Junction nach aussen, weist OpenCode jeden Schreibvorgang ab - auch
|
||||
# dann, wenn die passenden Muster (relativ, kanonisch, ueber den Spiegel)
|
||||
# allesamt freigegeben sind. Nachgewiesen an Lauf v13.0.0-9b01, dessen
|
||||
# Fehlermeldung genau diese Regeln auflistet und den Aufruf dennoch
|
||||
# verweigert. Als echtes Verzeichnis innerhalb des Arbeitsverzeichnisses
|
||||
# entfaellt die Sonderbehandlung; der Inhalt wird nach dem Lauf umgezogen.
|
||||
ergebnisse = spiegel / output_dir.name
|
||||
ergebnisse.mkdir()
|
||||
log(
|
||||
f"Spiegel angelegt: {spiegel} ({ordner} Junctions, {dateien} Dateien, "
|
||||
f"{output_dir.name} -> {output_dir})"
|
||||
)
|
||||
return spiegel
|
||||
|
||||
|
||||
def uebernimm_ergebnisse(spiegel: Path, output_dir: Path, log) -> int:
|
||||
"""Erzeugte Dateien aus dem Spiegel ins Laufverzeichnis uebernehmen."""
|
||||
quelle = spiegel / output_dir.name
|
||||
if not quelle.is_dir():
|
||||
return 0
|
||||
anzahl = 0
|
||||
for pfad in sorted(quelle.rglob("*")):
|
||||
if not pfad.is_file():
|
||||
continue
|
||||
ziel = output_dir / pfad.relative_to(quelle)
|
||||
ziel.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(pfad), str(ziel))
|
||||
anzahl += 1
|
||||
log(f"{anzahl} Ergebnisdatei(en) aus dem Spiegel uebernommen")
|
||||
return anzahl
|
||||
|
||||
|
||||
def entferne_spiegel(spiegel: Path, log) -> None:
|
||||
"""Spiegel abbauen, ohne durch die Verknuepfungen hindurch zu loeschen.
|
||||
|
||||
``shutil.rmtree`` wuerde Junctions unter Umstaenden verfolgen und damit die
|
||||
Codebasis loeschen. Deshalb wird jeder Eintrag einzeln behandelt: Junctions
|
||||
per ``os.rmdir`` (entfernt nur die Verknuepfung), echte Dateien per
|
||||
``unlink``.
|
||||
"""
|
||||
if not spiegel.exists():
|
||||
return
|
||||
for eintrag in spiegel.iterdir():
|
||||
try:
|
||||
if eintrag.is_dir():
|
||||
# Junctions loest os.rmdir nur auf; ein echtes (leeres)
|
||||
# Ergebnisverzeichnis verschwindet damit ebenfalls.
|
||||
os.rmdir(eintrag)
|
||||
else:
|
||||
eintrag.unlink()
|
||||
except OSError as exc:
|
||||
log(f"Spiegeleintrag {eintrag} nicht entfernbar: {exc}")
|
||||
try:
|
||||
spiegel.rmdir()
|
||||
except OSError as exc:
|
||||
log(f"Spiegelverzeichnis {spiegel} nicht entfernbar: {exc}")
|
||||
|
||||
|
||||
def task_permissions(mode: str, custom_names: list[str]) -> str | dict[str, str]:
|
||||
if mode == "solo":
|
||||
return "deny"
|
||||
@@ -265,6 +374,7 @@ def build_run_config(
|
||||
agents_file: Path | None,
|
||||
provider: str = DEFAULT_PROVIDER,
|
||||
context_limit: int | None = None,
|
||||
zusatz_ausgaben: list[Path] | None = None,
|
||||
) -> dict:
|
||||
config = copy.deepcopy(base_config)
|
||||
provider_config = config.setdefault("provider", {}).setdefault(provider, {})
|
||||
@@ -283,6 +393,36 @@ def build_run_config(
|
||||
output_dir,
|
||||
git_worktree_root(root),
|
||||
)
|
||||
# Beim Spiegel-Arbeitsverzeichnis zeigt `Ergebnisse` per Junction auf das
|
||||
# Laufverzeichnis. Das Dateisystem loest das korrekt auf - OpenCode prueft
|
||||
# die Berechtigung aber gegen den *Pfadstring* des Werkzeugaufrufs, also
|
||||
# gegen "Ergebnisse/StRS.md" relativ zum Arbeitsverzeichnis. Ohne dieses
|
||||
# Muster wird der Schreibvorgang abgewiesen, obwohl das Ziel erlaubt ist.
|
||||
#
|
||||
# Bewusst ohne `resolve()`: Unter Windows folgt das der Junction und liefert
|
||||
# wieder den kanonischen Laufpfad - genau die relative Form, auf die es hier
|
||||
# ankommt, ginge dabei verloren.
|
||||
worktree = git_worktree_root(root)
|
||||
for zusatz in zusatz_ausgaben or []:
|
||||
formen = [zusatz.as_posix()]
|
||||
try:
|
||||
formen.append(zusatz.relative_to(root).as_posix())
|
||||
except ValueError:
|
||||
pass
|
||||
# Liegt der Spiegel innerhalb des Arbeitsrepositories, prueft OpenCode
|
||||
# gegen den Pfad relativ zur Git-Worktree-Wurzel - derselbe Befund, der
|
||||
# schon Skill 10.0.2 ausgeloest hat. Ohne diese Form wird ein Ziel
|
||||
# abgewiesen, obwohl kanonische und relative Form freigegeben sind.
|
||||
if worktree is not None:
|
||||
try:
|
||||
formen.append(zusatz.relative_to(worktree).as_posix())
|
||||
except ValueError:
|
||||
pass
|
||||
for form in formen:
|
||||
normalisiert = form.rstrip("/")
|
||||
for muster in (normalisiert, normalisiert + "/**"):
|
||||
if muster not in output_patterns:
|
||||
output_patterns.append(muster)
|
||||
edit_permissions = {"*": "deny"}
|
||||
edit_permissions.update({pattern: "allow" for pattern in output_patterns})
|
||||
custom_agents: dict[str, dict] = {}
|
||||
@@ -967,6 +1107,28 @@ def main() -> int:
|
||||
"1 und verliert bei mehr."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--spiegel-basis",
|
||||
default=None,
|
||||
help=(
|
||||
"Basisverzeichnis fuer das Spiegel-Arbeitsverzeichnis; Standard ist "
|
||||
"'_meta' im Laufverzeichnis. Damit bleibt der Lauf selbstenthalten "
|
||||
"und alle Artefakte liegen unterhalb der Versuchsreihe. Ein Spiegel "
|
||||
"im Benutzerprofil (%TEMP%) waere zudem dem Zugriffsbereich von "
|
||||
"Virenschutz und Controlled Folder Access ausgesetzt."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--arbeitsverzeichnis",
|
||||
default="root",
|
||||
choices=("root", "spiegel"),
|
||||
help=(
|
||||
"root: OpenCode arbeitet direkt im Codebasis-Root (bisheriges "
|
||||
"Verhalten). spiegel: ein Verzeichnis aus Verknuepfungen auf die "
|
||||
"Codebasis, in dem 'Ergebnisse/' auf das Laufverzeichnis zeigt - "
|
||||
"loest relative Ausgabepfade auf, ohne den Snapshot zu beruehren."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lmstudio-context",
|
||||
default="max",
|
||||
@@ -1081,6 +1243,21 @@ def main() -> int:
|
||||
sys.stderr.write(line + "\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
spiegel: Path | None = None
|
||||
arbeitswurzel = root
|
||||
if args.arbeitsverzeichnis == "spiegel":
|
||||
# resolve() ist hier wesentlich: Unter Windows liefern sowohl
|
||||
# tempfile.gettempdir() als auch verkuerzte Nutzerpfade die
|
||||
# 8.3-Kurzform ("C:/Users/CHRIST~1/..."), OpenCode prueft seine Regeln
|
||||
# aber gegen den aufgeloesten Langpfad. Ohne die Aufloesung passt kein
|
||||
# absolutes Freigabemuster.
|
||||
basis = (
|
||||
Path(args.spiegel_basis).resolve() if args.spiegel_basis else meta_dir
|
||||
)
|
||||
basis.mkdir(parents=True, exist_ok=True)
|
||||
spiegel = basis / "spiegel"
|
||||
arbeitswurzel = erstelle_spiegel(root, spiegel, output_dir, log)
|
||||
|
||||
model_ref, upstream_model = normalize_model(args.model, provider)
|
||||
base_config = json.loads(template_path.read_text(encoding="utf-8-sig"))
|
||||
|
||||
@@ -1114,11 +1291,12 @@ def main() -> int:
|
||||
model_ref,
|
||||
upstream_model,
|
||||
args.mode,
|
||||
root,
|
||||
arbeitswurzel,
|
||||
output_dir,
|
||||
agents_file,
|
||||
provider=provider,
|
||||
context_limit=context_limit,
|
||||
zusatz_ausgaben=[spiegel / output_dir.name] if spiegel else None,
|
||||
)
|
||||
config_path.write_text(
|
||||
json.dumps(run_config, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
@@ -1140,7 +1318,7 @@ def main() -> int:
|
||||
"--title",
|
||||
args.title,
|
||||
"--dir",
|
||||
str(root),
|
||||
str(arbeitswurzel),
|
||||
]
|
||||
effort_applied = args.effort in variants
|
||||
if effort_applied:
|
||||
@@ -1171,7 +1349,7 @@ def main() -> int:
|
||||
)
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=root,
|
||||
cwd=arbeitswurzel,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -1262,11 +1440,13 @@ def main() -> int:
|
||||
exit_code = process.wait(timeout=5)
|
||||
|
||||
duration_s = time.monotonic() - start_time
|
||||
if spiegel is not None:
|
||||
uebernimm_ergebnisse(spiegel, output_dir, log)
|
||||
session = None
|
||||
if session_id:
|
||||
try:
|
||||
session = export_session(
|
||||
opencode, session_id, env, root, session_path, log
|
||||
opencode, session_id, env, arbeitswurzel, session_path, log
|
||||
)
|
||||
except Exception as exc: # Sessionexport darf RawResult nicht verhindern.
|
||||
errors.append(f"Sessionexport fehlgeschlagen: {exc}")
|
||||
@@ -1295,6 +1475,8 @@ def main() -> int:
|
||||
result["is_error"] = True
|
||||
if result["subtype"] == "success":
|
||||
result["subtype"] = "error"
|
||||
result["arbeitsverzeichnis"] = args.arbeitsverzeichnis
|
||||
result["arbeitswurzel"] = str(arbeitswurzel)
|
||||
result["start_time"] = start_iso
|
||||
result["end_time"] = utc_now()
|
||||
result["opencode_path"] = str(opencode)
|
||||
@@ -1302,6 +1484,9 @@ def main() -> int:
|
||||
raw_result_path.write_text(
|
||||
json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
if spiegel is not None:
|
||||
entferne_spiegel(spiegel, log)
|
||||
|
||||
log(
|
||||
f"Ende: Exitcode={exit_code}; Status={result['subtype']}; "
|
||||
f"Turns={result['num_turns']}; Tokens={result['usage']['total_tokens']}; "
|
||||
|
||||
Reference in New Issue
Block a user