Viewing files at commit 54b9920be8dfCommit detailsView latest

Browse files
scripts/task_loop.py 1396 lines · 55.0 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 platform
34
import random
35
import re
36
import shutil
37
import socket
38
import subprocess
39
import tempfile
40
import threading
41
import time
42
from datetime import datetime, timezone
43
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
44
from pathlib import Path
45
from typing import Any
46
47
from oakbench import remotes as oakbench_remotes
48
from oakbench import tokens as oakbench_tokens
49
from oakbench.diskprobe import bounded_tree_usage
50
from oakbench.environment import ENV_ISOLATION_VERSION, base_env as oakbench_base_env
51
from oakbench.environment import command_display
52
from oakbench.execution import run_timed
53
from oakbench.results import ResultsStore
54
from oakbench.rows import row_returncode
55
from oakbench.runlock import measurement_lock
56
from oakbench.subjects import (
57
    DEFAULT_OAK_REPO,
58
    Subject,
59
    load_subjects,
60
    source_metadata,
61
    subject_details,
62
    subject_versions,
63
)
64
65
ROOT = Path(__file__).resolve().parents[1]
66
DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-task-loop"
67
SKIP_RETURNCODE = 77
68
VIRTUAL_BRANCH_RE = re.compile(r"virtual branch ([^\s)]+)")
69
PHASES = ("task", "followup")
70
TASK_LOOP_SCENARIO = "task_loop"
71
AGENT_TASK_SCENARIO = "agent_task_baseline_v1"
72
INTERRUPTED_PUBLICATION_SCENARIO = "interrupted_publication_after_request_v1"
73
INTERRUPTED_PUBLICATION_REQUEST_TIMEOUT_S = 2.0
74
INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S = 30.0
75
INTERRUPTED_PUBLICATION_MAX_REQUEST_BYTES = 64 * 1024 * 1024
76
77
78
def base_env() -> dict[str, str]:
79
    return oakbench_base_env(
80
        author_name="Oak Task Loop",
81
        author_email="[email protected]",
82
        oak_author="oak-task-loop",
83
    )
84
85
86
def parse_args() -> argparse.Namespace:
87
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
88
    parser.add_argument("--subjects", help="Comma-separated subject names from config/subjects.toml")
89
    parser.add_argument("--config", type=Path, default=ROOT / "config" / "subjects.toml")
90
    parser.add_argument("--git-bin", type=Path)
91
    parser.add_argument("--oak-installed-bin", type=Path)
92
    parser.add_argument("--oak-local-bin", type=Path)
93
    parser.add_argument("--runs", type=int, default=3)
94
    parser.add_argument(
95
        "--scenarios",
96
        default=TASK_LOOP_SCENARIO,
97
        help=(
98
            "Comma-separated scenarios: task_loop, agent_task_baseline_v1, and/or "
99
            "interrupted_publication_after_request_v1. The interruption "
100
            "scenario is a loopback-only Oak fixture and never contacts a remote service."
101
        ),
102
    )
103
    parser.add_argument(
104
        "--git-remote",
105
        choices=("github", "local"),
106
        default="github",
107
        help=(
108
            "github: agent-default track, real push to GIT_BENCH_REMOTE "
109
            "(transport-comparable to oak). local: core-equivalent track, "
110
            "local bare remote (no network in git's loop)."
111
        ),
112
    )
113
    parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
114
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "task-loop")
115
    parser.add_argument("--oak-repo", type=Path, default=DEFAULT_OAK_REPO)
116
    parser.add_argument("--admitted-output-chars", type=int, default=20_000)
117
    parser.add_argument("--keep-workdirs", action="store_true")
118
    parser.add_argument(
119
        "--randomize-subject-order",
120
        action="store_true",
121
        help="Shuffle subject order independently in each run using --random-seed.",
122
    )
123
    parser.add_argument("--random-seed", type=int, default=20260908)
124
    return parser.parse_args()
125
126
127
def run_untimed(command: list[str], cwd: Path | None, timeout: float = 300.0) -> subprocess.CompletedProcess[str]:
128
    return subprocess.run(
129
        command,
130
        cwd=cwd,
131
        env=base_env(),
132
        text=True,
133
        stdout=subprocess.PIPE,
134
        stderr=subprocess.PIPE,
135
        timeout=timeout,
136
        check=False,
137
    )
138
139
140
def step_row(
141
    subject: Subject,
142
    scenario: str,
143
    operation: str,
144
    command: list[str],
145
    cwd: Path,
146
    run_index: int,
147
    metadata: dict[str, Any],
148
    admitted_output_chars: int,
149
    *,
150
    kind: str = "vcs",
151
    expected_returncodes: tuple[int, ...] = (0,),
152
    preserve_stdout: bool = False,
153
    preserve_stderr: bool = False,
154
    env: dict[str, str] | None = None,
155
    timeout_seconds: float | None = None,
156
) -> dict[str, Any]:
157
    capture = run_timed(
158
        command,
159
        cwd,
160
        env or base_env(),
161
        admitted_output_chars,
162
        timeout_seconds=timeout_seconds,
163
    )
164
    expected = not capture.timed_out and capture.returncode in expected_returncodes
165
    is_vcs = 1 if kind == "vcs" else 0
166
    row = {
167
        **metadata,
168
        "subject": subject.name,
169
        "subject_kind": subject.kind,
170
        "subject_label": subject.label,
171
        "scenario": scenario,
172
        "run": run_index,
173
        "operation": operation,
174
        "step_kind": kind,
175
        "elapsed_ms": round(capture.elapsed_ms, 3),
176
        "returncode": 0 if expected else (capture.returncode or 1),
177
        "process_returncode": capture.returncode,
178
        "timed_out": capture.timed_out,
179
        "expected_returncodes": list(expected_returncodes),
180
        "command": command,
181
        "tool_call_count": 1,
182
        "terminal_tool_call_count": 1,
183
        "vcs_tool_call_count": is_vcs,
184
        **oakbench_tokens.interaction_token_fields(
185
            command_display(command),
186
            capture.stdout_text,
187
            capture.stderr_text,
188
            capture.stdout_bytes,
189
            capture.stderr_bytes,
190
            capture.stdout_truncated,
191
            capture.stderr_truncated,
192
        ),
193
    }
194
    if not expected:
195
        row["stderr"] = capture.stderr_text[-4000:]
196
    if preserve_stdout:
197
        row["stdout"] = capture.stdout_text
198
    if preserve_stderr:
199
        row["stderr"] = capture.stderr_text
200
    return row
201
202
203
def skip_row(
204
    subject: Subject,
205
    scenario: str,
206
    run_index: int,
207
    metadata: dict[str, Any],
208
    reason: str,
209
) -> dict[str, Any]:
210
    return {
211
        **metadata,
212
        "subject": subject.name,
213
        "subject_kind": subject.kind,
214
        "subject_label": subject.label,
215
        "scenario": scenario,
216
        "run": run_index,
217
        "operation": "task_loop.skipped",
218
        "step_kind": "summary",
219
        "elapsed_ms": 0.0,
220
        "returncode": SKIP_RETURNCODE,
221
        "command": [],
222
        "skipped": True,
223
        "skip_reason": reason,
224
        "tool_call_count": 0,
225
        "terminal_tool_call_count": 0,
226
        "vcs_tool_call_count": 0,
227
    }
