Viewing files at commit b4b51d4daa88Commit detailsView latest

Browse files
scripts/task_loop.py 2637 lines · 105.8 KB · python
1
#!/usr/bin/env python3
2
"""The full task loop, end to end — the product's signature motion.
3
4
oak:  mount -> edit -> commit -> push -> desc -> end, then REMOUNT for a
5
      follow-up task (the warm-cache story) and repeat edit/commit/push/end.
6
git:  worktree add -> branch edit -> add/commit -> push -> worktree remove,
7
      then a second worktree for the follow-up. The task description lives in
8
      the commit message git already paid for; oak pays a separate `desc`
9
      call. That asymmetry is the measurement, not a harness gap
10
      (docs/command-semantics.md): this is agent-default-track, whole-task
11
      evidence, never a step-by-step equivalence claim.
12
13
Transport honesty: oak rows always cross the network to a real oak server.
14
git runs against GitHub (--git-remote github, transport-comparable) or a
15
local bare repo (--git-remote local, the core-equivalent track). Rows carry
16
remote_transport/remote_server; latency comparisons across transports are
17
forbidden — cross-subject evidence at unlike transports is tool calls and
18
tokens only.
19
20
Setup:
21
  OAK_BENCH_SYNC_REPO=oak/bench-sync-tmp \\
22
  [email protected]:oakdotspace/bench-task-loop-tmp.git \\
23
  python3 scripts/task_loop.py --runs 3
24
25
Unconfigured remotes emit skip rows (returncode 77), never silent absence.
26
"""
27
28
from __future__ import annotations
29
30
import argparse
31
import hashlib
32
import json
33
import os
34
import platform
35
import random
36
import re
37
import shutil
38
import socket
39
import sqlite3
40
import stat
41
import subprocess
42
import tempfile
43
import threading
44
import time
45
import urllib.error
46
import urllib.parse
47
import urllib.request
48
from datetime import datetime, timezone
49
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
50
from pathlib import Path
51
from typing import Any
52
53
from oakbench import remotes as oakbench_remotes
54
from oakbench import tokens as oakbench_tokens
55
from oakbench.byteproxy import CountingProxy
56
from oakbench.diskprobe import bounded_tree_usage
57
from oakbench.environment import ENV_ISOLATION_VERSION, base_env as oakbench_base_env
58
from oakbench.environment import command_display
59
from oakbench.execution import run_timed, stop_owned_process
60
from oakbench.results import ResultsStore
61
from oakbench.rows import row_returncode
62
from oakbench.runlock import measurement_lock
63
from oakbench.subjects import (
64
    DEFAULT_OAK_REPO,
65
    Subject,
66
    binary_sha256,
67
    load_subjects,
68
    source_metadata,
69
    subject_details,
70
    subject_version,
71
    subject_versions,
72
)
73
74
ROOT = Path(__file__).resolve().parents[1]
75
DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-task-loop"
76
SKIP_RETURNCODE = 77
77
VIRTUAL_BRANCH_RE = re.compile(r"virtual branch ([^\s)]+)")
78
PHASES = ("task", "followup")
79
TASK_LOOP_SCENARIO = "task_loop"
80
AGENT_TASK_SCENARIO = "agent_task_baseline_v1"
81
AGENT_TASK_OAK_LOCAL_SCENARIO = "agent_task_oak_local_v1"
82
INTERRUPTED_PUBLICATION_SCENARIO = "interrupted_publication_after_request_v1"
83
LEGACY_SELECTED_MATERIALIZATION_SCENARIO = "legacy_selected_clone_materialization_v1"
84
INTERRUPTED_PUBLICATION_REQUEST_TIMEOUT_S = 2.0
85
INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S = 30.0
86
INTERRUPTED_PUBLICATION_MAX_REQUEST_BYTES = 64 * 1024 * 1024
87
LOCAL_OAK_SERVER_READY_TIMEOUT_S = 10.0
88
89
90
def legacy_selected_receipt_evidence(receipt: dict[str, Any]) -> dict[str, Any]:
91
    result = receipt.get("result") if isinstance(receipt.get("result"), dict) else {}
92
    observed = receipt.get("observed") if isinstance(receipt.get("observed"), dict) else {}
93
    phases = observed.get("pull_phases") if isinstance(observed.get("pull_phases"), list) else []
94
    phase_names = [phase.get("name") for phase in phases if isinstance(phase, dict)]
95
    materialization = (
96
        observed.get("materialization")
97
        if isinstance(observed.get("materialization"), dict)
98
        else {}
99
    )
100
    return {
101
        "legacy_route_observed": phase_names == ["initial", "legacy_selected_branch"],
102
        "pull_phase_names": phase_names,
103
        "pull_phases": phases,
104
        "materialization_passes_observed": materialization.get("passes_observed"),
105
        "written_paths": materialization.get("written_paths"),
106
        "snapshot_bound": result.get("snapshot_bound"),
107
        "head": result.get("head"),
108
        "manifest": result.get("manifest"),
109
    }
110
111
112
def legacy_selected_receipt_contract(evidence: dict[str, Any]) -> dict[str, Any]:
113
    phases = []
114
    for phase in evidence.get("pull_phases", []):
115
        if not isinstance(phase, dict):
116
            continue
117
        phases.append(
118
            {
119
                key: value
120
                for key, value in phase.items()
121
                if key not in {"materialization_passes"}
122
            }
123
        )
124
    return {
125
        "legacy_route_observed": evidence.get("legacy_route_observed"),
126
        "pull_phases": phases,
127
        "snapshot_bound": evidence.get("snapshot_bound"),
128
        "head": evidence.get("head"),
129
        "manifest": evidence.get("manifest"),
130
        "written_paths": evidence.get("written_paths"),
131
    }
132
133
134
def normalized_cache_contract(snapshot: dict[str, Any] | None) -> list[dict[str, Any]] | None:
135
    if not isinstance(snapshot, dict) or not isinstance(snapshot.get("rows"), list):
136
        return None
137
    return [
138
        {key: row.get(key) for key in ("path", "size", "blob_hash")}
139
        for row in snapshot["rows"]
140
        if isinstance(row, dict)
141
    ]
142
143
144
def normalized_status(value: dict[str, Any] | list[Any] | None) -> dict[str, Any] | None:
145
    if not isinstance(value, dict):
146
        return None
147
    return {
148
        key: value.get(key)
149
        for key in ("branch", "parent", "branch_status", "head", "working_changes", "branch_changes")
150
    }
151
152
153
def materialization_timing_is_claimable(
154
    metadata: dict[str, Any], runs: int, randomize_order: bool, claim_timing: bool
155
) -> bool:
156
    return bool(
157
        claim_timing
158
        and metadata.get("measurement_lock") == "held"
159
        and randomize_order
160
        and runs >= 10
161
    )
162
163
164
def isolated_oak_fixture_env(task_config_root: Path) -> dict[str, str]:
165
    task_config_root.mkdir(mode=0o700, parents=True, exist_ok=True)
166
    retained = {
167
        "PATH",
168
        "TMPDIR",
169
        "TMP",
170
        "TEMP",
171
        "HOME",
172
        "USER",
173
        "USERNAME",
174
        "SHELL",
175
        "SYSTEMROOT",
176
        "WINDIR",
177
        "COMSPEC",
178
        "PATHEXT",
179
        "SSL_CERT_FILE",
180
        "SSL_CERT_DIR",
181
    }
182
    env = {key: value for key, value in os.environ.items() if key in retained}
183
    env.update(
184
        {
185
            "GIT_AUTHOR_NAME": "Oak Materialization Bench",
186
            "GIT_AUTHOR_EMAIL": "[email protected]",
187
            "GIT_COMMITTER_NAME": "Oak Materialization Bench",
188
            "GIT_COMMITTER_EMAIL": "[email protected]",
189
            "GIT_CONFIG_GLOBAL": os.devnull,
190
            "GIT_CONFIG_NOSYSTEM": "1",
191
            "OAK_AUTHOR": "oak-materialization-bench",
192
            "OAK_NO_UPDATE_CHECK": "1",
193
            "OAK_BENCH_ENV_ISOLATION_VERSION": ENV_ISOLATION_VERSION,
194
            "NO_COLOR": "1",
195
            "CLICOLOR": "0",
196
            "LC_ALL": "C",
197
            "LANG": "C",
198
            "TZ": "UTC",
199
            "GIT_AUTHOR_DATE": "2026-01-01T00:00:00Z",
200
            "GIT_COMMITTER_DATE": "2026-01-01T00:00:00Z",
201
        }
202
    )
203
    return env
204
205
206
def normalized_worktree_snapshot(root: Path) -> list[dict[str, Any]]:
207
    rows: list[dict[str, Any]] = []
208
    for path in sorted(root.rglob("*")):
209
        relative = path.relative_to(root)
210
        if relative.parts and relative.parts[0] in {".oak", ".git"}:
211
            continue
212
        info = path.lstat()
213
        if stat.S_ISDIR(info.st_mode):
214
            continue
215
        row: dict[str, Any] = {
216
            "path": relative.as_posix(),
217
            "mode": stat.S_IMODE(info.st_mode),
218
        }
219
        if stat.S_ISLNK(info.st_mode):
220
            row.update({"kind": "symlink", "target": os.readlink(path)})
221
        else:
222
            row.update({"kind": "file", "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
223
        rows.append(row)
224
    return rows
225
226
227
def json_command(
228
    command: list[str], cwd: Path, env: dict[str, str]
229
) -> dict[str, Any] | list[Any] | None:
230
    result = run_bounded_harness_command(command, cwd, env=env)
231
    if result.returncode != 0:
232
        return None
233
    try:
234
        value = json.loads(result.stdout)
235
    except (TypeError, ValueError):
236
        return None
237
    return value if isinstance(value, (dict, list)) else None
238
239
240
def normalized_history(value: dict[str, Any] | list[Any] | None) -> list[dict[str, Any]] | None:
241
    if not isinstance(value, list):
242
        return None
243
    rows: list[dict[str, Any]] = []
244
    for item in value:
245
        if not isinstance(item, dict):
246
            return None
247
        rows.append(
248
            {
249
                key: item.get(key)
250
                for key in (
251
                    "hash",
252
                    "timestamp",
253
                    "branch",
254
                    "description_or_subject",
255
                    "files_changed",
256
                )
257
            }
258
        )
259
    return rows
260
261
262
def normalized_oak_artifacts(root: Path) -> list[str] | None:
263
    oak_dir = root / ".oak"
264
    if not oak_dir.is_dir():
265
        return None
266
    return sorted(
267
        path.relative_to(oak_dir).as_posix() + ("/" if path.is_dir() else "")
268
        for path in oak_dir.rglob("*")
269
    )
270
271
272
def stat_cache_snapshot(root: Path) -> dict[str, Any] | None:
273
    db = root / ".oak/oak.db"
274
    if not db.is_file():
275
        return {"rows": None, "all_metadata_valid": False, "error": "oak_db_missing"}
276
    try:
277
        connection = sqlite3.connect(str(db))
278
        connection.execute("PRAGMA query_only = ON")
279
        raw_rows = connection.execute(
280
            "SELECT path, mtime_ns, ctime_ns, size, blob_hash FROM stat_cache ORDER BY path"
281
        ).fetchall()
282
    except sqlite3.Error as error:
283
        return {"rows": None, "all_metadata_valid": False, "error": str(error)}
284
    finally:
285
        if "connection" in locals():
286
            connection.close()
287
    rows: list[dict[str, Any]] = []
288
    valid = True
289
    for path, mtime_ns, ctime_ns, size, blob_hash in raw_rows:
290
        file_path = root / path
291
        try:
292
            info = file_path.stat()
293
        except OSError:
294
            valid = False
295
            continue
296
        expected_ctime = info.st_ctime_ns if os.name == "posix" else 0
297
        metadata_valid = (
298
            int(size) == info.st_size
299
            and int(mtime_ns) == info.st_mtime_ns
300
            and int(ctime_ns) == expected_ctime
301
        )
302
        valid = valid and metadata_valid
303
        rows.append(
304
            {
305
                "path": str(path),
306
                "size": int(size),
307
                "blob_hash": str(blob_hash),
308
                "metadata_valid": metadata_valid,
309
            }
310
        )
311
    return {"rows": rows, "all_metadata_valid": valid}
312
313
314
def base_env() -> dict[str, str]:
315
    return oakbench_base_env(
316
        author_name="Oak Task Loop",
317
        author_email="[email protected]",
318
        oak_author="oak-task-loop",
319
    )
320
321
322
def parse_args() -> argparse.Namespace:
323
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
324
    parser.add_argument("--subjects", help="Comma-separated subject names from config/subjects.toml")
325
    parser.add_argument("--config", type=Path, default=ROOT / "config" / "subjects.toml")
326
    parser.add_argument("--git-bin", type=Path)
327
    parser.add_argument("--oak-installed-bin", type=Path)
328
    parser.add_argument("--oak-local-bin", type=Path)
329
    parser.add_argument(
330
        "--oak-serve-bin",
331
        type=Path,
332
        help="Immutable Oak binary used only as the fixed loopback server and fixture writer.",
333
    )
334
    parser.add_argument(
335
        "--oak-serve-source",
336
        type=Path,
337
        help="Read-only source checkout that produced --oak-serve-bin.",
338
    )
339
    parser.add_argument(
340
        "--oak-serve-source-head",
341
        help="Exact 64-hex Oak source head that produced --oak-serve-bin.",
342
    )
343
    parser.add_argument(
344
        "--materialization-baseline-subject",
345
        help="Named control subject required by the legacy materialization scenario.",
346
    )
347
    parser.add_argument(
348
        "--claim-materialization-timing",
349
        action="store_true",
350
        help=(
351
            "Mark materialization timing claimable only with a held measurement lock, "
352
            "randomized subject order, and at least 10 runs after an externally coordinated quiet window."
353
        ),
354
    )
355
    parser.add_argument("--runs", type=int, default=3)
356
    parser.add_argument(
357
        "--scenarios",
358
        default=TASK_LOOP_SCENARIO,
359
        help=(
360
            "Comma-separated scenarios: task_loop, agent_task_baseline_v1, "
361
            "agent_task_oak_local_v1, legacy_selected_clone_materialization_v1, "
362
            "and/or interrupted_publication_after_request_v1. "
363
            "The interruption "
364
            "scenario is a loopback-only Oak fixture and never contacts a remote service."
365
        ),
366
    )
367
    parser.add_argument(
368
        "--git-remote",
369
        choices=("github", "local"),
370
        default="github",
371
        help=(
372
            "github: agent-default track, real push to GIT_BENCH_REMOTE "
373
            "(transport-comparable to oak). local: core-equivalent track, "
374
            "local bare remote (no network in git's loop)."
375
        ),
376
    )
377
    parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
378
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "task-loop")
379
    parser.add_argument("--oak-repo", type=Path, default=DEFAULT_OAK_REPO)
380
    parser.add_argument("--admitted-output-chars", type=int, default=20_000)
381
    parser.add_argument("--keep-workdirs", action="store_true")
382
    parser.add_argument(
383
        "--randomize-subject-order",
384
        action="store_true",
385
        help="Shuffle subject order independently in each run using --random-seed.",
386
    )
387
    parser.add_argument("--random-seed", type=int, default=20260908)
388
    return parser.parse_args()
389
390
391
def run_untimed(command: list[str], cwd: Path | None, timeout: float = 300.0) -> subprocess.CompletedProcess[str]:
392
    return subprocess.run(
393
        command,
394
        cwd=cwd,
395
        env=base_env(),
396
        text=True,
397
        stdout=subprocess.PIPE,
398
        stderr=subprocess.PIPE,
399
        timeout=timeout,
400
        check=False,
401
    )
402
403
404
def step_row(
405
    subject: Subject,
406
    scenario: str,
407
    operation: str,
408
    command: list[str],
409
    cwd: Path,
410
    run_index: int,
411
    metadata: dict[str, Any],
412
    admitted_output_chars: int,
413
    *,
414
    kind: str = "vcs",
415
    expected_returncodes: tuple[int, ...] = (0,),
416
    preserve_stdout: bool = False,
417
    preserve_stderr: bool = False,
418
    env: dict[str, str] | None = None,
419
    timeout_seconds: float | None = None,
420
) -> dict[str, Any]:
421
    capture = run_timed(
422
        command,
423
        cwd,
424
        env or base_env(),
425
        admitted_output_chars,
426
        timeout_seconds=timeout_seconds,
427
    )
428
    expected = not capture.timed_out and capture.returncode in expected_returncodes
429
    is_vcs = 1 if kind == "vcs" else 0
430
    row = {
431
        **metadata,
432
        "subject": subject.name,
433
        "subject_kind": subject.kind,
434
        "subject_label": subject.label,
435
        "scenario": scenario,
436
        "run": run_index,
437
        "operation": operation,
438
        "step_kind": kind,
439
        "elapsed_ms": round(capture.elapsed_ms, 3),
440
        "returncode": 0 if expected else (capture.returncode or 1),
441
        "process_returncode": capture.returncode,
442
        "timed_out": capture.timed_out,
443
        "expected_returncodes": list(expected_returncodes),
444
        "command": command,
445
        "tool_call_count": 1,
446
        "terminal_tool_call_count": 1,
447
        "vcs_tool_call_count": is_vcs,
448
        **oakbench_tokens.interaction_token_fields(
449
            command_display(command),
450
            capture.stdout_text,
451
            capture.stderr_text,
452
            capture.stdout_bytes,
453
            capture.stderr_bytes,
454
            capture.stdout_truncated,
455
            capture.stderr_truncated,
456
        ),
457
    }
458
    if not expected:
459
        row["stderr"] = capture.stderr_text[-4000:]
460
    if preserve_stdout:
461
        row["stdout"] = capture.stdout_text
462
    if preserve_stderr:
463
        row["stderr"] = capture.stderr_text
464
    return row
465
466
467
def skip_row(
468
    subject: Subject,
469
    scenario: str,
470
    run_index: int,
471
    metadata: dict[str, Any],
472
    reason: str,
473
) -> dict[str, Any]:
474
    return {
475
        **metadata,
476
        "subject": subject.name,
477
        "subject_kind": subject.kind,
478
        "subject_label": subject.label,
479
        "scenario": scenario,
480
        "run": run_index,
481
        "operation": "task_loop.skipped",
482
        "step_kind": "summary",
483
        "elapsed_ms": 0.0,
484
        "returncode": SKIP_RETURNCODE,
485
        "command": [],
486
        "skipped": True,
487
        "skip_reason": reason,
488
        "tool_call_count": 0,
489
        "terminal_tool_call_count": 0,
490
        "vcs_tool_call_count": 0,
491
    }
492
493
494
def unmeasured_step_row(
495
    subject: Subject,
496
    scenario: str,
497
    operation: str,
498
    run_index: int,
499
    metadata: dict[str, Any],
500
    reason: str,
501
) -> dict[str, Any]:
502
    row = skip_row(subject, scenario, run_index, metadata, reason)
503
    row["operation"] = operation
504
    return row
505
506
507
def harness_step_row(
508
    subject: Subject,
509
    scenario: str,
510
    operation: str,
511
    run_index: int,
512
    metadata: dict[str, Any],
513
    elapsed_ms: float,
514
    *,
515
    returncode: int = 0,
516
    harness_command_count: int = 0,
517
) -> dict[str, Any]:
518
    return {
519
        **metadata,
520
        "subject": subject.name,
521
        "subject_kind": subject.kind,
522
        "subject_label": subject.label,
523
        "scenario": scenario,
524
        "run": run_index,
525
        "operation": operation,
526
        "step_kind": "harness",
527
        "elapsed_ms": round(elapsed_ms, 3),
528
        "returncode": returncode,
529
        "command": [],
530
        "tool_call_count": 0,
531
        "terminal_tool_call_count": 0,
532
        "vcs_tool_call_count": 0,
533
        "harness_command_count": harness_command_count,
534
        "stdout_bytes": 0,
535
        "stderr_bytes": 0,
536
        "retry_count": 0,
537
        "human_action_count": 0,
538
    }
539
540
541
def phase_total_row(
542
    rows: list[dict[str, Any]],
543
    subject: Subject,
544
    scenario: str,
545
    run_index: int,
546
    metadata: dict[str, Any],
547
    operation: str,
548
) -> dict[str, Any]:
549
    failures = [row for row in rows if row_returncode(row) not in (0, SKIP_RETURNCODE)]
550
    unmeasured = [
551
        str(row["operation"])
552
        for row in rows
553
        if row_returncode(row) == SKIP_RETURNCODE
554
    ]
555
    phase_elapsed_ms: dict[str, float] = {}
556
    for row in rows:
557
        phase = str(row.get("operation", "unknown")).split(".", 1)[0]
558
        phase_elapsed_ms[phase] = phase_elapsed_ms.get(phase, 0.0) + float(
559
            row.get("elapsed_ms", 0.0)
560
        )
561
    return {
562
        **metadata,
563
        "subject": subject.name,
564
        "subject_kind": subject.kind,
565
        "subject_label": subject.label,
566
        "scenario": scenario,
567
        "run": run_index,
568
        "operation": operation,
569
        "step_kind": "summary",
570
        "elapsed_ms": round(sum(float(row.get("elapsed_ms", 0)) for row in rows), 3),
571
        "returncode": 1 if failures else 0,
572
        "coverage_complete": not unmeasured,
573
        "unmeasured_operations": unmeasured,
574
        "command": [row["command"] for row in rows],
575
        "tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("tool_call_count")) for row in rows),
576
        "terminal_tool_call_count": sum(
577
            oakbench_tokens.int_or_zero(row.get("terminal_tool_call_count")) for row in rows
578
        ),
579
        "vcs_tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("vcs_tool_call_count")) for row in rows),
580
        "agent_tool_call_count": sum(
581
            oakbench_tokens.int_or_zero(row.get("tool_call_count")) for row in rows
582
        ),
583
        "harness_command_count": sum(
584
            oakbench_tokens.int_or_zero(row.get("harness_command_count")) for row in rows
585
        ),
586
        "retry_count": sum(oakbench_tokens.int_or_zero(row.get("retry_count")) for row in rows),
587
        "human_action_count": sum(
588
            oakbench_tokens.int_or_zero(row.get("human_action_count")) for row in rows
589
        ),
590
        "phase_elapsed_ms": {
591
            phase: round(elapsed, 3) for phase, elapsed in sorted(phase_elapsed_ms.items())
592
        },
593
        **oakbench_tokens.summed_token_fields(
594
            rows, "sum_of_task_loop_steps_command_plus_admitted_output_chars_div_4"
595
        ),
596
        "summarized_operations": [row["operation"] for row in rows],
597
    }
598
599
600
def has_lane_failures(rows: list[dict[str, Any]]) -> bool:
601
    return any(row_returncode(row) not in (0, SKIP_RETURNCODE) for row in rows)
602
603
604
def enforce_oracle_result(total: dict[str, Any], oracle_ok: bool | None) -> dict[str, Any]:
605
    if oracle_ok is False:
606
        total["returncode"] = 1
607
        total["stderr"] = "oracle_payload_missing_from_remote_branch"
608
    return total
609
610
611
def interrupted_publication_fields(
612
    *,
613
    request_count: int,
614
    request_bytes: int | None,
615
    declared_request_bytes: int | None,
616
    request_framing_valid: bool,
617
    request_body_timed_out: bool,
618
    local_head_before: str | None,
619
    local_head_after: str | None,
620
    status_after: dict[str, Any] | None,
621
    worktree_payload_before: str | None,
622
    worktree_payload_after: str | None,
623
    cli_observation: dict[str, str],
624
) -> dict[str, Any]:
625
    """Evidence fields for a single interrupted mutation attempt.
626
627
    The fixture's receipt of a full request is ground truth for the harness,
628
    not a server durability receipt available to the CLI.  A disconnect after
629
    that point is therefore unknown to the client and must never imply replay.
630
    """
631
    request_complete = bool(
632
        request_count == 1
633
        and request_framing_valid
634
        and declared_request_bytes is not None
635
        and request_bytes == declared_request_bytes
636
    )
637
    if request_count == 0:
638
        delivery_state = "no_request_observed"
639
        fixture_outcome = "not_sent_observed"
640
    elif not request_framing_valid:
641
        delivery_state = "invalid_request_framing"
642
        fixture_outcome = "indeterminate_after_invalid_request_disconnect"
643
    elif request_complete:
644
        delivery_state = "complete_request_observed"
645
        fixture_outcome = "unknown_after_complete_request_disconnect"
646
    else:
647
        delivery_state = "partial_request_observed"
648
        fixture_outcome = "indeterminate_after_partial_request_disconnect"
649
    payload_preserved = bool(
650
        worktree_payload_before
651
        and worktree_payload_before == worktree_payload_after
652
    )
653
    unpublished_state_preserved = bool(status_after and status_after.get("needs_push"))
654
    return {
655
        "publication_outcome": cli_observation["reported_outcome"],
656
        "cli_observation": cli_observation["observation"],
657
        "fixture_delivery_state": delivery_state,
658
        "fixture_outcome_knowledge": fixture_outcome,
659
        "fixture_received_complete_request_before_disconnect": request_complete,
660
        "fixture_request_framing_valid": request_framing_valid,
661
        "fixture_request_body_timed_out": request_body_timed_out,
662
        "fixture_declared_request_bytes": declared_request_bytes,
663
        "mutation_requests_observed": request_count,
664
        "blind_replay_requests": max(0, request_count - 1),
665
        "network_request_bytes_observed": request_bytes,
666
        "local_head_preserved": bool(local_head_before) and local_head_before == local_head_after,
667
        "worktree_payload_preserved": payload_preserved,
668
        "unpublished_state_preserved": unpublished_state_preserved,
669
        "local_work_preserved": payload_preserved and unpublished_state_preserved,
670
        "working_tree_dirty_after": None if status_after is None else status_after.get("dirty"),
671
    }
672
673
674
def cli_publication_observation(row: dict[str, Any], *, json_requested: bool) -> dict[str, str]:
675
    """Classify only what the CLI reported, never what the fault fixture knows."""
676
    if row.get("timed_out"):
677
        return {"reported_outcome": "unreported", "observation": "process_timeout"}
678
    stdout = str(row.get("stdout") or "")
679
    stderr = str(row.get("stderr") or "")
680
    value = _parse_json_object(stdout) if json_requested else None
681
    if value is not None and row.get("process_returncode") == 0 and value.get("published") is True:
682
        return {"reported_outcome": "published", "observation": "json_success_receipt"}
683
    if value is not None and isinstance(value.get("error"), dict):
684
        message = str(value["error"].get("message") or "").lower()
685
        if any(marker in message for marker in ("unconfirmed", "unknown", "may have")):
686
            return {"reported_outcome": "unknown", "observation": "json_error_unknown"}
687
        return {
688
            "reported_outcome": "unreported",
689
            "observation": "json_error_without_publication_outcome",
690
        }
691
    message = f"{stdout}\n{stderr}".lower()
692
    if any(marker in message for marker in ("unconfirmed", "unknown", "may have")):
693
        return {"reported_outcome": "unknown", "observation": "text_error_unknown"}
694
    return {"reported_outcome": "unreported", "observation": "text_error_without_publication_outcome"}
695
696
697
def file_sha256(path: Path) -> str | None:
698
    try:
699
        return hashlib.sha256(path.read_bytes()).hexdigest()
700
    except OSError:
701
        return None
702
703
704
class _DisconnectAfterPushHandler(BaseHTTPRequestHandler):
705
    """Minimal loopback fixture for a complete request with a lost reply."""
706
707
    server_version = "oakbench-interruption/1"
708
709
    def log_message(self, _format: str, *_args: object) -> None:
710
        return
711
712
    def _json(self, status: int, payload: dict[str, Any]) -> None:
713
        encoded = json.dumps(payload).encode()
714
        self.send_response(status)
715
        self.send_header("Content-Type", "application/json")
716
        self.send_header("Content-Length", str(len(encoded)))
717
        self.end_headers()
718
        self.wfile.write(encoded)
719
720
    def do_GET(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
721
        self._json(404, {"error": "absent synthetic repo"})
722
723
    def do_POST(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
724
        declared_size: int | None = None
725
        framing_valid = False
726
        body = b""
727
        body_timed_out = False
728
        raw_sizes = self.headers.get_all("Content-Length", failobj=[])
729
        try:
730
            raw_size = raw_sizes[0].strip(" \t") if len(raw_sizes) == 1 else ""
731
            parsed_size = int(raw_size) if raw_size.isascii() and raw_size.isdecimal() else -1
732
            framing_valid = (
733
                0 <= parsed_size <= INTERRUPTED_PUBLICATION_MAX_REQUEST_BYTES
734
                and self.headers.get("Transfer-Encoding") is None
735
            )
736
            if framing_valid:
737
                declared_size = parsed_size
738
                self.connection.settimeout(INTERRUPTED_PUBLICATION_REQUEST_TIMEOUT_S)
739
                while len(body) < parsed_size:
740
                    try:
741
                        chunk = self.rfile.read1(parsed_size - len(body))
742
                    except (socket.timeout, TimeoutError):
743
                        body_timed_out = True
744
                        break
745
                    except OSError:
746
                        break
747
                    if not chunk:
748
                        break
749
                    body += chunk
750
        except ValueError:
751
            framing_valid = False
752
        server = self.server
753
        if self.path == "/api/repos":
754
            self._json(200, {"name": "repo"})
755
            return
756
        if self.path == "/api/oak/repo/push":
757
            with server.receipt_lock:  # type: ignore[attr-defined]
758
                server.push_request_count += 1  # type: ignore[attr-defined]
759
                server.push_request_bytes += len(body)  # type: ignore[attr-defined]
760
                server.push_declared_request_bytes = declared_size  # type: ignore[attr-defined]
761
                server.push_request_framing_valid = framing_valid  # type: ignore[attr-defined]
762
                server.push_request_body_timed_out = body_timed_out  # type: ignore[attr-defined]
763
                server.request_complete_before_disconnect = (  # type: ignore[attr-defined]
764
                    framing_valid and len(body) == declared_size
765
                )
766
            self.close_connection = True
767
            try:
768
                self.connection.shutdown(socket.SHUT_RDWR)
769
            except OSError:
770
                pass
771
            self.connection.close()
772
            return
773
        self._json(404, {"error": "unexpected synthetic path"})
774
775
776
def start_interrupted_publication_server() -> tuple[ThreadingHTTPServer, threading.Thread]:
777
    server = ThreadingHTTPServer(("127.0.0.1", 0), _DisconnectAfterPushHandler)
778
    server.receipt_lock = threading.Lock()  # type: ignore[attr-defined]
779
    server.push_request_count = 0  # type: ignore[attr-defined]
780
    server.push_request_bytes = 0  # type: ignore[attr-defined]
781
    server.push_declared_request_bytes = None  # type: ignore[attr-defined]
782
    server.push_request_framing_valid = False  # type: ignore[attr-defined]
783
    server.push_request_body_timed_out = False  # type: ignore[attr-defined]
784
    server.request_complete_before_disconnect = False  # type: ignore[attr-defined]
785
    thread = threading.Thread(target=server.serve_forever, name="oakbench-interrupted-push", daemon=True)
786
    thread.start()
787
    return server, thread
788
789
790
def _parse_json_object(text: str | None) -> dict[str, Any] | None:
791
    try:
792
        value = json.loads(text or "")
793
    except (TypeError, json.JSONDecodeError):
794
        return None
795
    return value if isinstance(value, dict) else None
796
797
798
def run_interrupted_publication(
799
    subject: Subject,
800
    run_root: Path,
801
    run_index: int,
802
    metadata: dict[str, Any],
803
    admitted: int,
804
) -> list[dict[str, Any]]:
805
    """Run one real Oak push against a loopback server that drops its reply.
806
807
    The fixture observes request delivery but deliberately has no durable
808
    remote state. That makes the client's outcome unknown and lets the lane
809
    prove work preservation and absence of blind replay without production
810
    mutations or a fake success claim.
811
    """
812
    scenario = INTERRUPTED_PUBLICATION_SCENARIO
813
    metadata = {
814
        **metadata,
815
        "benchmark_track": "failure-evidence",
816
        "remote_transport": "loopback_fault_fixture",
817
        "remote_server": "127.0.0.1",
818
    }
819
    if subject.kind != "oak":
820
        return [
821
            skip_row(
822
                subject,
823
                scenario,
824
                run_index,
825
                metadata,
826
                "interrupted publication fixture currently exercises the actual Oak HTTP client only",
827
            )
828
        ]
829
830
    workspace = run_root / f"interrupted-{subject.name}-r{run_index}"
831
    workspace.mkdir(parents=True)
832
    rows: list[dict[str, Any]] = []
833
    vcs = str(subject.bin)
834
    rows.append(
835
        step_row(subject, scenario, "setup.init", [vcs, "init", "."], workspace, run_index, metadata, admitted)
836
    )
837
    if row_returncode(rows[-1]) != 0:
838
        return rows
839
    rows.append(
840
        edit_step(
841
            subject,
842
            scenario,
843
            "setup.edit",
844
            workspace,
845
            f"interrupted publication run={run_index}",
846
            run_index,
847
            metadata,
848
            admitted,
849
        )
850
    )
851
    rows.append(
852
        step_row(subject, scenario, "setup.checkpoint", [vcs, "commit"], workspace, run_index, metadata, admitted)
853
    )
854
    if row_returncode(rows[-1]) != 0:
855
        return rows
856
857
    before = run_untimed([vcs, "hash"], workspace)
858
    head_before = before.stdout.strip() if before.returncode == 0 else None
859
    payload_path = workspace / "task-notes.md"
860
    payload_before = file_sha256(payload_path)
861
    before_usage = bounded_tree_usage(workspace)
862
    push_help = run_untimed([vcs, "push", "--help"], workspace)
863
    push_supports_json = "--json" in (push_help.stdout + push_help.stderr)
864
    server, thread = start_interrupted_publication_server()
865
    host, port = server.server_address[:2]
866
    remote = f"http://{host}:{port}"
867
    fixture_env = base_env()
868
    fixture_env.pop("OAK_API_KEY", None)
869
    fixture_env.pop("OAK_REMOTE", None)
870
    fixture_env["OAK_NO_UPDATE_CHECK"] = "1"
871
    try:
872
        push_command = [vcs, "push", "--repo", "oak/repo", "--remote", remote]
873
        if push_supports_json:
874
            push_command.append("--json")
875
        publication_attempt = step_row(
876
            subject,
877
            scenario,
878
            "publication.attempt",
879
            push_command,
880
            workspace,
881
            run_index,
882
            metadata,
883
            admitted,
884
            expected_returncodes=tuple(range(1, 128)),
885
            preserve_stdout=True,
886
            preserve_stderr=True,
887
            env=fixture_env,
888
            timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
889
        )
890
        rows.append(publication_attempt)
891
    finally:
892
        server.shutdown()
893
        server.server_close()
894
        thread.join(timeout=2.0)
895
896
    state_row = step_row(
897
        subject,
898
        scenario,
899
        "reconcile.local_state",
900
        [vcs, "agent", "state", "--json", "--compact"],
901
        workspace,
902
        run_index,
903
        metadata,
904
        admitted,
905
        preserve_stdout=True,
906
        env=fixture_env,
907
    )
908
    rows.append(state_row)
909
    after = run_untimed([vcs, "hash"], workspace)
910
    head_after = after.stdout.strip() if after.returncode == 0 else None
911
    payload_after = file_sha256(payload_path)
912
    status_after = _parse_json_object(state_row.get("stdout"))
913
    with server.receipt_lock:  # type: ignore[attr-defined]
914
        request_count = int(server.push_request_count)  # type: ignore[attr-defined]
915
        request_bytes = int(server.push_request_bytes)  # type: ignore[attr-defined]
916
        declared_request_bytes = server.push_declared_request_bytes  # type: ignore[attr-defined]
917
        request_framing_valid = bool(server.push_request_framing_valid)  # type: ignore[attr-defined]
918
        request_body_timed_out = bool(server.push_request_body_timed_out)  # type: ignore[attr-defined]
919
    cli_observation = cli_publication_observation(
920
        publication_attempt,
921
        json_requested=push_supports_json,
922
    )
923
    evidence = interrupted_publication_fields(
924
        request_count=request_count,
925
        request_bytes=request_bytes if request_count else None,
926
        declared_request_bytes=declared_request_bytes,
927
        request_framing_valid=request_framing_valid,
928
        request_body_timed_out=request_body_timed_out,
929
        local_head_before=head_before,
930
        local_head_after=head_after,
931
        status_after=status_after,
932
        worktree_payload_before=payload_before,
933
        worktree_payload_after=payload_after,
934
        cli_observation=cli_observation,
935
    )
936
    evidence["workspace_usage_before"] = before_usage
937
    evidence["workspace_usage_after"] = bounded_tree_usage(workspace)
938
    evidence["remote_transport"] = "loopback_fault_fixture"
939
    evidence["remote_server"] = str(host)
940
    evidence["human_action_count"] = 0
941
    evidence["compatibility_probe_calls"] = 1
942
    evidence["harness_probe_calls"] = 3
943
    evidence["push_json_supported"] = push_supports_json
944
    total = phase_total_row(rows, subject, scenario, run_index, metadata, "interrupted_publication.total")
945
    evidence["harness_command_count"] = total["harness_command_count"] + 3
946
    total.update(evidence)
947
    if not (
948
        evidence["fixture_received_complete_request_before_disconnect"]
949
        and request_count == 1
950
        and evidence["local_head_preserved"]
951
        and evidence["local_work_preserved"]
952
        and not publication_attempt["timed_out"]
953
        and row_returncode(rows[-1]) == 0
954
    ):
955
        total["returncode"] = 1
956
        total["stderr"] = "interrupted_publication_evidence_incomplete"
957
    rows.append(total)
958
    return rows
959
960
961
def edit_step(
962
    subject: Subject,
963
    scenario: str,
964
    operation: str,
965
    workspace: Path,
966
    payload: str,
967
    run_index: int,
968
    metadata: dict[str, Any],
969
    admitted: int,
970
) -> dict[str, Any]:
971
    command = ["/bin/zsh", "-c", f"echo {payload!r} >> task-notes.md"]
972
    return step_row(
973
        subject, scenario, operation, command, workspace, run_index, metadata, admitted, kind="edit"
974
    )
975
976
977
def oak_virtual_branch(subject: Subject, mount_dir: Path) -> str | None:
978
    proc = run_untimed([str(subject.bin), "status"], mount_dir)
979
    match = VIRTUAL_BRANCH_RE.search(proc.stdout + proc.stderr)
980
    return match.group(1) if match else None
981
982
983
def run_oak_loop(
984
    subject: Subject,
985
    remote: oakbench_remotes.RemoteResolution,
986
    run_root: Path,
987
    run_index: int,
988
    metadata: dict[str, Any],
989
    admitted: int,
990
) -> list[dict[str, Any]]:
991
    scenario = "task_loop"
992
    meta = {**metadata, **remote.row_fields()}
993
    rows: list[dict[str, Any]] = []
994
    branches: dict[str, str | None] = {}
995
    payloads: dict[str, str] = {}
996
    mounts: list[Path] = []
997
    vcs = str(subject.bin)
998
    try:
999
        for phase in PHASES:
1000
            workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
1001
            payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
1002
            payloads[phase] = payload
1003
            acquire = step_row(
1004
                subject, scenario, f"{phase}.acquire", [vcs, "mount", remote.repo or "", str(workspace)],
1005
                run_root, run_index, meta, admitted,
1006
            )
1007
            rows.append(acquire)
1008
            if int(acquire["returncode"]) != 0:
1009
                return rows
1010
            mounts.append(workspace)
1011
            branches[phase] = oak_virtual_branch(subject, workspace)
1012
            rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
1013
            rows.append(step_row(subject, scenario, f"{phase}.snapshot", [vcs, "commit", "--no-verify"], workspace, run_index, meta, admitted))
1014
            rows.append(step_row(subject, scenario, f"{phase}.publish", [vcs, "push"], workspace, run_index, meta, admitted))
1015
            if phase == "task":
1016
                rows.append(
1017
                    step_row(
1018
                        subject, scenario, "task.describe",
1019
                        [vcs, "desc", f"bench task loop {metadata['bench_id']} r{run_index}"],
1020
                        workspace, run_index, meta, admitted,
1021
                    )
1022
                )
1023
            end = step_row(
1024
                subject, scenario, f"{phase}.end", [vcs, "mount", "end", str(workspace)],
1025
                run_root, run_index, meta, admitted,
1026
            )
1027
            rows.append(end)
1028
            if int(end["returncode"]) == 0 and workspace in mounts:
1029
                mounts.remove(workspace)
1030
1031
        # Untimed oracle: each phase's payload must be on its pushed branch.
1032
        verifier = run_root / f"tl-verify-{subject.name}-r{run_index}"
1033
        clone = run_untimed([vcs, "clone", remote.repo or "", str(verifier)], run_root)
1034
        oracle_ok: bool | None = None
1035
        if clone.returncode == 0:
1036
            oracle_ok = True
1037
            for phase in PHASES:
1038
                branch = branches.get(phase)
1039
                if not branch:
1040
                    oracle_ok = None
1041
                    break
1042
                switch = run_untimed([vcs, "switch", branch], verifier)
1043
                content = (verifier / "task-notes.md")
1044
                seen = content.read_text() if content.exists() else ""
1045
                if switch.returncode != 0 or payloads[phase] not in seen:
1046
                    oracle_ok = False
1047
        total = {
1048
            **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
1049
            "oracle_payload_on_remote_branch": oracle_ok,
1050
            "workspace_branches": branches,
1051
            "measurement_source": "verifier_clone_switch_grep; null oracle means branch name unparsed",
1052
        }
1053
        rows.append(enforce_oracle_result(total, oracle_ok))
1054
        return rows
1055
    finally:
1056
        for mount_dir in mounts:
1057
            run_untimed([vcs, "mount", "end", str(mount_dir), "-f"], run_root)
1058
1059
1060
def ensure_git_remote_seeded(subject: Subject, remote: oakbench_remotes.RemoteResolution, scratch: Path) -> str | None:
1061
    """Untimed: make sure the git remote has a main with a seed commit."""
1062
    vcs = str(subject.bin)
1063
    probe = run_untimed([vcs, "ls-remote", str(remote.repo), "refs/heads/main"], None)
1064
    if probe.returncode == 0 and probe.stdout.strip():
1065
        if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE:
1066
            head = run_untimed([vcs, "symbolic-ref", "HEAD", "refs/heads/main"], Path(str(remote.repo)))
1067
            if head.returncode != 0:
1068
                return f"git remote HEAD setup failed: {head.stderr.strip()[:200]}"
1069
        return None
1070
    seed = scratch / "git-seed"
1071
    if seed.exists():
1072
        shutil.rmtree(seed)
1073
    seed.mkdir(parents=True)
1074
    (seed / "README.md").write_text("Task-loop benchmark repo (disposable).\n")
1075
    (seed / "task-notes.md").write_text("# task notes\n")
1076
    for command in (
1077
        [vcs, "init", "-q", "-b", "main"],
1078
        [vcs, "add", "."],
1079
        [vcs, "commit", "-q", "-m", "seed"],
1080
        [vcs, "push", "-q", str(remote.repo), "main:main"],
1081
    ):
1082
        proc = run_untimed(command, seed)
1083
        if proc.returncode != 0:
1084
            return f"git remote seed `{' '.join(command[1:3])}` failed: {proc.stderr.strip()[:200]}"
1085
    if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE:
1086
        head = run_untimed([vcs, "symbolic-ref", "HEAD", "refs/heads/main"], Path(str(remote.repo)))
1087
        if head.returncode != 0:
1088
            return f"git remote HEAD setup failed: {head.stderr.strip()[:200]}"
1089
    return None
1090
1091
1092
def run_git_agent_task_baseline(
1093
    subject: Subject,
1094
    remote: oakbench_remotes.RemoteResolution,
1095
    run_root: Path,
1096
    run_index: int,
1097
    metadata: dict[str, Any],
1098
    admitted: int,
1099
    remote_create_ms: float = 0.0,
1100
) -> list[dict[str, Any]]:
1101
    """Measure a cold clone and a warm isolated worktree on one local remote.
1102
1103
    This is the core-equivalent, local-file cell. Hosted CI is deliberately a
1104
    skip row: a local commit oracle is not relabelled as provider CI.
1105
    """
1106
    scenario = AGENT_TASK_SCENARIO
1107
    meta = {
1108
        **metadata,
1109
        **remote.row_fields(),
1110
        "benchmark_track": (
1111
            "core-equivalent"
1112
            if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE
1113
            else "agent-default"
1114
        ),
1115
    }
1116
    vcs = str(subject.bin)
1117
    rows: list[dict[str, Any]] = [
1118
        harness_step_row(
1119
            subject,
1120
            scenario,
1121
            "setup.remote_container",
1122
            run_index,
1123
            meta,
1124
            remote_create_ms,
1125
            harness_command_count=1,
1126
        )
1127
    ]
1128
    setup_start = time.perf_counter()
1129
    seed_error = ensure_git_remote_seeded(subject, remote, run_root)
1130
    rows.append(
1131
        harness_step_row(
1132
            subject,
1133
            scenario,
1134
            "setup.remote_seed",
1135
            run_index,
1136
            meta,
1137
            (time.perf_counter() - setup_start) * 1000,
1138
            returncode=1 if seed_error else 0,
1139
            harness_command_count=6,
1140
        )
1141
    )
1142
    if seed_error:
1143
        rows[-1]["stderr"] = seed_error
1144
        return rows
1145
1146
    base = run_root / f"agent-task-{subject.name}-r{run_index}-cold"
1147
    cold_branch = oakbench_remotes.disposable_branch(
1148
        str(metadata["bench_id"]), "agent-task-cold", subject.name, run_index
1149
    )
1150
    warm_branch = oakbench_remotes.disposable_branch(
1151
        str(metadata["bench_id"]), "agent-task-warm", subject.name, run_index
1152
    )
1153
    cold_payload = f"cold task run={run_index} bench={metadata['bench_id']}"
1154
    warm_payload = f"warm task run={run_index} bench={metadata['bench_id']}"
1155
1156
    rows.append(
1157
        step_row(
1158
            subject,
1159
            scenario,
1160
            "cold.acquire",
1161
            [vcs, "clone", "-q", str(remote.repo), str(base)],
1162
            run_root,
1163
            run_index,
1164
            meta,
1165
            admitted,
1166
        )
1167
    )
1168
    if row_returncode(rows[-1]) != 0:
1169
        return rows
1170
    rows.append(
1171
        step_row(
1172
            subject, scenario, "cold.branch", [vcs, "switch", "-q", "-c", cold_branch],
1173
            base, run_index, meta, admitted,
1174
        )
1175
    )
1176
    rows.append(
1177
        step_row(
1178
            subject, scenario, "cold.inspect", [vcs, "status", "--porcelain=v1"],
1179
            base, run_index, meta, admitted,
1180
        )
1181
    )
1182
    rows.append(edit_step(subject, scenario, "cold.edit", base, cold_payload, run_index, meta, admitted))
1183
    rows.append(step_row(subject, scenario, "cold.checkpoint.add", [vcs, "add", "."], base, run_index, meta, admitted))
1184
    rows.append(
1185
        step_row(
1186
            subject, scenario, "cold.checkpoint.commit",
1187
            [vcs, "commit", "-q", "-m", f"agent task cold {metadata['bench_id']} r{run_index}"],
1188
            base, run_index, meta, admitted,
1189
        )
1190
    )
1191
    rows.append(
1192
        step_row(
1193
            subject, scenario, "cold.publish", [vcs, "push", "-q", "-u", "origin", cold_branch],
1194
            base, run_index, meta, admitted,
1195
        )
1196
    )
1197
    rows.append(
1198
        unmeasured_step_row(
1199
            subject, scenario, "cold.ci.observe", run_index, meta,
1200
            "local-file transport has no hosted CI provider; exact CI observation is unmeasured",
1201
        )
1202
    )
1203
    rows.append(
1204
        step_row(
1205
            subject, scenario, "cold.review_handoff",
1206
            [vcs, "show", "--format=%H%n%s", "--stat", "--oneline", "HEAD"],
1207
            base, run_index, meta, admitted,
1208
        )
1209
    )
1210
1211
    warm = run_root / f"agent-task-{subject.name}-r{run_index}-warm"
1212
    rows.append(
1213
        step_row(
1214
            subject, scenario, "warm.acquire",
1215
            [vcs, "worktree", "add", "-q", str(warm), "-b", warm_branch, "origin/main"],
1216
            base, run_index, meta, admitted,
1217
        )
1218
    )
1219
    if row_returncode(rows[-1]) == 0:
1220
        rows.append(
1221
            step_row(
1222
                subject, scenario, "warm.inspect", [vcs, "status", "--porcelain=v1"],
1223
                warm, run_index, meta, admitted,
1224
            )
1225
        )
1226
        rows.append(edit_step(subject, scenario, "warm.edit", warm, warm_payload, run_index, meta, admitted))
1227
        rows.append(step_row(subject, scenario, "warm.checkpoint.add", [vcs, "add", "."], warm, run_index, meta, admitted))
1228
        rows.append(
1229
            step_row(
1230
                subject, scenario, "warm.checkpoint.commit",
1231
                [vcs, "commit", "-q", "-m", f"agent task warm {metadata['bench_id']} r{run_index}"],
1232
                warm, run_index, meta, admitted,
1233
            )
1234
        )
1235
        rows.append(
1236
            step_row(
1237
                subject, scenario, "warm.publish", [vcs, "push", "-q", "-u", "origin", warm_branch],
1238
                warm, run_index, meta, admitted,
1239
            )
1240
        )
1241
        rows.append(
1242
            unmeasured_step_row(
1243
                subject, scenario, "warm.ci.observe", run_index, meta,
1244
                "local-file transport has no hosted CI provider; exact CI observation is unmeasured",
1245
            )
1246
        )
1247
        rows.append(
1248
            step_row(
1249
                subject, scenario, "warm.review_handoff",
1250
                [vcs, "show", "--format=%H%n%s", "--stat", "--oneline", "HEAD"],
1251
                warm, run_index, meta, admitted,
1252
            )
1253
        )
1254
1255
    usage_before_cleanup = {
1256
        "cold": bounded_tree_usage(base),
1257
        "warm": bounded_tree_usage(warm),
1258
    }
1259
    oracle_ok = True
1260
    for branch, payload in ((cold_branch, cold_payload), (warm_branch, warm_payload)):
1261
        show = run_untimed([vcs, "--git-dir", str(remote.repo), "show", f"{branch}:task-notes.md"], run_root)
1262
        if show.returncode != 0 or payload not in show.stdout:
1263
            oracle_ok = False
1264
1265
    if warm.exists():
1266
        rows.append(
1267
            step_row(
1268
                subject, scenario, "cleanup.warm",
1269
                [vcs, "worktree", "remove", str(warm)], base, run_index, meta, admitted,
1270
            )
1271
        )
1272
    cleanup_start = time.perf_counter()
1273
    shutil.rmtree(base, ignore_errors=True)
1274
    rows.append(
1275
        harness_step_row(
1276
            subject, scenario, "cleanup.cold", run_index, meta,
1277
            (time.perf_counter() - cleanup_start) * 1000,
1278
        )
1279
    )
1280
1281
    total = phase_total_row(rows, subject, scenario, run_index, meta, "agent_task.total")
1282
    total.update(
1283
        {
1284
            "oracle_payload_on_remote_branch": oracle_ok,
1285
            "acquisition_models": {
1286
                "cold": "fresh_full_clone",
1287
                "warm": "shared_object_worktree",
1288
            },
1289
            "repository_cache_state": {
1290
                "cold": "fresh_destination_no_shared_object_store",
1291
                "warm": "explicit_shared_object_store",
1292
            },
1293
            "os_cache_state": "unknown",
1294
            "os_cache_state_reason": "page_cache_not_purged_or_instrumented",
1295
            "workspace_usage_before_cleanup": usage_before_cleanup,
1296
            "network_bytes": None,
1297
            "network_bytes_reason": "local_file_transport_not_observed_by_byte_proxy",
1298
            "partial_clone_comparator": None,
1299
            "partial_clone_reason": "local file transport does not establish a network partial-clone comparison",
1300
            "human_action_count": 0,
1301
            "parallel_evidence_lane": "parallel_contention.py/workspace_per_task",
1302
            "oracle_probe_calls": 2,
1303
            "harness_command_count": total["harness_command_count"] + 2,
1304
        }
1305
    )
1306
    if not oracle_ok:
1307
        total["returncode"] = 1
1308
        total["stderr"] = "agent_task_remote_payload_oracle_failed"
1309
    rows.append(total)
1310
    return rows
1311
1312
1313
def reserve_loopback_port() -> int:
1314
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
1315
        listener.bind(("127.0.0.1", 0))
1316
        return int(listener.getsockname()[1])
1317
1318
1319
def wait_for_loopback_port(
1320
    process: subprocess.Popen[Any], port: int, timeout_s: float = LOCAL_OAK_SERVER_READY_TIMEOUT_S
1321
) -> bool:
1322
    deadline = time.monotonic() + timeout_s
1323
    while time.monotonic() < deadline:
1324
        if process.poll() is not None:
1325
            return False
1326
        try:
1327
            with socket.create_connection(("127.0.0.1", port), timeout=0.2):
1328
                return True
1329
        except OSError:
1330
            time.sleep(0.05)
1331
    return False
1332
1333
1334
def harness_command_row(
1335
    subject: Subject,
1336
    scenario: str,
1337
    operation: str,
1338
    command: list[str],
1339
    cwd: Path,
1340
    run_index: int,
1341
    metadata: dict[str, Any],
1342
) -> tuple[dict[str, Any], subprocess.CompletedProcess[str]]:
1343
    started = time.perf_counter()
1344
    result = run_bounded_harness_command(command, cwd)
1345
    row = harness_step_row(
1346
        subject,
1347
        scenario,
1348
        operation,
1349
        run_index,
1350
        metadata,
1351
        (time.perf_counter() - started) * 1000,
1352
        returncode=0 if result.returncode == 0 else result.returncode,
1353
        harness_command_count=1,
1354
    )
1355
    if result.returncode != 0:
1356
        row["stderr"] = str(result.stderr)[-4000:]
1357
    return row, result
1358
1359
1360
def run_bounded_harness_command(
1361
    command: list[str], cwd: Path, *, env: dict[str, str] | None = None
1362
) -> subprocess.CompletedProcess[str]:
1363
    capture = run_timed(
1364
        command,
1365
        cwd,
1366
        env or base_env(),
1367
        admitted_output_chars=4_000,
1368
        timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
1369
    )
1370
    return subprocess.CompletedProcess(
1371
        command,
1372
        capture.returncode,
1373
        capture.stdout_text,
1374
        capture.stderr_text,
1375
    )
1376
1377
1378
def remote_branch_head(remote: str, repo_spec: str, branch: str) -> str | None:
1379
    owner, repo = repo_spec.split("/", 1)
1380
    branch_segment = urllib.parse.quote(branch, safe="")
1381
    url = f"{remote}/api/{owner}/{repo}/branches/{branch_segment}"
1382
    try:
1383
        with urllib.request.urlopen(url, timeout=2.0) as response:
1384
            value = json.loads(response.read())
1385
    except (OSError, ValueError, urllib.error.URLError):
1386
        return None
1387
    head = value.get("head") if isinstance(value, dict) else None
1388
    return head if isinstance(head, str) else None
1389
1390
1391
def run_oak_local_agent_task_baseline(
1392
    subject: Subject,
1393
    run_root: Path,
1394
    run_index: int,
1395
    metadata: dict[str, Any],
1396
    admitted: int,
1397
) -> list[dict[str, Any]]:
1398
    """Exercise two fresh Oak checkouts against an owned loopback Oak server."""
1399
    run_root = run_root.resolve()
1400
    scenario = AGENT_TASK_OAK_LOCAL_SCENARIO
1401
    meta = {
1402
        **metadata,
1403
        "benchmark_track": "oak-local-loopback",
1404
        "remote_transport": "loopback_http",
1405
        "remote_server": "127.0.0.1",
1406
    }
1407
    rows: list[dict[str, Any]] = []
1408
    vcs = str(subject.bin)
1409
    server_root = run_root / f"oak-local-server-{subject.name}-r{run_index}"
1410
    seed = run_root / f"oak-local-seed-{subject.name}-r{run_index}"
1411
    cold = run_root / f"oak-local-cold-{subject.name}-r{run_index}"
1412
    followup = run_root / f"oak-local-followup-{subject.name}-r{run_index}"
1413
    verifier = run_root / f"oak-local-verifier-{subject.name}-r{run_index}"
1414
    repo_spec = f"qa/task-loop-{subject.name}-r{run_index}"
1415
    port = reserve_loopback_port()
1416
    remote = f"http://127.0.0.1:{port}"
1417
    server_root.mkdir(parents=True)
1418
    server_started = time.perf_counter()
1419
    server: subprocess.Popen[Any] | None = None
1420
    server_start_error: str | None = None
1421
    try:
1422
        server = subprocess.Popen(
1423
            [vcs, "serve", "--dir", str(server_root), "--host", "127.0.0.1", "--port", str(port)],
1424
            cwd=run_root,
1425
            env=base_env(),
1426
            stdout=subprocess.DEVNULL,
1427
            stderr=subprocess.DEVNULL,
1428
            start_new_session=os.name == "posix",
1429
        )
1430
    except OSError as error:
1431
        server_start_error = type(error).__name__
1432
    ready = server is not None and wait_for_loopback_port(server, port)
1433
    rows.append(
1434
        harness_step_row(
1435
            subject,
1436
            scenario,
1437
            "setup.server",
1438
            run_index,
1439
            meta,
1440
            (time.perf_counter() - server_started) * 1000,
1441
            returncode=0 if ready else 1,
1442
            harness_command_count=1,
1443
        )
1444
    )
1445
    branch: str | None = None
1446
    expected_head: str | None = None
1447
    expected_payload = "# task notes\n"
1448
    failure: str | None = None if ready else (
1449
        f"local_oak_server_start_failed:{server_start_error}"
1450
        if server_start_error else "local_oak_server_not_ready"
1451
    )
1452
    try:
1453
        if failure is None:
1454
            seed.mkdir(parents=True)
1455
            (seed / "task-notes.md").write_text(expected_payload)
1456
            for operation, command in (
1457
                ("setup.seed_init", [vcs, "init", "."]),
1458
                ("setup.seed_commit", [vcs, "commit"]),
1459
                ("setup.seed_push", [vcs, "push", "--repo", repo_spec, "--remote", remote]),
1460
            ):
1461
                row, result = harness_command_row(
1462
                    subject, scenario, operation, command, seed, run_index, meta
1463
                )
1464
                rows.append(row)
1465
                if result.returncode != 0:
1466
                    failure = f"{operation}_failed"
1467
                    break
1468
1469
        def agent_step(operation: str, command: list[str], cwd: Path, *, preserve: bool = False) -> dict[str, Any]:
1470
            row = step_row(
1471
                subject,
1472
                scenario,
1473
                operation,
1474
                command,
1475
                cwd,
1476
                run_index,
1477
                meta,
1478
                admitted,
1479
                preserve_stdout=preserve,
1480
                timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
1481
            )
1482
            rows.append(row)
1483
            return row
1484
1485
        if failure is None:
1486
            row = agent_step(
1487
                "cold.acquire",
1488
                [vcs, "clone", repo_spec, str(cold), "--remote", remote, "--allow-legacy-scope"],
1489
                run_root,
1490
            )
1491
            if row_returncode(row) != 0:
1492
                failure = "cold.acquire_failed"
1493
        if failure is None:
1494
            status = agent_step("cold.inspect", [vcs, "status", "--json"], cold, preserve=True)
1495
            status_value = _parse_json_object(status.get("stdout"))
1496
            branch_value = status_value.get("branch") if status_value else None
1497
            branch = branch_value if isinstance(branch_value, str) else None
1498
            if not branch:
1499
                failure = "cold.branch_identity_missing"
1500
        if failure is None:
1501
            cold_payload = f"cold task run={run_index} bench={metadata['bench_id']}\n"
1502
            expected_payload += cold_payload
1503
            rows.append(edit_step(subject, scenario, "cold.edit", cold, cold_payload.strip(), run_index, meta, admitted))
1504
            if row_returncode(agent_step("cold.checkpoint", [vcs, "commit"], cold)) != 0:
1505
                failure = "cold.checkpoint_failed"
1506
            elif row_returncode(
1507
                agent_step("cold.publish", [vcs, "push", "--repo", repo_spec, "--remote", remote], cold)
1508
            ) != 0:
1509
                failure = "cold.publish_failed"
1510
            rows.append(
1511
                unmeasured_step_row(
1512
                    subject, scenario, "cold.ci.observe", run_index, meta,
1513
                    "owned local oak serve has no configured CI provider",
1514
                )
1515
            )
1516
            rows.append(
1517
                unmeasured_step_row(
1518
                    subject, scenario, "cold.review_handoff", run_index, meta,
1519
                    "owned local oak serve does not provide hosted review semantics",
1520
                )
1521
            )
1522
        if failure is None and branch is not None:
1523
            row = agent_step(
1524
                "followup.acquire",
1525
                [
1526
                    vcs, "clone", repo_spec, str(followup), "--remote", remote,
1527
                    "--branch", branch, "--allow-legacy-scope",
1528
                ],
1529
                run_root,
1530
            )
1531
            if row_returncode(row) != 0:
1532
                failure = "followup.acquire_failed"
1533
        if failure is None:
1534
            agent_step("followup.inspect", [vcs, "status", "--json"], followup)
1535
            followup_payload = f"followup task run={run_index} bench={metadata['bench_id']}\n"
1536
            expected_payload += followup_payload
1537
            rows.append(
1538
                edit_step(
1539
                    subject, scenario, "followup.edit", followup,
1540
                    followup_payload.strip(), run_index, meta, admitted,
1541
                )
1542
            )
1543
            if row_returncode(agent_step("followup.checkpoint", [vcs, "commit"], followup)) != 0:
1544
                failure = "followup.checkpoint_failed"
1545
            else:
1546
                head_result = run_bounded_harness_command([vcs, "hash"], followup)
1547
                expected_head = head_result.stdout.strip() if head_result.returncode == 0 else None
1548
                if row_returncode(
1549
                    agent_step(
1550
                        "followup.publish",
1551
                        [vcs, "push", "--repo", repo_spec, "--remote", remote],
1552
                        followup,
1553
                    )
1554
                ) != 0:
1555
                    failure = "followup.publish_failed"
1556
            rows.append(
1557
                unmeasured_step_row(
1558
                    subject, scenario, "followup.ci.observe", run_index, meta,
1559
                    "owned local oak serve has no configured CI provider",
1560
                )
1561
            )
1562
            rows.append(
1563
                unmeasured_step_row(
1564
                    subject, scenario, "followup.review_handoff", run_index, meta,
1565
                    "owned local oak serve does not provide hosted review semantics",
1566
                )
1567
            )
1568
1569
        payload_ok = False
1570
        verifier_head: str | None = None
1571
        advertised_head: str | None = None
1572
        if failure is None and branch is not None:
1573
            clone = run_bounded_harness_command(
1574
                [
1575
                    vcs, "clone", repo_spec, str(verifier), "--remote", remote,
1576
                    "--branch", branch, "--allow-legacy-scope",
1577
                ],
1578
                run_root,
1579
            )
1580
            if clone.returncode == 0:
1581
                payload_path = verifier / "task-notes.md"
1582
                payload_ok = payload_path.exists() and payload_path.read_text() == expected_payload
1583
                verifier_result = run_bounded_harness_command([vcs, "hash"], verifier)
1584
                verifier_head = verifier_result.stdout.strip() if verifier_result.returncode == 0 else None
1585
                advertised_head = remote_branch_head(remote, repo_spec, branch)
1586
            else:
1587
                failure = "verifier_clone_failed"
1588
        head_ok = bool(expected_head and expected_head == verifier_head == advertised_head)
1589
        if not payload_ok:
1590
            failure = failure or "local_oak_payload_oracle_failed"
1591
        if not head_ok:
1592
            failure = failure or "local_oak_head_oracle_failed"
1593
    finally:
1594
        if server is not None:
1595
            stop_owned_process(server)
1596
        server_stopped = server is None or server.poll() is not None
1597
        cleanup_started = time.perf_counter()
1598
        for path in (seed, cold, followup, verifier, server_root):
1599
            shutil.rmtree(path, ignore_errors=True)
1600
        rows.append(
1601
            harness_step_row(
1602
                subject,
1603
                scenario,
1604
                "cleanup.local_server_and_checkouts",
1605
                run_index,
1606
                meta,
1607
                (time.perf_counter() - cleanup_started) * 1000,
1608
            )
1609
        )
1610
1611
    total = phase_total_row(rows, subject, scenario, run_index, meta, "agent_task_oak_local.total")
1612
    total.update(
1613
        {
1614
            "oracle_payload_on_remote_branch": payload_ok and head_ok,
1615
            "oracle_payload_matches": payload_ok,
1616
            "oracle_branch_head_matches": head_ok,
1617
            "server_process_stopped": server_stopped,
1618
            "acquisition_models": {
1619
                "cold": "fresh_full_clone",
1620
                "followup": "second_fresh_full_clone",
1621
            },
1622
            "repository_cache_state": {
1623
                "cold": "fresh_destination_no_shared_object_store",
1624
                "followup": "no_shared_object_store",
1625
            },
1626
            "os_cache_state": "unknown",
1627
            "os_cache_state_reason": "page_cache_not_purged_or_instrumented",
1628
            "network_bytes": None,
1629
            "network_bytes_reason": "loopback transport not observed by byte proxy",
1630
            "hosted_ci_configured": False,
1631
            "oracle_probe_calls": 4,
1632
            # The expected-head hash plus verifier clone/hash/HTTP head probe
1633
            # are oracle/setup commands, not agent-facing tool calls.
1634
            "harness_command_count": total["harness_command_count"] + 4,
1635
        }
1636
    )
1637
    if failure or not server_stopped:
1638
        total["returncode"] = 1
1639
        total["stderr"] = failure or "local_oak_server_cleanup_failed"
1640
    rows.append(total)
1641
    return rows
1642
1643
1644
def run_legacy_selected_materialization_campaign(
1645
    subjects: list[Subject],
1646
    server_bin: Path | None,
1647
    server_source: Path | None,
1648
    server_source_head: str | None,
1649
    baseline_subject_name: str | None,
1650
    run_root: Path,
1651
    runs: int,
1652
    metadata: dict[str, Any],
1653
    admitted: int,
1654
    randomize_order: bool,
1655
    random_seed: int,
1656
    claim_timing: bool,
1657
) -> list[dict[str, Any]]:
1658
    """Measure one legacy selected-branch clone against one fixed Serve fixture."""
1659
    scenario = LEGACY_SELECTED_MATERIALIZATION_SCENARIO
1660
    oak_subjects = [subject for subject in subjects if subject.kind == "oak"]
1661
    if not oak_subjects:
1662
        return []
1663
    if (
1664
        server_bin is None
1665
        or not server_bin.is_absolute()
1666
        or not server_bin.is_file()
1667
        or not os.access(server_bin, os.X_OK)
1668
    ):
1669
        return [
1670
            unmeasured_step_row(
1671
                subject,
1672
                scenario,
1673
                "scenario.skip",
1674
                0,
1675
                metadata,
1676
                "--oak-serve-bin must name an absolute executable used only as the fixed Serve fixture",
1677
            )
1678
            for subject in oak_subjects
1679
        ]
1680
    if server_source_head is None or re.fullmatch(r"[0-9a-f]{64}", server_source_head) is None:
1681
        return [
1682
            unmeasured_step_row(
1683
                subject,
1684
                scenario,
1685
                "scenario.skip",
1686
                0,
1687
                metadata,
1688
                "the fixed Serve binary's exact 64-hex --oak-serve-source-head is required",
1689
            )
1690
            for subject in oak_subjects
1691
        ]
1692
    baseline_subject = next(
1693
        (subject for subject in oak_subjects if subject.name == baseline_subject_name), None
1694
    )
1695
    if baseline_subject is None:
1696
        return [
1697
            unmeasured_step_row(
1698
                subject,
1699
                scenario,
1700
                "scenario.skip",
1701
                0,
1702
                metadata,
1703
                "--materialization-baseline-subject must name one enabled Oak subject",
1704
            )
1705
            for subject in oak_subjects
1706
        ]
1707
    git_bin = shutil.which("git")
1708
    if git_bin is None:
1709
        return [
1710
            unmeasured_step_row(
1711
                subject,
1712
                scenario,
1713
                "scenario.skip",
1714
                0,
1715
                metadata,
1716
                "git is required only to create the deterministic main-history fixture",
1717
            )
1718
            for subject in oak_subjects
1719
        ]
1720
1721
    server_bin = server_bin.resolve()
1722
    resolved_server_source: Path | None = None
1723
    if server_source is not None:
1724
        if not server_source.is_absolute() or not server_source.is_dir():
1725
            return [
1726
                unmeasured_step_row(
1727
                    subject,
1728
                    scenario,
1729
                    "scenario.skip",
1730
                    0,
1731
                    metadata,
1732
                    "--oak-serve-source must name an absolute read-only source checkout",
1733
                )
1734
                for subject in oak_subjects
1735
            ]
1736
        resolved_server_source = server_source.resolve()
1737
        source_head_result = run_bounded_harness_command(
1738
            [str(server_bin), "hash"],
1739
            resolved_server_source,
1740
            env=isolated_oak_fixture_env(run_root / "source-check"),
1741
        )
1742
        if source_head_result.returncode != 0 or source_head_result.stdout.strip() != server_source_head:
1743
            return [
1744
                unmeasured_step_row(
1745
                    subject,
1746
                    scenario,
1747
                    "scenario.skip",
1748
                    0,
1749
                    metadata,
1750
                    "fixed Serve source checkout does not match --oak-serve-source-head",
1751
                )
1752
                for subject in oak_subjects
1753
            ]
1754
    baseline_binary_sha256 = binary_sha256(baseline_subject.bin)
1755
    timing_claimable = materialization_timing_is_claimable(
1756
        metadata, runs, randomize_order, claim_timing
1757
    )
1758
    fixture_root = run_root / "legacy-selected-materialization-fixture"
1759
    server_root = fixture_root / "server"
1760
    git_seed = fixture_root / "git-seed.git"
1761
    seed = fixture_root / "seed"
1762
    task_config_root = fixture_root / "config"
1763
    for path in (server_root, git_seed, task_config_root):
1764
        path.mkdir(parents=True, exist_ok=True)
1765
    fixture_env = isolated_oak_fixture_env(task_config_root)
1766
    fixture_meta = {
1767
        **metadata,
1768
        "benchmark_track": "oak-local-loopback-materialization",
1769
        "remote_transport": "loopback_http",
1770
        "remote_server": "127.0.0.1",
1771
        "serve_binary": str(server_bin),
1772
        "serve_binary_sha256": binary_sha256(server_bin),
1773
        "serve_source": str(resolved_server_source) if resolved_server_source else None,
1774
        "serve_source_checkout_verification": (
1775
            "exact_head_verified" if resolved_server_source else "unavailable_binary_hash_and_head_supplied"
1776
        ),
1777
        "serve_source_head": server_source_head,
1778
        "serve_version": subject_version(Subject("serve_fixture", "oak", "Serve fixture", server_bin)),
1779
        "baseline_subject": baseline_subject.name,
1780
        "baseline_binary_sha256": baseline_binary_sha256,
1781
        "fixture_id": "legacy-selected-materialization-192x16k-v1",
1782
        "fixture_regular_files": 192,
1783
        "fixture_regular_file_bytes": 16 * 1024,
1784
        "fixture_empty_files": 1,
1785
        "fixture_executable_files": 8,
1786
        "fixture_selected_only_files": 1,
1787
        "os_cache_state": "warm_or_unknown_after_required_fixture_probe",
1788
        "materialization_timing_claimable": timing_claimable,
1789
        "materialization_timing_claim_reason": (
1790
            "explicit_controlled_window_requirements_met"
1791
            if timing_claimable
1792
            else "correctness_only_or_controlled_window_requirements_not_met"
1793
        ),
1794
    }
1795
    rows: list[dict[str, Any]] = []
1796
    setup_subject = baseline_subject
1797
1798
    def setup_command(operation: str, command: list[str], cwd: Path) -> bool:
1799
        started = time.perf_counter()
1800
        result = run_bounded_harness_command(command, cwd, env=fixture_env)
1801
        rows.append(
1802
            harness_step_row(
1803
                setup_subject,
1804
                scenario,
1805
                operation,
1806
                0,
1807
                fixture_meta,
1808
                (time.perf_counter() - started) * 1000,
1809
                returncode=0 if result.returncode == 0 else result.returncode,
1810
                harness_command_count=1,
1811
            )
1812
        )
1813
        if result.returncode != 0:
1814
            rows[-1]["stderr"] = result.stderr[-4000:]
1815
        return result.returncode == 0
1816
1817
    server: subprocess.Popen[Any] | None = None
1818
    proxy: CountingProxy | None = None
1819
    failure: str | None = None
1820
    expected_artifacts: list[str] | None = None
1821
    baseline_cache: dict[str, Any] | None = None
1822
    baseline_receipt_contract: dict[str, Any] | None = None
1823
    baseline_network_contract: dict[str, Any] | None = None
1824
    expected_status_contract: dict[str, Any] | None = None
1825
    selected_branch = "selected-materialization"
1826
    repo_spec = "qa/materialization-bench"
1827
    try:
1828
        for index in range(192):
1829
            directory = git_seed / f"group-{index % 12:02d}"
1830
            directory.mkdir(parents=True, exist_ok=True)
1831
            digest = hashlib.sha256(f"oak-materialization-{index}".encode()).digest()
1832
            content = (digest * ((16 * 1024 + len(digest) - 1) // len(digest)))[: 16 * 1024]
1833
            path = directory / f"file-{index:03d}.bin"
1834
            path.write_bytes(content)
1835
            if index < 8:
1836
                path.chmod(0o755)
1837
        (git_seed / "empty.txt").write_bytes(b"")
1838
        for operation, command in (
1839
            ("setup.git_init", [git_bin, "init", "-q", "-b", "main"]),
1840
            ("setup.git_add", [git_bin, "add", "."]),
1841
            ("setup.git_commit", [git_bin, "commit", "-q", "-m", "fixture main"]),
1842
        ):
1843
            if not setup_command(operation, command, git_seed):
1844
                failure = f"{operation}_failed"
1845
                break
1846
1847
        port = reserve_loopback_port()
1848
        direct_remote = f"http://127.0.0.1:{port}"
1849
        if failure is None:
1850
            server = subprocess.Popen(
1851
                [
1852
                    str(server_bin),
1853
                    "serve",
1854
                    "--dir",
1855
                    str(server_root),
1856
                    "--host",
1857
                    "127.0.0.1",
1858
                    "--port",
1859
                    str(port),
1860
                ],
1861
                cwd=fixture_root,
1862
                env=fixture_env,
1863
                stdout=subprocess.DEVNULL,
1864
                stderr=subprocess.DEVNULL,
1865
                start_new_session=os.name == "posix",
1866
            )
1867
            if not wait_for_loopback_port(server, port):
1868
                failure = "setup.server_not_ready"
1869
        if failure is None:
1870
            for operation, command, cwd in (
1871
                ("setup.oak_import", [str(server_bin), "clone", str(git_seed), str(seed)], fixture_root),
1872
                ("setup.branch", [str(server_bin), "switch", "-c", selected_branch], seed),
1873
            ):
1874
                if not setup_command(operation, command, cwd):
1875
                    failure = f"{operation}_failed"
1876
                    break
1877
        if failure is None:
1878
            (seed / "selected.txt").write_text("selected branch\n")
1879
            for operation, command in (
1880
                ("setup.oak_commit", [str(server_bin), "commit"]),
1881
                (
1882
                    "setup.oak_push",
1883
                    [str(server_bin), "push", "--repo", repo_spec, "--remote", direct_remote],
1884
                ),
1885
            ):
1886
                if not setup_command(operation, command, seed):
1887
                    failure = f"{operation}_failed"
1888
                    break
1889
1890
        expected_head_result = (
1891
            run_bounded_harness_command([str(server_bin), "hash"], seed, env=fixture_env)
1892
            if failure is None
1893
            else None
1894
        )
1895
        expected_head = (
1896
            expected_head_result.stdout.strip()
1897
            if expected_head_result is not None and expected_head_result.returncode == 0
1898
            else None
1899
        )
1900
        expected_history = (
1901
            normalized_history(json_command([str(server_bin), "log", "--json"], seed, fixture_env))
1902
            if failure is None
1903
            else None
1904
        )
1905
        expected_worktree = normalized_worktree_snapshot(seed) if failure is None else None
1906
        if not expected_head or expected_history is None or expected_worktree is None:
1907
            failure = failure or "setup.fixture_oracle_failed"
1908
1909
        if failure is None:
1910
            proxy = CountingProxy("127.0.0.1", port).start()
1911
            measured_remote = f"http://127.0.0.1:{proxy.address[1]}"
1912
            probe = fixture_root / "baseline-probe"
1913
            probe_env = isolated_oak_fixture_env(fixture_root / "baseline-probe-config")
1914
            proxy.reset_counts()
1915
            result = run_bounded_harness_command(
1916
                [
1917
                    str(baseline_subject.bin),
1918
                    "clone",
1919
                    "--json",
1920
                    "--allow-legacy-scope",
1921
                    "--remote",
1922
                    measured_remote,
1923
                    "--branch",
1924
                    selected_branch,
1925
                    repo_spec,
1926
                    str(probe),
1927
                ],
1928
                fixture_root,
1929
                env=probe_env,
1930
            )
1931
            receipt = _parse_json_object(result.stdout)
1932
            evidence = legacy_selected_receipt_evidence(receipt or {})
1933
            baseline_proxy_stats = proxy.snapshot()
1934
            baseline_cache = stat_cache_snapshot(probe)
1935
            expected_artifacts = normalized_oak_artifacts(probe)
1936
            baseline_receipt_contract = legacy_selected_receipt_contract(evidence)
1937
            baseline_network_contract = {
1938
                "connections": baseline_proxy_stats.connections,
1939
                "receipt": baseline_receipt_contract,
1940
            }
1941
            expected_status_contract = normalized_status(
1942
                json_command([str(baseline_subject.bin), "status", "--json"], probe, probe_env)
1943
            )
1944
            probe_checks = {
1945
                "returncode": result.returncode == 0,
1946
                "legacy_route": evidence["legacy_route_observed"],
1947
                "passes_two": evidence["materialization_passes_observed"] == 2,
1948
                "head": evidence["head"] == expected_head,
1949
                "manifest": bool(evidence["manifest"]),
1950
                "artifacts": expected_artifacts is not None,
1951
                "cache": normalized_cache_contract(baseline_cache) is not None,
1952
                "status": expected_status_contract is not None,
1953
                "network": baseline_proxy_stats.connection_errors == 0,
1954
            }
1955
            if not all(probe_checks.values()):
1956
                failed_checks = ",".join(key for key, passed in probe_checks.items() if not passed)
1957
                failure = f"baseline_probe_failed:{failed_checks}"
1958
                if isinstance(baseline_cache, dict) and baseline_cache.get("error"):
1959
                    failure += f":{baseline_cache['error']}"
1960
            shutil.rmtree(probe, ignore_errors=True)
1961
1962
        if failure is None and proxy is not None:
1963
            measured_remote = f"http://127.0.0.1:{proxy.address[1]}"
1964
            order_rng = random.Random(random_seed)
1965
            for run_index in range(runs):
1966
                run_subjects = list(oak_subjects)
1967
                if randomize_order:
1968
                    order_rng.shuffle(run_subjects)
1969
                for subject in run_subjects:
1970
                    destination = run_root / f"materialization-{subject.name}-r{run_index}"
1971
                    subject_env = isolated_oak_fixture_env(
1972
                        run_root / f"materialization-config-{subject.name}-r{run_index}"
1973
                    )
1974
                    proxy.reset_counts()
1975
                    row = step_row(
1976
                        subject,
1977
                        scenario,
1978
                        "legacy_selected.acquire",
1979
                        [
1980
                            str(subject.bin),
1981
                            "clone",
1982
                            "--json",
1983
                            "--allow-legacy-scope",
1984
                            "--remote",
1985
                            measured_remote,
1986
                            "--branch",
1987
                            selected_branch,
1988
                            repo_spec,
1989
                            str(destination),
1990
                        ],
1991
                        fixture_root,
1992
                        run_index,
1993
                        {
1994
                            **fixture_meta,
1995
                            "run_subject_order": [item.name for item in run_subjects],
1996
                        },
1997
                        admitted,
1998
                        preserve_stdout=True,
1999
                        env=subject_env,
2000
                        timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
2001
                    )
2002
                    receipt = _parse_json_object(row.get("stdout"))
2003
                    evidence = legacy_selected_receipt_evidence(receipt or {})
2004
                    proxy_stats = proxy.snapshot()
2005
                    cache_before_status = stat_cache_snapshot(destination)
2006
                    artifacts = normalized_oak_artifacts(destination)
2007
                    trial_meta = {
2008
                        **fixture_meta,
2009
                        "run_subject_order": [item.name for item in run_subjects],
2010
                    }
2011
                    status_row = step_row(
2012
                        subject,
2013
                        scenario,
2014
                        "legacy_selected.first_status",
2015
                        [str(subject.bin), "status", "--json"],
2016
                        destination,
2017
                        run_index,
2018
                        trial_meta,
2019
                        admitted,
2020
                        preserve_stdout=True,
2021
                        env=subject_env,
2022
                        timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
2023
                    )
2024
                    status = normalized_status(_parse_json_object(status_row.get("stdout")) or {})
2025
                    commit_row = step_row(
2026
                        subject,
2027
                        scenario,
2028
                        "legacy_selected.first_noop_commit",
2029
                        [str(subject.bin), "commit"],
2030
                        destination,
2031
                        run_index,
2032
                        trial_meta,
2033
                        admitted,
2034
                        env=subject_env,
2035
                        timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
2036
                    )
2037
                    history = normalized_history(
2038
                        json_command([str(subject.bin), "log", "--json"], destination, subject_env)
2039
                    )
2040
                    head_result = run_bounded_harness_command(
2041
                        [str(subject.bin), "hash"], destination, env=subject_env
2042
                    )
2043
                    local_head = head_result.stdout.strip() if head_result.returncode == 0 else None
2044
                    worktree = normalized_worktree_snapshot(destination) if destination.exists() else None
2045
                    advertised_head = remote_branch_head(direct_remote, repo_spec, selected_branch)
2046
                    subject_sha256 = binary_sha256(subject.bin)
2047
                    expected_passes = 2 if subject_sha256 == baseline_binary_sha256 else 1
2048
                    receipt_contract = legacy_selected_receipt_contract(evidence)
2049
                    network_contract = {
2050
                        "connections": proxy_stats.connections,
2051
                        "receipt": receipt_contract,
2052
                    }
2053
                    cache_contract = normalized_cache_contract(cache_before_status)
2054
                    baseline_cache_contract = normalized_cache_contract(baseline_cache)
2055
                    expected_cache_valid = subject_sha256 != baseline_binary_sha256
2056
                    cache_valid = bool(
2057
                        isinstance(cache_before_status, dict)
2058
                        and cache_before_status.get("all_metadata_valid") is expected_cache_valid
2059
                    )
2060
                    status_ok = status == expected_status_contract
2061
                    artifacts_ok = artifacts == expected_artifacts
2062
                    network_ok = network_contract == baseline_network_contract
2063
                    oracle_ok = bool(
2064
                        row_returncode(row) == 0
2065
                        and row_returncode(status_row) == 0
2066
                        and row_returncode(commit_row) == 0
2067
                        and evidence["legacy_route_observed"]
2068
                        and evidence["snapshot_bound"] is False
2069
                        and evidence["materialization_passes_observed"] == expected_passes
2070
                        and evidence["head"] == expected_head == local_head == advertised_head
2071
                        and evidence["manifest"] == baseline_receipt_contract["manifest"]
2072
                        and evidence["written_paths"] == len(expected_worktree)
2073
                        and receipt_contract == baseline_receipt_contract
2074
                        and history == expected_history
2075
                        and worktree == expected_worktree
2076
                        and status_ok
2077
                        and artifacts_ok
2078
                        and cache_contract == baseline_cache_contract
2079
                        and cache_valid
2080
                        and network_ok
2081
                        and proxy_stats.connection_errors == 0
2082
                    )
2083
                    row.update(
2084
                        {
2085
                            **evidence,
2086
                            "network_connections_observed": proxy_stats.connections,
2087
                            "network_client_to_server_bytes_observed": proxy_stats.client_to_upstream_bytes,
2088
                            "network_server_to_client_bytes_observed": proxy_stats.upstream_to_client_bytes,
2089
                            "network_connection_errors": proxy_stats.connection_errors,
2090
                            "normalized_network_work": network_contract,
2091
                            "oracle_network_work_matches_baseline": network_ok,
2092
                            "oracle_head_matches": expected_head == local_head == advertised_head,
2093
                            "oracle_manifest_matches": evidence["manifest"]
2094
                            == baseline_receipt_contract["manifest"],
2095
                            "oracle_receipt_contract_matches": receipt_contract
2096
                            == baseline_receipt_contract,
2097
                            "oracle_history_matches": history == expected_history,
2098
                            "oracle_worktree_bytes_and_modes_match": worktree == expected_worktree,
2099
                            "oracle_clean_status": status_ok,
2100
                            "oracle_local_artifact_names_match": artifacts_ok,
2101
                            "oracle_stat_cache_contract_matches": cache_contract
2102
                            == baseline_cache_contract,
2103
                            "stat_cache_metadata_valid_before_status": (
2104
                                cache_before_status.get("all_metadata_valid")
2105
                                if isinstance(cache_before_status, dict)
2106
                                else None
2107
                            ),
2108
                            "stat_cache_rows_before_status": (
2109
                                len(cache_before_status.get("rows", []))
2110
                                if isinstance(cache_before_status, dict)
2111
                                and isinstance(cache_before_status.get("rows"), list)
2112
                                else None
2113
                            ),
2114
                            "stat_cache_expected_valid_before_status": expected_cache_valid,
2115
                            "oracle_clone_residue_absent": artifacts_ok,
2116
                            "oracle_passed": oracle_ok,
2117
                            "repository_cache_state": "fresh_destination_no_shared_object_store",
2118
                        }
2119
                    )
2120
                    for followup_row in (status_row, commit_row):
2121
                        followup_row.update(
2122
                            {
2123
                                "materialization_timing_claimable": timing_claimable,
2124
                                "stat_cache_expected_valid_before_first_status": expected_cache_valid,
2125
                                "oracle_passed": oracle_ok,
2126
                            }
2127
                        )
2128
                    if not oracle_ok:
2129
                        row["returncode"] = 1
2130
                        row["stderr"] = "legacy_selected_materialization_oracle_failed"
2131
                        status_row["returncode"] = 1
2132
                        commit_row["returncode"] = 1
2133
                    total = phase_total_row(
2134
                        [row, status_row, commit_row],
2135
                        subject,
2136
                        scenario,
2137
                        run_index,
2138
                        trial_meta,
2139
                        "legacy_selected_clone_materialization.total",
2140
                    )
2141
                    total.update(
2142
                        {
2143
                            key: row[key]
2144
                            for key in (
2145
                                "materialization_passes_observed",
2146
                                "network_connections_observed",
2147
                                "network_client_to_server_bytes_observed",
2148
                                "network_server_to_client_bytes_observed",
2149
                                "oracle_passed",
2150
                                "oracle_head_matches",
2151
                                "oracle_manifest_matches",
2152
                                "oracle_receipt_contract_matches",
2153
                                "oracle_history_matches",
2154
                                "oracle_worktree_bytes_and_modes_match",
2155
                                "oracle_clean_status",
2156
                                "oracle_local_artifact_names_match",
2157
                                "oracle_stat_cache_contract_matches",
2158
                                "stat_cache_metadata_valid_before_status",
2159
                                "stat_cache_rows_before_status",
2160
                                "oracle_clone_residue_absent",
2161
                                "oracle_network_work_matches_baseline",
2162
                                "serve_binary_sha256",
2163
                                "serve_source_head",
2164
                                "fixture_id",
2165
                                "os_cache_state",
2166
                                "materialization_timing_claimable",
2167
                            )
2168
                        }
2169
                    )
2170
                    total.update(
2171
                        {
2172
                            "clone_elapsed_ms": row.get("elapsed_ms"),
2173
                            "first_status_elapsed_ms": status_row.get("elapsed_ms"),
2174
                            "first_noop_commit_elapsed_ms": commit_row.get("elapsed_ms"),
2175
                        }
2176
                    )
2177
                    rows.extend([row, status_row, commit_row, total])
2178
                    shutil.rmtree(destination, ignore_errors=True)
2179
    finally:
2180
        if proxy is not None:
2181
            proxy.stop()
2182
        if server is not None:
2183
            stop_owned_process(server)
2184
        shutil.rmtree(fixture_root, ignore_errors=True)
2185
2186
    if failure is not None:
2187
        rows.append(
2188
            harness_step_row(
2189
                setup_subject,
2190
                scenario,
2191
                "scenario.fixture_failure",
2192
                0,
2193
                fixture_meta,
2194
                0.0,
2195
                returncode=1,
2196
            )
2197
        )
2198
        rows[-1]["stderr"] = failure
2199
    return rows
2200
2201
2202
def run_git_loop(
2203
    subject: Subject,
2204
    remote: oakbench_remotes.RemoteResolution,
2205
    run_root: Path,
2206
    run_index: int,
2207
    metadata: dict[str, Any],
2208
    admitted: int,
2209
) -> list[dict[str, Any]]:
2210
    scenario = "task_loop"
2211
    meta = {
2212
        **metadata,
2213
        **remote.row_fields(),
2214
        "benchmark_track": (
2215
            "core-equivalent"
2216
            if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE
2217
            else "agent-default"
2218
        ),
2219
    }
2220
    vcs = str(subject.bin)
2221
    rows: list[dict[str, Any]] = []
2222
2223
    seed_error = ensure_git_remote_seeded(subject, remote, run_root)
2224
    if seed_error:
2225
        return [skip_row(subject, scenario, run_index, meta, seed_error)]
2226
2227
    # Untimed: the base clone is the long-lived checkout worktrees hang off;
2228
    # an agent fleet pays it once, not per task.
2229
    base = run_root / f"tl-git-base-r{run_index}"
2230
    clone = run_untimed([vcs, "clone", "-q", str(remote.repo), str(base)], run_root)
2231
    if clone.returncode != 0:
2232
        return [skip_row(subject, scenario, run_index, meta, f"base clone failed: {clone.stderr.strip()[:200]}")]
2233
2234
    branches: dict[str, str] = {}
2235
    payloads: dict[str, str] = {}
2236
    for phase in PHASES:
2237
        workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
2238
        branch = oakbench_remotes.disposable_branch(
2239
            str(metadata["bench_id"]), f"taskloop-{phase}", subject.name, run_index
2240
        )
2241
        branches[phase] = branch
2242
        payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
2243
        payloads[phase] = payload
2244
        acquire = step_row(
2245
            subject, scenario, f"{phase}.acquire",
2246
            [vcs, "worktree", "add", str(workspace), "-b", branch], base, run_index, meta, admitted,
2247
        )
2248
        rows.append(acquire)
2249
        if int(acquire["returncode"]) != 0:
2250
            return rows
2251
        rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
2252
        # Agent-default snapshot: git pays stage+commit as two calls.
2253
        rows.append(step_row(subject, scenario, f"{phase}.snapshot.add", [vcs, "add", "."], workspace, run_index, meta, admitted))
2254
        rows.append(
2255
            step_row(
2256
                subject, scenario, f"{phase}.snapshot.commit",
2257
                [vcs, "commit", "-q", "-m", f"bench task loop {metadata['bench_id']} r{run_index} {phase}"],
2258
                workspace, run_index, meta, admitted,
2259
            )
2260
        )
2261
        rows.append(
2262
            step_row(
2263
                subject, scenario, f"{phase}.publish",
2264
                [vcs, "push", "-q", "-u", "origin", branch], workspace, run_index, meta, admitted,
2265
            )
2266
        )
2267
        # No task.describe: git's description is the commit message already
2268
        # paid in {phase}.snapshot.commit (asymmetry documented in the lane doc).
2269
        rows.append(
2270
            step_row(
2271
                subject, scenario, f"{phase}.end",
2272
                [vcs, "worktree", "remove", str(workspace)], base, run_index, meta, admitted,
2273
            )
2274
        )
2275
2276
    # Untimed oracle: payloads visible on the remote branches.
2277
    oracle_ok: bool | None = True
2278
    for phase in PHASES:
2279
        fetch = run_untimed([vcs, "fetch", "-q", "origin", branches[phase]], base)
2280
        show = run_untimed([vcs, "show", f"FETCH_HEAD:task-notes.md"], base)
2281
        if fetch.returncode != 0 or payloads[phase] not in show.stdout:
2282
            oracle_ok = False
2283
    rows.append(
2284
        {
2285
            **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
2286
            "oracle_payload_on_remote_branch": oracle_ok,
2287
            "workspace_branches": branches,
2288
            "measurement_source": "fetch_plus_show_grep",
2289
        }
2290
    )
2291
    return rows
2292
2293
2294
def loop_summary_fields(rows: list[dict[str, Any]]) -> dict[str, Any]:
2295
    """followup-vs-task warm deltas computed from this run's step rows."""
2296
    def op_ms(operation: str) -> float | None:
2297
        for row in rows:
2298
            if row.get("operation") == operation:
2299
                return float(row.get("elapsed_ms", 0.0))
2300
        return None
2301
2302
    def phase_ms(prefix: str) -> float:
2303
        return sum(
2304
            float(row.get("elapsed_ms", 0.0))
2305
            for row in rows
2306
            if str(row.get("operation", "")).startswith(prefix + ".")
2307
        )
2308
2309
    task_acquire = op_ms("task.acquire")
2310
    followup_acquire = op_ms("followup.acquire")
2311
    fields: dict[str, Any] = {
2312
        "task_phase_ms": round(phase_ms("task"), 3),
2313
        "followup_phase_ms": round(phase_ms("followup"), 3),
2314
    }
2315
    if task_acquire is not None and followup_acquire is not None:
2316
        fields["followup_warm_delta_ms"] = round(followup_acquire - task_acquire, 3)
2317
        fields["acquire_ms_task"] = round(task_acquire, 3)
2318
        fields["acquire_ms_followup"] = round(followup_acquire, 3)
2319
    else:
2320
        fields["followup_warm_delta_ms"] = None
2321
    return fields
2322
2323
2324
def summary_text(rows: list[dict[str, Any]]) -> str:
2325
    totals = [row for row in rows if row.get("operation") == "task_loop.total"]
2326
    lines = [
2327
        "# Task Loop Summary",
2328
        "",
2329
        "Whole-task cost of the signature motion: acquire workspace -> edit -> snapshot ->",
2330
        "publish -> describe -> end, then a follow-up task in a fresh workspace.",
2331
        "Command sequences are NOT step-equivalent across subjects",
2332
        "(oak pays desc; git's description is its commit message). Latency across unlike",
2333
        "transports is never comparable; scripted call equivalents and estimated token proxies are.",
2334
        "",
2335
        "| Subject | Run | Transport | Wall ms | Scripted call equivalents | Cost-wt tokens (env) | Warm acquire delta ms | Oracle |",
2336
        "| --- | ---: | --- | ---: | ---: | ---: | ---: | --- |",
2337
    ]
2338
    for row in totals:
2339
        oracle = row.get("oracle_payload_on_remote_branch")
2340
        lines.append(
2341
            "| `{}` | {} | {} | {} | {} | {} | {} | {} |".format(
2342
                row["subject"],
2343
                row["run"],
2344
                row.get("remote_transport", "unmeasured"),
2345
                fmt_ms(row.get("elapsed_ms")),
2346
                row.get("tool_call_count", "unmeasured"),
2347
                row.get("estimated_cost_weighted_tokens_with_envelope", "unmeasured"),
2348
                row.get("followup_warm_delta_ms", "unmeasured"),
2349
                "pass" if oracle else ("FAIL" if oracle is False else "unmeasured"),
2350
            )
2351
        )
2352
    skips = [
2353
        row for row in rows
2354
        if row.get("skipped") and row.get("scenario") == TASK_LOOP_SCENARIO
2355
    ]
2356
    for row in skips:
2357
        lines.append(f"| `{row['subject']}` | {row['run']} | skipped | | | | | {row['skip_reason'][:70]} |")
2358
2359
    agent_totals = [
2360
        row
2361
        for row in rows
2362
        if row.get("operation") in ("agent_task.total", "agent_task_oak_local.total")
2363
    ]
2364
    if agent_totals:
2365
        lines.extend(
2366
            [
2367
                "",
2368
                "## Cold clone and warm isolated task",
2369
                "",
2370
                "Local-file Git rows compare a fresh full clone with a worktree sharing",
2371
                "the first clone's object store. Oak local rows use two independent fresh",
2372
                "clones against an owned loopback server; they do not claim a shared cache.",
2373
                "OS cache and network bytes remain unknown. Hosted CI/review are skips.",
2374
                "",
2375
                "| Subject | Run | Wall ms | Scripted call equivalents | Output bytes | Coverage | Oracle |",
2376
                "| --- | ---: | ---: | ---: | ---: | --- | --- |",
2377
            ]
2378
        )
2379
        for row in agent_totals:
2380
            lines.append(
2381
                "| `{}` | {} | {} | {} | {} | {} | {} |".format(
2382
                    row["subject"],
2383
                    row["run"],
2384
                    fmt_ms(row.get("elapsed_ms")),
2385
                    row.get("tool_call_count", "unmeasured"),
2386
                    row.get("raw_output_bytes", "unmeasured"),
2387
                    "complete" if row.get("coverage_complete") else "partial",
2388
                    "pass" if row.get("oracle_payload_on_remote_branch") else "FAIL",
2389
                )
2390
            )
2391
2392
    materialization_totals = [
2393
        row
2394
        for row in rows
2395
        if row.get("operation") == "legacy_selected_clone_materialization.total"
2396
    ]
2397
    if materialization_totals:
2398
        lines.extend(
2399
            [
2400
                "",
2401
                "## Legacy selected-branch materialization",
2402
                "",
2403
                "One fixed loopback Serve and one immutable fixture are shared across subjects.",
2404
                "Fresh destinations do not imply a cold OS page cache. Timing claims require",
2405
                "separate interleaved A/A noise and A/B campaigns.",
2406
                "",
2407
                "| Subject | Run | Clone ms | First status ms | First no-op commit ms | Passes | Timing claimable | Oracle |",
2408
                "| --- | ---: | ---: | ---: | ---: | ---: | --- | --- |",
2409
            ]
2410
        )
2411
        for row in materialization_totals:
2412
            lines.append(
2413
                "| `{}` | {} | {} | {} | {} | {} | {} | {} |".format(
2414
                    row["subject"],
2415
                    row["run"],
2416
                    fmt_ms(row.get("clone_elapsed_ms")),
2417
                    fmt_ms(row.get("first_status_elapsed_ms")),
2418
                    fmt_ms(row.get("first_noop_commit_elapsed_ms")),
2419
                    row.get("materialization_passes_observed", "unmeasured"),
2420
                    "yes" if row.get("materialization_timing_claimable") else "no",
2421
                    "pass" if row.get("oracle_passed") else "FAIL",
2422
                )
2423
            )
2424
2425
    interrupted_totals = [
2426
        row for row in rows if row.get("operation") == "interrupted_publication.total"
2427
    ]
2428
    if interrupted_totals:
2429
        lines.extend(
2430
            [
2431
                "",
2432
                "## Interrupted publication",
2433
                "",
2434
                "A loopback fixture validates the request framing and drops the response.",
2435
                "Fixture delivery knowledge is reported separately from the CLI's own outcome",
2436
                "claim. Request receipt is not durability proof; the harness never retries.",
2437
                "",
2438
                "| Subject | Run | Wall ms | Scripted call equivalents | Output bytes | CLI outcome | Fixture delivery | POSTs | Request bytes | Head/work preserved |",
2439
                "| --- | ---: | ---: | ---: | ---: | --- | --- | ---: | ---: | --- |",
2440
            ]
2441
        )
2442
        for row in interrupted_totals:
2443
            lines.append(
2444
                "| `{}` | {} | {} | {} | {} | `{}` | `{}` | {} | {} | {}/{} |".format(
2445
                    row["subject"],
2446
                    row["run"],
2447
                    fmt_ms(row.get("elapsed_ms")),
2448
                    row.get("tool_call_count", "unmeasured"),
2449
                    row.get("raw_output_bytes", "unmeasured"),
2450
                    row.get("publication_outcome", "unmeasured"),
2451
                    row.get("fixture_delivery_state", "unmeasured"),
2452
                    row.get("mutation_requests_observed", "unmeasured"),
2453
                    row.get("network_request_bytes_observed", "unmeasured"),
2454
                    "yes" if row.get("local_head_preserved") else "no",
2455
                    "yes" if row.get("local_work_preserved") else "no",
2456
                )
2457
            )
2458
    return "\n".join(lines) + "\n"
2459
2460
2461
def fmt_ms(value: Any) -> str:
2462
    try:
2463
        return f"{float(value):.0f}"
2464
    except (TypeError, ValueError):
2465
        return "unmeasured"
2466
2467
2468
def main() -> int:
2469
    args = parse_args()
2470
    scenarios = [item.strip() for item in args.scenarios.split(",") if item.strip()]
2471
    known_scenarios = {
2472
        TASK_LOOP_SCENARIO,
2473
        AGENT_TASK_SCENARIO,
2474
        AGENT_TASK_OAK_LOCAL_SCENARIO,
2475
        LEGACY_SELECTED_MATERIALIZATION_SCENARIO,
2476
        INTERRUPTED_PUBLICATION_SCENARIO,
2477
    }
2478
    unknown_scenarios = sorted(set(scenarios) - known_scenarios)
2479
    if unknown_scenarios:
2480
        raise SystemExit(
2481
            "Unknown scenarios: " + ", ".join(unknown_scenarios)
2482
            + "; valid: " + ", ".join(sorted(known_scenarios))
2483
        )
2484
    subjects = load_subjects(args)
2485
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
2486
    args.results.mkdir(parents=True, exist_ok=True)
2487
    args.workdir.mkdir(parents=True, exist_ok=True)
2488
    run_root = args.workdir / "runs" / timestamp
2489
    run_root.mkdir(parents=True, exist_ok=True)
2490
2491
    metadata = {
2492
        "bench_id": timestamp,
2493
        "profile": "task-loop",
2494
        "benchmark_track": "agent-default",
2495
        "timestamp_utc": timestamp,
2496
        "host": platform.node(),
2497
        "platform": platform.platform(),
2498
        "machine": platform.machine(),
2499
        "python": platform.python_version(),
2500
        "env_isolation_version": ENV_ISOLATION_VERSION,
2501
        "subject_versions": subject_versions(subjects),
2502
        "subject_details": subject_details(subjects),
2503
        "source": source_metadata(args.oak_repo),
2504
        "git_remote_mode": args.git_remote,
2505
        "trial_order_randomized": args.randomize_subject_order,
2506
        "trial_order_seed": args.random_seed if args.randomize_subject_order else None,
2507
    }
2508
2509
    rows: list[dict[str, Any]] = []
2510
    with measurement_lock("task_loop") as lock_info:
2511
        metadata["measurement_lock_wait_ms"] = lock_info.wait_ms
2512
        metadata["measurement_lock"] = "held" if lock_info.enabled else "disabled"
2513
        order_rng = random.Random(args.random_seed)
2514
        if LEGACY_SELECTED_MATERIALIZATION_SCENARIO in scenarios:
2515
            rows.extend(
2516
                run_legacy_selected_materialization_campaign(
2517
                    subjects,
2518
                    args.oak_serve_bin,
2519
                    args.oak_serve_source,
2520
                    args.oak_serve_source_head,
2521
                    args.materialization_baseline_subject,
2522
                    run_root,
2523
                    args.runs,
2524
                    metadata,
2525
                    args.admitted_output_chars,
2526
                    args.randomize_subject_order,
2527
                    args.random_seed,
2528
                    args.claim_materialization_timing,
2529
                )
2530
            )
2531
        ordinary_scenarios = [
2532
            scenario for scenario in scenarios if scenario != LEGACY_SELECTED_MATERIALIZATION_SCENARIO
2533
        ]
2534
        for run_index in range(args.runs):
2535
            run_subjects = list(subjects)
2536
            if args.randomize_subject_order:
2537
                order_rng.shuffle(run_subjects)
2538
            run_metadata = {
2539
                **metadata,
2540
                "run_subject_order": [subject.name for subject in run_subjects],
2541
            }
2542
            for subject in run_subjects:
2543
                for scenario in ordinary_scenarios:
2544
                    print(f"[run] {scenario} run={run_index} subject={subject.name}", flush=True)
2545
                    if scenario == INTERRUPTED_PUBLICATION_SCENARIO:
2546
                        rows.extend(
2547
                            run_interrupted_publication(
2548
                                subject, run_root, run_index, run_metadata, args.admitted_output_chars
2549
                            )
2550
                        )
2551
                        continue
2552
                    if scenario == AGENT_TASK_SCENARIO:
2553
                        if subject.kind != "git":
2554
                            rows.append(
2555
                                unmeasured_step_row(
2556
                                    subject,
2557
                                    scenario,
2558
                                    "agent_task.skipped",
2559
                                    run_index,
2560
                                    run_metadata,
2561
                                    "Oak end-to-end publication and exact CI require an explicitly configured disposable staging lane; production mutation is forbidden",
2562
                                )
2563
                            )
2564
                            continue
2565
                        remote_create_start = time.perf_counter()
2566
                        remote = oakbench_remotes.make_git_bare_remote(
2567
                            subject.bin, run_root, f"agent-task-remote-r{run_index}.git"
2568
                        )
2569
                        remote_create_ms = (time.perf_counter() - remote_create_start) * 1000
2570
                        rows.extend(
2571
                            run_git_agent_task_baseline(
2572
                                subject,
2573
                                remote,
2574
                                run_root,
2575
                                run_index,
2576
                                run_metadata,
2577
                                args.admitted_output_chars,
2578
                                remote_create_ms,
2579
                            )
2580
                        )
2581
                        continue
2582
                    if scenario == AGENT_TASK_OAK_LOCAL_SCENARIO:
2583
                        if subject.kind != "oak":
2584
                            rows.append(
2585
                                unmeasured_step_row(
2586
                                    subject,
2587
                                    scenario,
2588
                                    "agent_task_oak_local.skipped",
2589
                                    run_index,
2590
                                    run_metadata,
2591
                                    "owned local oak serve scenario applies only to Oak subjects",
2592
                                )
2593
                            )
2594
                            continue
2595
                        rows.extend(
2596
                            run_oak_local_agent_task_baseline(
2597
                                subject,
2598
                                run_root,
2599
                                run_index,
2600
                                run_metadata,
2601
                                args.admitted_output_chars,
2602
                            )
2603
                        )
2604
                        continue
2605
                    if subject.kind == "git":
2606
                        if args.git_remote == "github":
2607
                            remote = oakbench_remotes.resolve_git_github_remote()
2608
                        else:
2609
                            remote = oakbench_remotes.make_git_bare_remote(
2610
                                subject.bin, run_root, f"task-loop-remote-r{run_index}.git"
2611
                            )
2612
                        if not remote.resolved:
2613
                            rows.append(skip_row(subject, TASK_LOOP_SCENARIO, run_index, run_metadata, remote.skip_reason or ""))
2614
                            continue
2615
                        subject_rows = run_git_loop(subject, remote, run_root, run_index, run_metadata, args.admitted_output_chars)
2616
                    else:
2617
                        remote = oakbench_remotes.resolve_oak_remote("sync")
2618
                        if not remote.resolved:
2619
                            rows.append(skip_row(subject, TASK_LOOP_SCENARIO, run_index, run_metadata, remote.skip_reason or ""))
2620
                            continue
2621
                        subject_rows = run_oak_loop(subject, remote, run_root, run_index, run_metadata, args.admitted_output_chars)
2622
                    total = next((row for row in subject_rows if row.get("operation") == "task_loop.total"), None)
2623
                    if total is not None:
2624
                        total.update(loop_summary_fields(subject_rows))
2625
                    rows.extend(subject_rows)
2626
2627
    store = ResultsStore(args.results, lane="task-loop")
2628
    raw_path, summary_path = store.write(timestamp, rows, summary_text(rows))
2629
    if not args.keep_workdirs:
2630
        shutil.rmtree(run_root, ignore_errors=True)
2631
    print(f"[result] {raw_path}")
2632
    print(f"[summary] {summary_path}")
2633
    return 1 if has_lane_failures(rows) else 0
2634
2635
2636
if __name__ == "__main__":
2637
    raise SystemExit(main())