Viewing files at commit 62758990fe31Commit detailsView latest

Browse files
scripts/platform_lifecycle.py 4263 lines · 161.5 KB · python
1
#!/usr/bin/env python3
2
"""The platform lane: hosted integration workflows, end to end.
3
4
Measures GitHub-the-platform vs oak hosted workflows from "task branch exists
5
locally" to "change merged in main, present in local checkout" — including
6
the async waits agents actually experience (GitHub computes PR mergeability
7
asynchronously; that settling is a measured operation, observed by polling on
8
one local monotonic clock).
9
10
Drivers per platform:
11
  cli         shells out to gh/git/oak via run_timed — full token/output
12
              accounting from captured output, like every other CLI lane.
13
  harness-api stdlib urllib REST against the GitHub API — exact api_metrics
14
              (request_count/bytes, rate-limit header, request ids); token
15
              fields stay null because API JSON is not an agent transcript
16
              and token costs would be fiction (ADR-0002).
17
  fake-provider
18
              deterministic in-process branch triage provider for oracle
19
              coverage when no hosted platform is available; not comparable
20
              to real platform timings.
21
22
This lane runs against real hosted services and is EXCLUDED from devloop:
23
external variance must never poison changeset verdicts.
24
25
Without credentials/configured repos (the only path CI exercises) every
26
operation becomes a skip row with an explicit skip_reason and the process
27
exits 3 (all-skipped), never silent absence.
28
29
Setup:
30
  GITHUB_TOKEN=... python3 scripts/platform_lifecycle.py \\
31
    --platform github --driver harness-api --scenario pr_single_anatomy \\
32
    --repo your-org/disposable-bench-repo --runs 3
33
"""
34
35
from __future__ import annotations
36
37
import argparse
38
import concurrent.futures
39
import json
40
import os
41
import platform as platform_module
42
import re
43
import shlex
44
import shutil
45
import subprocess
46
import sys
47
import tempfile
48
import threading
49
import time
50
from dataclasses import dataclass
51
from datetime import datetime, timezone
52
from pathlib import Path
53
from typing import Any, Callable, Optional
54
55
from oakbench import platform_clock
56
from oakbench import tokens as oakbench_tokens
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
60
from oakbench.platform_api import GitHubApiError, GitHubClient, RateLimitBudgeter
61
from oakbench.remotes import disposable_branch
62
from oakbench.results import ResultsStore
63
from oakbench.rows import row_returncode
64
from oakbench.runlock import measurement_lock
65
from oakbench.runner import runner_fields, stamp_row
66
67
from mount_probe import parse_yaml_subset
68
69
ROOT = Path(__file__).resolve().parents[1]
70
DEFAULT_SPEC = ROOT / "scenarios" / "platform.yaml"
71
DEFAULT_SEMANTICS = ROOT / "config" / "platform_semantics.json"
72
DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-platform-lane"
73
SKIP_RETURNCODE = 77
74
EXIT_ALL_SKIPPED = 3
75
PR_URL_RE = re.compile(r"/pull/(\d+)")
76
STRUCTURAL_SOURCE = "structural_platform_semantics_zero_commands"
77
78
# integration_race_nN: N task branches each through publish -> PR -> merge,
79
# serially or as fast as RateLimitBudgeter pacing allows (project plan:
80
# default 500 content ops/hour). Injected pacing waits are REPORTED as
81
# race.pacing.injected, never subtracted from operation durations.
82
RACE_SCENARIO_RE = re.compile(r"^integration_race_n(\d+)$")
83
BRANCH_TRIAGE_SCENARIO_RE = re.compile(r"^branch_triage_n(\d+)$")
84
BRANCH_FLEET_SCENARIO_RE = re.compile(r"^branch_fleet_n(\d+)$")
85
DEFAULT_RACE_OPS_PER_HOUR = 500
86
# gh-cli race orchestration (N clones + interleaved gh polling under one
87
# budgeter) is deferred to Phase 4: run_timed's one-command-at-a-time
88
# accounting cannot yet attribute interleaved waits honestly across branches.
89
RACE_CLI_DEFER_REASON = "race_cli_driver_deferred"
90
# check-instant protection variant: the HARNESS posts this required status
91
# instantly via the commit status API (deterministic, free, no Actions).
92
CHECK_INSTANT_CONTEXT = "oakbench/check-instant"
93
CHECKS_GREEN_OPERATION = "integration.checks.green.settle"
94
BRANCH_TRIAGE_FAKE_DRIVER = "fake-provider"
95
BRANCH_TRIAGE_CLASSES = ("clean", "stale", "duplicate", "conflicting")
96
BRANCH_TRIAGE_ACTIONS = {
97
    "clean": "merge",
98
    "stale": "refresh_then_merge",
99
    "duplicate": "close_duplicate",
100
    "conflicting": "keep_open_manual_resolution",
101
}
102
BRANCH_FLEET_PROVIDER_FAKE = "fake"
103
BRANCH_FLEET_PROVIDER_GITHUB_API = "github:harness-api"
104
BRANCH_FLEET_PROVIDER_OAK_CLI = "oak:cli"
105
BRANCH_FLEET_LIVE_LARGE_LIMIT = 100
106
BRANCH_FLEET_LIVE_LARGE_ENV = "OAK_BENCH_BRANCH_FLEET_LIVE_LARGE"
107
BRANCH_FLEET_CLASSIFY_PARSE_STDOUT_BYTES = 50 * 1024 * 1024
108
BRANCH_FLEET_SEED_TRANSIENT_RETRIES = 2
109
BRANCH_FLEET_SEED_RETRY_DELAY_S = 2.0
110
BRANCH_FLEET_SEED_TIMEOUT_RETCODE = 124
111
BRANCH_FLEET_SEED_WORKERS_ENV = "OAK_BENCH_BRANCH_FLEET_SEED_WORKERS"
112
BRANCH_FLEET_CLEANUP_WORKERS_ENV = "OAK_BENCH_BRANCH_FLEET_CLEANUP_WORKERS"
113
BRANCH_FLEET_SEED_WORKERS_DEFAULT = 4
114
BRANCH_FLEET_CLEANUP_WORKERS_DEFAULT = 4
115
BRANCH_FLEET_WORKERS_MAX = 16
116
# Posting a commit status needs the REST API; gh/git in this lane cannot do
117
# it, so a cli-driver run under check-instant would merge on a protected repo
118
# without the required check ever existing (or fail outright) while its rows
119
# claimed the variant held. Honest gap instead: every operation row skips.
120
CHECK_INSTANT_CLI_SKIP_REASON = "protection_variant_requires_api_driver:check-instant"
121
122
123
# --------------------------------------------------------------------------
124
# Configuration
125
# --------------------------------------------------------------------------
126
127
128
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
129
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
130
    parser.add_argument("--platform", choices=("github", "oak"), default="github")
131
    parser.add_argument("--driver", choices=("cli", "harness-api", BRANCH_TRIAGE_FAKE_DRIVER), default="cli")
132
    parser.add_argument("--scenario", default="platform_capability_probe", help="Scenario name from scenarios/platform.yaml")
133
    parser.add_argument("--repo", default="", help="org/repo for github; oak repo for oak. Must be DISPOSABLE.")
134
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "platform")
135
    parser.add_argument("--runs", type=int, default=1)
136
    parser.add_argument("--protection-variant", default="protection-none")
137
    parser.add_argument("--semantics-class", default="integration-default")
138
    parser.add_argument("--token-env", default="GITHUB_TOKEN", help="NAME of the env var holding the PAT")
139
    parser.add_argument("--spec", type=Path, default=DEFAULT_SPEC)
140
    parser.add_argument("--semantics-config", type=Path, default=DEFAULT_SEMANTICS)
141
    parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
142
    parser.add_argument("--admitted-output-chars", type=int, default=20_000)
143
    parser.add_argument("--keep-workdirs", action="store_true")
144
    return parser.parse_args(argv)
145
146
147
def load_platform_spec(path: Path) -> dict[str, Any]:
148
    return parse_yaml_subset(path.read_text())
149
150
151
def scenario_by_name(spec: dict[str, Any], name: str) -> Optional[dict[str, Any]]:
152
    for scenario in spec.get("scenarios") or []:
153
        if isinstance(scenario, dict) and scenario.get("name") == name:
154
            return scenario
155
    return None
156
157
158
def load_semantics(path: Path) -> dict[str, Any]:
159
    return json.loads(path.read_text())
160
161
162
def compute_semantic_contract_match(
163
    semantics: dict[str, Any], semantics_class: str, protection_variant: str
164
) -> bool:
165
    """True when this run executes a declared semantics class under an
166
    implemented protection variant — the precondition for any cross-platform
167
    comparison of its rows."""
168
    classes = semantics.get("classes") or {}
169
    implemented = semantics.get("implemented_protection_variants") or []
170
    return semantics_class in classes and protection_variant in implemented
171
172
173
def platform_comparison_key(
174
    semantics_version: Any, semantics_class: str, protection_variant: str
175
) -> str:
176
    """Stable join key for cross-platform platform-lane comparisons.
177
178
    The protection variant is part of the key by design: a
179
    protection-none row and a check-instant row may share a semantics class,
180
    but they are not the same hosted workflow contract.
181
    """
182
    return f"{semantics_version}:{semantics_class}:{protection_variant}"
183
184
185
def platform_rows_comparable(row_a: dict[str, Any], row_b: dict[str, Any]) -> bool:
186
    """Pure guard for consumers: compare only same semantics+variant rows."""
187
    if row_a.get("semantic_contract_match") is not True:
188
        return False
189
    if row_b.get("semantic_contract_match") is not True:
190
        return False
191
    key_a = row_a.get("platform_comparison_key")
192
    key_b = row_b.get("platform_comparison_key")
193
    if key_a is not None or key_b is not None:
194
        return key_a == key_b
195
    # Backward-compatible fallback for older rows that predate the explicit key.
196
    return (
197
        row_a.get("platform_semantics_version") or row_a.get("semantics_version"),
198
        row_a.get("platform_semantics_class"),
199
        row_a.get("protection_variant"),
200
    ) == (
201
        row_b.get("platform_semantics_version") or row_b.get("semantics_version"),
202
        row_b.get("platform_semantics_class"),
203
        row_b.get("protection_variant"),
204
    )
205
206
207
def protection_variant_executable(
208
    platform_name: str, driver: str, protection_variant: str
209
) -> bool:
210
    """Whether this driver can actually EXECUTE the protection variant.
211
212
    check-instant means the harness posts the required commit status via the
213
    REST API; on GitHub only the harness-api driver can do that, so a
214
    cli-driver run never executes the variant and its rows must never claim
215
    semantic_contract_match (the cli runner emits
216
    CHECK_INSTANT_CLI_SKIP_REASON skip rows instead). Everything else is
217
    unchanged: implementedness is compute_semantic_contract_match's job."""
218
    if driver == BRANCH_TRIAGE_FAKE_DRIVER:
219
        return False
220
    if (
221
        platform_name == "github"
222
        and driver == "cli"
223
        and protection_variant == "check-instant"
224
    ):
225
        return False
226
    return True
227
228
229
@dataclass(frozen=True)
230
class Settings:
231
    interval_s: float
232
    settle_timeout_s: float
233
    timeout_s: float
234
    admitted: int
235
236
237
def scenario_settings(spec: dict[str, Any], scenario: dict[str, Any], admitted: int) -> Settings:
238
    defaults = spec.get("defaults") or {}
239
    interval = scenario.get("poll_interval_s", defaults.get("settle_interval_s", 2))
240
    return Settings(
241
        interval_s=float(interval),
242
        settle_timeout_s=float(defaults.get("settle_timeout_s", 300)),
243
        timeout_s=float(defaults.get("timeout_seconds", 120)),
244
        admitted=admitted,
245
    )
246
247
248
# --------------------------------------------------------------------------
249
# Readiness: missing PAT/repos/binaries become skip rows, never failures.
250
# --------------------------------------------------------------------------
251
252
253
def readiness_skip_reason(
254
    platform_name: str,
255
    driver: str,
256
    repo: str,
257
    token_env: str,
258
    *,
259
    requires_remote: bool,
260
    which: Callable[[str], Optional[str]] = shutil.which,
261
    environ: Optional[dict[str, str]] = None,
262
) -> Optional[str]:
263
    if driver == BRANCH_TRIAGE_FAKE_DRIVER:
264
        return None
265
    env = os.environ if environ is None else environ
266
    if platform_name == "github":
267
        if driver == "cli" and which("gh") is None:
268
            return "gh_cli_missing"
269
        if not str(env.get(token_env, "")).strip():
270
            return f"github_token_missing:{token_env}"
271
        if requires_remote and not repo:
272
            return "github_repo_not_configured"
273
        return None
274
    if driver == "harness-api":
275
        return "oak_harness_api_not_implemented"
276
    if which("oak") is None:
277
        return "oak_cli_missing"
278
    if requires_remote and not repo:
279
        return "oak_remote_missing"
280
    if requires_remote and not oak_credentials_path(env).is_file():
281
        return "oak_credentials_missing"
282
    return None
283
284
285
def oak_cli_commit_push_skip_reason(
286
    env: dict[str, str],
287
    *,
288
    runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
289
) -> Optional[str]:
290
    """Return a skip reason when the live Oak CLI cannot publish explicitly."""
291
    try:
292
        probe = runner(
293
            ["oak", "commit", "--help"],
294
            capture_output=True,
295
            text=True,
296
            env=env,
297
            timeout=15,
298
            check=False,
299
        )
300
    except FileNotFoundError:
301
        return "oak_cli_missing"
302
    except subprocess.TimeoutExpired:
303
        return "oak_commit_push_capability_timeout"
304
    help_text = (probe.stdout or "") + "\n" + (probe.stderr or "")
305
    if probe.returncode != 0:
306
        return "oak_commit_push_capability_failed"
307
    if "--push" not in help_text:
308
        return "oak_commit_push_not_supported:requires_current_oak"
309
    return None
310
311
312
# --------------------------------------------------------------------------
313
# Row builders
314
# --------------------------------------------------------------------------
315
316
317
def base_env() -> dict[str, str]:
318
    return oakbench_base_env(
319
        author_name="Oak Platform Lane",
320
        author_email="[email protected]",
321
        oak_author="oak-platform-lane",
322
    )
323
324
325
def oak_credentials_path(environ: Optional[dict[str, str]] = None) -> Path:
326
    env = os.environ if environ is None else environ
327
    override = str(env.get("OAK_CREDENTIALS_FILE") or "").strip()
328
    if override:
329
        return Path(override).expanduser()
330
    return Path(str(env.get("HOME") or "")).expanduser() / ".oak" / "credentials"
331
332
333
def install_oak_credentials(env: dict[str, str]) -> bool:
334
    source = oak_credentials_path()
335
    if not source.is_file():
336
        return False
337
    dest = Path(env["HOME"]) / ".oak" / "credentials"
338
    dest.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
339
    shutil.copy2(source, dest)
340
    dest.chmod(0o600)
341
    return True
342
343
344
def cli_env(args: argparse.Namespace) -> dict[str, str]:
345
    env = base_env()
346
    token = os.environ.get(args.token_env, "")
347
    if args.platform == "github" and token:
348
        # gh reads GH_TOKEN; git over https uses it via gh's credential helper.
349
        env.setdefault("GH_TOKEN", token)
350
        env.setdefault("GITHUB_TOKEN", token)
351
    if args.platform == "oak":
352
        install_oak_credentials(env)
353
    return env
354
355
356
def null_token_fields() -> dict[str, Any]:
357
    return {
358
        "estimated_tokens_total": None,
359
        "estimated_tokens_input": None,
360
        "estimated_tokens_output": None,
361
        "estimated_tokens_agent_emitted": None,
362
        "estimated_tokens_agent_ingested": None,
363
        "estimated_cost_weighted_tokens": None,
364
        "token_fields_note": (
365
            "harness-api driver: REST JSON is not an agent transcript; "
366
            "token costs would be fiction. Null means unmeasured (ADR-0002)."
367
        ),
368
    }
369
370
371
def fake_provider_token_fields() -> dict[str, Any]:
372
    return {
373
        "estimated_tokens_total": None,
374
        "estimated_tokens_input": None,
375
        "estimated_tokens_output": None,
376
        "estimated_tokens_agent_emitted": None,
377
        "estimated_tokens_agent_ingested": None,
378
        "estimated_cost_weighted_tokens": None,
379
        "raw_output_bytes": None,
380
        "stdout_bytes": None,
381
        "stderr_bytes": None,
382
        "token_fields_note": (
383
            "fake-provider driver: in-process deterministic oracle fixture; "
384
            "no terminal command or agent-visible command output exists."
385
        ),
386
    }
387
388
389
def skip_row(
390
    meta: dict[str, Any],
391
    scenario: str,
392
    operation: str,
393
    run_index: int,
394
    reason: str,
395
    *,
396
    settings: Optional[Settings] = None,
397
) -> dict[str, Any]:
398
    row = {
399
        **meta,
400
        "scenario": scenario,
401
        "run": run_index,
402
        "operation": operation,
403
        "elapsed_ms": 0.0,
404
        "returncode": SKIP_RETURNCODE,
405
        "command": [],
406
        "skipped": True,
407
        "skip_reason": reason,
408
        "tool_call_count": 0,
409
    }
410
    if settings is not None and operation.endswith(".settle"):
411
        row.update(
412
            {
413
                "settled": None,
414
                "settle_elapsed_ms": None,
415
                "observed_wait_ms": 0.0,
416
                "poll_iterations": 0,
417
                "poll_interval_s": settings.interval_s,
418
                "poll_quantization_ms": settings.interval_s * 1000.0,
419
            }
420
        )
421
    return row
422
423
424
def cli_step_row(
425
    meta: dict[str, Any],
426
    scenario: str,
427
    operation: str,
428
    command: list[str],
429
    cwd: Path,
430
    run_index: int,
431
    env: dict[str, str],
432
    admitted: int,
433
    full_stdout_bytes: Optional[int] = None,
434
) -> dict[str, Any]:
435
    capture = run_timed(command, cwd, env, admitted, full_output_bytes=full_stdout_bytes)
436
    row = {
437
        **meta,
438
        "scenario": scenario,
439
        "run": run_index,
440
        "operation": operation,
441
        "elapsed_ms": round(capture.elapsed_ms, 3),
442
        "returncode": capture.returncode,
443
        "command": command,
444
        "tool_call_count": 1,
445
        "terminal_tool_call_count": 1,
446
        **oakbench_tokens.interaction_token_fields(
447
            command_display(command),
448
            capture.stdout_text,
449
            capture.stderr_text,
450
            capture.stdout_bytes,
451
            capture.stderr_bytes,
452
            capture.stdout_truncated,
453
            capture.stderr_truncated,
454
        ),
455
        "_stdout": capture.stdout_full_text
456
        if capture.stdout_full_text is not None
457
        else capture.stdout_text,
458
    }
459
    if capture.returncode != 0:
460
        row["stderr"] = capture.stderr_text[-4000:]
461
    return row
462
463
464
def cli_sequence_row(
465
    meta: dict[str, Any],
466
    scenario: str,
467
    operation: str,
468
    commands: list[list[str]],
469
    cwd: Path,
470
    run_index: int,
471
    env: dict[str, str],
472
    admitted: int,
473
) -> dict[str, Any]:
474
    start = platform_clock.monotonic_ms()
475
    stdout_parts: list[str] = []
476
    stderr_parts: list[str] = []
477
    failed: Optional[subprocess.CompletedProcess[str]] = None
478
    for command in commands:
479
        proc = run_untimed(command, cwd, env)
480
        stdout_parts.append(proc.stdout)
481
        stderr_parts.append(proc.stderr)
482
        if proc.returncode != 0:
483
            failed = proc
484
            break
485
    elapsed_ms = platform_clock.monotonic_ms() - start
486
    stdout = "\n".join(part for part in stdout_parts if part)
487
    stderr = "\n".join(part for part in stderr_parts if part)
488
    command_text = "\n".join(command_display(command) for command in commands)
489
    row = {
490
        **meta,
491
        "scenario": scenario,
492
        "run": run_index,
493
        "operation": operation,
494
        "elapsed_ms": round(elapsed_ms, 3),
495
        "returncode": 0 if failed is None else int(failed.returncode),
496
        "command": commands,
497
        "tool_call_count": len(commands if failed is None else commands[: len(stdout_parts)]),
498
        "terminal_tool_call_count": len(commands if failed is None else commands[: len(stdout_parts)]),
499
        **oakbench_tokens.interaction_token_fields(
500
            command_text,
501
            stdout[:admitted],
502
            stderr[:admitted],
503
            len(stdout.encode("utf-8")),
504
            len(stderr.encode("utf-8")),
505
            len(stdout) > admitted,
506
            len(stderr) > admitted,
507
        ),
508
        "_stdout": stdout,
509
    }
510
    if failed is not None:
511
        row["stderr"] = stderr[-4000:]
512
    return row
513
514
515
def settle_fields(result: dict[str, Any]) -> dict[str, Any]:
516
    return {
517
        "settled": result["settled"],
518
        "settle_elapsed_ms": result["elapsed_ms"],
519
        "observed_wait_ms": round(result["observed_wait_ms"], 3),
520
        "poll_iterations": result["poll_iterations"],
521
        "poll_interval_s": result["poll_interval_s"],
522
        "poll_quantization_ms": result["poll_quantization_ms"],
523
    }
524
525
526
def settle_row(
527
    meta: dict[str, Any],
528
    scenario: str,
529
    operation: str,
530
    run_index: int,
531
    poll_fn: Callable[[], Any],
532
    settings: Settings,
533
    command: list[str],
534
    poll_is_tool_call: bool,
535
    clock: Callable[[], float] = time.monotonic,
536
    sleep: Callable[[float], None] = time.sleep,
537
) -> dict[str, Any]:
538
    result = platform_clock.settle(
539
        poll_fn,
540
        interval_s=settings.interval_s,
541
        timeout_s=settings.settle_timeout_s,
542
        clock=clock,
543
        sleep=sleep,
544
    )
545
    # Print the resolution bound the way devloop prints detection limits: a
546
    # settle observed at this cadence can never claim finer precision.