228
229
230
def unmeasured_step_row(
231
    subject: Subject,
232
    scenario: str,
233
    operation: str,
234
    run_index: int,
235
    metadata: dict[str, Any],
236
    reason: str,
237
) -> dict[str, Any]:
238
    row = skip_row(subject, scenario, run_index, metadata, reason)
239
    row["operation"] = operation
240
    return row
241
242
243
def harness_step_row(
244
    subject: Subject,
245
    scenario: str,
246
    operation: str,
247
    run_index: int,
248
    metadata: dict[str, Any],
249
    elapsed_ms: float,
250
    *,
251
    returncode: int = 0,
252
    harness_command_count: int = 0,
253
) -> dict[str, Any]:
254
    return {
255
        **metadata,
256
        "subject": subject.name,
257
        "subject_kind": subject.kind,
258
        "subject_label": subject.label,
259
        "scenario": scenario,
260
        "run": run_index,
261
        "operation": operation,
262
        "step_kind": "harness",
263
        "elapsed_ms": round(elapsed_ms, 3),
264
        "returncode": returncode,
265
        "command": [],
266
        "tool_call_count": 0,
267
        "terminal_tool_call_count": 0,
268
        "vcs_tool_call_count": 0,
269
        "harness_command_count": harness_command_count,
270
        "stdout_bytes": 0,
271
        "stderr_bytes": 0,
272
        "retry_count": 0,
273
        "human_action_count": 0,
274
    }
275
276
277
def phase_total_row(
278
    rows: list[dict[str, Any]],
279
    subject: Subject,
280
    scenario: str,
281
    run_index: int,
282
    metadata: dict[str, Any],
283
    operation: str,
284
) -> dict[str, Any]:
285
    failures = [row for row in rows if row_returncode(row) not in (0, SKIP_RETURNCODE)]
286
    unmeasured = [
287
        str(row["operation"])
288
        for row in rows
289
        if row_returncode(row) == SKIP_RETURNCODE
290
    ]
291
    phase_elapsed_ms: dict[str, float] = {}
292
    for row in rows:
293
        phase = str(row.get("operation", "unknown")).split(".", 1)[0]
294
        phase_elapsed_ms[phase] = phase_elapsed_ms.get(phase, 0.0) + float(
295
            row.get("elapsed_ms", 0.0)
296
        )
297
    return {
298
        **metadata,
299
        "subject": subject.name,
300
        "subject_kind": subject.kind,
301
        "subject_label": subject.label,
302
        "scenario": scenario,
303
        "run": run_index,
304
        "operation": operation,
305
        "step_kind": "summary",
306
        "elapsed_ms": round(sum(float(row.get("elapsed_ms", 0)) for row in rows), 3),
307
        "returncode": 1 if failures else 0,
308
        "coverage_complete": not unmeasured,
309
        "unmeasured_operations": unmeasured,
310
        "command": [row["command"] for row in rows],
311
        "tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("tool_call_count")) for row in rows),
312
        "terminal_tool_call_count": sum(
313
            oakbench_tokens.int_or_zero(row.get("terminal_tool_call_count")) for row in rows
314
        ),
315
        "vcs_tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("vcs_tool_call_count")) for row in rows),
316
        "agent_tool_call_count": sum(
317
            oakbench_tokens.int_or_zero(row.get("tool_call_count")) for row in rows
318
        ),
319
        "harness_command_count": sum(
320
            oakbench_tokens.int_or_zero(row.get("harness_command_count")) for row in rows
321
        ),
322
        "retry_count": sum(oakbench_tokens.int_or_zero(row.get("retry_count")) for row in rows),
323
        "human_action_count": sum(
324
            oakbench_tokens.int_or_zero(row.get("human_action_count")) for row in rows
325
        ),
326
        "phase_elapsed_ms": {
327
            phase: round(elapsed, 3) for phase, elapsed in sorted(phase_elapsed_ms.items())
328
        },
329
        **oakbench_tokens.summed_token_fields(
330
            rows, "sum_of_task_loop_steps_command_plus_admitted_output_chars_div_4"
331
        ),
332
        "summarized_operations": [row["operation"] for row in rows],
333
    }
334
335
336
def has_lane_failures(rows: list[dict[str, Any]]) -> bool:
337
    return any(row_returncode(row) not in (0, SKIP_RETURNCODE) for row in rows)
338
339
340
def enforce_oracle_result(total: dict[str, Any], oracle_ok: bool | None) -> dict[str, Any]:
341
    if oracle_ok is False:
342
        total["returncode"] = 1
343
        total["stderr"] = "oracle_payload_missing_from_remote_branch"
344
    return total
345
346
347
def interrupted_publication_fields(
348
    *,
349
    request_count: int,
350
    request_bytes: int | None,
351
    declared_request_bytes: int | None,
352
    request_framing_valid: bool,
353
    request_body_timed_out: bool,
354
    local_head_before: str | None,
355
    local_head_after: str | None,
356
    status_after: dict[str, Any] | None,
357
    worktree_payload_before: str | None,
358
    worktree_payload_after: str | None,
359
    cli_observation: dict[str, str],
360
) -> dict[str, Any]:
361
    """Evidence fields for a single interrupted mutation attempt.
362
363
    The fixture's receipt of a full request is ground truth for the harness,
364
    not a server durability receipt available to the CLI.  A disconnect after
365
    that point is therefore unknown to the client and must never imply replay.
366
    """
367
    request_complete = bool(
368
        request_count == 1
369
        and request_framing_valid
370
        and declared_request_bytes is not None
371
        and request_bytes == declared_request_bytes
372
    )
373
    if request_count == 0:
374
        delivery_state = "no_request_observed"
375
        fixture_outcome = "not_sent_observed"
376
    elif not request_framing_valid:
377
        delivery_state = "invalid_request_framing"
378
        fixture_outcome = "indeterminate_after_invalid_request_disconnect"
379
    elif request_complete:
380
        delivery_state = "complete_request_observed"
381
        fixture_outcome = "unknown_after_complete_request_disconnect"
382
    else:
383
        delivery_state = "partial_request_observed"
384
        fixture_outcome = "indeterminate_after_partial_request_disconnect"
385
    payload_preserved = bool(
386
        worktree_payload_before
387
        and worktree_payload_before == worktree_payload_after
388
    )
389
    unpublished_state_preserved = bool(status_after and status_after.get("needs_push"))
390
    return {
391
        "publication_outcome": cli_observation["reported_outcome"],
392
        "cli_observation": cli_observation["observation"],
393
        "fixture_delivery_state": delivery_state,
394
        "fixture_outcome_knowledge": fixture_outcome,
395
        "fixture_received_complete_request_before_disconnect": request_complete,
396
        "fixture_request_framing_valid": request_framing_valid,
397
        "fixture_request_body_timed_out": request_body_timed_out,
398
        "fixture_declared_request_bytes": declared_request_bytes,
399
        "mutation_requests_observed": request_count,
400
        "blind_replay_requests": max(0, request_count - 1),
401
        "network_request_bytes_observed": request_bytes,
402
        "local_head_preserved": bool(local_head_before) and local_head_before == local_head_after,
403
        "worktree_payload_preserved": payload_preserved,
404
        "unpublished_state_preserved": unpublished_state_preserved,
405
        "local_work_preserved": payload_preserved and unpublished_state_preserved,
406
        "working_tree_dirty_after": None if status_after is None else status_after.get("dirty"),
407
    }
