Files
Christoph SchwörerandClaude Opus 5 3d5b691bfa Iteration 3: Modell- und Modusraster erweitert, Skill 7.0.0, V2/V3 vorbereitet
Neue gueltige Zellen in Iteration 3
- claude-opus-5/solo/high: 363 Anforderungen, 99,7 % mit Primaerbeleg,
  Belege je Anforderung Median 2,0, 38,1 Mio. Tokens
- claude-fable-5/solo/high: 241 Anforderungen, 98,3 % mit Primaerbeleg,
  89,0 % PRIMAER-Anteil, vollstaendig regelkonform, 30,6 Mio. Tokens

Damit sind 6 von 12 Zellen des Rasters belegt. Zwei Befunde daraus:

Die Belegdichte folgt dem Modell, nicht dem Effort. Opus erreicht Median 2,0
auch auf high; alle 44 Sonnet-Laeufe lagen bei 1,0. max hebt Opus auf 3,0.
Die frueher dem Effort zugeschriebene Verdopplung ist damit eingegrenzt.

Die Fable-Modellverletzung ist reproduziert und abgegrenzt. Bei builtin laufen
die Subagenten auf claude-opus-5[1m] statt Fable (zweiter Fall nach Iteration 1),
bei solo dagegen sauber. Nicht das Modell ist die Ursache, sondern Fable in
Kombination mit Delegation.

Fehlmessungen, vollstaendig protokolliert
- vier 429-Abbrueche (Session-Kontingent) aus dem Parallelblock 19:59;
  drei davon mit Teilbestand, einer ohne Ergebnis
- opus-5/builtin/high zum dritten Mal gescheitert: 790,7 Mio. Tokens ueber drei
  Anlaeufe ohne Artefakt. Zelle mit dieser Prompt-Version nicht messbar.

Skill 7.0.0 (MAJOR)
- Isolationsmechanismus modusabhaengig: --safe-mode schaltet MCP-Server und
  Custom-Agenten ab und ist mit V2/V3 unvereinbar. Smoke-Test verifiziert:
  mit Flag spawned=0, ohne Flag spawned=2. Ersatz fuer custom/MCP:
  --strict-mcp-config plus --disallowedTools Skill WebSearch WebFetch SlashCommand.
- 6.1.0: Pflichtpruefung leeres Ergebnisverzeichnis = Fehlmessung unabhaengig von
  is_error; CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0; Protokollfeld Gueltigkeit
- extract-subagenten.py: Start-Quittung wird nicht mehr als Ertragsmass ausgewiesen

Versuch 2 und 3 vorbereitet
- Prompt-Kette V1 -> V2 (02-A, angepasst an Agentendateien) -> V3 (02-B, MCP)
- V2: acht Rollen inkl. nicht delegierendem ISO-29148-Orchestrator
- V3: elf Rollen, fuenf Werkzeugserver, neue Belegklasse LAUFZEIT

Ablaufprotokoll um Phase 6 und 7 sowie die Vorbereitung von V2/V3 ergaenzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:03:00 +02:00

