1"""Contracts: the serialised research contract P=(Q,Θ,D,A,G,V,B,R,L), run manifests, hashing helpers.23The contract components map to concrete files:4 Q, Θ, D -> TASK.md (question, estimands, inputs, information-availability rules) [task bundle]5 A -> launcher flags (tool inventory, permission policy, model) [configs/*.yaml]6 G -> phase graph in the treatment protocol (arms B/C/D) and the harness state machine7 V -> gate_spec.json (oracle-free runtime checks) + hidden evaluate.py (final evaluation)8 B -> budget envelope (USD cap, wall seconds, turns per session, gate/review cycles)9 R -> recovery rules (protocol text) + harness resume/retry policy10 L -> run manifest, event stream, receipts, screenshot manifest (SQLite + files)11"""12from __future__ import annotations13import dataclasses, hashlib, json, os, pathlib, datetime, uuid14from dataclasses import dataclass, field, asdict15from typing import Optional1617ARMS = ("A", "B", "C", "D")18RUN_STATES = ("not_started", "prepared", "running", "completed", "timeout", "budget_exhausted", "error", "interrupted", "evaluated", "void")19# allowed transitions of a run (harness state machine)20RUN_TRANSITIONS = {21 "not_started": {"prepared", "error", "void"},22 "prepared": {"running", "error", "void"},23 "running": {"completed", "timeout", "budget_exhausted", "error", "interrupted"},24 "interrupted": {"running", "error"}, # resume after a standardized fault25 "completed": {"evaluated", "error"},26 "timeout": {"evaluated", "error"},27 "budget_exhausted": {"evaluated", "error"},28 "error": {"evaluated", "prepared"}, # infra error: can be re-prepared under the rerun policy29 "evaluated": set(), "void": set(),30}31PHASE_STATES = ("pending", "running", "passed", "failed", "blocked", "stale", "skipped_optional")3233def utcnow() -> str:34 return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds")3536def sha256_file(path) -> str:37 h = hashlib.sha256()38 with open(path, "rb") as fh:39 for chunk in iter(lambda: fh.read(1 << 20), b""):40 h.update(chunk)41 return h.hexdigest()4243def sha256_bytes(b: bytes) -> str:44 return hashlib.sha256(b).hexdigest()4546def sha256_text(s: str) -> str:47 return hashlib.sha256(s.encode("utf-8")).hexdigest()4849def dir_manifest(root, exclude=()) -> dict:50 out = {}51 root = pathlib.Path(root)52 for p in sorted(root.rglob("*")):53 if p.is_file():54 rel = p.relative_to(root).as_posix()55 if any(rel == e or rel.startswith(e.rstrip("/") + "/") for e in exclude):56 continue57 out[rel] = sha256_file(p)58 return out5960@dataclass61class Budget:62 max_budget_usd: float = 4.063 wall_seconds: int = 150064 max_turns_per_session: int = 8065 max_gate_cycles: int = 3 # arms C/D: controller-enforced gate feedback cycles66 max_review_cycles: int = 1 # arm D67 min_continuation_usd: float = 0.256869@dataclass70class RunManifest:71 run_id: str72 task_id: str73 split: str74 treatment: str75 repeat_index: int76 block_id: str = ""77 order_in_block: int = 078 node: str = ""79 task_hash: Optional[str] = None80 protocol_hash: Optional[str] = None81 effective_context_hash: Optional[str] = None82 code_commit: Optional[str] = None83 environment_id: Optional[str] = None84 client_version: Optional[str] = None85 model_requested: Optional[str] = None86 model_reported: Optional[str] = None87 session_ids: list = field(default_factory=list)88 started_at_utc: Optional[str] = None89 ended_at_utc: Optional[str] = None90 status: str = "not_started"91 terminal_reason: Optional[str] = None92 budget: dict = field(default_factory=dict)93 usage: dict = field(default_factory=dict)94 human_intervention_ids: list = field(default_factory=list)95 artifact_manifest: Optional[dict] = None96 evaluation_receipt: Optional[str] = None97 screenshot_ids: list = field(default_factory=list)98 fault: Optional[dict] = None99 notes: str = ""100101 def to_json(self) -> str:102 return json.dumps(asdict(self), indent=1, sort_keys=True)103104def new_run_id(split: str, seq: int) -> str:105 return f"R{split[0].upper()}{seq:04d}"106107def new_session_id() -> str:108 return str(uuid.uuid4())109