408
409
410
def cli_publication_observation(row: dict[str, Any], *, json_requested: bool) -> dict[str, str]:
411
    """Classify only what the CLI reported, never what the fault fixture knows."""
412
    if row.get("timed_out"):
413
        return {"reported_outcome": "unreported", "observation": "process_timeout"}
414
    stdout = str(row.get("stdout") or "")
415
    stderr = str(row.get("stderr") or "")
416
    value = _parse_json_object(stdout) if json_requested else None
417
    if value is not None and row.get("process_returncode") == 0 and value.get("published") is True:
418
        return {"reported_outcome": "published", "observation": "json_success_receipt"}
419
    if value is not None and isinstance(value.get("error"), dict):
420
        message = str(value["error"].get("message") or "").lower()
421
        if any(marker in message for marker in ("unconfirmed", "unknown", "may have")):
422
            return {"reported_outcome": "unknown", "observation": "json_error_unknown"}
423
        return {
424
            "reported_outcome": "unreported",
425
            "observation": "json_error_without_publication_outcome",
426
        }
427
    message = f"{stdout}\n{stderr}".lower()
428
    if any(marker in message for marker in ("unconfirmed", "unknown", "may have")):
429
        return {"reported_outcome": "unknown", "observation": "text_error_unknown"}
430
    return {"reported_outcome": "unreported", "observation": "text_error_without_publication_outcome"}
431
432
433
def file_sha256(path: Path) -> str | None:
434
    try:
435
        return hashlib.sha256(path.read_bytes()).hexdigest()
436
    except OSError:
437
        return None
438
439
440
class _DisconnectAfterPushHandler(BaseHTTPRequestHandler):
441
    """Minimal loopback fixture for a complete request with a lost reply."""
442
443
    server_version = "oakbench-interruption/1"
444
445
    def log_message(self, _format: str, *_args: object) -> None:
446
        return
447
448
    def _json(self, status: int, payload: dict[str, Any]) -> None:
449
        encoded = json.dumps(payload).encode()
450
        self.send_response(status)
451
        self.send_header("Content-Type", "application/json")
452
        self.send_header("Content-Length", str(len(encoded)))
453
        self.end_headers()
454
        self.wfile.write(encoded)
