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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
affde3a45f
commit
3d5b691bfa
@@ -0,0 +1,207 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user