547
    print(
548
        f"[quantization] {operation} poll_interval_s={settings.interval_s} "
549
        f"poll_quantization_ms={result['poll_quantization_ms']}",
550
        flush=True,
551
    )
552
    return {
553
        **meta,
554
        "scenario": scenario,
555
        "run": run_index,
556
        "operation": operation,
557
        # The wall time spent on this operation is always measured; the
558
        # settle duration itself is null on timeout (platform_clock policy).
559
        "elapsed_ms": round(result["observed_wait_ms"], 3),
560
        "returncode": 0 if result["settled"] else 1,
561
        "command": command,
562
        "tool_call_count": result["poll_iterations"] if poll_is_tool_call else None,
563
        **settle_fields(result),
564
    }
565
566
567
def structural_row(
568
    meta: dict[str, Any],
569
    scenario: str,
570
    operation: str,
571
    run_index: int,
572
    note: str,
573
) -> dict[str, Any]:
574
    """A platform-semantics zero: the operation has no equivalent because the
575
    platform completes it synchronously elsewhere. Zero commands were run
576
    (measured zero); the settle value is null (no equivalent, ADR-0002)."""
577
    return {
578
        **meta,
579
        "scenario": scenario,
580
        "run": run_index,
581
        "operation": operation,
582
        "elapsed_ms": 0.0,
583
        "returncode": 0,
584
        "command": [],
585
        "structural": True,
586
        "note": note,
587
        "settled": True,
588
        "settle_elapsed_ms": None,
589
        "poll_iterations": 0,
590
        "tool_call_count": 0,
591
        "measurement_source": STRUCTURAL_SOURCE,
592
    }
593
594
595
def metrics_snapshot(client: GitHubClient) -> dict[str, Any]:
596
    metrics = client.api_metrics
597
    return {**metrics, "request_ids": list(metrics["request_ids"])}