455
456
    def do_GET(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
457
        self._json(404, {"error": "absent synthetic repo"})
458
459
    def do_POST(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
460
        declared_size: int | None = None
461
        framing_valid = False
462
        body = b""
463
        body_timed_out = False
464
        raw_size = self.headers.get("Content-Length")
465
        try:
466
            parsed_size = int(raw_size) if raw_size is not None else -1
467
            framing_valid = (
468
                0 <= parsed_size <= INTERRUPTED_PUBLICATION_MAX_REQUEST_BYTES
469
                and self.headers.get("Transfer-Encoding") is None
470
            )
471
            if framing_valid:
472
                declared_size = parsed_size
473
                self.connection.settimeout(INTERRUPTED_PUBLICATION_REQUEST_TIMEOUT_S)
474
                while len(body) < parsed_size:
475
                    try:
476
                        chunk = self.rfile.read1(parsed_size - len(body))
477
                    except (socket.timeout, TimeoutError):
478
                        body_timed_out = True
479
                        break
480
                    except OSError:
481
                        break
482
                    if not chunk:
483
                        break
484
                    body += chunk
485
        except ValueError:
486
            framing_valid = False
487
        server = self.server
488
        if self.path == "/api/repos":
489
            self._json(200, {"name": "repo"})
490
            return
491
        if self.path == "/api/oak/repo/push":
492
            with server.receipt_lock:  # type: ignore[attr-defined]
493
                server.push_request_count += 1  # type: ignore[attr-defined]
494
                server.push_request_bytes += len(body)  # type: ignore[attr-defined]
495
                server.push_declared_request_bytes = declared_size  # type: ignore[attr-defined]
496
                server.push_request_framing_valid = framing_valid  # type: ignore[attr-defined]
497
                server.push_request_body_timed_out = body_timed_out  # type: ignore[attr-defined]
498
                server.request_complete_before_disconnect = (  # type: ignore[attr-defined]
499
                    framing_valid and len(body) == declared_size
500
                )
501
            self.close_connection = True
502
            try:
503
                self.connection.shutdown(socket.SHUT_RDWR)
504
            except OSError:
505
                pass
506
            self.connection.close()
507
            return
508
        self._json(404, {"error": "unexpected synthetic path"})
509
510
511
def start_interrupted_publication_server() -> tuple[ThreadingHTTPServer, threading.Thread]:
512
    server = ThreadingHTTPServer(("127.0.0.1", 0), _DisconnectAfterPushHandler)
513
    server.receipt_lock = threading.Lock()  # type: ignore[attr-defined]
514
    server.push_request_count = 0  # type: ignore[attr-defined]
515
    server.push_request_bytes = 0  # type: ignore[attr-defined]
516
    server.push_declared_request_bytes = None  # type: ignore[attr-defined]
517
    server.push_request_framing_valid = False  # type: ignore[attr-defined]
518
    server.push_request_body_timed_out = False  # type: ignore[attr-defined]
519
    server.request_complete_before_disconnect = False  # type: ignore[attr-defined]
520
    thread = threading.Thread(target=server.serve_forever, name="oakbench-interrupted-push", daemon=True)
521
    thread.start()
522
    return server, thread
523
524
525
def _parse_json_object(text: str | None) -> dict[str, Any] | None:
526
    try:
527
        value = json.loads(text or "")
528
    except (TypeError, json.JSONDecodeError):
529
        return None
530
    return value if isinstance(value, dict) else None
531
532
533
def run_interrupted_publication(
534
    subject: Subject,
535
    run_root: Path,
536
    run_index: int,
537
    metadata: dict[str, Any],
538
    admitted: int,
539
) -> list[dict[str, Any]]:
540
    """Run one real Oak push against a loopback server that drops its reply.
541
542
    The fixture observes request delivery but deliberately has no durable
543
    remote state. That makes the client's outcome unknown and lets the lane
544
    prove work preservation and absence of blind replay without production
545
    mutations or a fake success claim.
546
    """
547
    scenario = INTERRUPTED_PUBLICATION_SCENARIO
548
    metadata = {
549
        **metadata,
550
        "benchmark_track": "failure-evidence",
551
        "remote_transport": "loopback_fault_fixture",
552
        "remote_server": "127.0.0.1",
553
    }
554
    if subject.kind != "oak":
555
        return [
556
            skip_row(
557
                subject,
558
                scenario,
559
                run_index,
560
                metadata,
561
                "interrupted publication fixture currently exercises the actual Oak HTTP client only",
562
            )
563
        ]
564
565
    workspace = run_root / f"interrupted-{subject.name}-r{run_index}"
566
    workspace.mkdir(parents=True)
567
    rows: list[dict[str, Any]] = []
568
    vcs = str(subject.bin)
569
    rows.append(
570
        step_row(subject, scenario, "setup.init", [vcs, "init", "."], workspace, run_index, metadata, admitted)
571
    )
572
    if row_returncode(rows[-1]) != 0:
573
        return rows
574
    rows.append(
575
        edit_step(
576
            subject,
577
            scenario,
578
            "setup.edit",
579
            workspace,
580
            f"interrupted publication run={run_index}",
581
            run_index,
582
            metadata,
583
            admitted,
584
        )
585
    )
586
    rows.append(
587
        step_row(subject, scenario, "setup.checkpoint", [vcs, "commit"], workspace, run_index, metadata, admitted)
588
    )
589
    if row_returncode(rows[-1]) != 0:
590
        return rows
591
592
    before = run_untimed([vcs, "hash"], workspace)
593
    head_before = before.stdout.strip() if before.returncode == 0 else None
594
    payload_path = workspace / "task-notes.md"
595
    payload_before = file_sha256(payload_path)
596
    before_usage = bounded_tree_usage(workspace)
597
    push_help = run_untimed([vcs, "push", "--help"], workspace)
598
    push_supports_json = "--json" in (push_help.stdout + push_help.stderr)
599
    server, thread = start_interrupted_publication_server()
600
    host, port = server.server_address[:2]
601
    remote = f"http://{host}:{port}"
602
    fixture_env = base_env()
603
    fixture_env.pop("OAK_API_KEY", None)
604
    fixture_env.pop("OAK_REMOTE", None)
605
    fixture_env["OAK_NO_UPDATE_CHECK"] = "1"
606
    try:
607
        push_command = [vcs, "push", "--repo", "oak/repo", "--remote", remote]
608
        if push_supports_json:
609
            push_command.append("--json")
610
        publication_attempt = step_row(
611
            subject,
612
            scenario,
613
            "publication.attempt",
614
            push_command,
615
            workspace,
616
            run_index,
617
            metadata,
618
            admitted,
619
            expected_returncodes=tuple(range(1, 128)),
620
            preserve_stdout=True,
621
            preserve_stderr=True,
622
            env=fixture_env,
623
            timeout_seconds=INTERRUPTED_PUBLICATION_PROCESS_TIMEOUT_S,
624
        )
625
        rows.append(publication_attempt)
626
    finally:
627
        server.shutdown()
628
        server.server_close()
629
        thread.join(timeout=2.0)
630
631
    state_row = step_row(
632
        subject,
633
        scenario,
634
        "reconcile.local_state",
635
        [vcs, "agent", "state", "--json", "--compact"],
636
        workspace,
637
        run_index,
638
        metadata,
639
        admitted,
640
        preserve_stdout=True,
641
        env=fixture_env,
642
    )
643
    rows.append(state_row)
644
    after = run_untimed([vcs, "hash"], workspace)
645
    head_after = after.stdout.strip() if after.returncode == 0 else None
646
    payload_after = file_sha256(payload_path)
647
    status_after = _parse_json_object(state_row.get("stdout"))
648
    with server.receipt_lock:  # type: ignore[attr-defined]
649
        request_count = int(server.push_request_count)  # type: ignore[attr-defined]
650
        request_bytes = int(server.push_request_bytes)  # type: ignore[attr-defined]
651
        declared_request_bytes = server.push_declared_request_bytes  # type: ignore[attr-defined]
652
        request_framing_valid = bool(server.push_request_framing_valid)  # type: ignore[attr-defined]
653
        request_body_timed_out = bool(server.push_request_body_timed_out)  # type: ignore[attr-defined]
654
    cli_observation = cli_publication_observation(
655
        publication_attempt,
656
        json_requested=push_supports_json,
657
    )
658
    evidence = interrupted_publication_fields(
659
        request_count=request_count,
660
        request_bytes=request_bytes if request_count else None,
661
        declared_request_bytes=declared_request_bytes,
662
        request_framing_valid=request_framing_valid,
663
        request_body_timed_out=request_body_timed_out,
664
        local_head_before=head_before,
665
        local_head_after=head_after,
666
        status_after=status_after,
667
        worktree_payload_before=payload_before,
668
        worktree_payload_after=payload_after,
669
        cli_observation=cli_observation,
670
    )
671
    evidence["workspace_usage_before"] = before_usage
672
    evidence["workspace_usage_after"] = bounded_tree_usage(workspace)
673
    evidence["remote_transport"] = "loopback_fault_fixture"
674
    evidence["remote_server"] = str(host)
675
    evidence["human_action_count"] = 0
676
    evidence["compatibility_probe_calls"] = 1
677
    evidence["harness_probe_calls"] = 3
678
    evidence["push_json_supported"] = push_supports_json
679
    total = phase_total_row(rows, subject, scenario, run_index, metadata, "interrupted_publication.total")
680
    evidence["harness_command_count"] = total["harness_command_count"] + 3
681
    total.update(evidence)
682
    if not (
683
        evidence["fixture_received_complete_request_before_disconnect"]
684
        and request_count == 1
685
        and evidence["local_head_preserved"]
686
        and evidence["local_work_preserved"]
687
        and not publication_attempt["timed_out"]
688
        and row_returncode(rows[-1]) == 0
689
    ):
690
        total["returncode"] = 1
691
        total["stderr"] = "interrupted_publication_evidence_incomplete"
692
    rows.append(total)
693
    return rows
694
695
696
def edit_step(
697
    subject: Subject,
698
    scenario: str,
699
    operation: str,
700
    workspace: Path,
701
    payload: str,
702
    run_index: int,
703
    metadata: dict[str, Any],
704
    admitted: int,
705
) -> dict[str, Any]:
706
    command = ["/bin/zsh", "-c", f"echo {payload!r} >> task-notes.md"]
707
    return step_row(
708
        subject, scenario, operation, command, workspace, run_index, metadata, admitted, kind="edit"
709
    )
710
711
712
def oak_virtual_branch(subject: Subject, mount_dir: Path) -> str | None:
713
    proc = run_untimed([str(subject.bin), "status"], mount_dir)
714
    match = VIRTUAL_BRANCH_RE.search(proc.stdout + proc.stderr)
715
    return match.group(1) if match else None
716
717
718
def run_oak_loop(
719
    subject: Subject,
720
    remote: oakbench_remotes.RemoteResolution,
721
    run_root: Path,
722
    run_index: int,
723
    metadata: dict[str, Any],
724
    admitted: int,
725
) -> list[dict[str, Any]]:
726
    scenario = "task_loop"
727
    meta = {**metadata, **remote.row_fields()}
728
    rows: list[dict[str, Any]] = []
729
    branches: dict[str, str | None] = {}
730
    payloads: dict[str, str] = {}
731
    mounts: list[Path] = []
732
    vcs = str(subject.bin)
733
    try:
734
        for phase in PHASES:
735
            workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
736
            payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
737
            payloads[phase] = payload
738
            acquire = step_row(
739
                subject, scenario, f"{phase}.acquire", [vcs, "mount", remote.repo or "", str(workspace)],
740
                run_root, run_index, meta, admitted,
741
            )
742
            rows.append(acquire)
743
            if int(acquire["returncode"]) != 0:
744
                return rows
745
            mounts.append(workspace)
746
            branches[phase] = oak_virtual_branch(subject, workspace)
747
            rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
748
            rows.append(step_row(subject, scenario, f"{phase}.snapshot", [vcs, "commit", "--no-verify"], workspace, run_index, meta, admitted))
749
            rows.append(step_row(subject, scenario, f"{phase}.publish", [vcs, "push"], workspace, run_index, meta, admitted))
750
            if phase == "task":
751
                rows.append(
752
                    step_row(
753
                        subject, scenario, "task.describe",
754
                        [vcs, "desc", f"bench task loop {metadata['bench_id']} r{run_index}"],
755
                        workspace, run_index, meta, admitted,
756
                    )
757
                )
758
            end = step_row(
759
                subject, scenario, f"{phase}.end", [vcs, "mount", "end", str(workspace)],
760
                run_root, run_index, meta, admitted,
761
            )
762
            rows.append(end)
763
            if int(end["returncode"]) == 0 and workspace in mounts:
764
                mounts.remove(workspace)
765
766
        # Untimed oracle: each phase's payload must be on its pushed branch.
767
        verifier = run_root / f"tl-verify-{subject.name}-r{run_index}"
768
        clone = run_untimed([vcs, "clone", remote.repo or "", str(verifier)], run_root)
769
        oracle_ok: bool | None = None
770
        if clone.returncode == 0:
771
            oracle_ok = True
772
            for phase in PHASES:
773
                branch = branches.get(phase)
774
                if not branch:
775
                    oracle_ok = None
776
                    break
777
                switch = run_untimed([vcs, "switch", branch], verifier)
778
                content = (verifier / "task-notes.md")
779
                seen = content.read_text() if content.exists() else ""
780
                if switch.returncode != 0 or payloads[phase] not in seen:
781
                    oracle_ok = False
782
        total = {
783
            **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
784
            "oracle_payload_on_remote_branch": oracle_ok,
785
            "workspace_branches": branches,
786
            "measurement_source": "verifier_clone_switch_grep; null oracle means branch name unparsed",
787
        }
788
        rows.append(enforce_oracle_result(total, oracle_ok))
789
        return rows
790
    finally:
791
        for mount_dir in mounts:
792
            run_untimed([vcs, "mount", "end", str(mount_dir), "-f"], run_root)
793
794
795
def ensure_git_remote_seeded(subject: Subject, remote: oakbench_remotes.RemoteResolution, scratch: Path) -> str | None:
796
    """Untimed: make sure the git remote has a main with a seed commit."""
797
    vcs = str(subject.bin)
798
    probe = run_untimed([vcs, "ls-remote", str(remote.repo), "refs/heads/main"], None)
799
    if probe.returncode == 0 and probe.stdout.strip():
800
        if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE:
801
            head = run_untimed([vcs, "symbolic-ref", "HEAD", "refs/heads/main"], Path(str(remote.repo)))
802
            if head.returncode != 0:
803
                return f"git remote HEAD setup failed: {head.stderr.strip()[:200]}"
804
        return None
805
    seed = scratch / "git-seed"
806
    if seed.exists():
807
        shutil.rmtree(seed)
808
    seed.mkdir(parents=True)
809
    (seed / "README.md").write_text("Task-loop benchmark repo (disposable).\n")
810
    (seed / "task-notes.md").write_text("# task notes\n")
811
    for command in (
812
        [vcs, "init", "-q", "-b", "main"],
813
        [vcs, "add", "."],
814
        [vcs, "commit", "-q", "-m", "seed"],
815
        [vcs, "push", "-q", str(remote.repo), "main:main"],
816
    ):
817
        proc = run_untimed(command, seed)
818
        if proc.returncode != 0:
819
            return f"git remote seed `{' '.join(command[1:3])}` failed: {proc.stderr.strip()[:200]}"
820
    if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE:
821
        head = run_untimed([vcs, "symbolic-ref", "HEAD", "refs/heads/main"], Path(str(remote.repo)))
822
        if head.returncode != 0:
823
            return f"git remote HEAD setup failed: {head.stderr.strip()[:200]}"
824
    return None
825
826
827
def run_git_agent_task_baseline(
828
    subject: Subject,
829
    remote: oakbench_remotes.RemoteResolution,
830
    run_root: Path,
831
    run_index: int,
832
    metadata: dict[str, Any],
833
    admitted: int,
834
    remote_create_ms: float = 0.0,
835
) -> list[dict[str, Any]]:
836
    """Measure a cold clone and a warm isolated worktree on one local remote.
837
838
    This is the core-equivalent, local-file cell. Hosted CI is deliberately a
839
    skip row: a local commit oracle is not relabelled as provider CI.
840
    """
841
    scenario = AGENT_TASK_SCENARIO
842
    meta = {
843
        **metadata,
844
        **remote.row_fields(),
845
        "benchmark_track": (
846
            "core-equivalent"
847
            if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE
848
            else "agent-default"
849
        ),
850
    }
851
    vcs = str(subject.bin)
852
    rows: list[dict[str, Any]] = [
853
        harness_step_row(
854
            subject,
855
            scenario,
856
            "setup.remote_container",
857
            run_index,
858
            meta,
859
            remote_create_ms,
860
            harness_command_count=1,
861
        )
862
    ]
863
    setup_start = time.perf_counter()
864
    seed_error = ensure_git_remote_seeded(subject, remote, run_root)
865
    rows.append(
866
        harness_step_row(
867
            subject,
868
            scenario,
869
            "setup.remote_seed",
870
            run_index,
871
            meta,
872
            (time.perf_counter() - setup_start) * 1000,
873
            returncode=1 if seed_error else 0,
874
            harness_command_count=6,
875
        )
876
    )
877
    if seed_error:
878
        rows[-1]["stderr"] = seed_error
879
        return rows
880
881
    base = run_root / f"agent-task-{subject.name}-r{run_index}-cold"
882
    cold_branch = oakbench_remotes.disposable_branch(
883
        str(metadata["bench_id"]), "agent-task-cold", subject.name, run_index
884
    )
885
    warm_branch = oakbench_remotes.disposable_branch(
886
        str(metadata["bench_id"]), "agent-task-warm", subject.name, run_index
887
    )
888
    cold_payload = f"cold task run={run_index} bench={metadata['bench_id']}"
889
    warm_payload = f"warm task run={run_index} bench={metadata['bench_id']}"
890
891
    rows.append(
892
        step_row(
893
            subject,
894
            scenario,
895
            "cold.acquire",
896
            [vcs, "clone", "-q", str(remote.repo), str(base)],
897
            run_root,
898
            run_index,
899
            meta,
900
            admitted,
901
        )
902
    )
903
    if row_returncode(rows[-1]) != 0:
904
        return rows
905
    rows.append(
906
        step_row(
907
            subject, scenario, "cold.branch", [vcs, "switch", "-q", "-c", cold_branch],
908
            base, run_index, meta, admitted,
909
        )
910
    )
911
    rows.append(
912
        step_row(
913
            subject, scenario, "cold.inspect", [vcs, "status", "--porcelain=v1"],
914
            base, run_index, meta, admitted,
915
        )
916
    )
917
    rows.append(edit_step(subject, scenario, "cold.edit", base, cold_payload, run_index, meta, admitted))
918
    rows.append(step_row(subject, scenario, "cold.checkpoint.add", [vcs, "add", "."], base, run_index, meta, admitted))
919
    rows.append(
920
        step_row(
921
            subject, scenario, "cold.checkpoint.commit",
922
            [vcs, "commit", "-q", "-m", f"agent task cold {metadata['bench_id']} r{run_index}"],
923
            base, run_index, meta, admitted,
924
        )
925
    )
926
    rows.append(
927
        step_row(
928
            subject, scenario, "cold.publish", [vcs, "push", "-q", "-u", "origin", cold_branch],
929
            base, run_index, meta, admitted,
930
        )
931
    )
932
    rows.append(
933
        unmeasured_step_row(
934
            subject, scenario, "cold.ci.observe", run_index, meta,
935
            "local-file transport has no hosted CI provider; exact CI observation is unmeasured",
936
        )
937
    )
938
    rows.append(
939
        step_row(
940
            subject, scenario, "cold.review_handoff",
941
            [vcs, "show", "--format=%H%n%s", "--stat", "--oneline", "HEAD"],
942
            base, run_index, meta, admitted,
943
        )
944
    )
945
946
    warm = run_root / f"agent-task-{subject.name}-r{run_index}-warm"
947
    rows.append(
948
        step_row(
949
            subject, scenario, "warm.acquire",
950
            [vcs, "worktree", "add", "-q", str(warm), "-b", warm_branch, "origin/main"],
951
            base, run_index, meta, admitted,
952
        )
953
    )
954
    if row_returncode(rows[-1]) == 0:
955
        rows.append(
956
            step_row(
957
                subject, scenario, "warm.inspect", [vcs, "status", "--porcelain=v1"],
958
                warm, run_index, meta, admitted,
959
            )
960
        )
961
        rows.append(edit_step(subject, scenario, "warm.edit", warm, warm_payload, run_index, meta, admitted))
962
        rows.append(step_row(subject, scenario, "warm.checkpoint.add", [vcs, "add", "."], warm, run_index, meta, admitted))
963
        rows.append(
964
            step_row(
965
                subject, scenario, "warm.checkpoint.commit",
966
                [vcs, "commit", "-q", "-m", f"agent task warm {metadata['bench_id']} r{run_index}"],
967
                warm, run_index, meta, admitted,
968
            )
969
        )
970
        rows.append(
971
            step_row(
972
                subject, scenario, "warm.publish", [vcs, "push", "-q", "-u", "origin", warm_branch],
973
                warm, run_index, meta, admitted,
974
            )
975
        )
976
        rows.append(
977
            unmeasured_step_row(
978
                subject, scenario, "warm.ci.observe", run_index, meta,
979
                "local-file transport has no hosted CI provider; exact CI observation is unmeasured",
980
            )
981
        )
982
        rows.append(
983
            step_row(
984
                subject, scenario, "warm.review_handoff",
985
                [vcs, "show", "--format=%H%n%s", "--stat", "--oneline", "HEAD"],
986
                warm, run_index, meta, admitted,
987
            )
988
        )
989
990
    usage_before_cleanup = {
991
        "cold": bounded_tree_usage(base),
992
        "warm": bounded_tree_usage(warm),
993
    }
994
    oracle_ok = True
995
    for branch, payload in ((cold_branch, cold_payload), (warm_branch, warm_payload)):
996
        show = run_untimed([vcs, "--git-dir", str(remote.repo), "show", f"{branch}:task-notes.md"], run_root)
997
        if show.returncode != 0 or payload not in show.stdout:
998
            oracle_ok = False
999
1000
    if warm.exists():
1001
        rows.append(
1002
            step_row(
1003
                subject, scenario, "cleanup.warm",
1004
                [vcs, "worktree", "remove", str(warm)], base, run_index, meta, admitted,
1005
            )
1006
        )
1007
    cleanup_start = time.perf_counter()
1008
    shutil.rmtree(base, ignore_errors=True)
1009
    rows.append(
1010
        harness_step_row(
1011
            subject, scenario, "cleanup.cold", run_index, meta,
1012
            (time.perf_counter() - cleanup_start) * 1000,
1013
        )
1014
    )
1015
1016
    total = phase_total_row(rows, subject, scenario, run_index, meta, "agent_task.total")
1017
    total.update(
1018
        {
1019
            "oracle_payload_on_remote_branch": oracle_ok,
1020
            "acquisition_models": {
1021
                "cold": "fresh_full_clone",
1022
                "warm": "shared_object_worktree",
1023
            },
1024
            "repository_cache_state": {
1025
                "cold": "fresh_destination_no_shared_object_store",
1026
                "warm": "explicit_shared_object_store",
1027
            },
1028
            "os_cache_state": "unknown",
1029
            "os_cache_state_reason": "page_cache_not_purged_or_instrumented",
1030
            "workspace_usage_before_cleanup": usage_before_cleanup,
1031
            "network_bytes": None,
1032
            "network_bytes_reason": "local_file_transport_not_observed_by_byte_proxy",
1033
            "partial_clone_comparator": None,
1034
            "partial_clone_reason": "local file transport does not establish a network partial-clone comparison",
1035
            "human_action_count": 0,
1036
            "parallel_evidence_lane": "parallel_contention.py/workspace_per_task",
1037
            "oracle_probe_calls": 2,
1038
            "harness_command_count": total["harness_command_count"] + 2,
1039
        }
1040
    )
1041
    if not oracle_ok:
1042
        total["returncode"] = 1
1043
        total["stderr"] = "agent_task_remote_payload_oracle_failed"
1044
    rows.append(total)
1045
    return rows
1046
1047
1048
def run_git_loop(
1049
    subject: Subject,
1050
    remote: oakbench_remotes.RemoteResolution,
1051
    run_root: Path,
1052
    run_index: int,
1053
    metadata: dict[str, Any],
1054
    admitted: int,
1055
) -> list[dict[str, Any]]:
1056
    scenario = "task_loop"
1057
    meta = {
1058
        **metadata,
1059
        **remote.row_fields(),
1060
        "benchmark_track": (
1061
            "core-equivalent"
1062
            if remote.transport == oakbench_remotes.TRANSPORT_LOCAL_FILE
1063
            else "agent-default"
1064
        ),
1065
    }
1066
    vcs = str(subject.bin)
1067
    rows: list[dict[str, Any]] = []
1068
1069
    seed_error = ensure_git_remote_seeded(subject, remote, run_root)
1070
    if seed_error:
1071
        return [skip_row(subject, scenario, run_index, meta, seed_error)]
1072
1073
    # Untimed: the base clone is the long-lived checkout worktrees hang off;
1074
    # an agent fleet pays it once, not per task.
1075
    base = run_root / f"tl-git-base-r{run_index}"
1076
    clone = run_untimed([vcs, "clone", "-q", str(remote.repo), str(base)], run_root)
1077
    if clone.returncode != 0:
1078
        return [skip_row(subject, scenario, run_index, meta, f"base clone failed: {clone.stderr.strip()[:200]}")]
1079
1080
    branches: dict[str, str] = {}
1081
    payloads: dict[str, str] = {}
1082
    for phase in PHASES:
1083
        workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
1084
        branch = oakbench_remotes.disposable_branch(
1085
            str(metadata["bench_id"]), f"taskloop-{phase}", subject.name, run_index
1086
        )
1087
        branches[phase] = branch
1088
        payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
1089
        payloads[phase] = payload
1090
        acquire = step_row(
1091
            subject, scenario, f"{phase}.acquire",
1092
            [vcs, "worktree", "add", str(workspace), "-b", branch], base, run_index, meta, admitted,
1093
        )
1094
        rows.append(acquire)
1095
        if int(acquire["returncode"]) != 0:
1096
            return rows
1097
        rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
1098
        # Agent-default snapshot: git pays stage+commit as two calls.
1099
        rows.append(step_row(subject, scenario, f"{phase}.snapshot.add", [vcs, "add", "."], workspace, run_index, meta, admitted))
1100
        rows.append(
1101
            step_row(
1102
                subject, scenario, f"{phase}.snapshot.commit",
1103
                [vcs, "commit", "-q", "-m", f"bench task loop {metadata['bench_id']} r{run_index} {phase}"],
1104
                workspace, run_index, meta, admitted,
1105
            )
1106
        )
1107
        rows.append(
1108
            step_row(
1109
                subject, scenario, f"{phase}.publish",
1110
                [vcs, "push", "-q", "-u", "origin", branch], workspace, run_index, meta, admitted,
1111
            )
1112
        )
1113
        # No task.describe: git's description is the commit message already
1114
        # paid in {phase}.snapshot.commit (asymmetry documented in the lane doc).
1115
        rows.append(
1116
            step_row(
1117
                subject, scenario, f"{phase}.end",
1118
                [vcs, "worktree", "remove", str(workspace)], base, run_index, meta, admitted,
1119
            )
1120
        )
1121
1122
    # Untimed oracle: payloads visible on the remote branches.
1123
    oracle_ok: bool | None = True
1124
    for phase in PHASES:
1125
        fetch = run_untimed([vcs, "fetch", "-q", "origin", branches[phase]], base)
1126
        show = run_untimed([vcs, "show", f"FETCH_HEAD:task-notes.md"], base)
1127
        if fetch.returncode != 0 or payloads[phase] not in show.stdout:
1128
            oracle_ok = False
1129
    rows.append(
1130
        {
1131
            **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
1132
            "oracle_payload_on_remote_branch": oracle_ok,
1133
            "workspace_branches": branches,
1134
            "measurement_source": "fetch_plus_show_grep",
1135
        }
1136
    )
1137
    return rows
1138
1139
1140
def loop_summary_fields(rows: list[dict[str, Any]]) -> dict[str, Any]:
1141
    """followup-vs-task warm deltas computed from this run's step rows."""
1142
    def op_ms(operation: str) -> float | None:
1143
        for row in rows:
1144
            if row.get("operation") == operation:
1145
                return float(row.get("elapsed_ms", 0.0))
1146
        return None
1147
1148
    def phase_ms(prefix: str) -> float:
1149
        return sum(
1150
            float(row.get("elapsed_ms", 0.0))
1151
            for row in rows
1152
            if str(row.get("operation", "")).startswith(prefix + ".")
1153
        )
1154
1155
    task_acquire = op_ms("task.acquire")
1156
    followup_acquire = op_ms("followup.acquire")
1157
    fields: dict[str, Any] = {
1158
        "task_phase_ms": round(phase_ms("task"), 3),
1159
        "followup_phase_ms": round(phase_ms("followup"), 3),
1160
    }
1161
    if task_acquire is not None and followup_acquire is not None:
1162
        fields["followup_warm_delta_ms"] = round(followup_acquire - task_acquire, 3)
1163
        fields["acquire_ms_task"] = round(task_acquire, 3)
1164
        fields["acquire_ms_followup"] = round(followup_acquire, 3)
1165
    else:
1166
        fields["followup_warm_delta_ms"] = None
1167
    return fields
1168
1169
1170
def summary_text(rows: list[dict[str, Any]]) -> str:
1171
    totals = [row for row in rows if row.get("operation") == "task_loop.total"]
1172
    lines = [
1173
        "# Task Loop Summary",
1174
        "",
1175
        "Whole-task cost of the signature motion: acquire workspace -> edit -> snapshot ->",
1176
        "publish -> describe -> end, then a follow-up task in a fresh workspace.",
1177
        "Command sequences are NOT step-equivalent across subjects",
1178
        "(oak pays desc; git's description is its commit message). Latency across unlike",
1179
        "transports is never comparable; scripted call equivalents and estimated token proxies are.",
1180
        "",
1181
        "| Subject | Run | Transport | Wall ms | Scripted call equivalents | Cost-wt tokens (env) | Warm acquire delta ms | Oracle |",
1182
        "| --- | ---: | --- | ---: | ---: | ---: | ---: | --- |",
1183
    ]
1184
    for row in totals:
1185
        oracle = row.get("oracle_payload_on_remote_branch")
1186
        lines.append(
1187
            "| `{}` | {} | {} | {} | {} | {} | {} | {} |".format(
1188
                row["subject"],
1189
                row["run"],
1190
                row.get("remote_transport", "unmeasured"),
1191
                fmt_ms(row.get("elapsed_ms")),
1192
                row.get("tool_call_count", "unmeasured"),
1193
                row.get("estimated_cost_weighted_tokens_with_envelope", "unmeasured"),
1194
                row.get("followup_warm_delta_ms", "unmeasured"),
1195
                "pass" if oracle else ("FAIL" if oracle is False else "unmeasured"),
1196
            )
1197
        )
1198
    skips = [
1199
        row for row in rows
1200
        if row.get("skipped") and row.get("scenario") == TASK_LOOP_SCENARIO
1201
    ]
1202
    for row in skips:
1203
        lines.append(f"| `{row['subject']}` | {row['run']} | skipped | | | | | {row['skip_reason'][:70]} |")
1204
1205
    agent_totals = [row for row in rows if row.get("operation") == "agent_task.total"]
1206
    if agent_totals:
1207
        lines.extend(
1208
            [
1209
                "",
1210
                "## Cold clone and warm isolated task",
1211
                "",
1212
                "Local-file Git rows compare a fresh full clone with a worktree sharing",
1213
                "the first clone's object store. OS cache state and network bytes remain",
1214
                "unknown. Hosted CI is a skip, not a local-oracle success.",
1215
                "",
1216
                "| Subject | Run | Wall ms | Scripted call equivalents | Output bytes | Coverage | Oracle |",
1217
                "| --- | ---: | ---: | ---: | ---: | --- | --- |",
1218
            ]
1219
        )
1220
        for row in agent_totals:
1221
            lines.append(
1222
                "| `{}` | {} | {} | {} | {} | {} | {} |".format(
1223
                    row["subject"],
1224
                    row["run"],
1225
                    fmt_ms(row.get("elapsed_ms")),
1226
                    row.get("tool_call_count", "unmeasured"),
1227
                    row.get("raw_output_bytes", "unmeasured"),
1228
                    "complete" if row.get("coverage_complete") else "partial",
1229
                    "pass" if row.get("oracle_payload_on_remote_branch") else "FAIL",
1230
                )
1231
            )
1232
1233
    interrupted_totals = [
1234
        row for row in rows if row.get("operation") == "interrupted_publication.total"
1235
    ]
1236
    if interrupted_totals:
1237
        lines.extend(
1238
            [
1239
                "",
1240
                "## Interrupted publication",
1241
                "",
1242
                "A loopback fixture validates the request framing and drops the response.",
1243
                "Fixture delivery knowledge is reported separately from the CLI's own outcome",
1244
                "claim. Request receipt is not durability proof; the harness never retries.",
1245
                "",
1246
                "| Subject | Run | Wall ms | Scripted call equivalents | Output bytes | CLI outcome | Fixture delivery | POSTs | Request bytes | Head/work preserved |",
1247
                "| --- | ---: | ---: | ---: | ---: | --- | --- | ---: | ---: | --- |",
1248
            ]
1249
        )
1250
        for row in interrupted_totals:
1251
            lines.append(
1252
                "| `{}` | {} | {} | {} | {} | `{}` | `{}` | {} | {} | {}/{} |".format(
1253
                    row["subject"],
1254
                    row["run"],
1255
                    fmt_ms(row.get("elapsed_ms")),
1256
                    row.get("tool_call_count", "unmeasured"),
1257
                    row.get("raw_output_bytes", "unmeasured"),
1258
                    row.get("publication_outcome", "unmeasured"),
1259
                    row.get("fixture_delivery_state", "unmeasured"),
1260
                    row.get("mutation_requests_observed", "unmeasured"),
1261
                    row.get("network_request_bytes_observed", "unmeasured"),
1262
                    "yes" if row.get("local_head_preserved") else "no",
1263
                    "yes" if row.get("local_work_preserved") else "no",
1264
                )
1265
            )
1266
    return "\n".join(lines) + "\n"
1267
1268
1269
def fmt_ms(value: Any) -> str:
1270
    try:
1271
        return f"{float(value):.0f}"
1272
    except (TypeError, ValueError):
1273
        return "unmeasured"
1274
1275
1276
def main() -> int:
1277
    args = parse_args()
1278
    scenarios = [item.strip() for item in args.scenarios.split(",") if item.strip()]
1279
    known_scenarios = {TASK_LOOP_SCENARIO, AGENT_TASK_SCENARIO, INTERRUPTED_PUBLICATION_SCENARIO}
1280
    unknown_scenarios = sorted(set(scenarios) - known_scenarios)
1281
    if unknown_scenarios:
1282
        raise SystemExit(
1283
            "Unknown scenarios: " + ", ".join(unknown_scenarios)
1284
            + "; valid: " + ", ".join(sorted(known_scenarios))
1285
        )
1286
    subjects = load_subjects(args)
1287
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
1288
    args.results.mkdir(parents=True, exist_ok=True)
1289
    args.workdir.mkdir(parents=True, exist_ok=True)
1290
    run_root = args.workdir / "runs" / timestamp
1291
    run_root.mkdir(parents=True, exist_ok=True)
1292
1293
    metadata = {
1294
        "bench_id": timestamp,
1295
        "profile": "task-loop",
1296
        "benchmark_track": "agent-default",
1297
        "timestamp_utc": timestamp,
1298
        "host": platform.node(),
1299
        "platform": platform.platform(),
1300
        "machine": platform.machine(),
1301
        "python": platform.python_version(),
1302
        "env_isolation_version": ENV_ISOLATION_VERSION,
1303
        "subject_versions": subject_versions(subjects),
1304
        "subject_details": subject_details(subjects),
1305
        "source": source_metadata(args.oak_repo),
1306
        "git_remote_mode": args.git_remote,
1307
        "trial_order_randomized": args.randomize_subject_order,
1308
        "trial_order_seed": args.random_seed if args.randomize_subject_order else None,
1309
    }
1310
1311
    rows: list[dict[str, Any]] = []
1312
    with measurement_lock("task_loop") as lock_info:
1313
        metadata["measurement_lock_wait_ms"] = lock_info.wait_ms
1314
        metadata["measurement_lock"] = "held" if lock_info.enabled else "disabled"
1315
        order_rng = random.Random(args.random_seed)
1316
        for run_index in range(args.runs):
1317
            run_subjects = list(subjects)
1318
            if args.randomize_subject_order:
1319
                order_rng.shuffle(run_subjects)
1320
            run_metadata = {
1321
                **metadata,
1322
                "run_subject_order": [subject.name for subject in run_subjects],
1323
            }
1324
            for subject in run_subjects:
1325
                for scenario in scenarios:
1326
                    print(f"[run] {scenario} run={run_index} subject={subject.name}", flush=True)
1327
                    if scenario == INTERRUPTED_PUBLICATION_SCENARIO:
1328
                        rows.extend(
1329
                            run_interrupted_publication(
1330
                                subject, run_root, run_index, run_metadata, args.admitted_output_chars
1331
                            )
1332
                        )
1333
                        continue
1334
                    if scenario == AGENT_TASK_SCENARIO:
1335
                        if subject.kind != "git":
1336
                            rows.append(
1337
                                unmeasured_step_row(
1338
                                    subject,
1339
                                    scenario,
1340
                                    "agent_task.skipped",
1341
                                    run_index,
1342
                                    run_metadata,
1343
                                    "Oak end-to-end publication and exact CI require an explicitly configured disposable staging lane; production mutation is forbidden",
1344
                                )
1345
                            )
1346
                            continue
1347
                        remote_create_start = time.perf_counter()
1348
                        remote = oakbench_remotes.make_git_bare_remote(
1349
                            subject.bin, run_root, f"agent-task-remote-r{run_index}.git"
1350
                        )
1351
                        remote_create_ms = (time.perf_counter() - remote_create_start) * 1000
1352
                        rows.extend(
1353
                            run_git_agent_task_baseline(
1354
                                subject,
1355
                                remote,
1356
                                run_root,
1357
                                run_index,
1358
                                run_metadata,
1359
                                args.admitted_output_chars,
1360
                                remote_create_ms,
1361
                            )
1362
                        )
1363
                        continue
1364
                    if subject.kind == "git":
1365
                        if args.git_remote == "github":
1366
                            remote = oakbench_remotes.resolve_git_github_remote()
1367
                        else:
1368
                            remote = oakbench_remotes.make_git_bare_remote(
1369
                                subject.bin, run_root, f"task-loop-remote-r{run_index}.git"
1370
                            )
1371
                        if not remote.resolved:
1372
                            rows.append(skip_row(subject, TASK_LOOP_SCENARIO, run_index, run_metadata, remote.skip_reason or ""))
1373
                            continue
1374
                        subject_rows = run_git_loop(subject, remote, run_root, run_index, run_metadata, args.admitted_output_chars)
1375
                    else:
1376
                        remote = oakbench_remotes.resolve_oak_remote("sync")
1377
                        if not remote.resolved:
1378
                            rows.append(skip_row(subject, TASK_LOOP_SCENARIO, run_index, run_metadata, remote.skip_reason or ""))
1379
                            continue
1380
                        subject_rows = run_oak_loop(subject, remote, run_root, run_index, run_metadata, args.admitted_output_chars)
1381
                    total = next((row for row in subject_rows if row.get("operation") == "task_loop.total"), None)
1382
                    if total is not None:
1383
                        total.update(loop_summary_fields(subject_rows))
1384
                    rows.extend(subject_rows)
1385
1386
    store = ResultsStore(args.results, lane="task-loop")
1387
    raw_path, summary_path = store.write(timestamp, rows, summary_text(rows))
1388
    if not args.keep_workdirs:
1389
        shutil.rmtree(run_root, ignore_errors=True)
1390
    print(f"[result] {raw_path}")
1391
    print(f"[summary] {summary_path}")
1392
    return 1 if has_lane_failures(rows) else 0
1393
1394
1395
if __name__ == "__main__":
1396
    raise SystemExit(main())