208 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Create and verify an isolated Codex analysis workspace."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
def fail(message: str) -> None:
raise SystemExit(message)
def resolved(path: str) -> Path:
return Path(path).resolve()
def native_path(path: Path) -> str:
value = str(path)
if os.name == "nt" and not value.startswith("\\\\?\\"):
return "\\\\?\\" + value
return value
def allowed_workspace(workspace: Path, meta: Path) -> bool:
temporary_root = Path(tempfile.gettempdir()).resolve()
return (
(workspace.parent == meta and workspace.name == "workspace")
or (workspace.parent == temporary_root and workspace.name.startswith("codex-experiment-"))
)
def git_output(cwd: Path, *args: str) -> bytes:
completed = subprocess.run(
["git", "-C", str(cwd), *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return completed.stdout
def source_files(source: Path) -> tuple[list[Path], str | None]:
try:
top = resolved(git_output(source, "rev-parse", "--show-toplevel").decode().strip())
relative_source = source.relative_to(top)
raw = git_output(
top,
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
relative_source.as_posix(),
)
paths = []
for entry in raw.split(b"\0"):
if not entry:
continue
candidate = resolved(top / os.fsdecode(entry))
if candidate.is_file() and candidate.is_relative_to(source):
paths.append(candidate)
return sorted(set(paths), key=lambda item: item.as_posix().casefold()), str(top)
except (subprocess.CalledProcessError, ValueError):
paths = [item for item in source.rglob("*") if item.is_file() and ".git" not in item.parts]
return sorted(paths, key=lambda item: item.as_posix().casefold()), None
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with open(native_path(path), "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest().upper()
def manifest(workspace: Path) -> dict[str, dict[str, int | str]]:
result: dict[str, dict[str, int | str]] = {}
workspace_native = native_path(workspace)
files: list[tuple[str, Path]] = []
for directory, _, names in os.walk(workspace_native):
for name in names:
full_path = Path(directory) / name
relative = os.path.relpath(str(full_path), workspace_native).replace("\\", "/")
files.append((relative, full_path))
for relative, path in sorted(files, key=lambda item: item[0].casefold()):
result[relative] = {"size": os.stat(str(path)).st_size, "sha256": sha256(path)}
return result
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def create(source_arg: str, workspace_arg: str, meta_arg: str) -> None:
source = resolved(source_arg)
workspace = resolved(workspace_arg)
meta = resolved(meta_arg)
if not source.is_dir():
fail(f"Source directory does not exist: {source}")
if not allowed_workspace(workspace, meta):
fail("Workspace must be _meta/workspace or a codex-experiment-* directory in system temp")
if workspace.exists():
before_manifest = meta / "workspace-manifest-before.json"
if workspace.parent == meta and workspace.name == "workspace" and not before_manifest.exists():
shutil.rmtree(native_path(workspace))
else:
fail(f"Workspace already exists: {workspace}")
files, git_root = source_files(source)
workspace.mkdir(parents=True)
for path in files:
relative = path.relative_to(source)
target = workspace / relative
os.makedirs(native_path(target.parent), exist_ok=True)
try:
shutil.copy2(native_path(path), native_path(target), follow_symlinks=False)
except OSError as error:
fail(f"Failed to copy {path} -> {target}: {error}")
before = manifest(workspace)
write_json(meta / "workspace-manifest-before.json", before)
write_json(
meta / "workspace-source.json",
{
"source": str(source),
"workspace": str(workspace),
"git_root": git_root,
"file_count": len(before),
"total_bytes": sum(int(item["size"]) for item in before.values()),
},
)
print(f"Created isolated workspace with {len(before)} files: {workspace}")
def verify(workspace_arg: str, meta_arg: str) -> None:
workspace = resolved(workspace_arg)
meta = resolved(meta_arg)
before_path = meta / "workspace-manifest-before.json"
if not workspace.is_dir() or not before_path.is_file():
fail("Workspace or before-manifest is missing")
before = json.loads(before_path.read_text(encoding="utf-8"))
after = manifest(workspace)
added = sorted(set(after) - set(before), key=str.casefold)
removed = sorted(set(before) - set(after), key=str.casefold)
modified = sorted(
(path for path in set(before) & set(after) if before[path] != after[path]),
key=str.casefold,
)
write_json(meta / "workspace-manifest-after.json", after)
result = {
"unchanged": not (added or removed or modified),
"added": added,
"removed": removed,
"modified": modified,
"file_count_before": len(before),
"file_count_after": len(after),
}
write_json(meta / "workspace-integrity.json", result)
print(json.dumps(result, ensure_ascii=False))
if not result["unchanged"]:
raise SystemExit(3)
def cleanup(workspace_arg: str, meta_arg: str) -> None:
workspace = resolved(workspace_arg)
meta = resolved(meta_arg)
if not allowed_workspace(workspace, meta):
fail("Refusing to remove a directory outside the approved experiment workspace locations")
if workspace.exists():
shutil.rmtree(native_path(workspace))
print(f"Removed temporary workspace: {workspace}")
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
create_parser = subparsers.add_parser("create")
create_parser.add_argument("source")
create_parser.add_argument("workspace")
create_parser.add_argument("meta")
verify_parser = subparsers.add_parser("verify")
verify_parser.add_argument("workspace")
verify_parser.add_argument("meta")
cleanup_parser = subparsers.add_parser("cleanup")
cleanup_parser.add_argument("workspace")
cleanup_parser.add_argument("meta")
args = parser.parse_args()
if args.command == "create":
create(args.source, args.workspace, args.meta)
elif args.command == "verify":
verify(args.workspace, args.meta)
else:
cleanup(args.workspace, args.meta)
if __name__ == "__main__":
main()