598
599
600
def metrics_delta(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
601
    delta_count = after["request_count"] - before["request_count"]
602
    ids = after["request_ids"][-delta_count:] if delta_count else []
603
    return {
604
        "request_count": delta_count,
605
        "request_bytes": after["request_bytes"] - before["request_bytes"],
606
        "response_bytes": after["response_bytes"] - before["response_bytes"],
607
        "rate_limit_remaining": after["rate_limit_remaining"],
608
        "request_ids": ids,
609
    }
610
611
612
def total_row(
613
    meta: dict[str, Any],
614
    scenario: str,
615
    run_index: int,
616
    rows: list[dict[str, Any]],
617
    driver: str,
618
) -> dict[str, Any]:
619
    failures = [
620
        row for row in rows if row_returncode(row) not in (0, SKIP_RETURNCODE)
621
    ]
622
    row: dict[str, Any] = {
623
        **meta,
624
        "scenario": scenario,
625
        "run": run_index,
626
        "operation": "integration.total",
627
        "elapsed_ms": round(sum(float(row.get("elapsed_ms") or 0.0) for row in rows), 3),
628
        "returncode": 1 if failures else 0,
629
        "command": [row["command"] for row in rows],
630
        "summarized_operations": [row["operation"] for row in rows],
631
        "poll_iterations_total": sum(int(row.get("poll_iterations") or 0) for row in rows),
632
    }
633
    if driver == "harness-api":
634
        row.update(null_token_fields())
635
        row["tool_call_count"] = None
636
        deltas = [row_["api_metrics"] for row_ in rows if isinstance(row_.get("api_metrics"), dict)]
637
        row["api_metrics"] = {
638
            "request_count": sum(delta["request_count"] for delta in deltas),
639
            "request_bytes": sum(delta["request_bytes"] for delta in deltas),
640
            "response_bytes": sum(delta["response_bytes"] for delta in deltas),
641
            "rate_limit_remaining": deltas[-1]["rate_limit_remaining"] if deltas else None,
642
        }
643
    else:
644
        row["tool_call_count"] = sum(int(row_.get("tool_call_count") or 0) for row_ in rows)
645
        row.update(
646
            oakbench_tokens.summed_token_fields(
647
                rows, "sum_of_platform_lifecycle_steps_command_plus_admitted_output_chars_div_4"
648
            )
649
        )
650
    return row
651
652
653
# --------------------------------------------------------------------------
654
# With-credentials runners (not exercised in CI; kept simple and linear).
655
# Each runner is driven by the scenario's operation list so emitted
656
# operations always match scenarios/platform.yaml (ADR-0005).
657
# --------------------------------------------------------------------------
658
659
660
def run_untimed(
661
    command: list[str], cwd: Optional[Path], env: dict[str, str], timeout: float = 300.0
662
) -> subprocess.CompletedProcess[str]:
663
    return subprocess.run(
664
        command,
665
        cwd=cwd,
666
        env=env,
667
        text=True,
668
        stdout=subprocess.PIPE,
669
        stderr=subprocess.PIPE,
670
        timeout=timeout,
671
        check=False,
672
    )
673
674
675
def run_operations(
676
    operations: list[str],
677
    handlers: dict[str, Callable[[], dict[str, Any]]],
678
    meta: dict[str, Any],
679
    scenario_name: str,
680
    run_index: int,
681
    driver: str,
682
) -> list[dict[str, Any]]:
683
    """Run the scenario's declared operations in order; a failed step turns
684
    the remaining steps into skip rows (a recorded gap, never a cascade of
685
    misleading failures)."""
686
    rows: list[dict[str, Any]] = []
687
    aborted_at: Optional[str] = None
688
    for operation in operations:
689
        if operation == "integration.total":
690
            rows.append(total_row(meta, scenario_name, run_index, rows, driver))
691
            continue
692
        if aborted_at is not None:
693
            rows.append(
694
                skip_row(meta, scenario_name, operation, run_index, f"prior_step_failed:{aborted_at}")
695
            )
696
            continue
697
        handler = handlers.get(operation)
698
        if handler is None:
699
            rows.append(skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented"))
700
            continue
701
        row = handler()
702
        row.pop("_stdout", None)
703
        rows.append(row)
704
        if row_returncode(row) not in (0, SKIP_RETURNCODE):
705
            aborted_at = operation
706
    return rows
707
708
709
def run_capability_probe(
710
    args: argparse.Namespace, meta: dict[str, Any], run_index: int, settings: Settings
711
) -> list[dict[str, Any]]:
712
    scenario_name = "platform_capability_probe"
713
    if args.driver == "harness-api":
714
        if not args.repo:
715
            return [skip_row(meta, scenario_name, "capability.probe", run_index, "github_repo_not_configured")]
716
        client = GitHubClient(token=os.environ.get(args.token_env))
717
        return [
718
            api_step_row(
719
                meta, scenario_name, "capability.probe", run_index, client,
720
                lambda: client.get_repo(args.repo), [f"api:GET /repos/{args.repo}"],
721
            )
722
        ]
723
    command = ["gh", "auth", "status"] if args.platform == "github" else ["oak", "--version"]
724
    return [
725
        cli_step_row(
726
            meta, scenario_name, "capability.probe", command, Path.cwd(), run_index,
727
            cli_env(args), settings.admitted,
728
        )
729
    ]
730
731
732
def api_step_row(
733
    meta: dict[str, Any],
734
    scenario: str,
735
    operation: str,
736
    run_index: int,
737
    client: GitHubClient,
738
    func: Callable[[], Any],
739
    command: list[str],
740
) -> dict[str, Any]:
741
    before = metrics_snapshot(client)
742
    start = platform_clock.monotonic_ms()
743
    error: Optional[GitHubApiError] = None
744
    try:
745
        func()
746
    except GitHubApiError as exc:
747
        error = exc
748
    elapsed_ms = platform_clock.monotonic_ms() - start
749
    row = {
750
        **meta,
751
        "scenario": scenario,
752
        "run": run_index,
753
        "operation": operation,
754
        "elapsed_ms": round(elapsed_ms, 3),
755
        "returncode": 0 if error is None else 1,
756
        "command": command,
757
        "tool_call_count": None,
758
        "api_metrics": metrics_delta(before, metrics_snapshot(client)),
759
        **null_token_fields(),
760
    }
761
    if error is not None:
762
        row["stderr"] = str(error)[:4000]
763
    return row
764
765
766
def api_settle_row(
767
    meta: dict[str, Any],
768
    scenario: str,
769
    operation: str,
770
    run_index: int,
771
    client: GitHubClient,
772
    poll_fn: Callable[[], Any],
773
    settings: Settings,
774
    command: list[str],
775
    clock: Callable[[], float] = time.monotonic,
776
    sleep: Callable[[float], None] = time.sleep,
777
) -> dict[str, Any]:
778
    before = metrics_snapshot(client)
779
    row = settle_row(
780
        meta, scenario, operation, run_index, poll_fn, settings, command,
781
        poll_is_tool_call=False, clock=clock, sleep=sleep,
782
    )
783
    row["api_metrics"] = metrics_delta(before, metrics_snapshot(client))
784
    row.update(null_token_fields())
785
    return row
786
787
788
def api_check_instant_checks_green_row(
789
    meta: dict[str, Any],
790
    scenario_name: str,
791
    run_index: int,
792
    client: GitHubClient,
793
    repo: str,
794
    head_sha: str,
795
    settings: Settings,
796
    *,
797
    pace: Callable[[], None] = lambda: None,
798
    clock: Callable[[], float] = time.monotonic,
799
    sleep: Callable[[float], None] = time.sleep,
800
) -> dict[str, Any]:
801
    """check-instant, executed: the harness posts the required status itself
802
    via the commit status API (deterministic, free, no Actions), then settles
803
    on the combined status going green before merge. Shared by the race
804
    runner and the non-race harness-api runner so the variant means the same
805
    thing wherever it is executed (ADR-0005)."""
806
    operation = CHECKS_GREEN_OPERATION
807
    command = [
808
        f"api:POST /repos/{repo}/statuses/<head-sha>",
809
        f"api:GET /repos/{repo}/commits/<head-sha>/status (poll)",
810
    ]
811
    before = metrics_snapshot(client)
812
    error_text: Optional[str] = None
813
    post_start = platform_clock.monotonic_ms()
814
    if not head_sha:
815
        error_text = "head_sha_unknown_cannot_post_status"
816
    else:
817
        try:
818
            pace()
819
            client.post_status(
820
                repo, head_sha, "success", CHECK_INSTANT_CONTEXT,
821
                description="oakbench harness-posted instant check",
822
            )
823
        except GitHubApiError as exc:
824
            error_text = str(exc)[:4000]
825
    post_elapsed_ms = platform_clock.monotonic_ms() - post_start
826
    if error_text is not None:
827
        return {
828
            **meta,
829
            "scenario": scenario_name,
830
            "run": run_index,
831
            "operation": operation,
832
            "elapsed_ms": round(post_elapsed_ms, 3),
833
            "returncode": 1,
834
            "command": command,
835
            "tool_call_count": None,
836
            "stderr": error_text,
837
            "api_metrics": metrics_delta(before, metrics_snapshot(client)),
838
            **null_token_fields(),
839
        }
840
841
    def poll() -> bool:
842
        try:
843
            combined = client.get_combined_status(repo, head_sha)
844
        except GitHubApiError:
845
            return False
846
        return combined.get("state") == "success"
847
848
    result = platform_clock.settle(
849
        poll, interval_s=settings.interval_s, timeout_s=settings.settle_timeout_s,
850
        clock=clock, sleep=sleep,
851
    )
852
    return {
853
        **meta,
854
        "scenario": scenario_name,
855
        "run": run_index,
856
        "operation": operation,
857
        # post + settle: the harness-posted status is part of the
858
        # operation's measured cost, never hidden setup.
859
        "elapsed_ms": round(post_elapsed_ms + result["observed_wait_ms"], 3),
860
        "returncode": 0 if result["settled"] else 1,
861
        "command": command,
862
        "tool_call_count": None,
863
        **settle_fields(result),
864
        "status_post_elapsed_ms": round(post_elapsed_ms, 3),
865
        "status_context": CHECK_INSTANT_CONTEXT,
866
        "api_metrics": metrics_delta(before, metrics_snapshot(client)),
867
        **null_token_fields(),
868
    }
869
870
871
def run_github_api_scenario(
872
    args: argparse.Namespace,
873
    scenario: dict[str, Any],
874
    meta: dict[str, Any],
875
    run_index: int,
876
    settings: Settings,
877
    *,
878
    client: Optional[GitHubClient] = None,
879
    clock: Callable[[], float] = time.monotonic,
880
    sleep: Callable[[float], None] = time.sleep,
881
) -> list[dict[str, Any]]:
882
    scenario_name = str(scenario["name"])
883
    operations = [str(op) for op in scenario.get("operations") or []]
884
    check_instant = args.protection_variant == "check-instant"
885
    if check_instant and CHECKS_GREEN_OPERATION not in operations:
886
        # check-instant is only executed when the required status is actually
887
        # posted and settled green BEFORE the merge request; the non-race
888
        # anatomy scenarios do not declare the operation, so it is inserted
889
        # ahead of integration.merge.request (the race ordering).
890
        anchor = (
891
            operations.index("integration.merge.request")
892
            if "integration.merge.request" in operations
893
            else len(operations)
894
        )
895
        operations = operations[:anchor] + [CHECKS_GREEN_OPERATION] + operations[anchor:]
896
    repo = args.repo
897
    if client is None:
898
        client = GitHubClient(token=os.environ.get(args.token_env))
899
    branch = f"oakbench-platform-{meta['bench_id'].lower()}-r{run_index}"
900
    payload = f"platform lane {meta['bench_id']} run={run_index}\n".encode("utf-8")
901
    state: dict[str, Any] = {}
902
903
    try:
904
        state["base_sha"] = client.get_main_sha(repo)
905
    except (GitHubApiError, KeyError, json.JSONDecodeError) as exc:
906
        return [
907
            skip_row(meta, scenario_name, op, run_index, f"setup_failed:{str(exc)[:160]}")
908
            for op in operations
909
        ]
910
911
    def publish() -> dict[str, Any]:
912
        def do() -> None:
913
            client.create_ref(repo, f"refs/heads/{branch}", state["base_sha"])
914
            put = client.put_file(
915
                repo,
916
                path=f".oak-bench/platform-{run_index}.md",
917
                message=f"bench platform lane {meta['bench_id']} r{run_index}",
918
                content=payload,
919
                branch=branch,
920
            )
921
            commit = put.get("commit") if isinstance(put, dict) else None
922
            if isinstance(commit, dict) and commit.get("sha"):
923
                state["head_sha"] = str(commit["sha"])
924
        return api_step_row(
925
            meta, scenario_name, "branch.publish", run_index, client, do,
926
            [f"api:POST /repos/{repo}/git/refs", f"api:PUT /repos/{repo}/contents/..."],
927
        )
928
929
    def open_pr() -> dict[str, Any]:
930
        def do() -> None:
931
            pr = client.create_pr(
932
                repo, title=f"bench platform lane r{run_index}", head=branch, base="main"
933
            )
934
            state["pr_number"] = int(pr["number"])
935
            head = pr.get("head") or {}
936
            if not state.get("head_sha") and head.get("sha"):
937
                state["head_sha"] = str(head["sha"])
938
        return api_step_row(
939
            meta, scenario_name, "integration.open", run_index, client, do,
940
            [f"api:POST /repos/{repo}/pulls"],
941
        )
942
943
    def get_pr_quiet() -> Optional[dict[str, Any]]:
944
        try:
945
            return client.get_pr(repo, state["pr_number"])
946
        except GitHubApiError:
947
            return None
948
949
    def visible() -> dict[str, Any]:
950
        return api_settle_row(
951
            meta, scenario_name, "integration.visible.settle", run_index, client,
952
            lambda: get_pr_quiet() is not None, settings,
953
            [f"api:GET /repos/{repo}/pulls/<n> (poll)"], clock=clock, sleep=sleep,
954
        )
955
956
    def mergeable() -> dict[str, Any]:
957
        def poll() -> bool:
958
            pr = get_pr_quiet()
959
            return pr is not None and pr.get("mergeable") is not None
960
        return api_settle_row(
961
            meta, scenario_name, "integration.mergeable.settle", run_index, client,
962
            poll, settings, [f"api:GET /repos/{repo}/pulls/<n> mergeable (poll)"],
963
            clock=clock, sleep=sleep,
964
        )
965
966
    def checks_green() -> dict[str, Any]:
967
        # Reached only under check-instant (the operation is inserted above);
968
        # the status is posted after the PR is open and must settle green
969
        # before integration.merge.request runs.
970
        return api_check_instant_checks_green_row(
971
            meta, scenario_name, run_index, client, repo,
972
            str(state.get("head_sha") or ""), settings, clock=clock, sleep=sleep,
973
        )
974
975
    def merge() -> dict[str, Any]:
976
        return api_step_row(
977
            meta, scenario_name, "integration.merge.request", run_index, client,
978
            lambda: client.merge_pr(repo, state["pr_number"]),
979
            [f"api:PUT /repos/{repo}/pulls/<n>/merge"],
980
        )
981
982
    def merged() -> dict[str, Any]:
983
        def poll() -> bool:
984
            pr = get_pr_quiet()
985
            return bool(pr and pr.get("merged"))
986
        return api_settle_row(
987
            meta, scenario_name, "integration.merged.settle", run_index, client,
988
            poll, settings, [f"api:GET /repos/{repo}/pulls/<n> merged (poll)"],
989
            clock=clock, sleep=sleep,
990
        )
991
992
    def main_settle() -> dict[str, Any]:
993
        def poll() -> bool:
994
            try:
995
                return client.get_main_sha(repo) != state["base_sha"]
996
            except GitHubApiError:
997
                return False
998
        return api_settle_row(
999
            meta, scenario_name, "integration.main.settle", run_index, client,
1000
            poll, settings, [f"api:GET /repos/{repo}/git/ref/heads/main (poll)"],
1001
            clock=clock, sleep=sleep,
1002
        )
1003
1004
    def sync_local() -> dict[str, Any]:
1005
        row = api_step_row(
1006
            meta, scenario_name, "integration.sync.local", run_index, client,
1007
            lambda: client.get_main_sha(repo),
1008
            [f"api:GET /repos/{repo}/git/ref/heads/main"],
1009
        )
1010
        row["note"] = (
1011
            "harness-api driver has no local working copy; this is the API-side "
1012
            "equivalent (observe merged main ref). Local-checkout sync cost is a "
1013
            "cli-driver measurement."
1014
        )
1015
        return row
1016
1017
    handlers = {
1018
        "branch.publish": publish,
1019
        "integration.open": open_pr,
1020
        "integration.visible.settle": visible,
1021
        "integration.mergeable.settle": mergeable,
1022
        CHECKS_GREEN_OPERATION: checks_green,
1023
        "integration.merge.request": merge,
1024
        "integration.merged.settle": merged,
1025
        "integration.main.settle": main_settle,
1026
        "integration.sync.local": sync_local,
1027
    }
1028
    rows = run_operations(operations, handlers, meta, scenario_name, run_index, "harness-api")
1029
    try:
1030
        client.delete_ref(repo, f"heads/{branch}")
1031
    except GitHubApiError:
1032
        pass  # untimed cleanup; merged-branch deletion failures are not data
1033
    return rows
1034
1035
1036
def run_github_cli_scenario(
1037
    args: argparse.Namespace,
1038
    scenario: dict[str, Any],
1039
    meta: dict[str, Any],
1040
    run_index: int,
1041
    run_root: Path,
1042
    settings: Settings,
1043
) -> list[dict[str, Any]]:
1044
    scenario_name = str(scenario["name"])
1045
    operations = [str(op) for op in scenario.get("operations") or []]
1046
    if args.protection_variant == "check-instant":
1047
        # gh/git cannot post the harness commit status, so the cli driver
1048
        # cannot execute this variant: running anyway would merge (or fail)
1049
        # while the rows claimed the protection held. Honest gap instead.
1050
        return [
1051
            skip_row(
1052
                meta,
1053
                scenario_name,
1054
                op,
1055
                run_index,
1056
                CHECK_INSTANT_CLI_SKIP_REASON,
1057
                settings=settings,
1058
            )
1059
            for op in operations
1060
        ]
1061
    repo = args.repo
1062
    env = cli_env(args)
1063
    # Time the real Git, not the macOS xcrun trampoline, including the git
1064
    # that gh spawns internally (ADR-0011): put the resolved Git first on PATH.
1065
    from oakbench.subjects import which_measured_git
1066
    real_git = which_measured_git()
1067
    if real_git:
1068
        env = {**env, "PATH": str(Path(real_git).parent) + os.pathsep + env.get("PATH", os.environ.get("PATH", ""))}
1069
    git = real_git or "git"
1070
    branch = f"oakbench-platform-{meta['bench_id'].lower()}-r{run_index}"
1071
    workdir = run_root / f"github-cli-r{run_index}"
1072
    state: dict[str, Any] = {}
1073
1074
    # Untimed setup: the lane's precondition is "task branch exists locally".
1075
    def setup() -> Optional[str]:
1076
        clone = run_untimed(["gh", "repo", "clone", repo, str(workdir)], run_root, env)
1077
        if clone.returncode != 0:
1078
            return f"setup_failed:clone:{clone.stderr.strip()[:120]}"
1079
        for command in (
1080
            [git, "switch", "-c", branch],
1081
            ["/bin/zsh", "-c", f"mkdir -p .oak-bench && echo 'platform lane r{run_index}' > .oak-bench/platform-{run_index}.md"],
1082
            [git, "add", "."],
1083
            [git, "commit", "-q", "-m", f"bench platform lane {meta['bench_id']} r{run_index}"],
1084
        ):
1085
            proc = run_untimed(command, workdir, env)
1086
            if proc.returncode != 0:
1087
                return f"setup_failed:{command[0]}:{proc.stderr.strip()[:120]}"
1088
        ls = run_untimed([git, "ls-remote", "origin", "refs/heads/main"], workdir, env)
1089
        state["base_sha"] = ls.stdout.split()[0] if ls.returncode == 0 and ls.stdout.split() else ""
1090
        return None
1091
1092
    setup_error = setup()
1093
    if setup_error:
1094
        return [skip_row(meta, scenario_name, op, run_index, setup_error) for op in operations]
1095
1096
    def remote_main_sha() -> str:
1097
        ls = run_untimed([git, "ls-remote", "origin", "refs/heads/main"], workdir, env)
1098
        parts = ls.stdout.split()
1099
        return parts[0] if ls.returncode == 0 and parts else ""
1100
1101
    def pr_number() -> str:
1102
        return str(state.get("pr_number", ""))
1103
1104
    def publish() -> dict[str, Any]:
1105
        return cli_step_row(
1106
            meta, scenario_name, "branch.publish",
1107
            [git, "push", "-q", "-u", "origin", branch], workdir, run_index, env, settings.admitted,
1108
        )
1109
1110
    def open_pr() -> dict[str, Any]:
1111
        row = cli_step_row(
1112
            meta, scenario_name, "integration.open",
1113
            [
1114
                "gh", "pr", "create", "--repo", repo, "--head", branch, "--base", "main",
1115
                "--title", f"bench platform lane r{run_index}",
1116
                "--body", f"Disposable platform-lane benchmark PR ({meta['bench_id']}).",
1117
            ],
1118
            workdir, run_index, env, settings.admitted,
1119
        )
1120
        match = PR_URL_RE.search(row.get("_stdout", ""))
1121
        if match:
1122
            state["pr_number"] = int(match.group(1))
1123
        elif row["returncode"] == 0:
1124
            row["returncode"] = 1
1125
            row["stderr"] = "pr_number_unparsed_from_gh_output"
1126
        return row
1127
1128
    def gh_pr_json(field: str) -> str:
1129
        proc = run_untimed(
1130
            ["gh", "pr", "view", pr_number(), "--repo", repo, "--json", field, "-q", f".{field}"],
1131
            workdir, env,
1132
        )
1133
        return proc.stdout.strip() if proc.returncode == 0 else ""
1134
1135
    def visible() -> dict[str, Any]:
1136
        return settle_row(
1137
            meta, scenario_name, "integration.visible.settle", run_index,
1138
            lambda: bool(gh_pr_json("state")), settings,
1139
            ["gh", "pr", "view", "<n>", "--json", "state", "(poll)"], poll_is_tool_call=True,
1140
        )
1141
1142
    def mergeable() -> dict[str, Any]:
1143
        return settle_row(
1144
            meta, scenario_name, "integration.mergeable.settle", run_index,
1145
            lambda: gh_pr_json("mergeable") not in ("", "UNKNOWN"), settings,
1146
            ["gh", "pr", "view", "<n>", "--json", "mergeable", "(poll)"], poll_is_tool_call=True,
1147
        )
1148
1149
    def merge() -> dict[str, Any]:
1150
        return cli_step_row(
1151
            meta, scenario_name, "integration.merge.request",
1152
            ["gh", "pr", "merge", pr_number(), "--repo", repo, "--merge"],
1153
            workdir, run_index, env, settings.admitted,
1154
        )
1155
1156
    def merged() -> dict[str, Any]:
1157
        return settle_row(
1158
            meta, scenario_name, "integration.merged.settle", run_index,
1159
            lambda: gh_pr_json("state") == "MERGED", settings,
1160
            ["gh", "pr", "view", "<n>", "--json", "state", "(poll)"], poll_is_tool_call=True,
1161
        )
1162
1163
    def main_settle() -> dict[str, Any]:
1164
        return settle_row(
1165
            meta, scenario_name, "integration.main.settle", run_index,
1166
            lambda: remote_main_sha() not in ("", state["base_sha"]), settings,
1167
            ["git", "ls-remote", "origin", "refs/heads/main", "(poll)"], poll_is_tool_call=True,
1168
        )
1169
1170
    def sync_local() -> dict[str, Any]:
1171
        run_untimed([git, "switch", "-q", "main"], workdir, env)
1172
        return cli_step_row(
1173
            meta, scenario_name, "integration.sync.local",
1174
            [git, "pull", "-q", "--ff-only", "origin", "main"],
1175
            workdir, run_index, env, settings.admitted,
1176
        )
1177
1178
    handlers = {
1179
        "branch.publish": publish,
1180
        "integration.open": open_pr,
1181
        "integration.visible.settle": visible,
1182
        "integration.mergeable.settle": mergeable,
1183
        "integration.merge.request": merge,
1184
        "integration.merged.settle": merged,
1185
        "integration.main.settle": main_settle,
1186
        "integration.sync.local": sync_local,
1187
    }
1188
    rows = run_operations(operations, handlers, meta, scenario_name, run_index, "cli")
1189
    run_untimed([git, "push", "-q", "origin", "--delete", branch], workdir, env)  # untimed cleanup
1190
    return rows
1191
1192
1193
def run_oak_cli_scenario(
1194
    args: argparse.Namespace,
1195
    scenario: dict[str, Any],
1196
    meta: dict[str, Any],
1197
    run_index: int,
1198
    run_root: Path,
1199
    settings: Settings,
1200
) -> list[dict[str, Any]]:
1201
    scenario_name = str(scenario["name"])
1202
    operations = [str(op) for op in scenario.get("operations") or []]
1203
    env = cli_env(args)
1204
    workdir = run_root / f"oak-cli-r{run_index}"
1205
1206
    # Untimed setup: mount and commit so the task branch exists locally.
1207
    mount = run_untimed(["oak", "mount", args.repo, str(workdir)], run_root, env)
1208
    if mount.returncode != 0:
1209
        reason = f"setup_failed:oak_mount:{mount.stderr.strip()[:120]}"
1210
        return [skip_row(meta, scenario_name, op, run_index, reason) for op in operations]
1211
    payload_dir = workdir / ".oak-bench"
1212
    payload_dir.mkdir(parents=True, exist_ok=True)
1213
    (payload_dir / f"platform-{run_index}.md").write_text(
1214
        f"platform lane {meta['bench_id']} run={run_index}\n"
1215
    )
1216
    commit = run_untimed(["oak", "commit", "--no-verify"], workdir, env)
1217
    if commit.returncode != 0:
1218
        run_untimed(["oak", "mount", "end", str(workdir), "-f"], run_root, env)
1219
        reason = f"setup_failed:oak_commit:{commit.stderr.strip()[:120]}"
1220
        return [skip_row(meta, scenario_name, op, run_index, reason) for op in operations]
1221
1222
    def cli(operation: str, command: list[str]) -> Callable[[], dict[str, Any]]:
1223
        return lambda: cli_step_row(
1224
            meta, scenario_name, operation, command, workdir, run_index, env, settings.admitted
1225
        )
1226
1227
    def structural(operation: str, note: str) -> Callable[[], dict[str, Any]]:
1228
        return lambda: structural_row(meta, scenario_name, operation, run_index, note)
1229
1230
    # oak merge is synchronous: zero polls is structural (a measured zero —
1231
    # no commands run), and the settle value is null because the async
1232
    # concept has no equivalent on this platform (ADR-0002).
1233
    handlers = {
1234
        "branch.publish": cli("branch.publish", ["oak", "push"]),
1235
        "integration.open": structural(
1236
            "integration.open",
1237
            "integration object exists at publish on oak; no separate open call",
1238
        ),
1239
        "integration.visible.settle": structural(
1240
            "integration.visible.settle",
1241
            "oak push returns after the branch is visible on the server; nothing to poll",
1242
        ),
1243
        "integration.mergeable.settle": structural(
1244
            "integration.mergeable.settle", "no async mergeability concept"
1245
        ),
1246
        "integration.merge.request": cli("integration.merge.request", ["oak", "merge"]),
1247
        "integration.merged.settle": structural(
1248
            "integration.merged.settle", "oak merge is synchronous; merged state holds at return"
1249
        ),
1250
        "integration.main.settle": structural(
1251
            "integration.main.settle", "oak merge returns after main is updated; nothing to poll"
1252
        ),
1253
        "integration.sync.local": cli("integration.sync.local", ["oak", "switch", "main"]),
1254
    }
1255
    rows = run_operations(operations, handlers, meta, scenario_name, run_index, "cli")
1256
    run_untimed(["oak", "mount", "end", str(workdir), "-f"], run_root, env)  # untimed cleanup
1257
    return rows
1258
1259
1260
# --------------------------------------------------------------------------
1261
# integration_race_nN: the flagship race family. N task branches each go
1262
# through publish -> PR -> merge (per-branch anatomy rows reuse the
1263
# vocabulary above), landing serially or as fast as RateLimitBudgeter pacing
1264
# allows, plus race-level rows: race.publish.window (first publish start ->
1265
# last publish end), race.integrate.window (first integration.open -> last
1266
# integration.main.settle), race.pacing.injected (sum of budgeter-injected
1267
# waits - REPORTED, never subtracted), race.total. clock/sleep are injectable
1268
# so tests never wait on real time (fake transports only, no network).
1269
# --------------------------------------------------------------------------
1270
1271
1272
def is_race_scenario(scenario: dict[str, Any]) -> bool:
1273
    return RACE_SCENARIO_RE.match(str(scenario.get("name", ""))) is not None
1274
1275
1276
def race_branch_count(scenario: dict[str, Any]) -> int:
1277
    if scenario.get("branch_count") is not None:
1278
        return int(scenario["branch_count"])
1279
    match = RACE_SCENARIO_RE.match(str(scenario.get("name", "")))
1280
    return int(match.group(1)) if match else 1
1281
1282
1283
def race_branch_skip(
1284
    meta: dict[str, Any],
1285
    scenario_name: str,
1286
    operation: str,
1287
    run_index: int,
1288
    reason: str,
1289
    branch_index: int,
1290
) -> dict[str, Any]:
1291
    row = skip_row(meta, scenario_name, operation, run_index, reason)
1292
    row["race_branch"] = branch_index
1293
    return row
1294
1295
1296
def race_window_ms(
1297
    timeline: dict[str, list[tuple[float, float]]], start_op: str, end_op: str
1298
) -> Optional[float]:
1299
    """First start of ``start_op`` -> last end of ``end_op``, on the race's
1300
    single monotonic clock. None when either endpoint was never observed."""
1301
    starts = timeline.get(start_op) or []
1302
    ends = timeline.get(end_op) or []
1303
    if not starts or not ends:
1304
        return None
1305
    return (max(end for _, end in ends) - min(start for start, _ in starts)) * 1000.0
1306
1307
1308
def race_summary_rows(
1309
    meta: dict[str, Any],
1310
    scenario_name: str,
1311
    run_index: int,
1312
    driver: str,
1313
    race_operations: list[str],
1314
    branch_rows: list[dict[str, Any]],
1315
    timeline: dict[str, list[tuple[float, float]]],
1316
    budgeter: RateLimitBudgeter,
1317
    race_start: float,
1318
    race_end: float,
1319
    branch_count: int,
1320
) -> list[dict[str, Any]]:
1321
    failures = [
1322
        row for row in branch_rows
1323
        if row_returncode(row) not in (0, SKIP_RETURNCODE)
1324
    ]
1325
    failed_operations = {str(row.get("operation")) for row in failures}
1326
1327
    def base_row(operation: str, elapsed_ms: float, returncode: int, note: str) -> dict[str, Any]:
1328
        row: dict[str, Any] = {
1329
            **meta,
1330
            "scenario": scenario_name,
1331
            "run": run_index,
1332
            "operation": operation,
1333
            "elapsed_ms": round(elapsed_ms, 3),
1334
            "returncode": returncode,
1335
            "command": [],
1336
            "tool_call_count": None,
1337
            "race_branch_count": branch_count,
1338
            "note": note,
1339
        }
1340
        if driver == "harness-api":
1341
            row.update(null_token_fields())
1342
        return row
1343
1344
    def window_row(operation: str, start_op: str, end_op: str, note: str) -> dict[str, Any]:
1345
        window = race_window_ms(timeline, start_op, end_op)
1346
        if window is None:
1347
            # The window's endpoints were never observed (e.g. every branch
1348
            # aborted before reaching them): a recorded gap, never a zero.
1349
            row = skip_row(
1350
                meta, scenario_name, operation, run_index,
1351
                f"race_window_unobserved:{start_op}->{end_op}",
1352
            )
1353
            row["race_branch_count"] = branch_count
1354
            return row
1355
        complete = (
1356
            len(timeline.get(start_op) or []) == branch_count
1357
            and len(timeline.get(end_op) or []) == branch_count
1358
            and start_op not in failed_operations
1359
            and end_op not in failed_operations
1360
        )
1361
        row = base_row(operation, window, 0 if complete else 1, note)
1362
        row["window_start_operation"] = start_op
1363
        row["window_end_operation"] = end_op
1364
        return row
1365
1366
    def pacing_row() -> dict[str, Any]:
1367
        row = base_row(
1368
            "race.pacing.injected",
1369
            budgeter.injected_wait_s_total * 1000.0,
1370
            0,
1371
            "sum of RateLimitBudgeter-injected pacing waits across the race; "
1372
            "REPORTED here, never subtracted from any operation duration",
1373
        )
1374
        row["budget_ops_per_hour"] = budgeter.ops_per_hour
1375
        return row
1376
1377
    def total_row_race() -> dict[str, Any]:
1378
        row = base_row(
1379
            "race.total",
1380
            (race_end - race_start) * 1000.0,
1381
            1 if failures else 0,
1382
            "whole-race wall clock on one local monotonic clock: before the "
1383
            "first branch's publish to after the last branch's final operation",
1384
        )
1385
        row["summarized_operations"] = [r["operation"] for r in branch_rows]
1386
        row["poll_iterations_total"] = sum(
1387
            int(r.get("poll_iterations") or 0) for r in branch_rows
1388
        )
1389
        if driver == "harness-api":
1390
            deltas = [
1391
                r["api_metrics"] for r in branch_rows if isinstance(r.get("api_metrics"), dict)
1392
            ]
1393
            row["api_metrics"] = {
1394
                "request_count": sum(delta["request_count"] for delta in deltas),
1395
                "request_bytes": sum(delta["request_bytes"] for delta in deltas),
1396
                "response_bytes": sum(delta["response_bytes"] for delta in deltas),
1397
                "rate_limit_remaining": deltas[-1]["rate_limit_remaining"] if deltas else None,
1398
            }
1399
        else:
1400
            row["tool_call_count"] = sum(
1401
                int(r.get("tool_call_count") or 0) for r in branch_rows
1402
            )
1403
            row.update(
1404
                oakbench_tokens.summed_token_fields(
1405
                    branch_rows,
1406
                    "sum_of_platform_lifecycle_steps_command_plus_admitted_output_chars_div_4",
1407
                )
1408
            )
1409
        return row
1410
1411
    emitters: dict[str, Callable[[], dict[str, Any]]] = {
1412
        "race.publish.window": lambda: window_row(
1413
            "race.publish.window", "branch.publish", "branch.publish",
1414
            "first branch.publish start -> last branch.publish end across all branches",
1415
        ),
1416
        "race.integrate.window": lambda: window_row(
1417
            "race.integrate.window", "integration.open", "integration.main.settle",
1418
            "first integration.open start -> last integration.main.settle end across all branches",
1419
        ),
1420
        "race.pacing.injected": pacing_row,
1421
        "race.total": total_row_race,
1422
    }
1423
    rows: list[dict[str, Any]] = []
1424
    for operation in race_operations:
1425
        emitter = emitters.get(operation)
1426
        if emitter is None:
1427
            rows.append(
1428
                skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented")
1429
            )
1430
            continue
1431
        rows.append(emitter())
1432
    return rows
1433
1434
1435
# A branch factory does the branch's untimed setup and returns
1436
# (setup_error_or_None, operation handlers, untimed cleanup callable).
1437
RaceBranchFactory = Callable[
1438
    [int, str],
1439
    tuple[Optional[str], dict[str, Callable[[], dict[str, Any]]], Callable[[], None]],
1440
]
1441
1442
1443
def run_race_operations(
1444
    scenario: dict[str, Any],
1445
    meta: dict[str, Any],
1446
    run_index: int,
1447
    driver: str,
1448
    branch_count: int,
1449
    make_branch: RaceBranchFactory,
1450
    budgeter: RateLimitBudgeter,
1451
    clock: Callable[[], float],
1452
) -> list[dict[str, Any]]:
1453
    """Race orchestration shared by drivers: loop branch-0..N-1 serially,
1454
    run each branch's declared per-branch operations, record a start/end
1455
    timeline for the race windows, then emit the race-level rows. A failed
1456
    step turns every remaining per-branch step into a skip row (a recorded
1457
    gap, never a cascade of misleading failures)."""
1458
    scenario_name = str(scenario["name"])
1459
    operations = [str(op) for op in scenario.get("operations") or []]
1460
    branch_operations = [op for op in operations if not op.startswith("race.")]
1461
    race_operations = [op for op in operations if op.startswith("race.")]
1462
1463
    rows: list[dict[str, Any]] = []
1464
    timeline: dict[str, list[tuple[float, float]]] = {}
1465
    cleanups: list[Callable[[], None]] = []
1466
    aborted_at: Optional[str] = None
1467
    race_start = clock()
1468
1469
    for branch_index in range(branch_count):
1470
        branch = disposable_branch(meta["bench_id"], scenario_name, f"b{branch_index}", run_index)
1471
        if aborted_at is not None:
1472
            rows.extend(
1473
                race_branch_skip(
1474
                    meta, scenario_name, operation, run_index,
1475
                    f"prior_step_failed:{aborted_at}", branch_index,
1476
                )
1477
                for operation in branch_operations
1478
            )
1479
            continue
1480
        setup_error, handlers, cleanup = make_branch(branch_index, branch)
1481
        cleanups.append(cleanup)
1482
        if setup_error is not None:
1483
            aborted_at = f"branch-{branch_index}:setup"
1484
            rows.extend(
1485
                race_branch_skip(meta, scenario_name, operation, run_index, setup_error, branch_index)
1486
                for operation in branch_operations
1487
            )
1488
            continue
1489
        for operation in branch_operations:
1490
            if aborted_at is not None:
1491
                rows.append(
1492
                    race_branch_skip(
1493
                        meta, scenario_name, operation, run_index,
1494
                        f"prior_step_failed:{aborted_at}", branch_index,
1495
                    )
1496
                )
1497
                continue
1498
            handler = handlers.get(operation)
1499
            if handler is None:
1500
                rows.append(
1501
                    race_branch_skip(
1502
                        meta, scenario_name, operation, run_index,
1503
                        "operation_not_implemented", branch_index,
1504
                    )
1505
                )
1506
                continue
1507
            injected_before = budgeter.injected_wait_s_total
1508
            start = clock()
1509
            row = handler()
1510
            end = clock()
1511
            row.pop("_stdout", None)
1512
            row["race_branch"] = branch_index
1513
            row["race_branch_name"] = branch
1514
            injected_s = budgeter.injected_wait_s_total - injected_before
1515
            if injected_s > 0:
1516
                row["pacing_injected_ms"] = round(injected_s * 1000.0, 3)
1517
                row["pacing_note"] = (
1518
                    "elapsed_ms includes this budgeter-injected wait; it is "
1519
                    "reported here and summed into race.pacing.injected, never subtracted"
1520
                )
1521
            timeline.setdefault(operation, []).append((start, end))
1522
            rows.append(row)
1523
            if row_returncode(row) not in (0, SKIP_RETURNCODE):
1524
                aborted_at = f"branch-{branch_index}:{operation}"
1525
1526
    race_end = clock()
1527
    rows.extend(
1528
        race_summary_rows(
1529
            meta, scenario_name, run_index, driver, race_operations, list(rows),
1530
            timeline, budgeter, race_start, race_end, branch_count,
1531
        )
1532
    )
1533
    for cleanup in cleanups:
1534
        cleanup()  # untimed best-effort cleanup; cleanup failures are not data
1535
    return rows
1536
1537
1538
def run_github_api_race(
1539
    args: argparse.Namespace,
1540
    scenario: dict[str, Any],
1541
    meta: dict[str, Any],
1542
    run_index: int,
1543
    settings: Settings,
1544
    *,
1545
    client: Optional[GitHubClient] = None,
1546
    budgeter: Optional[RateLimitBudgeter] = None,
1547
    sleep: Callable[[float], None] = time.sleep,
1548
    clock: Callable[[], float] = time.monotonic,
1549
) -> list[dict[str, Any]]:
1550
    scenario_name = str(scenario["name"])
1551
    repo = args.repo
1552
    if client is None:
1553
        client = GitHubClient(token=os.environ.get(args.token_env))
1554
    if budgeter is None:
1555
        budgeter = RateLimitBudgeter(DEFAULT_RACE_OPS_PER_HOUR)
1556
    check_instant = args.protection_variant == "check-instant"
1557
1558
    def pace_content_op() -> None:
1559
        # Pace before every content (write) op; the budgeter accumulates the
1560
        # injected waits, which race.pacing.injected reports.
1561
        wait = budgeter.pace()
1562
        if wait > 0:
1563
            sleep(wait)
1564
1565
    def make_branch(branch_index: int, branch: str) -> tuple[
1566
        Optional[str], dict[str, Callable[[], dict[str, Any]]], Callable[[], None]
1567
    ]:
1568
        state: dict[str, Any] = {}
1569
        try:
1570
            # Serial landing: each branch baselines whatever main is now.
1571
            state["base_sha"] = client.get_main_sha(repo)
1572
        except (GitHubApiError, KeyError, json.JSONDecodeError) as exc:
1573
            return f"setup_failed:{str(exc)[:160]}", {}, lambda: None
1574
1575
        payload = (
1576
            f"platform race {meta['bench_id']} run={run_index} branch={branch_index}\n"
1577
        ).encode("utf-8")
1578
1579
        def publish() -> dict[str, Any]:
1580
            def do() -> None:
1581
                pace_content_op()
1582
                client.create_ref(repo, f"refs/heads/{branch}", state["base_sha"])
1583
                pace_content_op()
1584
                put = client.put_file(
1585
                    repo,
1586
                    path=f".oak-bench/race-{run_index}-b{branch_index}.md",
1587
                    message=f"bench race {meta['bench_id']} r{run_index} b{branch_index}",
1588
                    content=payload,
1589
                    branch=branch,
1590
                )
1591
                commit = put.get("commit") if isinstance(put, dict) else None
1592
                if isinstance(commit, dict) and commit.get("sha"):
1593
                    state["head_sha"] = str(commit["sha"])
1594
            return api_step_row(
1595
                meta, scenario_name, "branch.publish", run_index, client, do,
1596
                [f"api:POST /repos/{repo}/git/refs", f"api:PUT /repos/{repo}/contents/..."],
1597
            )
1598
1599
        def open_pr() -> dict[str, Any]:
1600
            def do() -> None:
1601
                pace_content_op()
1602
                pr = client.create_pr(
1603
                    repo,
1604
                    title=f"bench race {scenario_name} r{run_index} b{branch_index}",
1605
                    head=branch,
1606
                    base="main",
1607
                )
1608
                state["pr_number"] = int(pr["number"])
1609
                head = pr.get("head") or {}
1610
                if not state.get("head_sha") and head.get("sha"):
1611
                    state["head_sha"] = str(head["sha"])
1612
            return api_step_row(
1613
                meta, scenario_name, "integration.open", run_index, client, do,
1614
                [f"api:POST /repos/{repo}/pulls"],
1615
            )
1616
1617
        def get_pr_quiet() -> Optional[dict[str, Any]]:
1618
            try:
1619
                return client.get_pr(repo, state["pr_number"])
1620
            except GitHubApiError:
1621
                return None
1622
1623
        def visible() -> dict[str, Any]:
1624
            return api_settle_row(
1625
                meta, scenario_name, "integration.visible.settle", run_index, client,
1626
                lambda: get_pr_quiet() is not None, settings,
1627
                [f"api:GET /repos/{repo}/pulls/<n> (poll)"], clock=clock, sleep=sleep,
1628
            )
1629
1630
        def mergeable() -> dict[str, Any]:
1631
            def poll() -> bool:
1632
                pr = get_pr_quiet()
1633
                return pr is not None and pr.get("mergeable") is not None
1634
            return api_settle_row(
1635
                meta, scenario_name, "integration.mergeable.settle", run_index, client,
1636
                poll, settings, [f"api:GET /repos/{repo}/pulls/<n> mergeable (poll)"],
1637
                clock=clock, sleep=sleep,
1638
            )
1639
1640
        def checks_green() -> dict[str, Any]:
1641
            if not check_instant:
1642
                return structural_row(
1643
                    meta, scenario_name, CHECKS_GREEN_OPERATION, run_index,
1644
                    "protection-none: no required status checks; nothing to wait for",
1645
                )
1646
            return api_check_instant_checks_green_row(
1647
                meta, scenario_name, run_index, client, repo,
1648
                str(state.get("head_sha") or ""), settings,
1649
                pace=pace_content_op, clock=clock, sleep=sleep,
1650
            )
1651
1652
        def merge() -> dict[str, Any]:
1653
            def do() -> None:
1654
                pace_content_op()
1655
                client.merge_pr(repo, state["pr_number"])
1656
            return api_step_row(
1657
                meta, scenario_name, "integration.merge.request", run_index, client, do,
1658
                [f"api:PUT /repos/{repo}/pulls/<n>/merge"],
1659
            )
1660
1661
        def merged() -> dict[str, Any]:
1662
            def poll() -> bool:
1663
                pr = get_pr_quiet()
1664
                return bool(pr and pr.get("merged"))
1665
            return api_settle_row(
1666
                meta, scenario_name, "integration.merged.settle", run_index, client,
1667
                poll, settings, [f"api:GET /repos/{repo}/pulls/<n> merged (poll)"],
1668
                clock=clock, sleep=sleep,
1669
            )
1670
1671
        def main_settle() -> dict[str, Any]:
1672
            def poll() -> bool:
1673
                try:
1674
                    return client.get_main_sha(repo) != state["base_sha"]
1675
                except GitHubApiError:
1676
                    return False
1677
            return api_settle_row(
1678
                meta, scenario_name, "integration.main.settle", run_index, client,
1679
                poll, settings, [f"api:GET /repos/{repo}/git/ref/heads/main (poll)"],
1680
                clock=clock, sleep=sleep,
1681
            )
1682
1683
        def cleanup() -> None:
1684
            try:
1685
                client.delete_ref(repo, f"heads/{branch}")
1686
            except GitHubApiError:
1687
                pass  # untimed cleanup; merged-branch deletion failures are not data
1688
1689
        handlers = {
1690
            "branch.publish": publish,
1691
            "integration.open": open_pr,
1692
            "integration.visible.settle": visible,
1693
            "integration.mergeable.settle": mergeable,
1694
            "integration.checks.green.settle": checks_green,
1695
            "integration.merge.request": merge,
1696
            "integration.merged.settle": merged,
1697
            "integration.main.settle": main_settle,
1698
        }
1699
        return None, handlers, cleanup
1700
1701
    return run_race_operations(
1702
        scenario, meta, run_index, "harness-api", race_branch_count(scenario),
1703
        make_branch, budgeter, clock,
1704
    )
1705
1706
1707
def run_oak_cli_race(
1708
    args: argparse.Namespace,
1709
    scenario: dict[str, Any],
1710
    meta: dict[str, Any],
1711
    run_index: int,
1712
    run_root: Path,
1713
    settings: Settings,
1714
    *,
1715
    budgeter: Optional[RateLimitBudgeter] = None,
1716
    sleep: Callable[[float], None] = time.sleep,
1717
    clock: Callable[[], float] = time.monotonic,
1718
) -> list[dict[str, Any]]:
1719
    scenario_name = str(scenario["name"])
1720
    env = cli_env(args)
1721
    if budgeter is None:
1722
        budgeter = RateLimitBudgeter(DEFAULT_RACE_OPS_PER_HOUR)
1723
1724
    def pace_content_op() -> None:
1725
        wait = budgeter.pace()
1726
        if wait > 0:
1727
            sleep(wait)
1728
1729
    def make_branch(branch_index: int, branch: str) -> tuple[
1730
        Optional[str], dict[str, Callable[[], dict[str, Any]]], Callable[[], None]
1731
    ]:
1732
        workdir = run_root / f"oak-cli-race-r{run_index}-b{branch_index}"
1733
        mount = run_untimed(["oak", "mount", args.repo, str(workdir)], run_root, env)
1734
        if mount.returncode != 0:
1735
            return (
1736
                f"setup_failed:oak_mount:{mount.stderr.strip()[:120]}", {}, lambda: None
1737
            )
1738
1739
        def cleanup() -> None:
1740
            run_untimed(["oak", "mount", "end", str(workdir), "-f"], run_root, env)
1741
1742
        payload_dir = workdir / ".oak-bench"
1743
        payload_dir.mkdir(parents=True, exist_ok=True)
1744
        (payload_dir / f"race-{run_index}-b{branch_index}.md").write_text(
1745
            f"platform race {meta['bench_id']} run={run_index} branch={branch_index}\n"
1746
        )
1747
        commit = run_untimed(["oak", "commit", "--no-verify"], workdir, env)
1748
        if commit.returncode != 0:
1749
            return f"setup_failed:oak_commit:{commit.stderr.strip()[:120]}", {}, cleanup
1750
1751
        def paced_cli(operation: str, command: list[str]) -> Callable[[], dict[str, Any]]:
1752
            def handler() -> dict[str, Any]:
1753
                pace_content_op()
1754
                return cli_step_row(
1755
                    meta, scenario_name, operation, command, workdir, run_index, env,
1756
                    settings.admitted,
1757
                )
1758
            return handler
1759
1760
        def structural(operation: str, note: str) -> Callable[[], dict[str, Any]]:
1761
            return lambda: structural_row(meta, scenario_name, operation, run_index, note)
1762
1763
        handlers = {
1764
            "branch.publish": paced_cli("branch.publish", ["oak", "push"]),
1765
            "integration.open": structural(
1766
                "integration.open",
1767
                "integration object exists at publish on oak; no separate open call",
1768
            ),
1769
            "integration.visible.settle": structural(
1770
                "integration.visible.settle",
1771
                "oak push returns after the branch is visible on the server; nothing to poll",
1772
            ),
1773
            "integration.mergeable.settle": structural(
1774
                "integration.mergeable.settle", "no async mergeability concept"
1775
            ),
1776
            "integration.checks.green.settle": structural(
1777
                "integration.checks.green.settle",
1778
                "no hosted status-check API on oak; check-instant is a GitHub-side "
1779
                "harness construct with no equivalent operation here (ADR-0002)",
1780
            ),
1781
            "integration.merge.request": paced_cli(
1782
                "integration.merge.request", ["oak", "merge"]
1783
            ),
1784
            "integration.merged.settle": structural(
1785
                "integration.merged.settle",
1786
                "oak merge is synchronous; merged state holds at return",
1787
            ),
1788
            "integration.main.settle": structural(
1789
                "integration.main.settle",
1790
                "oak merge returns after main is updated; nothing to poll",
1791
            ),
1792
        }
1793
        return None, handlers, cleanup
1794
1795
    return run_race_operations(
1796
        scenario, meta, run_index, "cli", race_branch_count(scenario),
1797
        make_branch, budgeter, clock,
1798
    )
1799
1800
1801
def run_race_with_credentials(
1802
    args: argparse.Namespace,
1803
    scenario: dict[str, Any],
1804
    meta: dict[str, Any],
1805
    run_index: int,
1806
    run_root: Path,
1807
    settings: Settings,
1808
) -> list[dict[str, Any]]:
1809
    if args.platform == "github" and args.driver == "harness-api":
1810
        return run_github_api_race(args, scenario, meta, run_index, settings)
1811
    if args.platform == "github":
1812
        # gh-cli race orchestration is deferred to Phase 4 (see
1813
        # RACE_CLI_DEFER_REASON): per-op skip rows, a recorded gap, never
1814
        # silently different semantics.
1815
        scenario_name = str(scenario["name"])
1816
        return [
1817
            skip_row(meta, scenario_name, str(operation), run_index, RACE_CLI_DEFER_REASON)
1818
            for operation in scenario.get("operations") or []
1819
        ]
1820
    return run_oak_cli_race(args, scenario, meta, run_index, run_root, settings)
1821
1822
1823
# --------------------------------------------------------------------------
1824
# branch_fleet_nN: end-to-end branch fleet triage and landing.
1825
#
1826
# N open branches are seeded; roughly 30% are constructed as conflicts and the
1827
# rest are clean. The timed workflow is: classify -> plan -> merge clean
1828
# branches -> leave conflicts open -> sync/observe final main -> oracle. This
1829
# is the agent-scale workflow Git/GitHub makes agents perform with many round
1830
# trips, while Oak should collapse classification into one checkout-free batch.
1831
# --------------------------------------------------------------------------
1832
1833
1834
def is_branch_fleet_scenario(scenario: dict[str, Any]) -> bool:
1835
    return BRANCH_FLEET_SCENARIO_RE.match(str(scenario.get("name", ""))) is not None
1836
1837
1838
def branch_fleet_count(scenario: dict[str, Any]) -> int:
1839
    if scenario.get("branch_count") is not None:
1840
        return int(scenario["branch_count"])
1841
    match = BRANCH_FLEET_SCENARIO_RE.match(str(scenario.get("name", "")))
1842
    return int(match.group(1)) if match else 10
1843
1844
1845
def branch_fleet_live_skip_reason(
1846
    scenario: dict[str, Any],
1847
    driver: str,
1848
    environ: Optional[dict[str, str]] = None,
1849
) -> Optional[str]:
1850
    env = os.environ if environ is None else environ
1851
    if driver == BRANCH_TRIAGE_FAKE_DRIVER:
1852
        return None
1853
    if branch_fleet_count(scenario) <= BRANCH_FLEET_LIVE_LARGE_LIMIT:
1854
        return None
1855
    if str(env.get(BRANCH_FLEET_LIVE_LARGE_ENV) or "").strip() == "1":
1856
        return None
1857
    return (
1858
        f"branch_fleet_large_live_requires_opt_in:"
1859
        f"{BRANCH_FLEET_LIVE_LARGE_ENV}=1"
1860
    )
1861
1862
1863
def branch_fleet_worker_count(
1864
    branch_count: int,
1865
    *,
1866
    env_name: str,
1867
    default: int,
1868
    environ: Optional[dict[str, str]] = None,
1869
) -> int:
1870
    env = os.environ if environ is None else environ
1871
    raw = str(env.get(env_name) or "").strip()
1872
    if raw:
1873
        try:
1874
            configured = int(raw)
1875
        except ValueError:
1876
            configured = default
1877
    else:
1878
        configured = default
1879
    bounded = max(1, min(configured, BRANCH_FLEET_WORKERS_MAX))
1880
    return max(1, min(bounded, max(1, branch_count)))
1881
1882
1883
def branch_fleet_seed_worker_count(
1884
    branch_count: int, environ: Optional[dict[str, str]] = None
1885
) -> int:
1886
    return branch_fleet_worker_count(
1887
        branch_count,
1888
        env_name=BRANCH_FLEET_SEED_WORKERS_ENV,
1889
        default=BRANCH_FLEET_SEED_WORKERS_DEFAULT,
1890
        environ=environ,
1891
    )
1892
1893
1894
def branch_fleet_cleanup_worker_count(
1895
    branch_count: int, environ: Optional[dict[str, str]] = None
1896
) -> int:
1897
    return branch_fleet_worker_count(
1898
        branch_count,
1899
        env_name=BRANCH_FLEET_CLEANUP_WORKERS_ENV,
1900
        default=BRANCH_FLEET_CLEANUP_WORKERS_DEFAULT,
1901
        environ=environ,
1902
    )
1903
1904
1905
def branch_fleet_conflict_count(branch_count: int) -> int:
1906
    # The launch/demo shape is "10 branches, 3 conflicts, 7 clean"; scale that
1907
    # ratio for larger fleets while keeping at least one conflict when N > 1.
1908
    if branch_count <= 1:
1909
        return 0
1910
    return max(1, round(branch_count * 0.3))
1911
1912
1913
def branch_fleet_specs(
1914
    *, bench_id: str, scenario_name: str, run_index: int, branch_count: int
1915
) -> list[dict[str, Any]]:
1916
    conflict_count = branch_fleet_conflict_count(branch_count)
1917
    clean_count = branch_count - conflict_count
1918
    prefix = disposable_branch(bench_id, scenario_name, "fleet", run_index)
1919
    specs: list[dict[str, Any]] = []
1920
    for index in range(branch_count):
1921
        kind = "conflicting" if index >= clean_count else "clean"
1922
        specs.append(
1923
            {
1924
                "index": index,
1925
                "name": f"{prefix}-{kind[:4]}-{index:04d}",
1926
                "kind": kind,
1927
                "expected_action": "leave_conflict_open" if kind == "conflicting" else "merge",
1928
                "path": (
1929
                    f".oak-bench/fleet/conflict-{index:04d}.txt"
1930
                    if kind == "conflicting"
1931
                    else f".oak-bench/fleet/clean-{index:04d}.txt"
1932
                ),
1933
            }
1934
        )
1935
    return specs
1936
1937
1938
def branch_fleet_plan(reviewed: list[dict[str, Any]]) -> dict[str, Any]:
1939
    actions: list[dict[str, str]] = []
1940
    merge_order: list[str] = []
1941
    clean_detected = 0
1942
    conflict_detected = 0
1943
    unknown_detected = 0
1944
    for branch in reviewed:
1945
        name = str(branch["name"])
1946
        mergeable = branch.get("mergeable")
1947
        if mergeable is True:
1948
            classification = "clean"
1949
            action = "merge"
1950
            clean_detected += 1
1951
            merge_order.append(name)
1952
        elif mergeable is False:
1953
            classification = "conflicting"
1954
            action = "leave_conflict_open"
1955
            conflict_detected += 1
1956
        else:
1957
            classification = "unknown"
1958
            action = "review"
1959
            unknown_detected += 1
1960
        actions.append({"branch": name, "classification": classification, "action": action})
1961
    return {
1962
        "actions": actions,
1963
        "merge_order": merge_order,
1964
        "detected_counts": {
1965
            "clean": clean_detected,
1966
            "conflicting": conflict_detected,
1967
            "unknown": unknown_detected,
1968
        },
1969
    }
1970
1971
1972
def branch_fleet_oracle(
1973
    specs: list[dict[str, Any]],
1974
    plan: dict[str, Any],
1975
    apply_result: dict[str, Any],
1976
) -> dict[str, Any]:
1977
    expected_actions = [
1978
        {
1979
            "branch": str(spec["name"]),
1980
            "classification": str(spec["kind"]),
1981
            "action": str(spec["expected_action"]),
1982
        }
1983
        for spec in specs
1984
    ]
1985
    expected_merge_order = [
1986
        str(spec["name"]) for spec in specs if spec["expected_action"] == "merge"
1987
    ]
1988
    expected_survivors = [
1989
        str(spec["name"])
1990
        for spec in specs
1991
        if spec["expected_action"] == "leave_conflict_open"
1992
    ]
1993
    apply_succeeded = apply_result.get("apply_succeeded") is not False
1994
    final_survivors = sorted(str(name) for name in apply_result.get("final_survivors") or [])
1995
    return {
1996
        "apply_succeeded": apply_succeeded,
1997
        "action_plan_correct": plan.get("actions") == expected_actions,
1998
        "merge_order_correct": plan.get("merge_order") == expected_merge_order,
1999
        "final_survivors_correct": apply_succeeded and final_survivors == sorted(expected_survivors),
2000
        "expected_action_plan": expected_actions,
2001
        "expected_merge_order": expected_merge_order,
2002
        "expected_final_survivors": sorted(expected_survivors),
2003
        "final_survivors": final_survivors,
2004
    }
2005
2006
2007
def branch_fleet_survivors_from_sync_stdout(
2008
    stdout: str,
2009
    specs: list[dict[str, Any]],
2010
) -> list[str]:
2011
    payload = json.loads(stdout)
2012
    if isinstance(payload, list):
2013
        rows = payload
2014
    elif isinstance(payload, dict):
2015
        if "branches" in payload:
2016
            rows = payload["branches"]
2017
        elif "rows" in payload:
2018
            rows = payload["rows"]
2019
        else:
2020
            raise ValueError("branch_list_missing_rows")
2021
        if not isinstance(rows, list):
2022
            raise ValueError("branch_list_rows_not_list")
2023
    else:
2024
        raise ValueError("branch_list_not_array_or_object")
2025
    expected_names = {str(spec["name"]) for spec in specs}
2026
    names = set()
2027
    for item in rows:
2028
        if not isinstance(item, dict):
2029
            raise ValueError("branch_list_row_not_object")
2030
        name = item.get("name") or item.get("branch")
2031
        if not name:
2032
            raise ValueError("branch_list_row_missing_name")
2033
        names.add(str(name))
2034
    return sorted(name for name in names if name in expected_names)
2035
2036
2037
BRANCH_FLEET_WORKFLOW_OPERATIONS = (
2038
    "fleet.classify",
2039
    "fleet.plan",
2040
    "fleet.apply",
2041
    "fleet.sync",
2042
    "fleet.oracle",
2043
)
2044
2045
2046
def branch_fleet_workflow_summary_fields(rows: list[dict[str, Any]]) -> dict[str, Any]:
2047
    setup_rows = [row for row in rows if row.get("operation") == "fleet.seed"]
2048
    workflow_rows = [
2049
        row for row in rows
2050
        if row.get("operation") in BRANCH_FLEET_WORKFLOW_OPERATIONS
2051
    ]
2052
    workflow_failures = [
2053
        row for row in workflow_rows
2054
        if row_returncode(row) not in (0, SKIP_RETURNCODE)
2055
    ]
2056
    workflow_skips = [
2057
        row for row in workflow_rows
2058
        if row_returncode(row) == SKIP_RETURNCODE
2059
    ]
2060
    workflow_returncode: Optional[int]
2061
    if not workflow_rows:
2062
        workflow_returncode = None
2063
    elif workflow_failures:
2064
        workflow_returncode = 1
2065
    elif len(workflow_skips) == len(workflow_rows):
2066
        workflow_returncode = SKIP_RETURNCODE
2067
    else:
2068
        workflow_returncode = 0
2069
    tool_counts = [
2070
        int(row["tool_call_count"])
2071
        for row in workflow_rows
2072
        if row.get("tool_call_count") is not None
2073
    ]
2074
    return {
2075
        "branch_fleet_workflow_operations": [row["operation"] for row in workflow_rows],
2076
        "branch_fleet_setup_operations": [row["operation"] for row in setup_rows],
2077
        "branch_fleet_setup_excluded_from_workflow": True,
2078
        "branch_fleet_workflow_elapsed_ms": round(
2079
            sum(float(row.get("elapsed_ms") or 0.0) for row in workflow_rows),
2080
            3,
2081
        ),
2082
        "branch_fleet_setup_elapsed_ms": round(
2083
            sum(float(row.get("elapsed_ms") or 0.0) for row in setup_rows),
2084
            3,
2085
        ),
2086
        "branch_fleet_workflow_failure_count": len(workflow_failures),
2087
        "branch_fleet_workflow_skip_count": len(workflow_skips),
2088
        "branch_fleet_workflow_returncode": workflow_returncode,
2089
        "branch_fleet_workflow_tool_call_count": sum(tool_counts) if tool_counts else None,
2090
    }
2091
2092
2093
def branch_fleet_metric_fields(
2094
    *,
2095
    branch_count: int,
2096
    reviewed: Optional[list[dict[str, Any]]] = None,
2097
    plan: Optional[dict[str, Any]] = None,
2098
    apply_result: Optional[dict[str, Any]] = None,
2099
    oracle: Optional[dict[str, Any]] = None,
2100
) -> dict[str, Any]:
2101
    reviewed = reviewed or []
2102
    counts = ((plan or {}).get("detected_counts") or {})
2103
    if not counts and reviewed:
2104
        counts = {"clean": 0, "conflicting": 0, "unknown": 0}
2105
        for branch in reviewed:
2106
            if branch.get("mergeable") is True:
2107
                counts["clean"] += 1
2108
            elif branch.get("mergeable") is False:
2109
                counts["conflicting"] += 1
2110
            else:
2111
                counts["unknown"] += 1
2112
    merged = list((apply_result or {}).get("merged_branches") or [])
2113
    survivors = list((apply_result or {}).get("final_survivors") or [])
2114
    return {
2115
        "branch_count": branch_count,
2116
        "branches_seeded": branch_count,
2117
        "branches_reviewed": len(reviewed),
2118
        "branches_merged": len(merged),
2119
        "branches_left_open": len(survivors),
2120
        "branches_closed": len(merged),
2121
        "clean_detected_count": int(counts.get("clean", 0)),
2122
        "conflict_detected_count": int(counts.get("conflicting", 0)),
2123
        "unknown_detected_count": int(counts.get("unknown", 0)),
2124
        "merge_order_correct": (oracle or {}).get("merge_order_correct"),
2125
    }
2126
2127
2128
def branch_fleet_row(
2129
    meta: dict[str, Any],
2130
    scenario_name: str,
2131
    operation: str,
2132
    run_index: int,
2133
    elapsed_ms: float,
2134
    returncode: int = 0,
2135
    *,
2136
    provider: str,
2137
    command: Optional[list[Any]] = None,
2138
    **fields: Any,
2139
) -> dict[str, Any]:
2140
    row = {
2141
        **meta,
2142
        "scenario": scenario_name,
2143
        "run": run_index,
2144
        "operation": operation,
2145
        "elapsed_ms": round(elapsed_ms, 3),
2146
        "returncode": returncode,
2147
        "command": command or [],
2148
        "branch_fleet_provider": provider,
2149
        **fields,
2150
    }
2151
    if provider in (BRANCH_FLEET_PROVIDER_GITHUB_API, BRANCH_FLEET_PROVIDER_FAKE):
2152
        row.update(null_token_fields() if provider == BRANCH_FLEET_PROVIDER_GITHUB_API else fake_provider_token_fields())
2153
        row.setdefault("tool_call_count", None if provider == BRANCH_FLEET_PROVIDER_GITHUB_API else 0)
2154
    return row
2155
2156
2157
def run_branch_fleet_fake_scenario(
2158
    scenario: dict[str, Any],
2159
    meta: dict[str, Any],
2160
    run_index: int,
2161
) -> list[dict[str, Any]]:
2162
    scenario_name = str(scenario["name"])
2163
    operations = [str(op) for op in scenario.get("operations") or []]
2164
    count = branch_fleet_count(scenario)
2165
    specs = branch_fleet_specs(
2166
        bench_id=str(meta.get("bench_id") or "bench"),
2167
        scenario_name=scenario_name,
2168
        run_index=run_index,
2169
        branch_count=count,
2170
    )
2171
    rows: list[dict[str, Any]] = []
2172
    reviewed: list[dict[str, Any]] = []
2173
    plan: dict[str, Any] = {}
2174
    apply_result: dict[str, Any] = {
2175
        "apply_succeeded": False,
2176
        "merged_branches": [],
2177
        "final_survivors": [],
2178
    }
2179
    oracle: dict[str, Any] = {}
2180
    total_start = platform_clock.monotonic_ms()
2181
2182
    for operation in operations:
2183
        start = platform_clock.monotonic_ms()
2184
        if operation == "fleet.seed":
2185
            rows.append(
2186
                branch_fleet_row(
2187
                    meta, scenario_name, operation, run_index,
2188
                    platform_clock.monotonic_ms() - start,
2189
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2190
                    seeded_branch_classes=[spec["kind"] for spec in specs],
2191
                    **branch_fleet_metric_fields(branch_count=count),
2192
                )
2193
            )
2194
        elif operation == "fleet.classify":
2195
            reviewed = [
2196
                {"name": spec["name"], "mergeable": spec["kind"] == "clean"}
2197
                for spec in specs
2198
            ]
2199
            rows.append(
2200
                branch_fleet_row(
2201
                    meta, scenario_name, operation, run_index,
2202
                    platform_clock.monotonic_ms() - start,
2203
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2204
                    reviewed_branches=[row["name"] for row in reviewed],
2205
                    **branch_fleet_metric_fields(branch_count=count, reviewed=reviewed),
2206
                )
2207
            )
2208
        elif operation == "fleet.plan":
2209
            plan = branch_fleet_plan(reviewed)
2210
            rows.append(
2211
                branch_fleet_row(
2212
                    meta, scenario_name, operation, run_index,
2213
                    platform_clock.monotonic_ms() - start,
2214
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2215
                    action_plan=plan["actions"],
2216
                    merge_order=plan["merge_order"],
2217
                    **branch_fleet_metric_fields(branch_count=count, reviewed=reviewed, plan=plan),
2218
                )
2219
            )
2220
        elif operation == "fleet.apply":
2221
            apply_result = {
2222
                "apply_succeeded": True,
2223
                "merged_branches": list(plan.get("merge_order") or []),
2224
                "final_survivors": [
2225
                    action["branch"]
2226
                    for action in plan.get("actions") or []
2227
                    if action["action"] == "leave_conflict_open"
2228
                ],
2229
            }
2230
            rows.append(
2231
                branch_fleet_row(
2232
                    meta, scenario_name, operation, run_index,
2233
                    platform_clock.monotonic_ms() - start,
2234
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2235
                    **apply_result,
2236
                    **branch_fleet_metric_fields(
2237
                        branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
2238
                    ),
2239
                )
2240
            )
2241
        elif operation == "fleet.sync":
2242
            rows.append(
2243
                branch_fleet_row(
2244
                    meta, scenario_name, operation, run_index,
2245
                    platform_clock.monotonic_ms() - start,
2246
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2247
                    note="fake provider has no local checkout; final state already materialized in memory",
2248
                    **branch_fleet_metric_fields(
2249
                        branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
2250
                    ),
2251
                )
2252
            )
2253
        elif operation == "fleet.oracle":
2254
            oracle = branch_fleet_oracle(specs, plan, apply_result)
2255
            ok = all(
2256
                bool(oracle[key])
2257
                for key in (
2258
                    "apply_succeeded",
2259
                    "action_plan_correct",
2260
                    "merge_order_correct",
2261
                    "final_survivors_correct",
2262
                )
2263
            )
2264
            fields = {
2265
                **oracle,
2266
                **branch_fleet_metric_fields(
2267
                    branch_count=count,
2268
                    reviewed=reviewed,
2269
                    plan=plan,
2270
                    apply_result=apply_result,
2271
                    oracle=oracle,
2272
                ),
2273
            }
2274
            rows.append(
2275
                branch_fleet_row(
2276
                    meta, scenario_name, operation, run_index,
2277
                    platform_clock.monotonic_ms() - start,
2278
                    returncode=0 if ok else 1,
2279
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2280
                    **fields,
2281
                )
2282
            )
2283
        elif operation == "fleet.total":
2284
            failures = [
2285
                row for row in rows
2286
                if row_returncode(row) not in (0, SKIP_RETURNCODE)
2287
            ]
2288
            rows.append(
2289
                branch_fleet_row(
2290
                    meta, scenario_name, operation, run_index,
2291
                    platform_clock.monotonic_ms() - total_start,
2292
                    returncode=1 if failures else 0,
2293
                    provider=BRANCH_FLEET_PROVIDER_FAKE,
2294
                    summarized_operations=[row["operation"] for row in rows],
2295
                    **branch_fleet_workflow_summary_fields(rows),
2296
                    **branch_fleet_metric_fields(
2297
                        branch_count=count,
2298
                        reviewed=reviewed,
2299
                        plan=plan,
2300
                        apply_result=apply_result,
2301
                        oracle=oracle,
2302
                    ),
2303
                )
2304
            )
2305
        else:
2306
            rows.append(skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented"))
2307
    return rows
2308
2309
2310
def run_github_api_branch_fleet_scenario(
2311
    args: argparse.Namespace,
2312
    scenario: dict[str, Any],
2313
    meta: dict[str, Any],
2314
    run_index: int,
2315
    settings: Settings,
2316
    *,
2317
    client: Optional[GitHubClient] = None,
2318
    clock: Callable[[], float] = time.monotonic,
2319
    sleep: Callable[[float], None] = time.sleep,
2320
) -> list[dict[str, Any]]:
2321
    scenario_name = str(scenario["name"])
2322
    operations = [str(op) for op in scenario.get("operations") or []]
2323
    count = branch_fleet_count(scenario)
2324
    specs = branch_fleet_specs(
2325
        bench_id=str(meta.get("bench_id") or "bench"),
2326
        scenario_name=scenario_name,
2327
        run_index=run_index,
2328
        branch_count=count,
2329
    )
2330
    repo = args.repo
2331
    if client is None:
2332
        client = GitHubClient(token=os.environ.get(args.token_env))
2333
2334
    state: dict[str, Any] = {"prs": {}, "heads": {}}
2335
    reviewed: list[dict[str, Any]] = []
2336
    plan: dict[str, Any] = {}
2337
    apply_result: dict[str, Any] = {
2338
        "apply_succeeded": False,
2339
        "merged_branches": [],
2340
        "final_survivors": [],
2341
    }
2342
    oracle: dict[str, Any] = {}
2343
    total_start = platform_clock.monotonic_ms()
2344
2345
    def seed() -> dict[str, Any]:
2346
        def do() -> None:
2347
            base_sha = client.get_main_sha(repo)
2348
            state["base_sha"] = base_sha
2349
            for spec in specs:
2350
                branch = str(spec["name"])
2351
                path = str(spec["path"])
2352
                client.create_ref(repo, f"refs/heads/{branch}", base_sha)
2353
                put = client.put_file(
2354
                    repo,
2355
                    path=path,
2356
                    message=f"bench branch fleet {meta['bench_id']} {branch}",
2357
                    content=(
2358
                        f"branch fleet {meta['bench_id']} run={run_index} "
2359
                        f"branch={branch} kind={spec['kind']}\n"
2360
                    ).encode("utf-8"),
2361
                    branch=branch,
2362
                )
2363
                commit = put.get("commit") if isinstance(put, dict) else None
2364
                if isinstance(commit, dict) and commit.get("sha"):
2365
                    state["heads"][branch] = str(commit["sha"])
2366
                pr = client.create_pr(
2367
                    repo,
2368
                    title=f"bench branch fleet {scenario_name} {branch}",
2369
                    head=branch,
2370
                    base="main",
2371
                    body="Disposable branch-fleet benchmark PR.",
2372
                )
2373
                state["prs"][branch] = int(pr["number"])
2374
            # Make conflict branches genuinely conflict with main: each conflict
2375
            # branch added the same path from the old base; now main adds that
2376
            # path with different content, producing add/add conflicts.
2377
            for spec in specs:
2378
                if spec["kind"] != "conflicting":
2379
                    continue
2380
                client.put_file(
2381
                    repo,
2382
                    path=str(spec["path"]),
2383
                    message=f"bench branch fleet conflicting main side {meta['bench_id']}",
2384
                    content=(
2385
                        f"main-side conflict {meta['bench_id']} run={run_index} "
2386
                        f"branch={spec['name']}\n"
2387
                    ).encode("utf-8"),
2388
                    branch="main",
2389
                )
2390
2391
        row = api_step_row(
2392
            meta,
2393
            scenario_name,
2394
            "fleet.seed",
2395
            run_index,
2396
            client,
2397
            do,
2398
            [
2399
                f"api:GET /repos/{repo}/git/ref/heads/main",
2400
                f"api:POST /repos/{repo}/git/refs x{count}",
2401
                f"api:PUT /repos/{repo}/contents/... x{count + branch_fleet_conflict_count(count)}",
2402
                f"api:POST /repos/{repo}/pulls x{count}",
2403
            ],
2404
        )
2405
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_GITHUB_API
2406
        row.update(branch_fleet_metric_fields(branch_count=count))
2407
        row["seeded_branch_classes"] = [spec["kind"] for spec in specs]
2408
        return row
2409
2410
    def classify() -> dict[str, Any]:
2411
        nonlocal reviewed
2412
        before = metrics_snapshot(client)
2413
        start = platform_clock.monotonic_ms()
2414
        polls = 0
2415
        settled = False
2416
        last_reviewed: list[dict[str, Any]] = []
2417
        error_text: Optional[str] = None
2418
        deadline = clock() + settings.settle_timeout_s
2419
        while True:
2420
            polls += 1
2421
            try:
2422
                current: list[dict[str, Any]] = []
2423
                all_known = True
2424
                for spec in specs:
2425
                    branch = str(spec["name"])
2426
                    pr = client.get_pr(repo, int(state["prs"][branch]))
2427
                    mergeable = pr.get("mergeable")
2428
                    if mergeable is None:
2429
                        all_known = False
2430
                    current.append(
2431
                        {
2432
                            "name": branch,
2433
                            "mergeable": mergeable,
2434
                            "pr_number": int(state["prs"][branch]),
2435
                            "expected_kind": spec["kind"],
2436
                        }
2437
                    )
2438
                last_reviewed = current
2439
                if all_known:
2440
                    settled = True
2441
                    break
2442
            except (GitHubApiError, KeyError, json.JSONDecodeError) as exc:
2443
                error_text = str(exc)[:4000]
2444
                break
2445
            if clock() >= deadline:
2446
                break
2447
            sleep(settings.interval_s)
2448
        reviewed = last_reviewed
2449
        elapsed_ms = platform_clock.monotonic_ms() - start
2450
        row = branch_fleet_row(
2451
            meta,
2452
            scenario_name,
2453
            "fleet.classify",
2454
            run_index,
2455
            elapsed_ms,
2456
            returncode=0 if settled and error_text is None else 1,
2457
            provider=BRANCH_FLEET_PROVIDER_GITHUB_API,
2458
            command=[f"api:GET /repos/{repo}/pulls/<n> mergeable x{count} (poll)"],
2459
            reviewed_branches=[branch["name"] for branch in reviewed],
2460
            settled=settled,
2461
            settle_elapsed_ms=round(elapsed_ms, 3) if settled else None,
2462
            observed_wait_ms=round(elapsed_ms, 3),
2463
            poll_iterations=polls,
2464
            poll_interval_s=settings.interval_s,
2465
            poll_quantization_ms=settings.interval_s * 1000.0,
2466
            api_metrics=metrics_delta(before, metrics_snapshot(client)),
2467
            **branch_fleet_metric_fields(branch_count=count, reviewed=reviewed),
2468
        )
2469
        if error_text is not None:
2470
            row["stderr"] = error_text
2471
        return row
2472
2473
    def plan_row() -> dict[str, Any]:
2474
        nonlocal plan
2475
        start = platform_clock.monotonic_ms()
2476
        plan = branch_fleet_plan(reviewed)
2477
        return branch_fleet_row(
2478
            meta,
2479
            scenario_name,
2480
            "fleet.plan",
2481
            run_index,
2482
            platform_clock.monotonic_ms() - start,
2483
            provider=BRANCH_FLEET_PROVIDER_GITHUB_API,
2484
            action_plan=plan["actions"],
2485
            merge_order=plan["merge_order"],
2486
            **branch_fleet_metric_fields(branch_count=count, reviewed=reviewed, plan=plan),
2487
        )
2488
2489
    def apply() -> dict[str, Any]:
2490
        nonlocal apply_result
2491
2492
        def do() -> None:
2493
            merged: list[str] = []
2494
            survivors: list[str] = []
2495
            for action in plan.get("actions") or []:
2496
                branch = str(action["branch"])
2497
                if action["action"] == "merge":
2498
                    client.merge_pr(repo, int(state["prs"][branch]))
2499
                    merged.append(branch)
2500
                elif action["action"] == "leave_conflict_open":
2501
                    survivors.append(branch)
2502
            apply_result = {
2503
                "apply_succeeded": True,
2504
                "merged_branches": merged,
2505
                "final_survivors": survivors,
2506
            }
2507
            state["apply_result"] = apply_result
2508
2509
        row = api_step_row(
2510
            meta,
2511
            scenario_name,
2512
            "fleet.apply",
2513
            run_index,
2514
            client,
2515
            do,
2516
            [f"api:PUT /repos/{repo}/pulls/<n>/merge x{len(plan.get('merge_order') or [])}"],
2517
        )
2518
        # api_step_row cannot update this local binding through nested scope on
2519
        # older Python without nonlocal assignment inside do(); read the state
2520
        # value populated above.
2521
        apply_result = dict(
2522
            state.get("apply_result")
2523
            or {"apply_succeeded": False, "merged_branches": [], "final_survivors": []}
2524
        )
2525
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_GITHUB_API
2526
        row.update(apply_result)
2527
        row.update(
2528
            branch_fleet_metric_fields(
2529
                branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
2530
            )
2531
        )
2532
        return row
2533
2534
    def sync() -> dict[str, Any]:
2535
        row = api_step_row(
2536
            meta,
2537
            scenario_name,
2538
            "fleet.sync",
2539
            run_index,
2540
            client,
2541
            lambda: client.get_main_sha(repo),
2542
            [f"api:GET /repos/{repo}/git/ref/heads/main"],
2543
        )
2544
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_GITHUB_API
2545
        row["note"] = (
2546
            "harness-api driver has no local checkout; sync observes the final main ref."
2547
        )
2548
        row.update(
2549
            branch_fleet_metric_fields(
2550
                branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
2551
            )
2552
        )
2553
        return row
2554
2555
    def oracle_row() -> dict[str, Any]:
2556
        nonlocal oracle
2557
        start = platform_clock.monotonic_ms()
2558
        oracle = branch_fleet_oracle(specs, plan, apply_result)
2559
        ok = all(
2560
            bool(oracle[key])
2561
            for key in (
2562
                "apply_succeeded",
2563
                "action_plan_correct",
2564
                "merge_order_correct",
2565
                "final_survivors_correct",
2566
            )
2567
        )
2568
        fields = {
2569
            **oracle,
2570
            **branch_fleet_metric_fields(
2571
                branch_count=count,
2572
                reviewed=reviewed,
2573
                plan=plan,
2574
                apply_result=apply_result,
2575
                oracle=oracle,
2576
            ),
2577
        }
2578
        return branch_fleet_row(
2579
            meta,
2580
            scenario_name,
2581
            "fleet.oracle",
2582
            run_index,
2583
            platform_clock.monotonic_ms() - start,
2584
            returncode=0 if ok else 1,
2585
            provider=BRANCH_FLEET_PROVIDER_GITHUB_API,
2586
            **fields,
2587
        )
2588
2589
    def total() -> dict[str, Any]:
2590
        failures = [
2591
            row for row in rows
2592
            if row_returncode(row) not in (0, SKIP_RETURNCODE)
2593
        ]
2594
        row = branch_fleet_row(
2595
            meta,
2596
            scenario_name,
2597
            "fleet.total",
2598
            run_index,
2599
            platform_clock.monotonic_ms() - total_start,
2600
            returncode=1 if failures else 0,
2601
            provider=BRANCH_FLEET_PROVIDER_GITHUB_API,
2602
            summarized_operations=[row["operation"] for row in rows],
2603
            **branch_fleet_workflow_summary_fields(rows),
2604
            **branch_fleet_metric_fields(
2605
                branch_count=count,
2606
                reviewed=reviewed,
2607
                plan=plan,
2608
                apply_result=apply_result,
2609
                oracle=oracle,
2610
            ),
2611
        )
2612
        deltas = [
2613
            r["api_metrics"] for r in rows if isinstance(r.get("api_metrics"), dict)
2614
        ]
2615
        row["api_metrics"] = {
2616
            "request_count": sum(delta["request_count"] for delta in deltas),
2617
            "request_bytes": sum(delta["request_bytes"] for delta in deltas),
2618
            "response_bytes": sum(delta["response_bytes"] for delta in deltas),
2619
            "rate_limit_remaining": deltas[-1]["rate_limit_remaining"] if deltas else None,
2620
        }
2621
        return row
2622
2623
    handlers: dict[str, Callable[[], dict[str, Any]]] = {
2624
        "fleet.seed": seed,
2625
        "fleet.classify": classify,
2626
        "fleet.plan": plan_row,
2627
        "fleet.apply": apply,
2628
        "fleet.sync": sync,
2629
        "fleet.oracle": oracle_row,
2630
        "fleet.total": total,
2631
    }
2632
2633
    rows: list[dict[str, Any]] = []
2634
    aborted_at: Optional[str] = None
2635
    for operation in operations:
2636
        if aborted_at is not None:
2637
            rows.append(skip_row(meta, scenario_name, operation, run_index, f"prior_step_failed:{aborted_at}"))
2638
            continue
2639
        handler = handlers.get(operation)
2640
        if handler is None:
2641
            rows.append(skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented"))
2642
            continue
2643
        row = handler()
2644
        rows.append(row)
2645
        if row_returncode(row) not in (0, SKIP_RETURNCODE):
2646
            aborted_at = operation
2647
2648
    for spec in specs:
2649
        try:
2650
            client.delete_ref(repo, f"heads/{spec['name']}")
2651
        except GitHubApiError:
2652
            pass
2653
    return rows
2654
2655
2656
def oak_branch_fleet_mergeable(row: dict[str, Any]) -> Optional[bool]:
2657
    if row.get("merge_allowed") is True or row.get("recommended_action") in {
2658
        "merge",
2659
        "validate_then_merge",
2660
    }:
2661
        return True
2662
    if row.get("vcs_merge_safe") is False or row.get("recommended_action") in {
2663
        "resolve",
2664
        "keep_open_manual_resolution",
2665
    }:
2666
        return False
2667
    mergeability = str(row.get("mergeability") or "").lower()
2668
    if mergeability in {"clean", "mergeable"}:
2669
        return True
2670
    if mergeability in {"conflict", "conflicts", "conflicting"}:
2671
        return False
2672
    return None
2673
2674
2675
def oak_branch_fleet_cleanup_commands(
2676
    specs: list[dict[str, Any]], advancers: list[str]
2677
) -> list[list[str]]:
2678
    branches = [str(spec["name"]) for spec in specs] + list(advancers)
2679
    return oak_branch_fleet_cleanup_commands_for_branches(branches)
2680
2681
2682
def oak_branch_fleet_cleanup_commands_for_branches(branches: list[str]) -> list[list[str]]:
2683
    return [
2684
        ["oak", "close", "--remote", "--reason", "stale", "--json", branch]
2685
        for branch in branches
2686
    ]
2687
2688
2689
def oak_branch_fleet_apply_commands(merge_order: list[str]) -> list[list[str]]:
2690
    return [["oak", "merge", branch] for branch in merge_order]
2691
2692
2693
def oak_branch_fleet_cleanup_fields(failures: list[dict[str, Any]]) -> dict[str, Any]:
2694
    fields: dict[str, Any] = {"cleanup_failure_count": len(failures)}
2695
    if failures:
2696
        fields["cleanup_failures"] = failures
2697
    return fields
2698
2699
2700
@dataclass(frozen=True)
2701
class OakBranchFleetSeedStep:
2702
    branch: str
2703
    phase: str
2704
    phase_index: int
2705
    phase_total: int
2706
    commands: list[list[str]]
2707
    cleanup_after_command_index: int
2708
2709
2710
def oak_branch_fleet_write_command(rel_path: Path, content: str) -> list[str]:
2711
    return [
2712
        "/bin/zsh",
2713
        "-c",
2714
        "mkdir -p "
2715
        + shlex.quote(str(rel_path.parent))
2716
        + " && printf %s "
2717
        + shlex.quote(content)
2718
        + " > "
2719
        + shlex.quote(str(rel_path)),
2720
    ]
2721
2722
2723
def oak_branch_fleet_seed_steps(
2724
    specs: list[dict[str, Any]],
2725
    meta: dict[str, Any],
2726
    run_index: int,
2727
) -> tuple[list[OakBranchFleetSeedStep], list[str]]:
2728
    steps: list[OakBranchFleetSeedStep] = []
2729
    branch_total = len(specs)
2730
    for index, spec in enumerate(specs, start=1):
2731
        branch = str(spec["name"])
2732
        rel_path = Path(str(spec["path"]))
2733
        content = (
2734
            f"branch fleet {meta['bench_id']} run={run_index} "
2735
            f"branch={branch} kind={spec['kind']}\n"
2736
        )
2737
        steps.append(
2738
            OakBranchFleetSeedStep(
2739
                branch=branch,
2740
                phase="branch",
2741
                phase_index=index,
2742
                phase_total=branch_total,
2743
                commands=[
2744
                    ["oak", "switch", "--clean", "-c", branch],
2745
                    oak_branch_fleet_write_command(rel_path, content),
2746
                    ["oak", "commit", "--no-verify", "--push"],
2747
                ],
2748
                cleanup_after_command_index=2,
2749
            )
2750
        )
2751
2752
    conflict_specs = [spec for spec in specs if spec["kind"] == "conflicting"]
2753
    advancers: list[str] = []
2754
    conflict_total = len(conflict_specs)
2755
    for index, spec in enumerate(conflict_specs, start=1):
2756
        advancer = f"{spec['name']}-main-side"
2757
        advancers.append(advancer)
2758
        rel_path = Path(str(spec["path"]))
2759
        content = (
2760
            f"main-side conflict {meta['bench_id']} run={run_index} "
2761
            f"branch={spec['name']}\n"
2762
        )
2763
        steps.append(
2764
            OakBranchFleetSeedStep(
2765
                branch=advancer,
2766
                phase="conflict",
2767
                phase_index=index,
2768
                phase_total=conflict_total,
2769
                commands=[
2770
                    ["oak", "switch", "--clean", "-c", advancer],
2771
                    oak_branch_fleet_write_command(rel_path, content),
2772
                    ["oak", "commit", "--no-verify", "--push"],
2773
                    ["oak", "merge"],
2774
                ],
2775
                cleanup_after_command_index=2,
2776
            )
2777
        )
2778
    return steps, advancers
2779
2780
2781
def oak_branch_fleet_seed_diagnostics(
2782
    *,
2783
    branch_count: int,
2784
    conflict_count: int,
2785
    command_count: int,
2786
    completed_count: int,
2787
    completed_branch_count: int,
2788
    completed_conflict_count: int,
2789
    completed_command_count: int,
2790
    last_completed_branch: Optional[str],
2791
    failed_command_index: Optional[int],
2792
    failed_branch: Optional[str],
2793
) -> dict[str, Any]:
2794
    return {
2795
        "seed_branch_count": branch_count,
2796
        "seed_conflict_count": conflict_count,
2797
        "seed_command_count": command_count,
2798
        "seed_completed_count": completed_count,
2799
        "seed_completed_branch_count": completed_branch_count,
2800
        "seed_completed_conflict_count": completed_conflict_count,
2801
        "seed_completed_command_count": completed_command_count,
2802
        "last_completed_branch": last_completed_branch,
2803
        "failed_command_index": failed_command_index,
2804
        "failed_branch": failed_branch,
2805
    }
2806
2807
2808
def oak_branch_fleet_seed_text(value: Any) -> str:
2809
    if value is None:
2810
        return ""
2811
    if isinstance(value, bytes):
2812
        return value.decode("utf-8", errors="replace")
2813
    return str(value)
2814
2815
2816
def oak_branch_fleet_seed_exception_result(
2817
    command: list[str], exc: BaseException
2818
) -> subprocess.CompletedProcess[str]:
2819
    if isinstance(exc, subprocess.TimeoutExpired):
2820
        stdout = oak_branch_fleet_seed_text(exc.stdout)
2821
        stderr = oak_branch_fleet_seed_text(exc.stderr)
2822
        timeout = "" if exc.timeout is None else f":{exc.timeout}s"
2823
        message = f"seed_command_timeout{timeout}"
2824
        return subprocess.CompletedProcess(
2825
            command,
2826
            BRANCH_FLEET_SEED_TIMEOUT_RETCODE,
2827
            stdout,
2828
            "\n".join(part for part in (message, stderr) if part),
2829
        )
2830
    return subprocess.CompletedProcess(
2831
        command,
2832
        1,
2833
        "",
2834
        f"seed_command_exception:{type(exc).__name__}:{exc}",
2835
    )
2836
2837
2838
def oak_branch_fleet_seed_transient_failure(proc: subprocess.CompletedProcess[str]) -> bool:
2839
    text = f"{proc.stderr}\n{proc.stdout}".lower()
2840
    return any(
2841
        marker in text
2842
        for marker in (
2843
            "http 502",
2844
            "502 bad gateway",
2845
            "http 503",
2846
            "503 service unavailable",
2847
            "http 504",
2848
            "504 gateway timeout",
2849
        )
2850
    )
2851
2852
2853
def oak_branch_fleet_seed_progress(step: OakBranchFleetSeedStep, done: bool) -> str:
2854
    if step.phase == "conflict":
2855
        verb = "advanced conflicts" if done else "advancing conflict"
2856
        return f"[branch-fleet seed] {verb} {step.phase_index}/{step.phase_total} branch={step.branch}"
2857
    verb = "seeded" if done else "seeding"
2858
    return f"[branch-fleet seed] {verb} {step.phase_index}/{step.phase_total} branch={step.branch}"
2859
2860
2861
def oak_branch_fleet_seed_row(
2862
    meta: dict[str, Any],
2863
    scenario_name: str,
2864
    seed_steps: list[OakBranchFleetSeedStep],
2865
    cwd: Path,
2866
    run_index: int,
2867
    env: dict[str, str],
2868
    admitted: int,
2869
    branch_count: int,
2870
    conflict_count: int,
2871
    *,
2872
    progress: Optional[Callable[[str], None]] = None,
2873
    cleanup_branches: Optional[list[str]] = None,
2874
    runner: Optional[
2875
        Callable[[list[str], Optional[Path], dict[str, str]], subprocess.CompletedProcess[str]]
2876
    ] = None,
2877
    retry_limit: int = BRANCH_FLEET_SEED_TRANSIENT_RETRIES,
2878
    retry_delay_s: float = BRANCH_FLEET_SEED_RETRY_DELAY_S,
2879
    retry_sleep: Callable[[float], None] = time.sleep,
2880
    command_index_offset: int = 0,
2881
) -> dict[str, Any]:
2882
    if runner is None:
2883
        runner = run_untimed
2884
    emit = progress or (lambda message: print(message, file=sys.stderr, flush=True))
2885
    planned_commands = [command for step in seed_steps for command in step.commands]
2886
    command_count = len(planned_commands)
2887
    start = platform_clock.monotonic_ms()
2888
    stdout_parts: list[str] = []
2889
    stderr_parts: list[str] = []
2890
    attempted_commands: list[list[str]] = []
2891
    completed_count = 0
2892
    last_completed_branch: Optional[str] = None
2893
    failed_command_index: Optional[int] = None
2894
    failed_branch: Optional[str] = None
2895
    failed_command: Optional[list[str]] = None
2896
    failed_proc: Optional[subprocess.CompletedProcess[str]] = None
2897
    completed_branch_count = 0
2898
    completed_conflict_count = 0
2899
    retry_count = 0
2900
    retry_records: list[dict[str, Any]] = []
2901
    planned_command_index = 0
2902
2903
    for step in seed_steps:
2904
        emit(oak_branch_fleet_seed_progress(step, done=False))
2905
        step_failed = False
2906
        for step_command_index, command in enumerate(step.commands):
2907
            command_index = command_index_offset + planned_command_index
2908
            attempt = 0
2909
            while True:
2910
                try:
2911
                    proc = runner(command, cwd, env)
2912
                except Exception as exc:
2913
                    proc = oak_branch_fleet_seed_exception_result(command, exc)
2914
                attempted_commands.append(command)
2915
                stdout_parts.append(proc.stdout)
2916
                stderr_parts.append(proc.stderr)
2917
                if proc.returncode == 0:
2918
                    break
2919
                if attempt < retry_limit and oak_branch_fleet_seed_transient_failure(proc):
2920
                    attempt += 1
2921
                    retry_count += 1
2922
                    retry_records.append(
2923
                        {
2924
                            "branch": step.branch,
2925
                            "command_index": command_index,
2926
                            "attempt": attempt,
2927
                            "returncode": proc.returncode,
2928
                            "stderr": proc.stderr[-1000:],
2929
                        }
2930
                    )
2931
                    emit(
2932
                        "[branch-fleet seed] retrying transient failure "
2933
                        f"{attempt}/{retry_limit} branch={step.branch} "
2934
                        f"command_index={command_index}"
2935
                    )
2936
                    retry_sleep(retry_delay_s)
2937
                    continue
2938
                failed_command_index = command_index
2939
                failed_branch = step.branch
2940
                failed_command = command
2941
                failed_proc = proc
2942
                step_failed = True
2943
                break
2944
            planned_command_index += 1
2945
            if step_failed:
2946
                break
2947
            if (
2948
                cleanup_branches is not None
2949
                and step_command_index == step.cleanup_after_command_index
2950
                and step.branch not in cleanup_branches
2951
            ):
2952
                cleanup_branches.append(step.branch)
2953
        if step_failed:
2954
            break
2955
        completed_count += 1
2956
        if step.phase == "conflict":
2957
            completed_conflict_count += 1
2958
        else:
2959
            completed_branch_count += 1
2960
        last_completed_branch = step.branch
2961
        emit(oak_branch_fleet_seed_progress(step, done=True))
2962
2963
    elapsed_ms = platform_clock.monotonic_ms() - start
2964
    stdout = "\n".join(part for part in stdout_parts if part)
2965
    stderr = "\n".join(part for part in stderr_parts if part)
2966
    command_text = "\n".join(command_display(command) for command in planned_commands)
2967
    row = {
2968
        **meta,
2969
        "scenario": scenario_name,
2970
        "run": run_index,
2971
        "operation": "fleet.seed",
2972
        "elapsed_ms": round(elapsed_ms, 3),
2973
        "returncode": 0 if failed_proc is None else int(failed_proc.returncode),
2974
        "command": planned_commands,
2975
        "tool_call_count": len(attempted_commands),
2976
        "terminal_tool_call_count": len(attempted_commands),
2977
        "branch_fleet_provider": BRANCH_FLEET_PROVIDER_OAK_CLI,
2978
        "seed_retry_count": retry_count,
2979
        **oakbench_tokens.interaction_token_fields(
2980
            command_text,
2981
            stdout[:admitted],
2982
            stderr[:admitted],
2983
            len(stdout.encode("utf-8")),
2984
            len(stderr.encode("utf-8")),
2985
            len(stdout) > admitted,
2986
            len(stderr) > admitted,
2987
        ),
2988
        "_stdout": stdout,
2989
        **oak_branch_fleet_seed_diagnostics(
2990
            branch_count=branch_count,
2991
            conflict_count=conflict_count,
2992
            command_count=command_count,
2993
            completed_count=completed_count,
2994
            completed_branch_count=completed_branch_count,
2995
            completed_conflict_count=completed_conflict_count,
2996
            completed_command_count=len(attempted_commands),
2997
            last_completed_branch=last_completed_branch,
2998
            failed_command_index=failed_command_index,
2999
            failed_branch=failed_branch,
3000
        ),
3001
    }
3002
    if retry_records:
3003
        row["seed_retries"] = retry_records
3004
    if failed_proc is not None:
3005
        row["failure_reason"] = "seed_command_failed"
3006
        row["failed_command"] = failed_command
3007
        row["failed_command_stdout"] = failed_proc.stdout[-1000:]
3008
        row["failed_command_stderr"] = failed_proc.stderr[-1000:]
3009
        row["stderr"] = stderr[-4000:] or failed_proc.stdout[-4000:]
3010
    return row
3011
3012
3013
def oak_branch_fleet_seed_step_chunks(
3014
    seed_steps: list[OakBranchFleetSeedStep],
3015
    worker_count: int,
3016
) -> list[tuple[int, int, list[OakBranchFleetSeedStep]]]:
3017
    indexed_branch_steps = [
3018
        (index, step) for index, step in enumerate(seed_steps) if step.phase == "branch"
3019
    ]
3020
    if not indexed_branch_steps:
3021
        return []
3022
    bounded_workers = max(1, min(worker_count, len(indexed_branch_steps)))
3023
    base_size, remainder = divmod(len(indexed_branch_steps), bounded_workers)
3024
    chunks: list[tuple[int, int, list[OakBranchFleetSeedStep]]] = []
3025
    start = 0
3026
    for worker_id in range(bounded_workers):
3027
        size = base_size + (1 if worker_id < remainder else 0)
3028
        pairs = indexed_branch_steps[start:start + size]
3029
        if not pairs:
3030
            continue
3031
        first_step_index = pairs[0][0]
3032
        command_offset = sum(len(step.commands) for step in seed_steps[:first_step_index])
3033
        chunks.append((worker_id, command_offset, [step for _index, step in pairs]))
3034
        start += size
3035
    return chunks
3036
3037
3038
def oak_branch_fleet_seed_clone_failure_row(
3039
    meta: dict[str, Any],
3040
    scenario_name: str,
3041
    run_index: int,
3042
    *,
3043
    worker_id: int,
3044
    command: list[str],
3045
    proc: subprocess.CompletedProcess[str],
3046
    elapsed_ms: float,
3047
    admitted: int,
3048
    command_index_offset: int,
3049
    failed_branch: Optional[str],
3050
    branch_count: int,
3051
    command_count: int,
3052
) -> dict[str, Any]:
3053
    stdout = proc.stdout or ""
3054
    stderr = proc.stderr or ""
3055
    return {
3056
        **meta,
3057
        "scenario": scenario_name,
3058
        "run": run_index,
3059
        "operation": "fleet.seed",
3060
        "elapsed_ms": round(elapsed_ms, 3),
3061
        "returncode": int(proc.returncode),
3062
        "command": [command],
3063
        "tool_call_count": 1,
3064
        "terminal_tool_call_count": 1,
3065
        "branch_fleet_provider": BRANCH_FLEET_PROVIDER_OAK_CLI,
3066
        "seed_worker_id": worker_id,
3067
        "seed_retry_count": 0,
3068
        "failure_reason": "seed_worker_clone_failed",
3069
        "failed_command": command,
3070
        "failed_command_stdout": stdout[-1000:],
3071
        "failed_command_stderr": stderr[-1000:],
3072
        "stderr": stderr[-4000:] or stdout[-4000:],
3073
        **oakbench_tokens.interaction_token_fields(
3074
            command_display(command),
3075
            stdout[:admitted],
3076
            stderr[:admitted],
3077
            len(stdout.encode("utf-8")),
3078
            len(stderr.encode("utf-8")),
3079
            len(stdout) > admitted,
3080
            len(stderr) > admitted,
3081
        ),
3082
        **oak_branch_fleet_seed_diagnostics(
3083
            branch_count=branch_count,
3084
            conflict_count=0,
3085
            command_count=command_count,
3086
            completed_count=0,
3087
            completed_branch_count=0,
3088
            completed_conflict_count=0,
3089
            completed_command_count=0,
3090
            last_completed_branch=None,
3091
            failed_command_index=command_index_offset,
3092
            failed_branch=failed_branch,
3093
        ),
3094
    }
3095
3096
3097
def oak_branch_fleet_aggregate_seed_rows(
3098
    meta: dict[str, Any],
3099
    scenario_name: str,
3100
    run_index: int,
3101
    *,
3102
    seed_rows: list[dict[str, Any]],
3103
    elapsed_ms: float,
3104
    planned_commands: list[list[str]],
3105
    branch_count: int,
3106
    conflict_count: int,
3107
    worker_count: int,
3108
    worker_clone_count: int,
3109
) -> dict[str, Any]:
3110
    failed_rows = [
3111
        row for row in seed_rows
3112
        if row_returncode(row) not in (0, SKIP_RETURNCODE)
3113
    ]
3114
    worker_rows = [
3115
        row for row in seed_rows
3116
        if isinstance(row.get("seed_worker_id"), int)
3117
    ]
3118
    failed_worker_rows = [
3119
        row for row in worker_rows
3120
        if row_returncode(row) not in (0, SKIP_RETURNCODE)
3121
    ]
3122
    failed_rows.sort(
3123
        key=lambda row: (
3124
            row.get("failed_command_index")
3125
            if row.get("failed_command_index") is not None
3126
            else 1_000_000_000
3127
        )
3128
    )
3129
    failed = failed_rows[0] if failed_rows else None
3130
    retry_records: list[dict[str, Any]] = []
3131
    for row in seed_rows:
3132
        retry_records.extend(row.get("seed_retries") or [])
3133
    last_completed_branch: Optional[str] = None
3134
    for row in seed_rows:
3135
        if row.get("last_completed_branch") is not None:
3136
            last_completed_branch = str(row["last_completed_branch"])
3137
    command_count = len(planned_commands)
3138
    row = {
3139
        **meta,
3140
        "scenario": scenario_name,
3141
        "run": run_index,
3142
        "operation": "fleet.seed",
3143
        "elapsed_ms": round(elapsed_ms, 3),
3144
        "returncode": 0 if failed is None else row_returncode(failed),
3145
        "command": planned_commands,
3146
        "tool_call_count": sum(int(child.get("tool_call_count") or 0) for child in seed_rows),
3147
        "terminal_tool_call_count": sum(
3148
            int(child.get("terminal_tool_call_count") or 0) for child in seed_rows
3149
        ),
3150
        "branch_fleet_provider": BRANCH_FLEET_PROVIDER_OAK_CLI,
3151
        "seed_parallel": worker_count > 1,
3152
        "seed_worker_count": worker_count,
3153
        "seed_worker_clone_count": worker_clone_count,
3154
        "seed_worker_success_count": len(worker_rows) - len(failed_worker_rows),
3155
        "seed_conflict_serial": True,
3156
        "seed_retry_count": sum(int(child.get("seed_retry_count") or 0) for child in seed_rows),
3157
        **oakbench_tokens.summed_token_fields(
3158
            seed_rows,
3159
            "sum_of_branch_fleet_seed_worker_rows_command_plus_admitted_output_chars_div_4",
3160
        ),
3161
        **oak_branch_fleet_seed_diagnostics(
3162
            branch_count=branch_count,
3163
            conflict_count=conflict_count,
3164
            command_count=command_count,
3165
            completed_count=sum(int(child.get("seed_completed_count") or 0) for child in seed_rows),
3166
            completed_branch_count=sum(
3167
                int(child.get("seed_completed_branch_count") or 0) for child in seed_rows
3168
            ),
3169
            completed_conflict_count=sum(
3170
                int(child.get("seed_completed_conflict_count") or 0) for child in seed_rows
3171
            ),
3172
            completed_command_count=sum(
3173
                int(child.get("seed_completed_command_count") or 0) for child in seed_rows
3174
            ),
3175
            last_completed_branch=last_completed_branch,
3176
            failed_command_index=None if failed is None else failed.get("failed_command_index"),
3177
            failed_branch=None if failed is None else failed.get("failed_branch"),
3178
        ),
3179
        "_stdout": "\n".join(str(child.get("_stdout") or "") for child in seed_rows),
3180
    }
3181
    if retry_records:
3182
        row["seed_retries"] = retry_records
3183
    if failed is not None:
3184
        row["failure_reason"] = failed.get("failure_reason") or "seed_command_failed"
3185
        row["failed_command"] = failed.get("failed_command")
3186
        row["failed_command_stdout"] = failed.get("failed_command_stdout", "")
3187
        row["failed_command_stderr"] = failed.get("failed_command_stderr", "")
3188
        row["stderr"] = failed.get("stderr") or row.get("failed_command_stderr", "")
3189
    return row
3190
3191
3192
def oak_branch_fleet_run_cleanup(
3193
    cleanup_commands: list[list[str]],
3194
    workdirs: list[Path],
3195
    env: dict[str, str],
3196
    worker_count: int,
3197
    runner: Optional[
3198
        Callable[[list[str], Optional[Path], dict[str, str]], subprocess.CompletedProcess[str]]
3199
    ] = None,
3200
) -> list[dict[str, Any]]:
3201
    if runner is None:
3202
        runner = run_untimed
3203
    if not cleanup_commands:
3204
        return []
3205
    existing_workdirs = [workdir for workdir in workdirs if workdir.exists()]
3206
    if not existing_workdirs:
3207
        return []
3208
    bounded_workers = max(1, min(worker_count, len(existing_workdirs), len(cleanup_commands)))
3209
    base_size, remainder = divmod(len(cleanup_commands), bounded_workers)
3210
    command_chunks: list[tuple[int, Path, list[tuple[int, list[str]]]]] = []
3211
    start = 0
3212
    for worker_id in range(bounded_workers):
3213
        size = base_size + (1 if worker_id < remainder else 0)
3214
        indexed = list(enumerate(cleanup_commands[start:start + size], start=start))
3215
        command_chunks.append((worker_id, existing_workdirs[worker_id], indexed))
3216
        start += size
3217
3218
    def run_chunk(
3219
        worker_id: int, cwd: Path, indexed_commands: list[tuple[int, list[str]]]
3220
    ) -> list[dict[str, Any]]:
3221
        failures: list[dict[str, Any]] = []
3222
        for command_index, command in indexed_commands:
3223
            try:
3224
                proc = runner(command, cwd, env)
3225
            except subprocess.TimeoutExpired as exc:
3226
                failures.append(
3227
                    {
3228
                        "branch": command[-1],
3229
                        "command": command,
3230
                        "command_index": command_index,
3231
                        "worker_id": worker_id,
3232
                        "cwd": str(cwd),
3233
                        "returncode": 124,
3234
                        "stdout": oak_branch_fleet_seed_text(exc.stdout)[-1000:],
3235
                        "stderr": f"cleanup_command_timeout:{exc.timeout}s",
3236
                        "failure_reason": "cleanup_command_failed",
3237
                    }
3238
                )
3239
                continue
3240
            except Exception as exc:
3241
                failures.append(
3242
                    {
3243
                        "branch": command[-1],
3244
                        "command": command,
3245
                        "command_index": command_index,
3246
                        "worker_id": worker_id,
3247
                        "cwd": str(cwd),
3248
                        "returncode": 1,
3249
                        "stdout": "",
3250
                        "stderr": f"cleanup_command_exception:{type(exc).__name__}:{exc}",
3251
                        "failure_reason": "cleanup_command_failed",
3252
                    }
3253
                )
3254
                continue
3255
            if proc.returncode != 0:
3256
                failures.append(
3257
                    {
3258
                        "branch": command[-1],
3259
                        "command": command,
3260
                        "command_index": command_index,
3261
                        "worker_id": worker_id,
3262
                        "cwd": str(cwd),
3263
                        "returncode": proc.returncode,
3264
                        "stdout": proc.stdout[-1000:],
3265
                        "stderr": proc.stderr[-1000:],
3266
                    }
3267
                )
3268
        return failures
3269
3270
    if bounded_workers == 1:
3271
        return run_chunk(command_chunks[0][0], command_chunks[0][1], command_chunks[0][2])
3272
3273
    failures: list[dict[str, Any]] = []
3274
    with concurrent.futures.ThreadPoolExecutor(max_workers=bounded_workers) as pool:
3275
        futures = [
3276
            pool.submit(run_chunk, worker_id, cwd, indexed_commands)
3277
            for worker_id, cwd, indexed_commands in command_chunks
3278
        ]
3279
        for future in concurrent.futures.as_completed(futures):
3280
            failures.extend(future.result())
3281
    failures.sort(key=lambda failure: int(failure.get("command_index") or 0))
3282
    return failures
3283
3284
3285
def run_oak_cli_branch_fleet_scenario(
3286
    args: argparse.Namespace,
3287
    scenario: dict[str, Any],
3288
    meta: dict[str, Any],
3289
    run_index: int,
3290
    run_root: Path,
3291
    settings: Settings,
3292
) -> list[dict[str, Any]]:
3293
    scenario_name = str(scenario["name"])
3294
    operations = [str(op) for op in scenario.get("operations") or []]
3295
    count = branch_fleet_count(scenario)
3296
    specs = branch_fleet_specs(
3297
        bench_id=str(meta.get("bench_id") or "bench"),
3298
        scenario_name=scenario_name,
3299
        run_index=run_index,
3300
        branch_count=count,
3301
    )
3302
    env = cli_env(args)
3303
    workdir = run_root / f"oak-branch-fleet-r{run_index}"
3304
    reviewed: list[dict[str, Any]] = []
3305
    plan: dict[str, Any] = {}
3306
    apply_result: dict[str, Any] = {
3307
        "apply_succeeded": False,
3308
        "merged_branches": [],
3309
        "final_survivors": [],
3310
    }
3311
    oracle: dict[str, Any] = {}
3312
    total_start = platform_clock.monotonic_ms()
3313
    cleanup_branches: list[str] = []
3314
    seed_workdirs: list[Path] = []
3315
3316
    def seed() -> dict[str, Any]:
3317
        nonlocal seed_workdirs
3318
        start = platform_clock.monotonic_ms()
3319
        seed_steps, _advancers = oak_branch_fleet_seed_steps(specs, meta, run_index)
3320
        planned_commands = [command for step in seed_steps for command in step.commands]
3321
        seed_command_count = sum(len(step.commands) for step in seed_steps)
3322
        conflict_count = len(_advancers)
3323
        clone = run_untimed(["oak", "clone", args.repo, str(workdir)], run_root, env)
3324
        if clone.returncode != 0:
3325
            row = branch_fleet_row(
3326
                meta,
3327
                scenario_name,
3328
                "fleet.seed",
3329
                run_index,
3330
                platform_clock.monotonic_ms() - start,
3331
                returncode=clone.returncode,
3332
                provider=BRANCH_FLEET_PROVIDER_OAK_CLI,
3333
                command=[["oak", "clone", args.repo, str(workdir)]],
3334
                stderr=clone.stderr[-4000:],
3335
                tool_call_count=1,
3336
                terminal_tool_call_count=1,
3337
                **oak_branch_fleet_seed_diagnostics(
3338
                    branch_count=count,
3339
                    conflict_count=conflict_count,
3340
                    command_count=seed_command_count,
3341
                    completed_count=0,
3342
                    completed_branch_count=0,
3343
                    completed_conflict_count=0,
3344
                    completed_command_count=0,
3345
                    last_completed_branch=None,
3346
                    failed_command_index=None,
3347
                    failed_branch=None,
3348
                ),
3349
                seed_parallel=False,
3350
                seed_worker_count=0,
3351
                seed_worker_clone_count=1,
3352
                seed_conflict_serial=True,
3353
                **branch_fleet_metric_fields(branch_count=count),
3354
            )
3355
            row["branches_seeded"] = 0
3356
            return row
3357
3358
        branch_steps = [step for step in seed_steps if step.phase == "branch"]
3359
        conflict_steps = [step for step in seed_steps if step.phase == "conflict"]
3360
        worker_count = branch_fleet_seed_worker_count(len(branch_steps), env)
3361
        branch_chunks = oak_branch_fleet_seed_step_chunks(seed_steps, worker_count)
3362
        progress_lock = threading.Lock()
3363
3364
        def emit_progress(message: str) -> None:
3365
            with progress_lock:
3366
                print(message, file=sys.stderr, flush=True)
3367
3368
        def worker_seed(
3369
            worker_id: int,
3370
            command_index_offset: int,
3371
            worker_steps: list[OakBranchFleetSeedStep],
3372
        ) -> tuple[int, dict[str, Any], list[str], Path]:
3373
            worker_dir = run_root / f"oak-branch-fleet-r{run_index}-seed-w{worker_id}"
3374
            worker_start = platform_clock.monotonic_ms()
3375
            worker_clone = run_untimed(["oak", "clone", args.repo, str(worker_dir)], run_root, env)
3376
            if worker_clone.returncode != 0:
3377
                return (
3378
                    worker_id,
3379
                    oak_branch_fleet_seed_clone_failure_row(
3380
                        meta,
3381
                        scenario_name,
3382
                        run_index,
3383
                        worker_id=worker_id,
3384
                        command=["oak", "clone", args.repo, str(worker_dir)],
3385
                        proc=worker_clone,
3386
                        elapsed_ms=platform_clock.monotonic_ms() - worker_start,
3387
                        admitted=settings.admitted,
3388
                        command_index_offset=command_index_offset,
3389
                        failed_branch=worker_steps[0].branch if worker_steps else None,
3390
                        branch_count=len(worker_steps),
3391
                        command_count=sum(len(step.commands) for step in worker_steps),
3392
                    ),
3393
                    [],
3394
                    worker_dir,
3395
                )
3396
            local_cleanup: list[str] = []
3397
            row = oak_branch_fleet_seed_row(
3398
                meta,
3399
                scenario_name,
3400
                worker_steps,
3401
                worker_dir,
3402
                run_index,
3403
                env,
3404
                settings.admitted,
3405
                len(worker_steps),
3406
                0,
3407
                progress=emit_progress,
3408
                cleanup_branches=local_cleanup,
3409
                command_index_offset=command_index_offset,
3410
            )
3411
            row["seed_worker_id"] = worker_id
3412
            row["seed_worker_clone_count"] = 1
3413
            row["elapsed_ms"] = round(platform_clock.monotonic_ms() - worker_start, 3)
3414
            return worker_id, row, local_cleanup, worker_dir
3415
3416
        branch_seed_results: list[tuple[int, dict[str, Any], list[str], Path]] = []
3417
        if branch_chunks:
3418
            with concurrent.futures.ThreadPoolExecutor(max_workers=len(branch_chunks)) as pool:
3419
                futures = [
3420
                    pool.submit(worker_seed, worker_id, command_offset, worker_steps)
3421
                    for worker_id, command_offset, worker_steps in branch_chunks
3422
                ]
3423
                for future in concurrent.futures.as_completed(futures):
3424
                    branch_seed_results.append(future.result())
3425
            branch_seed_results.sort(key=lambda result: result[0])
3426
3427
        seed_rows = [result[1] for result in branch_seed_results]
3428
        for _worker_id, _row, worker_cleanup, worker_dir in branch_seed_results:
3429
            cleanup_branches.extend(worker_cleanup)
3430
            seed_workdirs.append(worker_dir)
3431
3432
        branch_seed_failed = any(row_returncode(row) not in (0, SKIP_RETURNCODE) for row in seed_rows)
3433
        if not branch_seed_failed and conflict_steps:
3434
            conflict_cleanup: list[str] = []
3435
            conflict_row = oak_branch_fleet_seed_row(
3436
                meta,
3437
                scenario_name,
3438
                conflict_steps,
3439
                workdir,
3440
                run_index,
3441
                env,
3442
                settings.admitted,
3443
                0,
3444
                conflict_count,
3445
                progress=emit_progress,
3446
                cleanup_branches=conflict_cleanup,
3447
                command_index_offset=sum(len(step.commands) for step in branch_steps),
3448
            )
3449
            conflict_row["seed_worker_id"] = "conflict-serial"
3450
            conflict_row["seed_worker_clone_count"] = 0
3451
            seed_rows.append(conflict_row)
3452
            cleanup_branches.extend(conflict_cleanup)
3453
3454
        row = oak_branch_fleet_aggregate_seed_rows(
3455
            meta,
3456
            scenario_name,
3457
            run_index,
3458
            seed_rows=seed_rows,
3459
            elapsed_ms=platform_clock.monotonic_ms() - start,
3460
            planned_commands=planned_commands,
3461
            branch_count=count,
3462
            conflict_count=conflict_count,
3463
            worker_count=len(branch_chunks),
3464
            worker_clone_count=len(branch_seed_results) + 1,
3465
        )
3466
        row["seeded_branch_classes"] = [spec["kind"] for spec in specs]
3467
        row.update(branch_fleet_metric_fields(branch_count=count))
3468
        row["branches_seeded"] = int(row.get("seed_completed_branch_count") or 0)
3469
        return row
3470
3471
    def classify() -> dict[str, Any]:
3472
        nonlocal reviewed
3473
        row = cli_step_row(
3474
            meta,
3475
            scenario_name,
3476
            "fleet.classify",
3477
            [
3478
                "oak",
3479
                "branch",
3480
                "triage",
3481
                "--remote",
3482
                "--against",
3483
                "main",
3484
                "--status",
3485
                "open",
3486
                "--analysis-depth",
3487
                "manifest",
3488
                "--json",
3489
            ],
3490
            workdir,
3491
            run_index,
3492
            env,
3493
            settings.admitted,
3494
            full_stdout_bytes=BRANCH_FLEET_CLASSIFY_PARSE_STDOUT_BYTES,
3495
        )
3496
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_OAK_CLI
3497
        rows_payload: list[Any] = []
3498
        if row["returncode"] == 0:
3499
            if int(row.get("stdout_bytes") or 0) > BRANCH_FLEET_CLASSIFY_PARSE_STDOUT_BYTES:
3500
                row["returncode"] = 1
3501
                row["stderr"] = (
3502
                    "oak_branch_triage_stdout_exceeded_parse_limit:"
3503
                    f"{BRANCH_FLEET_CLASSIFY_PARSE_STDOUT_BYTES}"
3504
                )
3505
            else:
3506
                try:
3507
                    payload = json.loads(row.get("_stdout") or "{}")
3508
                    rows_payload = payload.get("branches") or payload.get("rows") or []
3509
                except json.JSONDecodeError as exc:
3510
                    row["returncode"] = 1
3511
                    row["stderr"] = f"oak_branch_triage_invalid_json:{exc}"
3512
        by_name = {
3513
            str(item.get("branch") or item.get("name")): item
3514
            for item in rows_payload
3515
            if isinstance(item, dict)
3516
        }
3517
        reviewed = []
3518
        for spec in specs:
3519
            actual = by_name.get(str(spec["name"])) or {}
3520
            reviewed.append(
3521
                {
3522
                    "name": str(spec["name"]),
3523
                    "mergeable": oak_branch_fleet_mergeable(actual),
3524
                    "expected_kind": spec["kind"],
3525
                    "raw": actual,
3526
                }
3527
            )
3528
        row["reviewed_branches"] = [branch["name"] for branch in reviewed]
3529
        row.update(branch_fleet_metric_fields(branch_count=count, reviewed=reviewed))
3530
        return row
3531
3532
    def plan_row() -> dict[str, Any]:
3533
        nonlocal plan
3534
        start = platform_clock.monotonic_ms()
3535
        plan = branch_fleet_plan(reviewed)
3536
        return branch_fleet_row(
3537
            meta,
3538
            scenario_name,
3539
            "fleet.plan",
3540
            run_index,
3541
            platform_clock.monotonic_ms() - start,
3542
            provider=BRANCH_FLEET_PROVIDER_OAK_CLI,
3543
            action_plan=plan["actions"],
3544
            merge_order=plan["merge_order"],
3545
            tool_call_count=0,
3546
            terminal_tool_call_count=0,
3547
            **branch_fleet_metric_fields(branch_count=count, reviewed=reviewed, plan=plan),
3548
        )
3549
3550
    def apply() -> dict[str, Any]:
3551
        nonlocal apply_result
3552
        merge_order = list(plan.get("merge_order") or [])
3553
        commands = oak_branch_fleet_apply_commands([str(branch) for branch in merge_order])
3554
        row = cli_sequence_row(
3555
            meta, scenario_name, "fleet.apply", commands, workdir, run_index, env, settings.admitted
3556
        )
3557
        survivors = [
3558
            action["branch"]
3559
            for action in plan.get("actions") or []
3560
            if action["action"] == "leave_conflict_open"
3561
        ]
3562
        apply_result = {
3563
            "apply_succeeded": row["returncode"] == 0,
3564
            "merged_branches": merge_order if row["returncode"] == 0 else [],
3565
            "final_survivors": survivors if row["returncode"] == 0 else [],
3566
        }
3567
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_OAK_CLI
3568
        row.update(apply_result)
3569
        row.update(
3570
            branch_fleet_metric_fields(
3571
                branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
3572
            )
3573
        )
3574
        return row
3575
3576
    def sync() -> dict[str, Any]:
3577
        nonlocal apply_result
3578
        row = cli_sequence_row(
3579
            meta,
3580
            scenario_name,
3581
            "fleet.sync",
3582
            [["oak", "branch", "list", "--remote", "--status", "open", "--json"]],
3583
            workdir,
3584
            run_index,
3585
            env,
3586
            settings.admitted,
3587
        )
3588
        row["branch_fleet_provider"] = BRANCH_FLEET_PROVIDER_OAK_CLI
3589
        if row["returncode"] == 0:
3590
            try:
3591
                final_survivors = branch_fleet_survivors_from_sync_stdout(
3592
                    str(row.get("_stdout") or ""), specs
3593
                )
3594
            except json.JSONDecodeError as exc:
3595
                row["returncode"] = 1
3596
                row["stderr"] = f"oak_branch_list_invalid_json:{exc}"
3597
            except ValueError as exc:
3598
                row["returncode"] = 1
3599
                row["stderr"] = f"oak_branch_list_schema_error:{exc}"
3600
            else:
3601
                apply_result = {
3602
                    **apply_result,
3603
                    "final_survivors": final_survivors,
3604
                }
3605
                row["final_survivors"] = final_survivors
3606
        row.update(
3607
            branch_fleet_metric_fields(
3608
                branch_count=count, reviewed=reviewed, plan=plan, apply_result=apply_result
3609
            )
3610
        )
3611
        return row
3612
3613
    def oracle_row() -> dict[str, Any]:
3614
        nonlocal oracle
3615
        start = platform_clock.monotonic_ms()
3616
        oracle = branch_fleet_oracle(specs, plan, apply_result)
3617
        ok = all(
3618
            bool(oracle[key])
3619
            for key in (
3620
                "apply_succeeded",
3621
                "action_plan_correct",
3622
                "merge_order_correct",
3623
                "final_survivors_correct",
3624
            )
3625
        )
3626
        fields = {
3627
            **oracle,
3628
            **branch_fleet_metric_fields(
3629
                branch_count=count,
3630
                reviewed=reviewed,
3631
                plan=plan,
3632
                apply_result=apply_result,
3633
                oracle=oracle,
3634
            ),
3635
        }
3636
        return branch_fleet_row(
3637
            meta,
3638
            scenario_name,
3639
            "fleet.oracle",
3640
            run_index,
3641
            platform_clock.monotonic_ms() - start,
3642
            returncode=0 if ok else 1,
3643
            provider=BRANCH_FLEET_PROVIDER_OAK_CLI,
3644
            tool_call_count=0,
3645
            terminal_tool_call_count=0,
3646
            **fields,
3647
        )
3648
3649
    def total() -> dict[str, Any]:
3650
        failures = [
3651
            row for row in rows
3652
            if row_returncode(row) not in (0, SKIP_RETURNCODE)
3653
        ]
3654
        row = branch_fleet_row(
3655
            meta,
3656
            scenario_name,
3657
            "fleet.total",
3658
            run_index,
3659
            platform_clock.monotonic_ms() - total_start,
3660
            returncode=1 if failures else 0,
3661
            provider=BRANCH_FLEET_PROVIDER_OAK_CLI,
3662
            summarized_operations=[row["operation"] for row in rows],
3663
            **branch_fleet_workflow_summary_fields(rows),
3664
            **branch_fleet_metric_fields(
3665
                branch_count=count,
3666
                reviewed=reviewed,
3667
                plan=plan,
3668
                apply_result=apply_result,
3669
                oracle=oracle,
3670
            ),
3671
        )
3672
        row["tool_call_count"] = sum(int(r.get("tool_call_count") or 0) for r in rows)
3673
        row.update(
3674
            oakbench_tokens.summed_token_fields(
3675
                rows, "sum_of_branch_fleet_steps_command_plus_admitted_output_chars_div_4"
3676
            )
3677
        )
3678
        return row
3679
3680
    handlers: dict[str, Callable[[], dict[str, Any]]] = {
3681
        "fleet.seed": seed,
3682
        "fleet.classify": classify,
3683
        "fleet.plan": plan_row,
3684
        "fleet.apply": apply,
3685
        "fleet.sync": sync,
3686
        "fleet.oracle": oracle_row,
3687
        "fleet.total": total,
3688
    }
3689
    rows: list[dict[str, Any]] = []
3690
    aborted_at: Optional[str] = None
3691
    for operation in operations:
3692
        if operation == "fleet.total":
3693
            row = total()
3694
            row.pop("_stdout", None)
3695
            rows.append(row)
3696
            continue
3697
        if aborted_at is not None:
3698
            rows.append(skip_row(meta, scenario_name, operation, run_index, f"prior_step_failed:{aborted_at}"))
3699
            continue
3700
        handler = handlers.get(operation)
3701
        if handler is None:
3702
            rows.append(skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented"))
3703
            continue
3704
        row = handler()
3705
        row.pop("_stdout", None)
3706
        rows.append(row)
3707
        if row_returncode(row) not in (0, SKIP_RETURNCODE):
3708
            aborted_at = operation
3709
3710
    cleanup_commands = oak_branch_fleet_cleanup_commands_for_branches(cleanup_branches)
3711
    cleanup_failures = oak_branch_fleet_run_cleanup(
3712
        cleanup_commands,
3713
        [workdir] + seed_workdirs,
3714
        env,
3715
        branch_fleet_cleanup_worker_count(len(cleanup_commands), env),
3716
    )
3717
    cleanup_fields = oak_branch_fleet_cleanup_fields(cleanup_failures)
3718
    for row in rows:
3719
        if row.get("operation") == "fleet.total":
3720
            row.update(cleanup_fields)
3721
            break
3722
    return rows
3723
3724
3725
# --------------------------------------------------------------------------
3726
# branch_triage_nN: seeded remote branch/PR review classification.
3727
#
3728
# The real provider path is intentionally skip-only for now. The fake provider
3729
# exercises the decision/oracle logic deterministically: N seeded branches cycle
3730
# through clean/stale/duplicate/conflicting, the plan decides merge/refresh/
3731
# close/leave-open actions, and the oracle checks both the action plan and the
3732
# final survivors.
3733
# --------------------------------------------------------------------------
3734
3735
3736
def is_branch_triage_scenario(scenario: dict[str, Any]) -> bool:
3737
    return BRANCH_TRIAGE_SCENARIO_RE.match(str(scenario.get("name", ""))) is not None
3738
3739
3740
def branch_triage_count(scenario: dict[str, Any]) -> int:
3741
    if scenario.get("branch_count") is not None:
3742
        return int(scenario["branch_count"])
3743
    match = BRANCH_TRIAGE_SCENARIO_RE.match(str(scenario.get("name", "")))
3744
    return int(match.group(1)) if match else 4
3745
3746
3747
def seeded_branch_triage_branches(branch_count: int) -> list[dict[str, Any]]:
3748
    branches: list[dict[str, Any]] = []
3749
    clean_patch_id = "patch-clean-0"
3750
    for index in range(branch_count):
3751
        expected_class = BRANCH_TRIAGE_CLASSES[index % len(BRANCH_TRIAGE_CLASSES)]
3752
        name = f"triage-{expected_class}-{index}"
3753
        branch: dict[str, Any] = {
3754
            "name": name,
3755
            "expected_class": expected_class,
3756
            "expected_action": BRANCH_TRIAGE_ACTIONS[expected_class],
3757
            "base_sha": "main-sha-0",
3758
            "head_sha": f"head-{index}",
3759
            "behind_by": 0,
3760
            "mergeable": True,
3761
            "conflict_paths": [],
3762
            "patch_id": f"patch-{expected_class}-{index}",
3763
        }
3764
        if expected_class == "clean":
3765
            branch["patch_id"] = clean_patch_id if index == 0 else f"patch-clean-{index}"
3766
        elif expected_class == "stale":
3767
            branch["behind_by"] = 2
3768
            branch["base_sha"] = "old-main-sha"
3769
        elif expected_class == "duplicate":
3770
            branch["patch_id"] = clean_patch_id
3771
            branch["duplicate_of"] = "triage-clean-0"
3772
        elif expected_class == "conflicting":
3773
            branch["mergeable"] = False
3774
            branch["conflict_paths"] = [f"src/conflict-{index}.txt"]
3775
        branches.append(branch)
3776
    return branches
3777
3778
3779
def classify_triage_branch(branch: dict[str, Any], seen_patch_ids: set[str]) -> str:
3780
    if branch.get("conflict_paths") or branch.get("mergeable") is False:
3781
        return "conflicting"
3782
    patch_id = str(branch.get("patch_id") or "")
3783
    if patch_id and patch_id in seen_patch_ids:
3784
        return "duplicate"
3785
    if int(branch.get("behind_by") or 0) > 0:
3786
        if patch_id:
3787
            seen_patch_ids.add(patch_id)
3788
        return "stale"
3789
    if patch_id:
3790
        seen_patch_ids.add(patch_id)
3791
    return "clean"
3792
3793
3794
def branch_triage_plan(reviewed: list[dict[str, Any]]) -> dict[str, Any]:
3795
    seen_patch_ids: set[str] = set()
3796
    actions: list[dict[str, str]] = []
3797
    merge_order: list[str] = []
3798
    detected_counts = {key: 0 for key in BRANCH_TRIAGE_CLASSES}
3799
    for branch in reviewed:
3800
        classification = classify_triage_branch(branch, seen_patch_ids)
3801
        detected_counts[classification] += 1
3802
        action = BRANCH_TRIAGE_ACTIONS[classification]
3803
        name = str(branch["name"])
3804
        actions.append({"branch": name, "classification": classification, "action": action})
3805
        if action in ("merge", "refresh_then_merge"):
3806
            merge_order.append(name)
3807
    return {
3808
        "actions": actions,
3809
        "merge_order": merge_order,
3810
        "detected_counts": detected_counts,
3811
    }
3812
3813
3814
def expected_triage_plan(branches: list[dict[str, Any]]) -> dict[str, Any]:
3815
    counts = {key: 0 for key in BRANCH_TRIAGE_CLASSES}
3816
    actions: list[dict[str, str]] = []
3817
    merge_order: list[str] = []
3818
    for branch in branches:
3819
        classification = str(branch["expected_class"])
3820
        action = str(branch["expected_action"])
3821
        counts[classification] += 1
3822
        name = str(branch["name"])
3823
        actions.append({"branch": name, "classification": classification, "action": action})
3824
        if action in ("merge", "refresh_then_merge"):
3825
            merge_order.append(name)
3826
    return {"actions": actions, "merge_order": merge_order, "detected_counts": counts}
3827
3828
3829
class FakeBranchTriageProvider:
3830
    def __init__(self, branch_count: int) -> None:
3831
        self._branches = seeded_branch_triage_branches(branch_count)
3832
        self._open = {str(branch["name"]) for branch in self._branches}
3833
        self.closed: list[str] = []
3834
3835
    def seed(self) -> list[dict[str, Any]]:
3836
        return [dict(branch) for branch in self._branches]
3837
3838
    def review(self) -> list[dict[str, Any]]:
3839
        return [dict(branch) for branch in self._branches if str(branch["name"]) in self._open]
3840
3841
    def apply(self, plan: dict[str, Any]) -> dict[str, Any]:
3842
        for action in plan.get("actions") or []:
3843
            branch = str(action["branch"])
3844
            decision = str(action["action"])
3845
            if decision in ("merge", "refresh_then_merge", "close_duplicate") and branch in self._open:
3846
                self._open.remove(branch)
3847
                self.closed.append(branch)
3848
        return {"branches_closed": len(self.closed), "final_survivors": sorted(self._open)}
3849
3850
3851
def branch_triage_metric_fields(
3852
    *,
3853
    branches_reviewed: int,
3854
    branches_closed: int,
3855
    plan: Optional[dict[str, Any]] = None,
3856
    oracle: Optional[dict[str, Any]] = None,
3857
) -> dict[str, Any]:
3858
    counts = ((plan or {}).get("detected_counts") or {})
3859
    return {
3860
        "branches_reviewed": branches_reviewed,
3861
        "branches_closed": branches_closed,
3862
        "merge_order_correct": (oracle or {}).get("merge_order_correct"),
3863
        "clean_detected_count": int(counts.get("clean", 0)),
3864
        "stale_detected_count": int(counts.get("stale", 0)),
3865
        "duplicate_detected_count": int(counts.get("duplicate", 0)),
3866
        "conflict_detected_count": int(counts.get("conflicting", 0)),
3867
    }
3868
3869
3870
def branch_triage_oracle(
3871
    seeded: list[dict[str, Any]],
3872
    plan: dict[str, Any],
3873
    apply_result: dict[str, Any],
3874
) -> dict[str, Any]:
3875
    expected = expected_triage_plan(seeded)
3876
    expected_survivors = sorted(
3877
        str(branch["name"])
3878
        for branch in seeded
3879
        if branch["expected_action"] == "keep_open_manual_resolution"
3880
    )
3881
    final_survivors = sorted(str(name) for name in apply_result.get("final_survivors") or [])
3882
    return {
3883
        "action_plan_correct": plan.get("actions") == expected["actions"],
3884
        "merge_order_correct": plan.get("merge_order") == expected["merge_order"],
3885
        "final_survivors_correct": final_survivors == expected_survivors,
3886
        "expected_action_plan": expected["actions"],
3887
        "expected_merge_order": expected["merge_order"],
3888
        "expected_final_survivors": expected_survivors,
3889
        "final_survivors": final_survivors,
3890
    }
3891
3892
3893
def fake_branch_triage_row(
3894
    meta: dict[str, Any],
3895
    scenario_name: str,
3896
    operation: str,
3897
    run_index: int,
3898
    elapsed_ms: float,
3899
    returncode: int = 0,
3900
    **fields: Any,
3901
) -> dict[str, Any]:
3902
    return {
3903
        **meta,
3904
        "scenario": scenario_name,
3905
        "run": run_index,
3906
        "operation": operation,
3907
        "elapsed_ms": round(elapsed_ms, 3),
3908
        "returncode": returncode,
3909
        "command": [],
3910
        "tool_call_count": 0,
3911
        "branch_triage_provider": "fake",
3912
        **fake_provider_token_fields(),
3913
        **fields,
3914
    }
3915
3916
3917
def run_branch_triage_fake_scenario(
3918
    scenario: dict[str, Any],
3919
    meta: dict[str, Any],
3920
    run_index: int,
3921
) -> list[dict[str, Any]]:
3922
    scenario_name = str(scenario["name"])
3923
    operations = [str(op) for op in scenario.get("operations") or []]
3924
    provider = FakeBranchTriageProvider(branch_triage_count(scenario))
3925
    rows: list[dict[str, Any]] = []
3926
    seeded: list[dict[str, Any]] = []
3927
    reviewed: list[dict[str, Any]] = []
3928
    plan: dict[str, Any] = {}
3929
    apply_result: dict[str, Any] = {"branches_closed": 0, "final_survivors": []}
3930
    oracle: dict[str, Any] = {}
3931
    total_start = platform_clock.monotonic_ms()
3932
3933
    for operation in operations:
3934
        start = platform_clock.monotonic_ms()
3935
        if operation == "triage.seed":
3936
            seeded = provider.seed()
3937
            rows.append(
3938
                fake_branch_triage_row(
3939
                    meta, scenario_name, operation, run_index,
3940
                    platform_clock.monotonic_ms() - start,
3941
                    branches_seeded=len(seeded),
3942
                    seeded_branch_classes=[branch["expected_class"] for branch in seeded],
3943
                    **branch_triage_metric_fields(branches_reviewed=0, branches_closed=0),
3944
                )
3945
            )
3946
        elif operation == "triage.review":
3947
            reviewed = provider.review()
3948
            rows.append(
3949
                fake_branch_triage_row(
3950
                    meta, scenario_name, operation, run_index,
3951
                    platform_clock.monotonic_ms() - start,
3952
                    branches_reviewed=len(reviewed),
3953
                    branches_closed=0,
3954
                    reviewed_branches=[branch["name"] for branch in reviewed],
3955
                )
3956
            )
3957
        elif operation == "triage.plan":
3958
            plan = branch_triage_plan(reviewed)
3959
            rows.append(
3960
                fake_branch_triage_row(
3961
                    meta, scenario_name, operation, run_index,
3962
                    platform_clock.monotonic_ms() - start,
3963
                    action_plan=plan["actions"],
3964
                    merge_order=plan["merge_order"],
3965
                    **branch_triage_metric_fields(
3966
                        branches_reviewed=len(reviewed), branches_closed=0, plan=plan
3967
                    ),
3968
                )
3969
            )
3970
        elif operation == "triage.apply":
3971
            apply_result = provider.apply(plan)
3972
            rows.append(
3973
                fake_branch_triage_row(
3974
                    meta, scenario_name, operation, run_index,
3975
                    platform_clock.monotonic_ms() - start,
3976
                    final_survivors=apply_result["final_survivors"],
3977
                    **branch_triage_metric_fields(
3978
                        branches_reviewed=len(reviewed),
3979
                        branches_closed=int(apply_result["branches_closed"]),
3980
                        plan=plan,
3981
                    ),
3982
                )
3983
            )
3984
        elif operation == "triage.oracle":
3985
            oracle = branch_triage_oracle(seeded, plan, apply_result)
3986
            ok = all(
3987
                bool(oracle[key])
3988
                for key in ("action_plan_correct", "merge_order_correct", "final_survivors_correct")
3989
            )
3990
            rows.append(
3991
                fake_branch_triage_row(
3992
                    meta, scenario_name, operation, run_index,
3993
                    platform_clock.monotonic_ms() - start,
3994
                    returncode=0 if ok else 1,
3995
                    **{
3996
                        **oracle,
3997
                        **branch_triage_metric_fields(
3998
                            branches_reviewed=len(reviewed),
3999
                            branches_closed=int(apply_result["branches_closed"]),
4000
                            plan=plan,
4001
                            oracle=oracle,
4002
                        ),
4003
                    },
4004
                )
4005
            )
4006
        elif operation == "triage.total":
4007
            failures = [
4008
                row for row in rows
4009
                if row_returncode(row) not in (0, SKIP_RETURNCODE)
4010
            ]
4011
            rows.append(
4012
                fake_branch_triage_row(
4013
                    meta, scenario_name, operation, run_index,
4014
                    platform_clock.monotonic_ms() - total_start,
4015
                    returncode=1 if failures else 0,
4016
                    summarized_operations=[row["operation"] for row in rows],
4017
                    final_survivors=apply_result.get("final_survivors", []),
4018
                    **branch_triage_metric_fields(
4019
                        branches_reviewed=len(reviewed),
4020
                        branches_closed=int(apply_result["branches_closed"]),
4021
                        plan=plan,
4022
                        oracle=oracle,
4023
                    ),
4024
                )
4025
            )
4026
        else:
4027
            rows.append(skip_row(meta, scenario_name, operation, run_index, "operation_not_implemented"))
4028
    return rows
4029
4030
4031
def run_branch_triage_real_skip(
4032
    args: argparse.Namespace,
4033
    scenario: dict[str, Any],
4034
    meta: dict[str, Any],
4035
    run_index: int,
4036
) -> list[dict[str, Any]]:
4037
    scenario_name = str(scenario["name"])
4038
    reason = f"branch_triage_real_provider_not_implemented:{args.platform}:{args.driver}"
4039
    return [
4040
        skip_row(meta, scenario_name, str(operation), run_index, reason)
4041
        for operation in scenario.get("operations") or []
4042
    ]
4043
4044
4045
def run_scenario_with_credentials(
4046
    args: argparse.Namespace,
4047
    scenario: dict[str, Any],
4048
    meta: dict[str, Any],
4049
    run_index: int,
4050
    run_root: Path,
4051
    settings: Settings,
4052
) -> list[dict[str, Any]]:
4053
    if scenario.get("name") == "platform_capability_probe":
4054
        return run_capability_probe(args, meta, run_index, settings)
4055
    if is_branch_fleet_scenario(scenario):
4056
        live_skip_reason = branch_fleet_live_skip_reason(scenario, args.driver)
4057
        if live_skip_reason is not None:
4058
            scenario_name = str(scenario["name"])
4059
            return [
4060
                skip_row(meta, scenario_name, str(operation), run_index, live_skip_reason)
4061
                for operation in scenario.get("operations") or []
4062
            ]
4063
        if args.driver == BRANCH_TRIAGE_FAKE_DRIVER:
4064
            return run_branch_fleet_fake_scenario(scenario, meta, run_index)
4065
        if args.platform == "github" and args.driver == "harness-api":
4066
            return run_github_api_branch_fleet_scenario(args, scenario, meta, run_index, settings)
4067
        if args.platform == "oak" and args.driver == "cli":
4068
            capability_skip = oak_cli_commit_push_skip_reason(cli_env(args))
4069
            if capability_skip is not None:
4070
                scenario_name = str(scenario["name"])
4071
                return [
4072
                    skip_row(meta, scenario_name, str(operation), run_index, capability_skip)
4073
                    for operation in scenario.get("operations") or []
4074
                ]
4075
            return run_oak_cli_branch_fleet_scenario(args, scenario, meta, run_index, run_root, settings)
4076
        scenario_name = str(scenario["name"])
4077
        reason = f"branch_fleet_provider_not_implemented:{args.platform}:{args.driver}"
4078
        return [
4079
            skip_row(meta, scenario_name, str(operation), run_index, reason)
4080
            for operation in scenario.get("operations") or []
4081
        ]
4082
    if is_branch_triage_scenario(scenario):
4083
        if args.driver == BRANCH_TRIAGE_FAKE_DRIVER:
4084
            return run_branch_triage_fake_scenario(scenario, meta, run_index)
4085
        return run_branch_triage_real_skip(args, scenario, meta, run_index)
4086
    if is_race_scenario(scenario):
4087
        return run_race_with_credentials(args, scenario, meta, run_index, run_root, settings)
4088
    if args.platform == "github" and args.driver == "harness-api":
4089
        return run_github_api_scenario(args, scenario, meta, run_index, settings)
4090
    if args.platform == "github":
4091
        return run_github_cli_scenario(args, scenario, meta, run_index, run_root, settings)
4092
    return run_oak_cli_scenario(args, scenario, meta, run_index, run_root, settings)
4093
4094
4095
# --------------------------------------------------------------------------
4096
# Summary + main
4097
# --------------------------------------------------------------------------
4098
4099
4100
def summary_text(rows: list[dict[str, Any]]) -> str:
4101
    lines = [
4102
        "# Platform Lane Summary",
4103
        "",
4104
        "Hosted integration workflow: task branch exists locally -> change merged",
4105
        "in main, present in local checkout. Round trips, bytes, polls, and tokens",
4106
        "are always comparable across platforms; wall clock only with per-platform",
4107
        "RTT context and an explicit caveat. Settle durations are bounded by their",
4108
        "poll quantization. This lane is excluded from devloop.",
4109
        "",
4110
        "| Platform | Driver | Scenario | Run | Operation | Wall ms | Polls | rc |",
4111
        "| --- | --- | --- | ---: | --- | ---: | ---: | ---: |",
4112
    ]
4113
    for row in rows:
4114
        if row.get("skipped"):
4115
            continue
4116
        lines.append(
4117
            "| {} | {} | {} | {} | `{}` | {} | {} | {} |".format(
4118
                row.get("platform"), row.get("driver"), row.get("scenario"), row.get("run"),
4119
                row.get("operation"), row.get("elapsed_ms"),
4120
                row.get("poll_iterations", ""), row.get("returncode"),
4121
            )
4122
        )
4123
    skips = [row for row in rows if row.get("skipped")]
4124
    if skips:
4125
        lines += ["", "## Skips", ""]
4126
        reasons: dict[str, int] = {}
4127
        for row in skips:
4128
            reasons[str(row.get("skip_reason"))] = reasons.get(str(row.get("skip_reason")), 0) + 1
4129
        for reason, count in sorted(reasons.items()):
4130
            lines.append(f"- `{reason}`: {count} row(s)")
4131
    return "\n".join(lines) + "\n"
4132
4133
4134
def main(argv: Optional[list[str]] = None) -> int:
4135
    args = parse_args(argv)
4136
    spec = load_platform_spec(args.spec)
4137
    semantics = load_semantics(args.semantics_config)
4138
    scenario = scenario_by_name(spec, args.scenario)
4139
    if scenario is None:
4140
        names = ", ".join(str(s.get("name")) for s in spec.get("scenarios") or [] if isinstance(s, dict))
4141
        print(f"unknown scenario {args.scenario!r}; known: {names}", file=sys.stderr)
4142
        return 2
4143
    declared_variants = semantics.get("protection_variants") or []
4144
    if args.protection_variant not in declared_variants:
4145
        print(
4146
            f"unknown protection variant {args.protection_variant!r}; declared: {', '.join(declared_variants)}",
4147
            file=sys.stderr,
4148
        )
4149
        return 2
4150
4151
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
4152
    settings = scenario_settings(spec, scenario, args.admitted_output_chars)
4153
    runner = runner_fields()
4154
    meta: dict[str, Any] = {
4155
        "bench_id": timestamp,
4156
        "profile": "platform",
4157
        "benchmark_track": "platform-hosted",
4158
        "timestamp_utc": timestamp,
4159
        "host": platform_module.node(),
4160
        "os_platform": platform_module.platform(),
4161
        "machine": platform_module.machine(),
4162
        "python": platform_module.python_version(),
4163
        "env_isolation_version": ENV_ISOLATION_VERSION,
4164
        "platform": args.platform,
4165
        "driver": args.driver,
4166
        "platform_semantics_class": args.semantics_class,
4167
        "protection_variant": args.protection_variant,
4168
        "platform_semantics_version": semantics.get("semantics_version"),
4169
        "platform_comparison_key": platform_comparison_key(
4170
            semantics.get("semantics_version"),
4171
            args.semantics_class,
4172
            args.protection_variant,
4173
        ),
4174
        # The contract only matches when the variant is declared+implemented
4175
        # AND this driver actually executes it (check-instant needs the
4176
        # status API, which the cli driver does not have).
4177
        "semantic_contract_match": (
4178
            compute_semantic_contract_match(
4179
                semantics, args.semantics_class, args.protection_variant
4180
            )
4181
            and protection_variant_executable(
4182
                args.platform, args.driver, args.protection_variant
4183
            )
4184
        ),
4185
        "semantics_version": semantics.get("semantics_version"),
4186
        "semantics_schema_version": semantics.get("schema_version"),
4187
        "repo": args.repo or None,
4188
        "token_env": args.token_env,
4189
        "excluded_from_devloop": True,
4190
        "clock": "time.monotonic (single local monotonic clock for all headline durations)",
4191
    }
4192
4193
    skip_reason = readiness_skip_reason(
4194
        args.platform,
4195
        args.driver,
4196
        args.repo,
4197
        args.token_env,
4198
        requires_remote=bool(scenario.get("requires_remote")),
4199
    )
4200
    if skip_reason is None and args.protection_variant not in (
4201
        semantics.get("implemented_protection_variants") or []
4202
    ):
4203
        skip_reason = f"protection_variant_not_implemented:{args.protection_variant}"
4204
4205
    operations = [str(op) for op in scenario.get("operations") or []]
4206
    scenario_name = str(scenario.get("name"))
4207
    rows: list[dict[str, Any]] = []
4208
    run_root = args.workdir / "runs" / timestamp
4209
4210
    with measurement_lock("platform_lifecycle") as lock_info:
4211
        meta["measurement_lock_wait_ms"] = lock_info.wait_ms
4212
        meta["measurement_lock"] = "held" if lock_info.enabled else "disabled"
4213
        for run_index in range(max(1, args.runs)):
4214
            if skip_reason is not None:
4215
                rows.extend(
4216
                    skip_row(
4217
                        meta,
4218
                        scenario_name,
4219
                        operation,
4220
                        run_index,
4221
                        skip_reason,
4222
                        settings=settings,
4223
                    )
4224
                    for operation in operations
4225
                )
4226
                continue
4227
            run_root.mkdir(parents=True, exist_ok=True)
4228
            print(f"[run] {scenario_name} run={run_index} platform={args.platform} driver={args.driver}", flush=True)
4229
            rows.extend(
4230
                run_scenario_with_credentials(args, scenario, meta, run_index, run_root, settings)
4231
            )
4232
4233
    for row in rows:
4234
        row.pop("_stdout", None)
4235
    stamped = [stamp_row(row, runner) for row in rows]
4236
    store = ResultsStore(args.results, lane="platform", filename_suffix="platform")
4237
    raw_path, summary_path = store.write(timestamp, stamped, summary_text(stamped))
4238
    print(f"[result] {raw_path}")
4239
    print(f"[summary] {summary_path}")
4240
    if skip_reason is None and not args.keep_workdirs:
4241
        shutil.rmtree(run_root, ignore_errors=True)
4242
4243
    if stamped and all(row.get("skipped") for row in stamped):
4244
        printed_skip_reason = skip_reason
4245
        if printed_skip_reason is None:
4246
            row_reasons = sorted(
4247
                {
4248
                    str(row.get("skip_reason"))
4249
                    for row in stamped
4250
                    if row.get("skip_reason")
4251
                }
4252
            )
4253
            printed_skip_reason = ", ".join(row_reasons) if row_reasons else None
4254
        print(f"[skip] all rows skipped: {printed_skip_reason}", file=sys.stderr)
4255
        return EXIT_ALL_SKIPPED
4256
    failures = [
4257
        row for row in stamped if row_returncode(row) not in (0, SKIP_RETURNCODE)
4258
    ]
4259
    return 1 if failures else 0
4260
4261
4262
if __name__ == "__main__":
4263
    raise SystemExit(main())