agentv 4.44.0-next.1 → 5.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/dist/{artifact-writer-72MQQELS.js → artifact-writer-QD52FC56.js} +12 -10
  3. package/dist/{chunk-KAGZRUO6.js → chunk-6JVM4GJI.js} +3 -2
  4. package/dist/chunk-6JVM4GJI.js.map +1 -0
  5. package/dist/{chunk-2N3YSEMI.js → chunk-6Q56CEXV.js} +333 -99
  6. package/dist/chunk-6Q56CEXV.js.map +1 -0
  7. package/dist/{chunk-5DYGN547.js → chunk-GZJMODLW.js} +1008 -663
  8. package/dist/chunk-GZJMODLW.js.map +1 -0
  9. package/dist/{chunk-CXHYTYHU.js → chunk-IBI7A5RP.js} +621 -191
  10. package/dist/chunk-IBI7A5RP.js.map +1 -0
  11. package/dist/{chunk-NPVGBFF6.js → chunk-IJEZFZJR.js} +2 -43
  12. package/dist/chunk-IJEZFZJR.js.map +1 -0
  13. package/dist/{chunk-BPGJ4HBU.js → chunk-RCIEHFQ2.js} +3 -3
  14. package/dist/{chunk-NCNDA3F6.js → chunk-UG4JHZYM.js} +1176 -224
  15. package/dist/chunk-UG4JHZYM.js.map +1 -0
  16. package/dist/cli.js +7 -7
  17. package/dist/dashboard/assets/index-BVMrdN8a.js +119 -0
  18. package/dist/dashboard/assets/{index-DitffAxS.js → index-D5_8DE_B.js} +1 -1
  19. package/dist/dashboard/assets/index-DZWXfWsv.css +1 -0
  20. package/dist/dashboard/index.html +2 -2
  21. package/dist/{dist-L4YCQOQ6.js → dist-KJ6PELQY.js} +45 -11
  22. package/dist/{docker-workspace-RPPXBT27-B4AQHVWA.js → docker-workspace-ZJ2ESVBI-6UDETQII.js} +2 -2
  23. package/dist/{exec-AR6JUUN5-6MBPURPR.js → exec-MYPAJJP7-C5HD5WG5.js} +2 -2
  24. package/dist/index.js +7 -7
  25. package/dist/{interactive-DRNRKLEQ.js → interactive-2WMVAASS.js} +7 -7
  26. package/dist/skills/agentv-bench/SKILL.md +3 -3
  27. package/dist/skills/agentv-bench/agents/analyzer.md +1 -1
  28. package/dist/skills/agentv-bench/references/autoresearch.md +1 -1
  29. package/dist/skills/agentv-bench/references/eval-yaml-spec.md +13 -3
  30. package/dist/skills/agentv-bench/references/migrating-from-skill-creator.md +2 -2
  31. package/dist/skills/agentv-bench/references/schemas.md +6 -6
  32. package/dist/skills/agentv-bench/references/subagent-pipeline.md +1 -1
  33. package/dist/skills/agentv-eval-writer/SKILL.md +10 -0
  34. package/dist/skills/agentv-eval-writer/references/eval-schema.json +3 -63
  35. package/dist/skills/agentv-eval-writer/references/experiment-schema.json +278 -0
  36. package/dist/{ts-eval-loader-S7ESROX2-6CB3HB6H.js → ts-eval-loader-RE43OAGX-AMRCUKBZ.js} +3 -3
  37. package/package.json +1 -1
  38. package/dist/chunk-2N3YSEMI.js.map +0 -1
  39. package/dist/chunk-5DYGN547.js.map +0 -1
  40. package/dist/chunk-CXHYTYHU.js.map +0 -1
  41. package/dist/chunk-KAGZRUO6.js.map +0 -1
  42. package/dist/chunk-NCNDA3F6.js.map +0 -1
  43. package/dist/chunk-NPVGBFF6.js.map +0 -1
  44. package/dist/dashboard/assets/index-1Hvy8sBy.js +0 -119
  45. package/dist/dashboard/assets/index-J5sPcQO3.css +0 -1
  46. /package/dist/{artifact-writer-72MQQELS.js.map → artifact-writer-QD52FC56.js.map} +0 -0
  47. /package/dist/{chunk-BPGJ4HBU.js.map → chunk-RCIEHFQ2.js.map} +0 -0
  48. /package/dist/{dist-L4YCQOQ6.js.map → dist-KJ6PELQY.js.map} +0 -0
  49. /package/dist/{docker-workspace-RPPXBT27-B4AQHVWA.js.map → docker-workspace-ZJ2ESVBI-6UDETQII.js.map} +0 -0
  50. /package/dist/{exec-AR6JUUN5-6MBPURPR.js.map → exec-MYPAJJP7-C5HD5WG5.js.map} +0 -0
  51. /package/dist/{interactive-DRNRKLEQ.js.map → interactive-2WMVAASS.js.map} +0 -0
  52. /package/dist/{ts-eval-loader-S7ESROX2-6CB3HB6H.js.map → ts-eval-loader-RE43OAGX-AMRCUKBZ.js.map} +0 -0
@@ -21,18 +21,22 @@ import {
21
21
  readTranscriptJsonl,
22
22
  releaseSharedWorkspaceSetup,
23
23
  resolveRepoCloneUrl
24
- } from "./chunk-5DYGN547.js";
24
+ } from "./chunk-GZJMODLW.js";
25
25
 
26
26
  // ../../packages/core/dist/index.js
27
- import { readFileSync } from "node:fs";
27
+ import { createHash } from "node:crypto";
28
+ import { readFile } from "node:fs/promises";
28
29
  import path from "node:path";
29
- import { randomUUID } from "node:crypto";
30
+ import { pathToFileURL } from "node:url";
31
+ import { readFileSync } from "node:fs";
30
32
  import path2 from "node:path";
31
- import micromatch from "micromatch";
32
- import { readFile } from "node:fs/promises";
33
+ import { randomUUID } from "node:crypto";
33
34
  import path3 from "node:path";
35
+ import micromatch from "micromatch";
36
+ import { readFile as readFile2 } from "node:fs/promises";
37
+ import path4 from "node:path";
34
38
  import { execFile, spawn } from "node:child_process";
35
- import { createHash } from "node:crypto";
39
+ import { createHash as createHash2, randomBytes } from "node:crypto";
36
40
  import {
37
41
  existsSync,
38
42
  mkdirSync,
@@ -44,17 +48,17 @@ import {
44
48
  } from "node:fs";
45
49
  import { cp, lstat, mkdtemp, readdir, rm, stat } from "node:fs/promises";
46
50
  import os from "node:os";
47
- import path4 from "node:path";
51
+ import path5 from "node:path";
48
52
  import { promisify } from "node:util";
49
53
  import * as childProcess from "node:child_process";
50
54
  import { existsSync as existsSync2 } from "node:fs";
51
55
  import { spawnSync } from "node:child_process";
52
56
  import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
53
57
  import { homedir } from "node:os";
54
- import path5 from "node:path";
58
+ import path6 from "node:path";
55
59
  import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
56
60
  import { homedir as homedir2 } from "node:os";
57
- import path6 from "node:path";
61
+ import path7 from "node:path";
58
62
  var TRACE_SESSION_SCHEMA_VERSION = "agentv.dashboard.trace_session.v1";
59
63
  function isRecord(value) {
60
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1478,6 +1482,429 @@ function isPrimitive(value) {
1478
1482
  function dropUndefined2(value) {
1479
1483
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
1480
1484
  }
1485
+ var EXPERIMENT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".ts", ".js", ".mts", ".mjs"]);
1486
+ var VALID_SANDBOXES = /* @__PURE__ */ new Set(["auto", "docker", "vercel"]);
1487
+ var VALID_REPEAT_STRATEGIES = /* @__PURE__ */ new Set([
1488
+ "pass_at_k",
1489
+ "mean",
1490
+ "confidence_interval"
1491
+ ]);
1492
+ function isExperimentFileReference(value) {
1493
+ const trimmed = value.trim();
1494
+ if (!trimmed) {
1495
+ return false;
1496
+ }
1497
+ return trimmed.includes("/") || trimmed.includes("\\") || EXPERIMENT_FILE_EXTENSIONS.has(path.extname(trimmed).toLowerCase());
1498
+ }
1499
+ function deriveExperimentNameFromPath(filePath) {
1500
+ return path.basename(filePath).replace(/\.experiment\.(ya?ml|[cm]?[jt]s)$/i, "").replace(/\.(ya?ml|[cm]?[jt]s)$/i, "");
1501
+ }
1502
+ async function loadExperimentConfig(filePath) {
1503
+ const resolvedPath = path.resolve(filePath);
1504
+ const ext = path.extname(resolvedPath).toLowerCase();
1505
+ let rawConfig;
1506
+ if (ext === ".yaml" || ext === ".yml") {
1507
+ rawConfig = parseYamlValue(await readFile(resolvedPath, "utf8"));
1508
+ } else if (EXPERIMENT_FILE_EXTENSIONS.has(ext)) {
1509
+ const moduleUrl = pathToFileURL(resolvedPath).href;
1510
+ const mod = await import(moduleUrl);
1511
+ rawConfig = mod.default ?? mod.config ?? mod;
1512
+ } else {
1513
+ throw new Error(
1514
+ `Unsupported experiment file extension '${ext}'. Use .yaml, .yml, .ts, .js, .mts, or .mjs.`
1515
+ );
1516
+ }
1517
+ return normalizeExperimentConfig(rawConfig, { sourcePath: resolvedPath });
1518
+ }
1519
+ function normalizeExperimentConfig(rawConfig, options = {}) {
1520
+ if (!isRecord2(rawConfig)) {
1521
+ throw new Error("Experiment config must be an object.");
1522
+ }
1523
+ const name = readOptionalString(rawConfig.name, "name");
1524
+ const agent = readOptionalString(rawConfig.agent, "agent");
1525
+ const target = readOptionalString(rawConfig.target, "target");
1526
+ const targets = readTargets(rawConfig.targets);
1527
+ const model = readOptionalString(rawConfig.model, "model");
1528
+ const agentOptions = readOptionalRecord(rawConfig.agent_options ?? rawConfig.agentOptions);
1529
+ const suites = readSuites(rawConfig.suites);
1530
+ const scripts = readScriptArray(rawConfig.scripts, "scripts");
1531
+ const repeat = readRepeat(rawConfig.repeat);
1532
+ const runs = readOptionalPositiveInteger(rawConfig.runs, "runs");
1533
+ if (repeat !== void 0 && runs !== void 0) {
1534
+ throw new Error("Experiment repeat and runs cannot both be set. Use repeat for AgentV config.");
1535
+ }
1536
+ const earlyExit = readOptionalBoolean(rawConfig.early_exit ?? rawConfig.earlyExit, "early_exit");
1537
+ const timeoutSeconds = readOptionalPositiveNumber(
1538
+ rawConfig.timeout_seconds ?? rawConfig.timeoutSeconds,
1539
+ "timeout_seconds"
1540
+ );
1541
+ const workers = readOptionalPositiveInteger(rawConfig.workers, "workers");
1542
+ const budgetUsd = readOptionalPositiveNumber(
1543
+ rawConfig.budget_usd ?? rawConfig.budgetUsd,
1544
+ "budget_usd"
1545
+ );
1546
+ const sandbox = readOptionalSandbox(rawConfig.sandbox);
1547
+ const workspace = readOptionalRecord(rawConfig.workspace);
1548
+ const setup = readSetup(rawConfig.setup);
1549
+ const configWithoutFingerprint = {
1550
+ ...name !== void 0 && { name },
1551
+ ...agent !== void 0 && { agent },
1552
+ ...target !== void 0 && { target },
1553
+ ...targets !== void 0 && { targets },
1554
+ ...model !== void 0 && { model },
1555
+ ...agentOptions !== void 0 && { agentOptions },
1556
+ ...suites !== void 0 && { suites },
1557
+ ...scripts !== void 0 && { scripts },
1558
+ ...repeat !== void 0 && { repeat },
1559
+ ...runs !== void 0 && { runs },
1560
+ ...earlyExit !== void 0 && { earlyExit },
1561
+ ...timeoutSeconds !== void 0 && { timeoutSeconds },
1562
+ ...workers !== void 0 && { workers },
1563
+ ...budgetUsd !== void 0 && { budgetUsd },
1564
+ ...sandbox !== void 0 && { sandbox },
1565
+ ...workspace !== void 0 && { workspace },
1566
+ ...setup !== void 0 && { setup },
1567
+ ...options.sourcePath !== void 0 && { sourcePath: options.sourcePath }
1568
+ };
1569
+ return {
1570
+ ...configWithoutFingerprint,
1571
+ fingerprint: fingerprintExperimentConfig(configWithoutFingerprint)
1572
+ };
1573
+ }
1574
+ function fingerprintExperimentConfig(config) {
1575
+ const stablePayload = toStableJsonValue(config);
1576
+ return createHash("sha256").update(JSON.stringify(stablePayload)).digest("hex");
1577
+ }
1578
+ function buildExperimentArtifactMetadata(config) {
1579
+ if (!config) {
1580
+ return void 0;
1581
+ }
1582
+ const targets = config.targets?.map((target) => typeof target === "string" ? target : target.name).filter((target) => target.trim().length > 0);
1583
+ return {
1584
+ ...config.name !== void 0 && { name: config.name },
1585
+ ...config.sourcePath !== void 0 && { source_path: config.sourcePath },
1586
+ ...config.fingerprint !== void 0 && { fingerprint: config.fingerprint },
1587
+ ...config.agent !== void 0 && { agent: config.agent },
1588
+ ...config.target !== void 0 && { target: config.target },
1589
+ ...targets && targets.length > 0 && { targets },
1590
+ ...config.model !== void 0 && { model: config.model },
1591
+ ...config.suites !== void 0 && { suites: config.suites.map(toSuiteArtifactMetadata) },
1592
+ ...config.repeat !== void 0 && {
1593
+ repeat: {
1594
+ count: config.repeat.count,
1595
+ strategy: config.repeat.strategy,
1596
+ ...config.repeat.costLimitUsd !== void 0 && {
1597
+ cost_limit_usd: config.repeat.costLimitUsd
1598
+ }
1599
+ }
1600
+ },
1601
+ ...config.runs !== void 0 && { runs: config.runs },
1602
+ ...config.earlyExit !== void 0 && { early_exit: config.earlyExit },
1603
+ ...config.timeoutSeconds !== void 0 && { timeout_seconds: config.timeoutSeconds },
1604
+ ...config.workers !== void 0 && { workers: config.workers },
1605
+ ...config.budgetUsd !== void 0 && { budget_usd: config.budgetUsd },
1606
+ ...config.sandbox !== void 0 && { sandbox: config.sandbox }
1607
+ };
1608
+ }
1609
+ function toSuiteArtifactMetadata(suite) {
1610
+ return {
1611
+ ref: suite.ref,
1612
+ ...suite.select !== void 0 && {
1613
+ select: {
1614
+ test_ids: suite.select.testIds
1615
+ }
1616
+ }
1617
+ };
1618
+ }
1619
+ function readRepeat(raw) {
1620
+ if (raw === void 0) {
1621
+ return void 0;
1622
+ }
1623
+ if (!isRecord2(raw)) {
1624
+ throw new Error("Experiment repeat must be an object.");
1625
+ }
1626
+ const count = readRequiredPositiveInteger(raw.count, "repeat.count");
1627
+ const strategy = readOptionalRepeatStrategy(raw.strategy);
1628
+ const costLimitUsd = readOptionalNonNegativeNumber(
1629
+ raw.cost_limit_usd ?? raw.costLimitUsd,
1630
+ "repeat.cost_limit_usd"
1631
+ );
1632
+ return {
1633
+ count,
1634
+ strategy: strategy ?? "pass_at_k",
1635
+ ...costLimitUsd !== void 0 && { costLimitUsd }
1636
+ };
1637
+ }
1638
+ function readTargets(raw) {
1639
+ if (raw === void 0) {
1640
+ return void 0;
1641
+ }
1642
+ if (!Array.isArray(raw)) {
1643
+ throw new Error("Experiment targets must be an array.");
1644
+ }
1645
+ return raw.map((entry, index) => {
1646
+ if (typeof entry === "string" && entry.trim().length > 0) {
1647
+ return entry.trim();
1648
+ }
1649
+ if (!isRecord2(entry)) {
1650
+ throw new Error(`Experiment targets[${index}] must be a string or object.`);
1651
+ }
1652
+ const name = readRequiredString(entry.name, `targets[${index}].name`);
1653
+ const useTarget = readOptionalString(
1654
+ entry.use_target ?? entry.useTarget,
1655
+ `targets[${index}].use_target`
1656
+ );
1657
+ const hooks = readOptionalRecord(entry.hooks);
1658
+ return {
1659
+ name,
1660
+ ...useTarget !== void 0 && { useTarget },
1661
+ ...hooks !== void 0 && { hooks }
1662
+ };
1663
+ });
1664
+ }
1665
+ function readSuites(raw) {
1666
+ if (raw === void 0) {
1667
+ return void 0;
1668
+ }
1669
+ if (!Array.isArray(raw)) {
1670
+ throw new Error("Experiment suites must be an array.");
1671
+ }
1672
+ if (raw.length === 0) {
1673
+ throw new Error("Experiment suites must not be empty.");
1674
+ }
1675
+ return raw.map((entry, index) => {
1676
+ if (!isRecord2(entry)) {
1677
+ throw new Error(`Experiment suites[${index}] must be an object.`);
1678
+ }
1679
+ const ref = readRequiredString(entry.ref, `suites[${index}].ref`);
1680
+ const select = readSuiteSelect(entry.select, `suites[${index}].select`);
1681
+ return {
1682
+ ref,
1683
+ ...select !== void 0 && { select }
1684
+ };
1685
+ });
1686
+ }
1687
+ function readSuiteSelect(raw, location) {
1688
+ if (raw === void 0) {
1689
+ return void 0;
1690
+ }
1691
+ if (!isRecord2(raw)) {
1692
+ throw new Error(`Experiment ${location} must be an object.`);
1693
+ }
1694
+ const testIds = readOptionalStringArray(raw.test_ids ?? raw.testIds, `${location}.test_ids`);
1695
+ if (testIds === void 0) {
1696
+ throw new Error(`Experiment ${location}.test_ids is required when select is set.`);
1697
+ }
1698
+ return { testIds };
1699
+ }
1700
+ function readScriptArray(raw, location) {
1701
+ if (raw === void 0) {
1702
+ return void 0;
1703
+ }
1704
+ if (!Array.isArray(raw)) {
1705
+ throw new Error(`Experiment ${location} must be an array.`);
1706
+ }
1707
+ return raw.map((entry, index) => readScript(entry, `${location}[${index}]`));
1708
+ }
1709
+ function readSetup(raw) {
1710
+ if (raw === void 0) {
1711
+ return void 0;
1712
+ }
1713
+ if (typeof raw === "function") {
1714
+ return raw;
1715
+ }
1716
+ return readScriptArray(raw, "setup");
1717
+ }
1718
+ function readScript(raw, location) {
1719
+ if (typeof raw === "string") {
1720
+ const script2 = raw.trim();
1721
+ if (!script2) {
1722
+ throw new Error(`Experiment ${location} must not be empty.`);
1723
+ }
1724
+ return { script: script2 };
1725
+ }
1726
+ if (!isRecord2(raw)) {
1727
+ throw new Error(`Experiment ${location} must be a string or object.`);
1728
+ }
1729
+ const command = readOptionalCommand(raw.command, `${location}.command`);
1730
+ const script = readOptionalStringOrStringArray(raw.script, `${location}.script`);
1731
+ if (command === void 0 && script === void 0) {
1732
+ throw new Error(`Experiment ${location} must define command or script.`);
1733
+ }
1734
+ const timeoutSeconds = readOptionalPositiveNumber(
1735
+ raw.timeout_seconds ?? raw.timeoutSeconds,
1736
+ `${location}.timeout_seconds`
1737
+ );
1738
+ const cwd = readOptionalString(raw.cwd, `${location}.cwd`);
1739
+ const env = readOptionalStringRecord(raw.env, `${location}.env`);
1740
+ return {
1741
+ ...command !== void 0 && { command },
1742
+ ...script !== void 0 && { script },
1743
+ ...timeoutSeconds !== void 0 && { timeoutSeconds },
1744
+ ...cwd !== void 0 && { cwd },
1745
+ ...env !== void 0 && { env }
1746
+ };
1747
+ }
1748
+ function readOptionalCommand(raw, location) {
1749
+ if (raw === void 0) {
1750
+ return void 0;
1751
+ }
1752
+ if (typeof raw === "string") {
1753
+ const command = raw.trim();
1754
+ if (!command) {
1755
+ throw new Error(`Experiment ${location} must not be empty.`);
1756
+ }
1757
+ return ["sh", "-c", command];
1758
+ }
1759
+ if (Array.isArray(raw) && raw.length > 0 && raw.every((entry) => typeof entry === "string" && entry.trim())) {
1760
+ return raw.map((entry) => entry.trim());
1761
+ }
1762
+ throw new Error(`Experiment ${location} must be a string or string array.`);
1763
+ }
1764
+ function readOptionalStringOrStringArray(raw, location) {
1765
+ if (raw === void 0) {
1766
+ return void 0;
1767
+ }
1768
+ if (typeof raw === "string") {
1769
+ const trimmed = raw.trim();
1770
+ if (!trimmed) {
1771
+ throw new Error(`Experiment ${location} must not be empty.`);
1772
+ }
1773
+ return trimmed;
1774
+ }
1775
+ if (Array.isArray(raw) && raw.length > 0 && raw.every((entry) => typeof entry === "string" && entry.trim())) {
1776
+ return raw.map((entry) => entry.trim());
1777
+ }
1778
+ throw new Error(`Experiment ${location} must be a string or string array.`);
1779
+ }
1780
+ function readOptionalStringArray(raw, location) {
1781
+ if (raw === void 0) {
1782
+ return void 0;
1783
+ }
1784
+ if (Array.isArray(raw) && raw.length > 0 && raw.every((entry) => typeof entry === "string" && entry.trim())) {
1785
+ return raw.map((entry) => entry.trim());
1786
+ }
1787
+ throw new Error(`Experiment ${location} must be a non-empty string array.`);
1788
+ }
1789
+ function readOptionalString(raw, location) {
1790
+ if (raw === void 0) {
1791
+ return void 0;
1792
+ }
1793
+ if (typeof raw !== "string" || raw.trim().length === 0) {
1794
+ throw new Error(`Experiment ${location} must be a non-empty string.`);
1795
+ }
1796
+ return raw.trim();
1797
+ }
1798
+ function readRequiredString(raw, location) {
1799
+ const value = readOptionalString(raw, location);
1800
+ if (value === void 0) {
1801
+ throw new Error(`Experiment ${location} is required.`);
1802
+ }
1803
+ return value;
1804
+ }
1805
+ function readRequiredPositiveInteger(raw, location) {
1806
+ const value = readOptionalPositiveInteger(raw, location);
1807
+ if (value === void 0) {
1808
+ throw new Error(`Experiment ${location} is required.`);
1809
+ }
1810
+ return value;
1811
+ }
1812
+ function readOptionalBoolean(raw, location) {
1813
+ if (raw === void 0) {
1814
+ return void 0;
1815
+ }
1816
+ if (typeof raw !== "boolean") {
1817
+ throw new Error(`Experiment ${location} must be a boolean.`);
1818
+ }
1819
+ return raw;
1820
+ }
1821
+ function readOptionalRepeatStrategy(raw) {
1822
+ if (raw === void 0) {
1823
+ return void 0;
1824
+ }
1825
+ if (typeof raw !== "string" || !VALID_REPEAT_STRATEGIES.has(raw)) {
1826
+ throw new Error(
1827
+ "Experiment repeat.strategy must be one of 'pass_at_k', 'mean', or 'confidence_interval'."
1828
+ );
1829
+ }
1830
+ return raw;
1831
+ }
1832
+ function readOptionalPositiveInteger(raw, location) {
1833
+ if (raw === void 0) {
1834
+ return void 0;
1835
+ }
1836
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
1837
+ throw new Error(`Experiment ${location} must be a positive integer.`);
1838
+ }
1839
+ return raw;
1840
+ }
1841
+ function readOptionalNonNegativeNumber(raw, location) {
1842
+ if (raw === void 0) {
1843
+ return void 0;
1844
+ }
1845
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
1846
+ throw new Error(`Experiment ${location} must be a non-negative number.`);
1847
+ }
1848
+ return raw;
1849
+ }
1850
+ function readOptionalPositiveNumber(raw, location) {
1851
+ if (raw === void 0) {
1852
+ return void 0;
1853
+ }
1854
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
1855
+ throw new Error(`Experiment ${location} must be a positive number.`);
1856
+ }
1857
+ return raw;
1858
+ }
1859
+ function readOptionalSandbox(raw) {
1860
+ if (raw === void 0) {
1861
+ return void 0;
1862
+ }
1863
+ if (typeof raw !== "string" || !VALID_SANDBOXES.has(raw)) {
1864
+ throw new Error("Experiment sandbox must be one of 'auto', 'docker', or 'vercel'.");
1865
+ }
1866
+ return raw;
1867
+ }
1868
+ function readOptionalRecord(raw) {
1869
+ if (raw === void 0) {
1870
+ return void 0;
1871
+ }
1872
+ if (!isRecord2(raw)) {
1873
+ throw new Error("Experiment object field must be an object.");
1874
+ }
1875
+ return raw;
1876
+ }
1877
+ function readOptionalStringRecord(raw, location) {
1878
+ if (raw === void 0) {
1879
+ return void 0;
1880
+ }
1881
+ if (!isRecord2(raw)) {
1882
+ throw new Error(`Experiment ${location} must be an object.`);
1883
+ }
1884
+ const entries = Object.entries(raw);
1885
+ if (!entries.every((entry) => typeof entry[1] === "string")) {
1886
+ throw new Error(`Experiment ${location} values must be strings.`);
1887
+ }
1888
+ return Object.fromEntries(entries);
1889
+ }
1890
+ function isRecord2(value) {
1891
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1892
+ }
1893
+ function toStableJsonValue(value) {
1894
+ if (typeof value === "function") {
1895
+ return "[function]";
1896
+ }
1897
+ if (value === null || typeof value !== "object") {
1898
+ return value;
1899
+ }
1900
+ if (Array.isArray(value)) {
1901
+ return value.map(toStableJsonValue);
1902
+ }
1903
+ const record = value;
1904
+ return Object.fromEntries(
1905
+ Object.keys(record).sort().map((key) => [key, toStableJsonValue(record[key])])
1906
+ );
1907
+ }
1481
1908
  function codeGraderInstruction(graderName, description) {
1482
1909
  const desc = description ? ` This grader: ${description}.` : "";
1483
1910
  return `Run \`agentv eval assert ${graderName} --agent-output <agent_output> --agent-input <original_prompt>\` and check the result.${desc} The command accepts --agent-output (the agent's full response text) and --agent-input (the original user prompt). It returns JSON on stdout: {"score": 0-1, "reasoning": "..."}. A score >= 0.5 means pass (exit 0); below 0.5 means fail (exit 1).`;
@@ -1717,7 +2144,7 @@ function transpileEvalYaml(suite, source = "EVAL.yaml") {
1717
2144
  function transpileEvalYamlFile(evalYamlPath) {
1718
2145
  const content = readFileSync(evalYamlPath, "utf8");
1719
2146
  const parsed = parseYamlValue(content);
1720
- return transpileEvalYaml(parsed, path.basename(evalYamlPath));
2147
+ return transpileEvalYaml(parsed, path2.basename(evalYamlPath));
1721
2148
  }
1722
2149
  function getOutputFilenames(result) {
1723
2150
  const names = /* @__PURE__ */ new Map();
@@ -1782,7 +2209,7 @@ async function releaseUnselectedPoolSlots(setup, selectedSlotPath) {
1782
2209
  }
1783
2210
  }
1784
2211
  async function prepareEvalWorkspace(options) {
1785
- const evalPath = path2.resolve(options.testFilePath);
2212
+ const evalPath = path3.resolve(options.testFilePath);
1786
2213
  const evalRunId = randomUUID();
1787
2214
  const evalCases = options.evalCases ?? await loadTests(evalPath, options.repoRoot, {
1788
2215
  verbose: options.verbose,
@@ -1794,7 +2221,7 @@ async function prepareEvalWorkspace(options) {
1794
2221
  filter: options.filter,
1795
2222
  evalPath
1796
2223
  });
1797
- const evalDir = path2.dirname(evalPath);
2224
+ const evalDir = path3.dirname(evalPath);
1798
2225
  const workers = options.maxConcurrency ?? 1;
1799
2226
  const retainOnSuccess = options.retainOnSuccess ?? (options.keepWorkspaces ? "keep" : "cleanup");
1800
2227
  const retainOnFailure = options.retainOnFailure ?? (options.cleanupWorkspaces ? "cleanup" : "keep");
@@ -1905,6 +2332,13 @@ var AgentVConfigSchema = external_exports.object({
1905
2332
  /** Response cache directory */
1906
2333
  path: external_exports.string().optional()
1907
2334
  }).optional(),
2335
+ /** Compatibility shorthand for experiments.default */
2336
+ defaultExperiment: external_exports.string().optional(),
2337
+ /** Experiment defaults */
2338
+ experiments: external_exports.object({
2339
+ /** Default experiment label or path used when eval runs omit --experiment */
2340
+ default: external_exports.string().optional()
2341
+ }).strict().optional(),
1908
2342
  /** Cost and duration limits */
1909
2343
  limits: external_exports.object({
1910
2344
  /** Maximum cost per run in USD */
@@ -1937,7 +2371,7 @@ var CONFIG_FILE_NAMES = [
1937
2371
  ];
1938
2372
  async function loadTsConfig(projectRoot) {
1939
2373
  const { existsSync: existsSync3 } = await import("node:fs");
1940
- const { pathToFileURL } = await import("node:url");
2374
+ const { pathToFileURL: pathToFileURL2 } = await import("node:url");
1941
2375
  const { join } = await import("node:path");
1942
2376
  for (const fileName of CONFIG_FILE_NAMES) {
1943
2377
  const filePath = join(projectRoot, fileName);
@@ -1945,7 +2379,7 @@ async function loadTsConfig(projectRoot) {
1945
2379
  continue;
1946
2380
  }
1947
2381
  try {
1948
- const fileUrl = pathToFileURL(filePath).href;
2382
+ const fileUrl = pathToFileURL2(filePath).href;
1949
2383
  const mod = await import(fileUrl);
1950
2384
  const config = mod.default ?? mod;
1951
2385
  return AgentVConfigSchema.parse(config);
@@ -2062,11 +2496,11 @@ async function scanRepoDeps(evalFilePaths) {
2062
2496
  return { repos: [...seen.values()], errors };
2063
2497
  }
2064
2498
  async function extractReposFromEvalFile(filePath) {
2065
- const content = await readFile(filePath, "utf8");
2499
+ const content = await readFile2(filePath, "utf8");
2066
2500
  const parsed = interpolateEnv(parseYamlValue(content), process.env);
2067
2501
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
2068
2502
  const obj = parsed;
2069
- const evalFileDir = path3.dirname(path3.resolve(filePath));
2503
+ const evalFileDir = path4.dirname(path4.resolve(filePath));
2070
2504
  const repos = [];
2071
2505
  const suiteRepos = await extractReposFromWorkspaceRaw(obj.workspace, evalFileDir);
2072
2506
  repos.push(...suiteRepos);
@@ -2082,8 +2516,8 @@ async function extractReposFromEvalFile(filePath) {
2082
2516
  }
2083
2517
  async function extractReposFromWorkspaceRaw(raw, evalFileDir) {
2084
2518
  if (typeof raw === "string") {
2085
- const workspaceFilePath = path3.resolve(evalFileDir, raw);
2086
- const content = await readFile(workspaceFilePath, "utf8");
2519
+ const workspaceFilePath = path4.resolve(evalFileDir, raw);
2520
+ const content = await readFile2(workspaceFilePath, "utf8");
2087
2521
  const parsed = interpolateEnv(parseYamlValue(content), process.env);
2088
2522
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
2089
2523
  return extractReposFromObject(parsed);
@@ -2125,11 +2559,126 @@ var GIT_ENV_INHERIT_ALLOWLIST = /* @__PURE__ */ new Set([
2125
2559
  "GIT_USERNAME"
2126
2560
  ]);
2127
2561
  var DEFAULT_RESULTS_BRANCH = AGENTV_RESULTS_PRIMARY_REF;
2128
- var MANAGED_RESULTS_REMOTE = "agentv-results";
2129
2562
  var GIT_EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
2130
2563
  var RESULTS_REPO_GENESIS_MESSAGE = "chore(results): initialize AgentV results branch";
2131
2564
  var RESULTS_REPO_GENESIS_DATE = "@0 +0000";
2132
2565
  var RESULT_INDEX_FILENAME2 = "index.jsonl";
2566
+ var RESULTS_REPO_GITATTRIBUTES_FILE = ".gitattributes";
2567
+ var RESULTS_REPO_GITATTRIBUTES_CONTENT = `# Managed by AgentV. Artifact-aware merge so results sync never force-pushes.
2568
+ # Append-only run index: union concurrent appends (lines are orthogonal).
2569
+ index.jsonl merge=union
2570
+ # Editable run overlay (tags/feedback): 3-way JSON set/field union via the
2571
+ # agentv-json driver; a genuine scalar conflict falls through to a human merge.
2572
+ metadata/runs/**/*.json merge=agentv-json
2573
+ `;
2574
+ var RESULTS_JSON_MERGE_DRIVER_NAME = "agentv-json";
2575
+ var RESULTS_JSON_MERGE_DRIVER_SCRIPT = `#!/usr/bin/env node
2576
+ // AgentV results overlay merge driver (merge=agentv-json).
2577
+ //
2578
+ // Performs a 3-way merge of AgentV's editable JSON overlay files (run tags /
2579
+ // feedback). Tag lists merge as a 3-way set so concurrent add/remove are
2580
+ // commutative; display-only bookkeeping scalars that always differ between
2581
+ // writers (updated_at) are regenerated to the larger value instead of
2582
+ // conflicting. Content-derived concurrency tokens (tag_revision) are dropped on
2583
+ // merge so the reader recomputes a token matching the merged content rather than
2584
+ // carrying a stale token that could bypass optimistic-concurrency checks. Any
2585
+ // other genuinely diverging scalar exits non-zero, leaving the file conflicted
2586
+ // so the caller can route it to a human GitHub merge.
2587
+ //
2588
+ // Invoked by git as: node json-merge-driver.mjs %O %A %B %P
2589
+ // %O ancestor, %A current (overwritten with the result), %B other, %P path.
2590
+ import { readFileSync, writeFileSync } from 'node:fs';
2591
+
2592
+ const [, , ancestorFile, currentFile, otherFile] = process.argv;
2593
+ const CONFLICT = Symbol('conflict');
2594
+ // Scalars that legitimately differ on every write and carry no merge meaning.
2595
+ const REGENERABLE_SCALARS = new Set(['updated_at']);
2596
+ // Content-derived tokens (a hash of {tags, updated_at}) that guard optimistic
2597
+ // concurrency. Carrying either side's pre-merge token forward would let a stale
2598
+ // client whose token happens to match bypass the concurrency check and silently
2599
+ // overwrite the merged set, so we drop them and let the reader recompute a token
2600
+ // that matches the merged content.
2601
+ const DROP_ON_MERGE = new Set(['tag_revision']);
2602
+
2603
+ function readJson(file) {
2604
+ try {
2605
+ return JSON.parse(readFileSync(file, 'utf8'));
2606
+ } catch {
2607
+ return undefined;
2608
+ }
2609
+ }
2610
+
2611
+ function isPrimitiveArray(value) {
2612
+ return (
2613
+ Array.isArray(value) &&
2614
+ value.every((entry) => entry === null || ['string', 'number', 'boolean'].includes(typeof entry))
2615
+ );
2616
+ }
2617
+
2618
+ function isPlainObject(value) {
2619
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
2620
+ }
2621
+
2622
+ function mergeSet(base, a, b) {
2623
+ const baseSet = new Set((isPrimitiveArray(base) ? base : []).map(String));
2624
+ const aSet = new Set(a.map(String));
2625
+ const bSet = new Set(b.map(String));
2626
+ const removed = new Set();
2627
+ for (const value of baseSet) {
2628
+ if (!aSet.has(value) || !bSet.has(value)) removed.add(value);
2629
+ }
2630
+ const merged = [];
2631
+ const seen = new Set();
2632
+ for (const value of [...a, ...b]) {
2633
+ const key = String(value);
2634
+ if (!removed.has(key) && !seen.has(key)) {
2635
+ seen.add(key);
2636
+ merged.push(value);
2637
+ }
2638
+ }
2639
+ return merged;
2640
+ }
2641
+
2642
+ function merge3(base, a, b, key) {
2643
+ if (JSON.stringify(a) === JSON.stringify(b)) return a;
2644
+ if (JSON.stringify(a) === JSON.stringify(base)) return b;
2645
+ if (JSON.stringify(b) === JSON.stringify(base)) return a;
2646
+ if (isPrimitiveArray(a) && isPrimitiveArray(b)) return mergeSet(base, a, b);
2647
+ if (isPlainObject(a) && isPlainObject(b)) {
2648
+ const keys = new Set([
2649
+ ...Object.keys(a),
2650
+ ...Object.keys(b),
2651
+ ...(isPlainObject(base) ? Object.keys(base) : []),
2652
+ ]);
2653
+ const out = {};
2654
+ for (const childKey of keys) {
2655
+ if (DROP_ON_MERGE.has(childKey)) continue;
2656
+ const merged = merge3(
2657
+ isPlainObject(base) ? base[childKey] : undefined,
2658
+ a[childKey],
2659
+ b[childKey],
2660
+ childKey,
2661
+ );
2662
+ if (merged === CONFLICT) return CONFLICT;
2663
+ if (merged !== undefined) out[childKey] = merged;
2664
+ }
2665
+ return out;
2666
+ }
2667
+ if (REGENERABLE_SCALARS.has(key)) {
2668
+ return [a, b].filter((value) => value !== undefined).sort().pop();
2669
+ }
2670
+ return CONFLICT;
2671
+ }
2672
+
2673
+ const ancestor = readJson(ancestorFile) ?? {};
2674
+ const current = readJson(currentFile);
2675
+ const other = readJson(otherFile);
2676
+ if (current === undefined || other === undefined) process.exit(1);
2677
+ const result = merge3(ancestor, current, other, '');
2678
+ if (result === CONFLICT) process.exit(1);
2679
+ writeFileSync(currentFile, \`\${JSON.stringify(result, null, 2)}\\n\`);
2680
+ process.exit(0);
2681
+ `;
2133
2682
  var activeResultsRepoSyncs = /* @__PURE__ */ new Set();
2134
2683
  function sanitizeRepoSlug(repo) {
2135
2684
  return repo.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
@@ -2144,13 +2693,13 @@ function withFriendlyGitHubAuthError(error) {
2144
2693
  }
2145
2694
  function expandHome(p) {
2146
2695
  if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
2147
- return path4.join(os.homedir(), p.slice(1));
2696
+ return path5.join(os.homedir(), p.slice(1));
2148
2697
  }
2149
2698
  return p;
2150
2699
  }
2151
2700
  function resolveLocalPath(p, baseDir) {
2152
2701
  const expanded = expandHome(p);
2153
- return path4.isAbsolute(expanded) ? expanded : path4.resolve(baseDir, expanded);
2702
+ return path5.isAbsolute(expanded) ? expanded : path5.resolve(baseDir, expanded);
2154
2703
  }
2155
2704
  function normalizeResultsConfig(config, options) {
2156
2705
  const baseDir = options?.baseDir ?? process.cwd();
@@ -2160,12 +2709,21 @@ function normalizeResultsConfig(config, options) {
2160
2709
  const repo = repoUrl ?? repoPath ?? "";
2161
2710
  const branch = config.branch?.trim() || (repoPath ? DEFAULT_RESULTS_BRANCH : void 0);
2162
2711
  const useStorageBranchWorktree = Boolean(repoPath || repoUrl && explicitClonePath && branch);
2163
- const remote = config.remote?.trim() || (repoUrl && useStorageBranchWorktree ? MANAGED_RESULTS_REMOTE : "origin");
2712
+ const remote = config.remote?.trim() || "origin";
2164
2713
  const autoPush = config.sync?.auto_push ?? config.auto_push === true;
2165
2714
  const requirePush = config.sync?.require_push === true;
2166
- const pushConflictPolicy = config.sync?.push_conflict_policy ?? "block";
2715
+ const configuredPushConflictPolicy = config.sync?.push_conflict_policy;
2716
+ if (configuredPushConflictPolicy === "backup_and_force_push") {
2717
+ throw new Error(
2718
+ "results.sync.push_conflict_policy: 'backup_and_force_push' is no longer supported. Remove the field or set it to 'block'; AgentV never force-pushes result branches."
2719
+ );
2720
+ }
2721
+ if (configuredPushConflictPolicy !== void 0 && configuredPushConflictPolicy !== "block") {
2722
+ throw new Error("results.sync.push_conflict_policy must be 'block'");
2723
+ }
2724
+ const pushConflictPolicy = configuredPushConflictPolicy ?? "block";
2167
2725
  const resolvedRepoPath = repoPath ? resolveLocalPath(repoPath, baseDir) : void 0;
2168
- const resolvedPath = explicitClonePath ? resolveLocalPath(explicitClonePath, baseDir) : repoUrl ? path4.join(getAgentvDataDir(), "results", sanitizeRepoSlug(repoUrl)) : resolvedRepoPath ?? path4.join(getAgentvDataDir(), "results", sanitizeRepoSlug(repo));
2726
+ const resolvedPath = explicitClonePath ? resolveLocalPath(explicitClonePath, baseDir) : repoUrl ? path5.join(getAgentvDataDir(), "results", sanitizeRepoSlug(repoUrl)) : resolvedRepoPath ?? path5.join(getAgentvDataDir(), "results", sanitizeRepoSlug(repo));
2169
2727
  return {
2170
2728
  mode: "github",
2171
2729
  repo,
@@ -2181,18 +2739,26 @@ function normalizeResultsConfig(config, options) {
2181
2739
  storageBranchWorktree: useStorageBranchWorktree
2182
2740
  };
2183
2741
  }
2742
+ var GITHUB_OWNER_REPO_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
2743
+ function isExplicitRemoteUrl(value) {
2744
+ return value.includes("://") || value.startsWith("git@");
2745
+ }
2184
2746
  function resolveResultsRepoUrl(repo) {
2185
- if (repo.includes("://") || repo.startsWith("git@")) {
2186
- return repo;
2747
+ const trimmed = repo.trim();
2748
+ if (isExplicitRemoteUrl(trimmed)) {
2749
+ return trimmed;
2750
+ }
2751
+ if (GITHUB_OWNER_REPO_SHORTHAND.test(trimmed)) {
2752
+ return `https://github.com/${trimmed}.git`;
2187
2753
  }
2188
- return `https://github.com/${repo}.git`;
2754
+ return trimmed;
2189
2755
  }
2190
2756
  function getResultsRepoLocalPaths(repo) {
2191
- const rootDir = path4.join(getAgentvDataDir(), "cache", "results-repo", sanitizeRepoSlug(repo));
2757
+ const rootDir = path5.join(getAgentvDataDir(), "cache", "results-repo", sanitizeRepoSlug(repo));
2192
2758
  return {
2193
2759
  rootDir,
2194
- repoDir: path4.join(rootDir, "repo"),
2195
- statusFile: path4.join(rootDir, "status.json")
2760
+ repoDir: path5.join(rootDir, "repo"),
2761
+ statusFile: path5.join(rootDir, "status.json")
2196
2762
  };
2197
2763
  }
2198
2764
  function usesStorageBranchWorktree(config) {
@@ -2209,7 +2775,7 @@ function readPersistedStatus(statusFile) {
2209
2775
  }
2210
2776
  }
2211
2777
  function writePersistedStatus(statusFile, status) {
2212
- mkdirSync(path4.dirname(statusFile), { recursive: true });
2778
+ mkdirSync(path5.dirname(statusFile), { recursive: true });
2213
2779
  writeFileSync(statusFile, `${JSON.stringify(status, null, 2)}
2214
2780
  `, "utf8");
2215
2781
  }
@@ -2447,22 +3013,95 @@ async function resolveGitTopLevel(repoDir) {
2447
3013
  const { stdout } = await runGit(["rev-parse", "--show-toplevel"], { cwd: repoDir });
2448
3014
  return stdout.trim() || repoDir;
2449
3015
  }
2450
- async function ensureResultsRepoRemote(repoDir, config) {
2451
- if (!config.repo_url) {
3016
+ async function ensureResultsMergeConfig(repoDir) {
3017
+ let gitDir;
3018
+ try {
3019
+ const { stdout } = await runGit(["rev-parse", "--git-common-dir"], { cwd: repoDir });
3020
+ const resolved = stdout.trim();
3021
+ gitDir = resolved ? path5.isAbsolute(resolved) ? resolved : path5.join(repoDir, resolved) : path5.join(repoDir, ".git");
3022
+ } catch {
2452
3023
  return;
2453
3024
  }
2454
- const remoteUrl = resolveResultsRepoUrl(config.repo_url);
2455
- const { stdout } = await runGit(["remote", "get-url", config.remote], {
3025
+ const driverScriptPath = path5.join(gitDir, "agentv", "json-merge-driver.mjs");
3026
+ let scriptCurrent = false;
3027
+ try {
3028
+ scriptCurrent = readFileSync2(driverScriptPath, "utf8") === RESULTS_JSON_MERGE_DRIVER_SCRIPT;
3029
+ } catch {
3030
+ scriptCurrent = false;
3031
+ }
3032
+ if (!scriptCurrent) {
3033
+ mkdirSync(path5.dirname(driverScriptPath), { recursive: true });
3034
+ writeFileSync(driverScriptPath, RESULTS_JSON_MERGE_DRIVER_SCRIPT, { mode: 493 });
3035
+ }
3036
+ const driverCommand = `node ${JSON.stringify(driverScriptPath)} %O %A %B %P`;
3037
+ await runGit(
3038
+ ["config", `merge.${RESULTS_JSON_MERGE_DRIVER_NAME}.name`, "AgentV results overlay JSON union"],
3039
+ { cwd: repoDir, check: false }
3040
+ );
3041
+ await runGit(["config", `merge.${RESULTS_JSON_MERGE_DRIVER_NAME}.driver`, driverCommand], {
2456
3042
  cwd: repoDir,
2457
3043
  check: false
2458
3044
  });
2459
- const existingUrl = stdout.trim();
2460
- if (!existingUrl) {
2461
- await runGit(["remote", "add", config.remote, remoteUrl], { cwd: repoDir });
3045
+ writeResultsInfoAttributes(gitDir);
3046
+ }
3047
+ var RESULTS_INFO_ATTRIBUTES_BEGIN = "# >>> agentv results merge attributes >>>";
3048
+ var RESULTS_INFO_ATTRIBUTES_END = "# <<< agentv results merge attributes <<<";
3049
+ function writeResultsInfoAttributes(gitDir) {
3050
+ const infoDir = path5.join(gitDir, "info");
3051
+ const attributesPath = path5.join(infoDir, "attributes");
3052
+ const managedBlock = `${RESULTS_INFO_ATTRIBUTES_BEGIN}
3053
+ ${RESULTS_REPO_GITATTRIBUTES_CONTENT.trimEnd()}
3054
+ ${RESULTS_INFO_ATTRIBUTES_END}
3055
+ `;
3056
+ let existing = "";
3057
+ try {
3058
+ existing = readFileSync2(attributesPath, "utf8");
3059
+ } catch {
3060
+ existing = "";
3061
+ }
3062
+ const blockPattern = new RegExp(
3063
+ `${escapeRegExp(RESULTS_INFO_ATTRIBUTES_BEGIN)}[\\s\\S]*?${escapeRegExp(
3064
+ RESULTS_INFO_ATTRIBUTES_END
3065
+ )}\\n?`
3066
+ );
3067
+ const withoutManaged = existing.replace(blockPattern, "");
3068
+ const next = withoutManaged.length > 0 && !withoutManaged.endsWith("\n") ? `${withoutManaged}
3069
+ ${managedBlock}` : `${withoutManaged}${managedBlock}`;
3070
+ if (next === existing) {
3071
+ return;
3072
+ }
3073
+ mkdirSync(infoDir, { recursive: true });
3074
+ writeFileSync(attributesPath, next);
3075
+ }
3076
+ function escapeRegExp(value) {
3077
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3078
+ }
3079
+ async function addResultsGitattributesToIndex(repoDir, indexEnv) {
3080
+ const { stdout: existing } = await runGit(
3081
+ ["cat-file", "-p", `:0:${RESULTS_REPO_GITATTRIBUTES_FILE}`],
3082
+ { cwd: repoDir, env: indexEnv, check: false }
3083
+ );
3084
+ if (existing === RESULTS_REPO_GITATTRIBUTES_CONTENT) {
2462
3085
  return;
2463
3086
  }
2464
- if (existingUrl !== remoteUrl) {
2465
- await runGit(["remote", "set-url", config.remote, remoteUrl], { cwd: repoDir });
3087
+ const scratchDir = mkdtempSync(path5.join(os.tmpdir(), "agentv-results-attrs-"));
3088
+ try {
3089
+ const scratchFile = path5.join(scratchDir, RESULTS_REPO_GITATTRIBUTES_FILE);
3090
+ writeFileSync(scratchFile, RESULTS_REPO_GITATTRIBUTES_CONTENT);
3091
+ const { stdout: blob } = await runGit(["hash-object", "-w", "--no-filters", scratchFile], {
3092
+ cwd: repoDir
3093
+ });
3094
+ await runGit(
3095
+ [
3096
+ "update-index",
3097
+ "--add",
3098
+ "--cacheinfo",
3099
+ `100644,${blob.trim()},${RESULTS_REPO_GITATTRIBUTES_FILE}`
3100
+ ],
3101
+ { cwd: repoDir, env: indexEnv }
3102
+ );
3103
+ } finally {
3104
+ rmSync(scratchDir, { recursive: true, force: true });
2466
3105
  }
2467
3106
  }
2468
3107
  function updateStatusFile(config, patch) {
@@ -2480,24 +3119,27 @@ async function ensureResultsRepoClone(config) {
2480
3119
  if (!await isGitRepository(normalized.repo_path)) {
2481
3120
  throw new Error(`Results repo_path is not a git repository: ${normalized.repo_path}`);
2482
3121
  }
2483
- return resolveGitTopLevel(normalized.repo_path);
3122
+ const topLevel = await resolveGitTopLevel(normalized.repo_path);
3123
+ await ensureResultsMergeConfig(topLevel);
3124
+ return topLevel;
2484
3125
  }
2485
3126
  const cachePaths = getResultsRepoLocalPaths(normalized.repo);
2486
3127
  const cloneDir = normalized.path;
2487
3128
  mkdirSync(cachePaths.rootDir, { recursive: true });
2488
- mkdirSync(path4.dirname(cloneDir), { recursive: true });
3129
+ mkdirSync(path5.dirname(cloneDir), { recursive: true });
2489
3130
  const cloneMissing = !existsSync(cloneDir);
2490
- const gitDir = path4.join(cloneDir, ".git");
3131
+ const gitDir = path5.join(cloneDir, ".git");
2491
3132
  const cloneEmpty = !cloneMissing && !existsSync(gitDir) && (await readdir(cloneDir)).length === 0;
2492
3133
  if (cloneMissing || cloneEmpty) {
2493
3134
  try {
2494
3135
  await runGit([
2495
3136
  "clone",
2496
3137
  "--filter=blob:none",
3138
+ ...normalized.remote === "origin" ? [] : ["--origin", normalized.remote],
2497
3139
  resolveResultsRepoUrl(normalized.repo_url ?? normalized.repo),
2498
3140
  cloneDir
2499
3141
  ]);
2500
- await ensureResultsRepoRemote(cloneDir, normalized);
3142
+ await ensureResultsMergeConfig(cloneDir);
2501
3143
  return cloneDir;
2502
3144
  } catch (error) {
2503
3145
  updateStatusFile(normalized, { last_error: withFriendlyGitHubAuthError(error).message });
@@ -2507,7 +3149,7 @@ async function ensureResultsRepoClone(config) {
2507
3149
  if (!existsSync(gitDir)) {
2508
3150
  throw new Error(`Results repo clone path is not a git repository: ${cloneDir}`);
2509
3151
  }
2510
- await ensureResultsRepoRemote(cloneDir, normalized);
3152
+ await ensureResultsMergeConfig(cloneDir);
2511
3153
  return cloneDir;
2512
3154
  }
2513
3155
  function getResultsRepoStatus(config) {
@@ -2653,7 +3295,7 @@ async function getStorageBranchWorktreeDirtyPaths(repoDir, config) {
2653
3295
  const comparisonRef = await gitRefExists(repoDir, localRef) ? localRef : await gitRefExists(repoDir, remoteRef) ? remoteRef : void 0;
2654
3296
  const dirtyPaths = [];
2655
3297
  for (const gitPath of safeDirtyPaths) {
2656
- const absolutePath = path4.join(repoDir, ...gitPath.split("/"));
3298
+ const absolutePath = path5.join(repoDir, ...gitPath.split("/"));
2657
3299
  if (!existsSync(absolutePath)) {
2658
3300
  if (await readGitText(repoDir, comparisonRef, gitPath) !== void 0) {
2659
3301
  dirtyPaths.push(gitPath);
@@ -2759,7 +3401,7 @@ async function hasInProgressGitConflict(repoDir) {
2759
3401
  check: false
2760
3402
  });
2761
3403
  const markerPath = stdout.trim();
2762
- const resolvedMarkerPath = path4.isAbsolute(markerPath) ? markerPath : path4.join(repoDir, markerPath);
3404
+ const resolvedMarkerPath = path5.isAbsolute(markerPath) ? markerPath : path5.join(repoDir, markerPath);
2763
3405
  if (markerPath && existsSync(resolvedMarkerPath)) {
2764
3406
  return true;
2765
3407
  }
@@ -2859,6 +3501,17 @@ var ResultsBranchPushConflictError = class extends Error {
2859
3501
  this.name = "ResultsBranchPushConflictError";
2860
3502
  }
2861
3503
  };
3504
+ function pendingMergeToWire(details) {
3505
+ return {
3506
+ temp_branch: details.tempBranch,
3507
+ target_branch: details.targetBranch,
3508
+ ...details.compareUrl !== void 0 && { compare_url: details.compareUrl },
3509
+ ...details.contributedRunCount !== void 0 && {
3510
+ contributed_run_count: details.contributedRunCount
3511
+ },
3512
+ created_at: details.createdAt
3513
+ };
3514
+ }
2862
3515
  function pushDetailsToWire(details) {
2863
3516
  if (!details) {
2864
3517
  return {};
@@ -2876,7 +3529,11 @@ function pushDetailsToWire(details) {
2876
3529
  ...details.forcePushedCommit !== void 0 && {
2877
3530
  force_pushed_commit: details.forcePushedCommit
2878
3531
  },
2879
- ...details.leaseCommit !== void 0 && { lease_commit: details.leaseCommit }
3532
+ ...details.leaseCommit !== void 0 && { lease_commit: details.leaseCommit },
3533
+ ...details.autoMergedRemote === true && { auto_merged_remote: true },
3534
+ ...details.pendingMerge !== void 0 && {
3535
+ pending_merge: pendingMergeToWire(details.pendingMerge)
3536
+ }
2880
3537
  };
2881
3538
  }
2882
3539
  function withBlockedStatus(status, blockReason, flags) {
@@ -2894,11 +3551,13 @@ function withPushConflictStatus(status, blockReason, details, flags) {
2894
3551
  return withBlockedStatus(
2895
3552
  {
2896
3553
  ...status,
2897
- sync_status: "push_conflict"
3554
+ sync_status: flags.syncStatus ?? "push_conflict"
2898
3555
  },
2899
3556
  blockReason,
2900
3557
  {
2901
- ...flags,
3558
+ pullPerformed: flags.pullPerformed,
3559
+ pushPerformed: flags.pushPerformed,
3560
+ commitCreated: flags.commitCreated,
2902
3561
  pushDetails: details
2903
3562
  }
2904
3563
  );
@@ -2914,12 +3573,12 @@ function withActionFlags(status, flags) {
2914
3573
  };
2915
3574
  }
2916
3575
  function isSafeResultsRepoPath(p) {
2917
- return RESULTS_REPO_TRACKED_DIRS.some((dir) => p === dir || p.startsWith(`${dir}/`));
3576
+ return p === RESULTS_REPO_GITATTRIBUTES_FILE || RESULTS_REPO_TRACKED_DIRS.some((dir) => p === dir || p.startsWith(`${dir}/`));
2918
3577
  }
2919
3578
  async function existingTrackedResultsDirs(repoDir) {
2920
3579
  const targets = [];
2921
3580
  for (const dir of RESULTS_REPO_TRACKED_DIRS) {
2922
- if (existsSync(path4.join(repoDir, dir))) {
3581
+ if (existsSync(path5.join(repoDir, dir))) {
2923
3582
  targets.push(dir);
2924
3583
  continue;
2925
3584
  }
@@ -2947,19 +3606,8 @@ function getPushTargetBranch(upstream, baseBranch, remote = "origin") {
2947
3606
  const prefix = `${remote}/`;
2948
3607
  return upstream?.startsWith(prefix) ? upstream.slice(prefix.length) : baseBranch;
2949
3608
  }
2950
- function timestampForBackupRef(date = /* @__PURE__ */ new Date()) {
2951
- const pad = (value) => String(value).padStart(2, "0");
2952
- return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}T${pad(
2953
- date.getUTCHours()
2954
- )}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
2955
- }
2956
- function slugifyBackupTargetBranch(branch) {
2957
- return branch.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "results";
2958
- }
2959
- function buildResultsBackupRef(targetBranch, remoteCommit) {
2960
- return `agentv/backups/${timestampForBackupRef()}-${slugifyBackupTargetBranch(
2961
- targetBranch
2962
- )}-${remoteCommit.slice(0, 7)}`;
3609
+ function formatShortSha(sha) {
3610
+ return sha ? sha.slice(0, 12) : "unknown";
2963
3611
  }
2964
3612
  async function getCommitSha(repoDir, ref) {
2965
3613
  if (!ref) {
@@ -2976,91 +3624,309 @@ function isNonFastForwardPushError(error) {
2976
3624
  const text = gitErrorText(error);
2977
3625
  return text.includes("non-fast-forward") || text.includes("fetch first") || text.includes("tip is behind its remote") || text.includes("note about fast-forwards") || text.includes("stale info");
2978
3626
  }
2979
- function formatShortSha(sha) {
2980
- return sha ? sha.slice(0, 12) : "unknown";
3627
+ var RESULTS_PUSH_MERGE_MAX_ATTEMPTS = 5;
3628
+ async function isAncestorCommit(repoDir, ancestor, descendant) {
3629
+ const { exitCode } = await runGit(["merge-base", "--is-ancestor", ancestor, descendant], {
3630
+ cwd: repoDir,
3631
+ check: false
3632
+ });
3633
+ return exitCode === 0;
2981
3634
  }
2982
- function buildBlockedPushConflictReason(details) {
2983
- return `Results branch push conflict on ${details.targetBranch}: remote ${formatShortSha(
2984
- details.remoteCommit
2985
- )}, local ${formatShortSha(details.localCommit)}. Configure results.sync.push_conflict_policy: backup_and_force_push to back up the remote ref before replacing it.`;
3635
+ async function getMergeBaseCommit(repoDir, a, b) {
3636
+ const { stdout, exitCode } = await runGit(["merge-base", a, b], { cwd: repoDir, check: false });
3637
+ const sha = stdout.trim();
3638
+ return exitCode === 0 && sha.length > 0 ? sha : void 0;
2986
3639
  }
2987
- async function resolveResultBranchPushConflict(params) {
2988
- await fetchResultsRepo(params.repoDir, params.normalized.remote, params.targetBranch);
2989
- const remoteRef = remoteBranchRef(params.targetBranch, params.normalized.remote);
2990
- const remoteCommit = await getCommitSha(params.repoDir, remoteRef);
2991
- const localCommit = await getCommitSha(params.repoDir, params.sourceRef);
2992
- const baseDetails = {
2993
- pushConflictPolicy: params.normalized.push_conflict_policy,
2994
- targetBranch: params.targetBranch,
2995
- ...remoteCommit !== void 0 && {
2996
- remoteCommit,
2997
- previousRemoteCommit: remoteCommit,
2998
- leaseCommit: remoteCommit
2999
- },
3000
- ...localCommit !== void 0 && { localCommit }
3001
- };
3002
- if (!remoteCommit) {
3003
- return {
3004
- blocked: true,
3005
- blockReason: `Results branch push conflict on ${params.targetBranch}: remote commit could not be resolved after fetch`,
3006
- details: baseDetails
3007
- };
3640
+ function buildNeedsHumanMergeReason(targetBranch, remoteCommit, localCommit) {
3641
+ return `Results branch ${targetBranch} diverged from the remote and could not be auto-merged: a genuine content conflict in the editable overlay remains (remote ${formatShortSha(
3642
+ remoteCommit
3643
+ )}, local ${formatShortSha(
3644
+ localCommit
3645
+ )}). The remote branch is unchanged and no history was rewritten; resolve it with a GitHub pull request.`;
3646
+ }
3647
+ function timestampForResultsRef(date = /* @__PURE__ */ new Date()) {
3648
+ const pad = (value) => String(value).padStart(2, "0");
3649
+ return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}T${pad(
3650
+ date.getUTCHours()
3651
+ )}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
3652
+ }
3653
+ function slugifyResultsTargetBranch(branch) {
3654
+ return branch.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "results";
3655
+ }
3656
+ function randomResultsRefToken() {
3657
+ return randomBytes(4).toString("hex").slice(0, 6);
3658
+ }
3659
+ function buildResultsSyncBranchName(targetBranch, token = randomResultsRefToken()) {
3660
+ return `agentv/results-sync/${timestampForResultsRef()}-${slugifyResultsTargetBranch(
3661
+ targetBranch
3662
+ )}-${token}`;
3663
+ }
3664
+ function buildResultsCompareUrl(remoteUrl, targetBranch, syncBranch) {
3665
+ if (!remoteUrl) {
3666
+ return void 0;
3008
3667
  }
3009
- if (params.normalized.push_conflict_policy === "block") {
3010
- return {
3011
- blocked: true,
3012
- blockReason: buildBlockedPushConflictReason(baseDetails),
3013
- details: baseDetails
3014
- };
3668
+ const match = remoteUrl.trim().match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
3669
+ if (!match) {
3670
+ return void 0;
3015
3671
  }
3016
- const backupRef = buildResultsBackupRef(params.targetBranch, remoteCommit);
3017
- const backupDetails = {
3018
- ...baseDetails,
3019
- backupRef,
3020
- backupCommit: remoteCommit
3021
- };
3022
- try {
3023
- await assertValidResultsBranchName(params.repoDir, backupRef);
3024
- await runGit(
3025
- ["push", "--porcelain", params.normalized.remote, `${remoteCommit}:refs/heads/${backupRef}`],
3026
- { cwd: params.repoDir }
3027
- );
3028
- } catch (error) {
3029
- return {
3030
- blocked: true,
3031
- blockReason: `Results branch backup creation failed for ${params.targetBranch} at ${formatShortSha(
3032
- remoteCommit
3033
- )}: ${getStatusMessage(error)}`,
3034
- details: backupDetails
3035
- };
3672
+ const [, owner, repo] = match;
3673
+ return `https://github.com/${owner}/${repo}/compare/${encodeURIComponent(
3674
+ targetBranch
3675
+ )}...${encodeURIComponent(syncBranch)}?expand=1`;
3676
+ }
3677
+ async function resolveRemotePushUrl(repoDir, remote) {
3678
+ for (const args of [
3679
+ ["remote", "get-url", "--push", remote],
3680
+ ["remote", "get-url", remote]
3681
+ ]) {
3682
+ const { stdout, exitCode } = await runGit(args, { cwd: repoDir, check: false });
3683
+ const url = stdout.trim();
3684
+ if (exitCode === 0 && url.length > 0) {
3685
+ return url;
3686
+ }
3036
3687
  }
3688
+ return void 0;
3689
+ }
3690
+ async function countContributedRunDirs(repoDir, remoteTargetCommit, sourceCommit) {
3691
+ if (!remoteTargetCommit) {
3692
+ return void 0;
3693
+ }
3694
+ const { stdout, exitCode } = await runGit(
3695
+ [
3696
+ "diff",
3697
+ "--name-only",
3698
+ `${remoteTargetCommit}...${sourceCommit}`,
3699
+ "--",
3700
+ `${RESULTS_REPO_RUNS_DIR}/`
3701
+ ],
3702
+ { cwd: repoDir, check: false }
3703
+ );
3704
+ if (exitCode !== 0) {
3705
+ return void 0;
3706
+ }
3707
+ const runDirs = /* @__PURE__ */ new Set();
3708
+ for (const line of stdout.split(/\r?\n/)) {
3709
+ const file = line.trim();
3710
+ if (!file) {
3711
+ continue;
3712
+ }
3713
+ const segments = file.split("/");
3714
+ if (segments[0] === RESULTS_REPO_RUNS_DIR && segments.length >= 3) {
3715
+ runDirs.add(`${segments[0]}/${segments[1]}/${segments[2]}`);
3716
+ }
3717
+ }
3718
+ return runDirs.size;
3719
+ }
3720
+ async function pushResultsSyncBranch(params) {
3721
+ const { repoDir, remote, sourceCommit, targetBranch } = params;
3722
+ const remoteTargetCommit = await getCommitSha(repoDir, remoteBranchRef(targetBranch, remote));
3723
+ let syncBranch = buildResultsSyncBranchName(targetBranch);
3724
+ await assertValidResultsBranchName(repoDir, syncBranch);
3037
3725
  try {
3038
- await runGit(
3039
- [
3040
- "push",
3041
- "--porcelain",
3042
- `--force-with-lease=refs/heads/${params.targetBranch}:${remoteCommit}`,
3043
- params.normalized.remote,
3044
- `${params.sourceRef}:refs/heads/${params.targetBranch}`
3045
- ],
3046
- { cwd: params.repoDir }
3047
- );
3726
+ await runGit(["push", "--porcelain", remote, `${sourceCommit}:refs/heads/${syncBranch}`], {
3727
+ cwd: repoDir
3728
+ });
3048
3729
  } catch (error) {
3049
- return {
3050
- blocked: true,
3051
- blockReason: `Results branch force push lease failed for ${params.targetBranch}; remote changed after backup ${backupRef} was created with lease ${formatShortSha(
3052
- remoteCommit
3053
- )}: ${getStatusMessage(error)}`,
3054
- details: backupDetails
3055
- };
3730
+ if (!isRefAlreadyExistsPushError(error)) {
3731
+ throw error;
3732
+ }
3733
+ syncBranch = buildResultsSyncBranchName(targetBranch);
3734
+ await assertValidResultsBranchName(repoDir, syncBranch);
3735
+ await runGit(["push", "--porcelain", remote, `${sourceCommit}:refs/heads/${syncBranch}`], {
3736
+ cwd: repoDir
3737
+ });
3056
3738
  }
3739
+ const compareUrl = buildResultsCompareUrl(
3740
+ await resolveRemotePushUrl(repoDir, remote),
3741
+ targetBranch,
3742
+ syncBranch
3743
+ );
3744
+ const contributedRunCount = await countContributedRunDirs(
3745
+ repoDir,
3746
+ remoteTargetCommit,
3747
+ sourceCommit
3748
+ ).catch(() => void 0);
3057
3749
  return {
3058
- blocked: false,
3059
- details: {
3060
- ...backupDetails,
3061
- ...localCommit !== void 0 && { forcePushedCommit: localCommit }
3750
+ tempBranch: syncBranch,
3751
+ targetBranch,
3752
+ ...compareUrl !== void 0 && { compareUrl },
3753
+ ...contributedRunCount !== void 0 && { contributedRunCount },
3754
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3755
+ };
3756
+ }
3757
+ function isRefAlreadyExistsPushError(error) {
3758
+ const text = gitErrorText(error);
3759
+ return text.includes("already exists") || text.includes("cannot lock ref") || text.includes("reference already exists");
3760
+ }
3761
+ async function mergeRemoteIntoCheckedOutBranch(repoDir, remoteCommit) {
3762
+ const args = ["merge", "--no-edit", "-m", "chore(results): merge remote results", remoteCommit];
3763
+ let result = await runGit(args, {
3764
+ cwd: repoDir,
3765
+ check: false,
3766
+ env: configuredGitCommitIdentityEnv()
3767
+ });
3768
+ if (result.exitCode !== 0 && isMissingGitIdentityError({ stderr: result.stderr })) {
3769
+ await runGit(["merge", "--abort"], { cwd: repoDir, check: false });
3770
+ result = await runGit(args, {
3771
+ cwd: repoDir,
3772
+ check: false,
3773
+ env: { ...configuredGitCommitIdentityEnv(), ...fallbackResultsRepoCommitEnv() }
3774
+ });
3775
+ }
3776
+ return result.exitCode;
3777
+ }
3778
+ async function resolveResultBranchPushConflict(params) {
3779
+ await ensureResultsMergeConfig(params.repoDir);
3780
+ const { repoDir, targetBranch } = params;
3781
+ const remote = params.normalized.remote;
3782
+ const localRef = params.sourceRef;
3783
+ const remoteRef = remoteBranchRef(targetBranch, remote);
3784
+ const branchCheckedOut = params.sourceRef === "HEAD";
3785
+ let lastDetails = {
3786
+ pushConflictPolicy: params.normalized.push_conflict_policy,
3787
+ targetBranch
3788
+ };
3789
+ const buildHumanMergeOutcome = async (reason, details) => {
3790
+ if (!details.localCommit) {
3791
+ return { blocked: true, blockReason: reason, details, syncStatus: "needs_human_merge" };
3792
+ }
3793
+ try {
3794
+ const pendingMerge = await pushResultsSyncBranch({
3795
+ repoDir,
3796
+ remote,
3797
+ sourceCommit: details.localCommit,
3798
+ targetBranch: details.targetBranch
3799
+ });
3800
+ return {
3801
+ blocked: true,
3802
+ blockReason: reason,
3803
+ details: { ...details, pendingMerge },
3804
+ syncStatus: "needs_human_merge"
3805
+ };
3806
+ } catch (error) {
3807
+ return {
3808
+ blocked: true,
3809
+ blockReason: `${reason} (could not create the temp branch for the GitHub merge: ${getStatusMessage(
3810
+ error
3811
+ )})`,
3812
+ details,
3813
+ syncStatus: "needs_human_merge"
3814
+ };
3062
3815
  }
3063
3816
  };
3817
+ for (let attempt = 1; attempt <= RESULTS_PUSH_MERGE_MAX_ATTEMPTS; attempt += 1) {
3818
+ await fetchResultsRepo(repoDir, remote, targetBranch);
3819
+ const remoteCommit = await getCommitSha(repoDir, remoteRef);
3820
+ const localCommit = await getCommitSha(repoDir, localRef);
3821
+ const details = {
3822
+ pushConflictPolicy: params.normalized.push_conflict_policy,
3823
+ targetBranch,
3824
+ ...remoteCommit !== void 0 && { remoteCommit, previousRemoteCommit: remoteCommit },
3825
+ ...localCommit !== void 0 && { localCommit }
3826
+ };
3827
+ lastDetails = details;
3828
+ if (!remoteCommit) {
3829
+ return {
3830
+ blocked: true,
3831
+ blockReason: `Results branch push conflict on ${targetBranch}: remote commit could not be resolved after fetch`,
3832
+ details,
3833
+ syncStatus: "push_conflict"
3834
+ };
3835
+ }
3836
+ if (!localCommit) {
3837
+ return {
3838
+ blocked: true,
3839
+ blockReason: `Results branch push conflict on ${targetBranch}: local commit could not be resolved`,
3840
+ details,
3841
+ syncStatus: "push_conflict"
3842
+ };
3843
+ }
3844
+ if (await isAncestorCommit(repoDir, localCommit, remoteCommit)) {
3845
+ if (branchCheckedOut) {
3846
+ await runGit(["merge", "--ff-only", remoteCommit], { cwd: repoDir, check: false });
3847
+ } else {
3848
+ await runGit(["update-ref", localRef, remoteCommit, localCommit], {
3849
+ cwd: repoDir,
3850
+ check: false
3851
+ });
3852
+ }
3853
+ return { blocked: false, details };
3854
+ }
3855
+ let pushSpec = branchCheckedOut ? "HEAD" : localCommit;
3856
+ let autoMergedRemote = false;
3857
+ if (!await isAncestorCommit(repoDir, remoteCommit, localCommit)) {
3858
+ if (branchCheckedOut) {
3859
+ const exitCode = await mergeRemoteIntoCheckedOutBranch(repoDir, remoteCommit);
3860
+ if (exitCode !== 0) {
3861
+ await runGit(["merge", "--abort"], { cwd: repoDir, check: false });
3862
+ return await buildHumanMergeOutcome(
3863
+ buildNeedsHumanMergeReason(targetBranch, remoteCommit, localCommit),
3864
+ details
3865
+ );
3866
+ }
3867
+ pushSpec = "HEAD";
3868
+ autoMergedRemote = true;
3869
+ } else {
3870
+ const base = await getMergeBaseCommit(repoDir, localCommit, remoteCommit);
3871
+ if (!base) {
3872
+ return await buildHumanMergeOutcome(
3873
+ buildNeedsHumanMergeReason(targetBranch, remoteCommit, localCommit),
3874
+ details
3875
+ );
3876
+ }
3877
+ const merge = await runGit(
3878
+ ["merge-tree", "--write-tree", `--merge-base=${base}`, localCommit, remoteCommit],
3879
+ { cwd: repoDir, check: false }
3880
+ );
3881
+ const mergedTree = merge.stdout.trim().split(/\r?\n/)[0]?.trim();
3882
+ if (merge.exitCode !== 0 || !mergedTree) {
3883
+ return await buildHumanMergeOutcome(
3884
+ buildNeedsHumanMergeReason(targetBranch, remoteCommit, localCommit),
3885
+ details
3886
+ );
3887
+ }
3888
+ const { stdout: mergeCommitOut } = await runGitWithFallbackCommitIdentity(
3889
+ [
3890
+ "commit-tree",
3891
+ mergedTree,
3892
+ "-p",
3893
+ localCommit,
3894
+ "-p",
3895
+ remoteCommit,
3896
+ "-m",
3897
+ "chore(results): merge remote results"
3898
+ ],
3899
+ { cwd: repoDir }
3900
+ );
3901
+ pushSpec = mergeCommitOut.trim();
3902
+ await runGit(["update-ref", localRef, pushSpec, localCommit], { cwd: repoDir });
3903
+ autoMergedRemote = true;
3904
+ }
3905
+ }
3906
+ try {
3907
+ await runGit(["push", "--porcelain", remote, `${pushSpec}:refs/heads/${targetBranch}`], {
3908
+ cwd: repoDir
3909
+ });
3910
+ } catch (error) {
3911
+ if (isNonFastForwardPushError(error)) {
3912
+ continue;
3913
+ }
3914
+ throw error;
3915
+ }
3916
+ const pushedCommit = await getCommitSha(repoDir, pushSpec) ?? localCommit;
3917
+ return {
3918
+ blocked: false,
3919
+ details: {
3920
+ ...details,
3921
+ localCommit: pushedCommit,
3922
+ ...autoMergedRemote && { autoMergedRemote }
3923
+ }
3924
+ };
3925
+ }
3926
+ return await buildHumanMergeOutcome(
3927
+ `Results branch ${targetBranch} could not be reconciled after ${RESULTS_PUSH_MERGE_MAX_ATTEMPTS} attempts because the remote kept advancing; retry sync. The remote branch is unchanged.`,
3928
+ lastDetails
3929
+ );
3064
3930
  }
3065
3931
  async function statusFromInspection(normalized, repoDir) {
3066
3932
  return withGitInspection(
@@ -3087,7 +3953,6 @@ async function getResultsRepoSyncStatus(config) {
3087
3953
  };
3088
3954
  }
3089
3955
  try {
3090
- await ensureResultsRepoRemote(normalized.path, normalized);
3091
3956
  if (usesStorageBranchWorktree(normalized)) {
3092
3957
  await fetchResultsRepo(normalized.path, normalized.remote, normalized.branch).catch(
3093
3958
  () => void 0
@@ -3252,7 +4117,8 @@ async function syncResultsRepoForProject(config) {
3252
4117
  return withPushConflictStatus(status2, outcome.blockReason, outcome.details, {
3253
4118
  pullPerformed,
3254
4119
  pushPerformed,
3255
- commitCreated
4120
+ commitCreated,
4121
+ syncStatus: outcome.syncStatus
3256
4122
  });
3257
4123
  }
3258
4124
  pushPerformed = true;
@@ -3369,7 +4235,8 @@ async function syncResultsRepoForProject(config) {
3369
4235
  return withPushConflictStatus(status2, outcome.blockReason, outcome.details, {
3370
4236
  pullPerformed,
3371
4237
  pushPerformed,
3372
- commitCreated
4238
+ commitCreated,
4239
+ syncStatus: outcome.syncStatus
3373
4240
  });
3374
4241
  }
3375
4242
  } else {
@@ -3502,7 +4369,8 @@ async function syncResultsRepoForProject(config) {
3502
4369
  return withPushConflictStatus(status, outcome.blockReason, outcome.details, {
3503
4370
  pullPerformed,
3504
4371
  pushPerformed,
3505
- commitCreated
4372
+ commitCreated,
4373
+ syncStatus: outcome.syncStatus
3506
4374
  });
3507
4375
  }
3508
4376
  pushPerformed = true;
@@ -3587,7 +4455,8 @@ async function syncResultsRepoForProject(config) {
3587
4455
  return withPushConflictStatus(status, outcome.blockReason, outcome.details, {
3588
4456
  pullPerformed,
3589
4457
  pushPerformed,
3590
- commitCreated
4458
+ commitCreated,
4459
+ syncStatus: outcome.syncStatus
3591
4460
  });
3592
4461
  }
3593
4462
  } else {
@@ -3625,6 +4494,44 @@ async function syncResultsRepoForProject(config) {
3625
4494
  activeResultsRepoSyncs.delete(syncKey);
3626
4495
  }
3627
4496
  }
4497
+ async function fastForwardResultsTowardTarget(repoDir, normalized, targetBranch) {
4498
+ const upstreamRef = remoteBranchRef(targetBranch, normalized.remote);
4499
+ if (!await gitRefExists(repoDir, upstreamRef)) {
4500
+ return;
4501
+ }
4502
+ const upstreamCommit = await getCommitSha(repoDir, upstreamRef);
4503
+ if (!upstreamCommit) {
4504
+ return;
4505
+ }
4506
+ if (usesStorageBranchWorktree(normalized) && normalized.branch) {
4507
+ const localRef = `refs/heads/${normalized.branch}`;
4508
+ if (!await gitRefExists(repoDir, localRef)) {
4509
+ await runGit(["update-ref", localRef, upstreamCommit], { cwd: repoDir, check: false });
4510
+ await runGit(["branch", "--set-upstream-to", upstreamRef, normalized.branch], {
4511
+ cwd: repoDir,
4512
+ check: false
4513
+ });
4514
+ return;
4515
+ }
4516
+ const localCommit = await getCommitSha(repoDir, localRef);
4517
+ if (localCommit && await isAncestorCommit(repoDir, localCommit, upstreamCommit)) {
4518
+ await runGit(["update-ref", localRef, upstreamCommit, localCommit], {
4519
+ cwd: repoDir,
4520
+ check: false
4521
+ });
4522
+ }
4523
+ return;
4524
+ }
4525
+ await runGit(["merge", "--ff-only", upstreamRef], { cwd: repoDir, check: false });
4526
+ }
4527
+ async function confirmResultsMergeAndPull(config) {
4528
+ const normalized = normalizeResultsConfig(config);
4529
+ const repoDir = await ensureResultsRepoClone(normalized);
4530
+ const targetBranch = normalized.branch ?? await resolveDefaultBranch(repoDir);
4531
+ await fetchResultsRepo(repoDir, normalized.remote, normalized.branch).catch(() => void 0);
4532
+ await fastForwardResultsTowardTarget(repoDir, normalized, targetBranch);
4533
+ return syncResultsRepoForProject(config);
4534
+ }
3628
4535
  async function checkoutResultsRepoBranch(config, branchName) {
3629
4536
  const normalized = normalizeResultsConfig(config);
3630
4537
  const repoDir = await ensureResultsRepoClone(normalized);
@@ -3645,8 +4552,8 @@ async function prepareResultsRepoBranch(config, branchName) {
3645
4552
  const cloneDir = await ensureResultsRepoClone(normalized);
3646
4553
  const baseBranch = await resolveDefaultBranch(cloneDir);
3647
4554
  await fetchResultsRepo(cloneDir, normalized.remote);
3648
- const worktreeRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-results-repo-"));
3649
- const worktreeDir = path4.join(worktreeRoot, "repo");
4555
+ const worktreeRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-results-repo-"));
4556
+ const worktreeDir = path5.join(worktreeRoot, "repo");
3650
4557
  await runGit(
3651
4558
  ["worktree", "add", "-B", branchName, worktreeDir, `${normalized.remote}/${baseBranch}`],
3652
4559
  {
@@ -3668,12 +4575,12 @@ async function prepareResultsRepoBranch(config, branchName) {
3668
4575
  }
3669
4576
  async function stageResultsArtifacts(params) {
3670
4577
  rmSync(params.destinationDir, { recursive: true, force: true });
3671
- mkdirSync(path4.dirname(params.destinationDir), { recursive: true });
4578
+ mkdirSync(path5.dirname(params.destinationDir), { recursive: true });
3672
4579
  await cp(params.sourceDir, params.destinationDir, { recursive: true });
3673
4580
  }
3674
4581
  function resolveResultsRepoRunsDir(config) {
3675
4582
  const normalized = normalizeResultsConfig(config);
3676
- return path4.join(normalized.path, RESULTS_REPO_RESULTS_DIR);
4583
+ return path5.join(normalized.path, RESULTS_REPO_RESULTS_DIR);
3677
4584
  }
3678
4585
  async function directorySizeBytes(targetPath) {
3679
4586
  const entry = await stat(targetPath);
@@ -3682,7 +4589,7 @@ async function directorySizeBytes(targetPath) {
3682
4589
  }
3683
4590
  let total = 0;
3684
4591
  for (const child of await readdir(targetPath, { withFileTypes: true })) {
3685
- total += await directorySizeBytes(path4.join(targetPath, child.name));
4592
+ total += await directorySizeBytes(path5.join(targetPath, child.name));
3686
4593
  }
3687
4594
  return total;
3688
4595
  }
@@ -3749,7 +4656,7 @@ async function assertValidResultsBranchName(repoDir, branch) {
3749
4656
  await runGit(["check-ref-format", "--branch", branch], { cwd: repoDir });
3750
4657
  }
3751
4658
  function normalizeDestinationPath(destinationPath) {
3752
- const normalized = destinationPath.split(path4.sep).join("/");
4659
+ const normalized = destinationPath.split(path5.sep).join("/");
3753
4660
  const segments = normalized.split("/").filter(Boolean);
3754
4661
  if (segments.length === 0 || normalized.startsWith("/") || segments.some((segment) => segment === "..") || normalized.split("").some((char) => {
3755
4662
  const code = char.charCodeAt(0);
@@ -3763,7 +4670,7 @@ async function listSourceFiles(sourceDir) {
3763
4670
  const entries = [];
3764
4671
  async function visit(dir) {
3765
4672
  for (const entry of await readdir(dir, { withFileTypes: true })) {
3766
- const absolutePath = path4.join(dir, entry.name);
4673
+ const absolutePath = path5.join(dir, entry.name);
3767
4674
  if (entry.isDirectory()) {
3768
4675
  await visit(absolutePath);
3769
4676
  } else if (entry.isFile() || entry.isSymbolicLink()) {
@@ -3775,16 +4682,16 @@ async function listSourceFiles(sourceDir) {
3775
4682
  entries.sort();
3776
4683
  return entries;
3777
4684
  }
3778
- function isRecord2(value) {
4685
+ function isRecord3(value) {
3779
4686
  return !!value && typeof value === "object" && !Array.isArray(value);
3780
4687
  }
3781
4688
  function artifactSidecarPointers(record) {
3782
- if (!isRecord2(record) || !isRecord2(record.artifact_pointers)) {
4689
+ if (!isRecord3(record) || !isRecord3(record.artifact_pointers)) {
3783
4690
  return [];
3784
4691
  }
3785
4692
  const pointers = [];
3786
4693
  for (const pointer of Object.values(record.artifact_pointers)) {
3787
- if (!isRecord2(pointer)) {
4694
+ if (!isRecord3(pointer)) {
3788
4695
  continue;
3789
4696
  }
3790
4697
  if (pointer.ref !== AGENTV_RESULTS_ARTIFACTS_REF || typeof pointer.path !== "string") {
@@ -3799,14 +4706,14 @@ function artifactSidecarPointers(record) {
3799
4706
  return pointers;
3800
4707
  }
3801
4708
  function artifactSidecarKey(destinationPath, pointerPath) {
3802
- return path4.posix.join(
4709
+ return path5.posix.join(
3803
4710
  RESULTS_REPO_RUNS_DIR,
3804
4711
  normalizeDestinationPath(destinationPath),
3805
4712
  normalizeDestinationPath(pointerPath)
3806
4713
  );
3807
4714
  }
3808
4715
  function collectArtifactSidecarPointers(sourceDir) {
3809
- const indexPath = path4.join(sourceDir, RESULT_INDEX_FILENAME2);
4716
+ const indexPath = path5.join(sourceDir, RESULT_INDEX_FILENAME2);
3810
4717
  if (!existsSync(indexPath)) {
3811
4718
  return [];
3812
4719
  }
@@ -3825,16 +4732,16 @@ function collectArtifactSidecarPointers(sourceDir) {
3825
4732
  }
3826
4733
  function resolveArtifactPointerSourcePath(sourceDir, pointerPath) {
3827
4734
  const normalizedPointerPath = normalizeDestinationPath(pointerPath);
3828
- const sourceRoot = path4.resolve(sourceDir);
3829
- const sourcePath = path4.resolve(sourceRoot, ...normalizedPointerPath.split("/"));
3830
- const relative = path4.relative(sourceRoot, sourcePath);
3831
- if (relative.startsWith("..") || path4.isAbsolute(relative)) {
4735
+ const sourceRoot = path5.resolve(sourceDir);
4736
+ const sourcePath = path5.resolve(sourceRoot, ...normalizedPointerPath.split("/"));
4737
+ const relative = path5.relative(sourceRoot, sourcePath);
4738
+ if (relative.startsWith("..") || path5.isAbsolute(relative)) {
3832
4739
  throw new Error(`Artifact pointer path escapes run directory: ${pointerPath}`);
3833
4740
  }
3834
4741
  return sourcePath;
3835
4742
  }
3836
4743
  function verifyArtifactPointerChecksum(pointer, content) {
3837
- const sha256 = createHash("sha256").update(content).digest("hex");
4744
+ const sha256 = createHash2("sha256").update(content).digest("hex");
3838
4745
  if (pointer.sha256 && pointer.sha256 !== sha256) {
3839
4746
  throw new Error(`Artifact pointer checksum mismatch for ${pointer.path}`);
3840
4747
  }
@@ -3846,7 +4753,7 @@ async function prepareArtifactSidecar(params) {
3846
4753
  if (params.pointers.length === 0) {
3847
4754
  return void 0;
3848
4755
  }
3849
- const sidecarRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-artifact-sidecar-"));
4756
+ const sidecarRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-artifact-sidecar-"));
3850
4757
  const copied = /* @__PURE__ */ new Set();
3851
4758
  const contentByPath = /* @__PURE__ */ new Map();
3852
4759
  try {
@@ -3862,8 +4769,8 @@ async function prepareArtifactSidecar(params) {
3862
4769
  if (copied.has(relativePointerPath)) {
3863
4770
  continue;
3864
4771
  }
3865
- const destinationPath = path4.join(sidecarRoot, ...relativePointerPath.split("/"));
3866
- mkdirSync(path4.dirname(destinationPath), { recursive: true });
4772
+ const destinationPath = path5.join(sidecarRoot, ...relativePointerPath.split("/"));
4773
+ mkdirSync(path5.dirname(destinationPath), { recursive: true });
3867
4774
  writeFileSync(destinationPath, content);
3868
4775
  copied.add(relativePointerPath);
3869
4776
  }
@@ -3890,12 +4797,12 @@ function rewritePublishedIndexLine(line, destinationPath) {
3890
4797
  } catch {
3891
4798
  return line;
3892
4799
  }
3893
- if (!isRecord2(record) || !isRecord2(record.artifact_pointers)) {
4800
+ if (!isRecord3(record) || !isRecord3(record.artifact_pointers)) {
3894
4801
  return line;
3895
4802
  }
3896
4803
  let changed = false;
3897
4804
  for (const pointer of Object.values(record.artifact_pointers)) {
3898
- if (!isRecord2(pointer)) {
4805
+ if (!isRecord3(pointer)) {
3899
4806
  continue;
3900
4807
  }
3901
4808
  if (pointer.ref !== AGENTV_RESULTS_ARTIFACTS_REF || typeof pointer.path !== "string") {
@@ -3913,26 +4820,26 @@ async function preparePublishedResultsSource(params) {
3913
4820
  if (params.pointers.length === 0) {
3914
4821
  return void 0;
3915
4822
  }
3916
- const publishedRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-results-published-"));
4823
+ const publishedRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-results-published-"));
3917
4824
  const omittedPaths = new Set(
3918
4825
  params.pointers.map((pointer) => normalizeDestinationPath(pointer.path))
3919
4826
  );
3920
4827
  try {
3921
4828
  const sourceFiles = await listSourceFiles(params.sourceDir);
3922
4829
  for (const sourceFile of sourceFiles) {
3923
- const relativeFile = path4.relative(params.sourceDir, sourceFile).split(path4.sep).join("/");
3924
- const destinationFile = path4.join(publishedRoot, ...relativeFile.split("/"));
4830
+ const relativeFile = path5.relative(params.sourceDir, sourceFile).split(path5.sep).join("/");
4831
+ const destinationFile = path5.join(publishedRoot, ...relativeFile.split("/"));
3925
4832
  if (relativeFile === RESULT_INDEX_FILENAME2) {
3926
4833
  const original = readFileSync2(sourceFile, "utf8");
3927
4834
  const rewritten = original.split(/\r?\n/).map((line) => rewritePublishedIndexLine(line, params.destinationPath)).join("\n");
3928
- mkdirSync(path4.dirname(destinationFile), { recursive: true });
4835
+ mkdirSync(path5.dirname(destinationFile), { recursive: true });
3929
4836
  writeFileSync(destinationFile, rewritten);
3930
4837
  continue;
3931
4838
  }
3932
4839
  if (omittedPaths.has(relativeFile)) {
3933
4840
  continue;
3934
4841
  }
3935
- mkdirSync(path4.dirname(destinationFile), { recursive: true });
4842
+ mkdirSync(path5.dirname(destinationFile), { recursive: true });
3936
4843
  await cp(sourceFile, destinationFile, { dereference: false });
3937
4844
  }
3938
4845
  } catch (error) {
@@ -3999,8 +4906,8 @@ async function commitStorageBranchWorktreePaths(params) {
3999
4906
  normalized
4000
4907
  });
4001
4908
  }
4002
- const indexRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-results-index-"));
4003
- const indexFile = path4.join(indexRoot, "index");
4909
+ const indexRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-results-index-"));
4910
+ const indexFile = path5.join(indexRoot, "index");
4004
4911
  const indexEnv = { GIT_INDEX_FILE: indexFile };
4005
4912
  try {
4006
4913
  if (base.baseRef) {
@@ -4008,8 +4915,9 @@ async function commitStorageBranchWorktreePaths(params) {
4008
4915
  } else {
4009
4916
  await runGit(["read-tree", "--empty"], { cwd: params.repoDir, env: indexEnv });
4010
4917
  }
4918
+ await addResultsGitattributesToIndex(params.repoDir, indexEnv);
4011
4919
  for (const gitPath of paths) {
4012
- const sourcePath = path4.join(params.repoDir, ...gitPath.split("/"));
4920
+ const sourcePath = path5.join(params.repoDir, ...gitPath.split("/"));
4013
4921
  if (!existsSync(sourcePath)) {
4014
4922
  await runGit(["update-index", "--force-remove", "--", gitPath], {
4015
4923
  cwd: params.repoDir,
@@ -4075,7 +4983,7 @@ async function commitResultsRunWithTemporaryIndex(params) {
4075
4983
  await assertValidResultsBranchName(params.repoDir, normalized.branch);
4076
4984
  await ensureResultsBranchNotCheckedOut(params.repoDir, normalized);
4077
4985
  const destinationRunPath = normalizeDestinationPath(params.destinationPath);
4078
- const destinationTreePath = path4.posix.join(RESULTS_REPO_RUNS_DIR, destinationRunPath);
4986
+ const destinationTreePath = path5.posix.join(RESULTS_REPO_RUNS_DIR, destinationRunPath);
4079
4987
  let base = await resolveStorageBranchBase({
4080
4988
  repoDir: params.repoDir,
4081
4989
  normalized,
@@ -4089,8 +4997,8 @@ async function commitResultsRunWithTemporaryIndex(params) {
4089
4997
  preferRemote: params.preferRemoteBase
4090
4998
  });
4091
4999
  }
4092
- const indexRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-results-index-"));
4093
- const indexFile = path4.join(indexRoot, "index");
5000
+ const indexRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-results-index-"));
5001
+ const indexFile = path5.join(indexRoot, "index");
4094
5002
  const indexEnv = { GIT_INDEX_FILE: indexFile };
4095
5003
  try {
4096
5004
  if (base.baseRef) {
@@ -4098,6 +5006,7 @@ async function commitResultsRunWithTemporaryIndex(params) {
4098
5006
  } else {
4099
5007
  await runGit(["read-tree", "--empty"], { cwd: params.repoDir, env: indexEnv });
4100
5008
  }
5009
+ await addResultsGitattributesToIndex(params.repoDir, indexEnv);
4101
5010
  const existingPaths = await getExistingRunTreePaths(
4102
5011
  params.repoDir,
4103
5012
  base.baseRef,
@@ -4112,8 +5021,8 @@ async function commitResultsRunWithTemporaryIndex(params) {
4112
5021
  }
4113
5022
  const sourceFiles = await listSourceFiles(params.sourceDir);
4114
5023
  for (const sourceFile of sourceFiles) {
4115
- const relativeFile = path4.relative(params.sourceDir, sourceFile).split(path4.sep).join("/");
4116
- const destinationFile = path4.posix.join(destinationTreePath, relativeFile);
5024
+ const relativeFile = path5.relative(params.sourceDir, sourceFile).split(path5.sep).join("/");
5025
+ const destinationFile = path5.posix.join(destinationTreePath, relativeFile);
4117
5026
  const fileStat = await lstat(sourceFile);
4118
5027
  let mode = fileStat.mode & 73 ? "100755" : "100644";
4119
5028
  let hashInputPath = sourceFile;
@@ -4189,7 +5098,7 @@ function buildDirectPushResult(normalized, changed, outcome) {
4189
5098
  ...outcome?.blocked === true && {
4190
5099
  blocked: true,
4191
5100
  block_reason: outcome.blockReason,
4192
- sync_status: "push_conflict"
5101
+ sync_status: outcome.syncStatus ?? "push_conflict"
4193
5102
  },
4194
5103
  ...pushDetailsToWire(details)
4195
5104
  };
@@ -4199,10 +5108,11 @@ function mergeDirectPushResults(normalized, results) {
4199
5108
  if (blocked) {
4200
5109
  return blocked;
4201
5110
  }
4202
- const detailed = [...results].reverse().find((result) => result.backup_ref !== void 0);
5111
+ const detailed = [...results].reverse().find((result) => result.backup_ref !== void 0 || result.auto_merged_remote === true);
4203
5112
  return {
4204
5113
  changed: results.some((result) => result.changed),
4205
5114
  push_conflict_policy: normalized.push_conflict_policy,
5115
+ ...detailed?.auto_merged_remote === true && { auto_merged_remote: true },
4206
5116
  ...detailed?.backup_ref !== void 0 && {
4207
5117
  backup_ref: detailed.backup_ref
4208
5118
  },
@@ -4386,7 +5296,7 @@ async function directPushResultsWithDetails(params) {
4386
5296
  }
4387
5297
  }
4388
5298
  function buildGitRunId(relativeRunPath) {
4389
- const normalized = relativeRunPath.split(path4.sep).join("/");
5299
+ const normalized = relativeRunPath.split(path5.sep).join("/");
4390
5300
  const segments = normalized.split("/").filter(Boolean);
4391
5301
  if (segments.length >= 2) {
4392
5302
  const experiment = segments.slice(0, -1).join("/");
@@ -4398,8 +5308,8 @@ function buildGitRunId(relativeRunPath) {
4398
5308
  }
4399
5309
  return segments[0] ?? relativeRunPath;
4400
5310
  }
4401
- function getRunExperiment(runId, benchmark) {
4402
- const experiment = benchmark.metadata?.experiment?.trim();
5311
+ function getRunExperiment(runId, summary) {
5312
+ const experiment = summary?.metadata?.experiment?.trim();
4403
5313
  if (experiment) {
4404
5314
  return experiment;
4405
5315
  }
@@ -4523,7 +5433,7 @@ async function readGitResultArtifact(params) {
4523
5433
  }
4524
5434
  function buildWipBranchName(runDir) {
4525
5435
  const hostname = os.hostname().replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 40);
4526
- const runBasename = path4.basename(runDir).replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 60);
5436
+ const runBasename = path5.basename(runDir).replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 60);
4527
5437
  return `agentv/wip/${hostname}/${runBasename}`;
4528
5438
  }
4529
5439
  async function setupWipWorktree(params) {
@@ -4542,8 +5452,8 @@ async function setupWipWorktree(params) {
4542
5452
  if (!baseRef) {
4543
5453
  throw new Error("Could not resolve a base ref for the WIP results branch");
4544
5454
  }
4545
- const worktreeRoot = await mkdtemp(path4.join(os.tmpdir(), "agentv-wip-"));
4546
- const worktreeDir = path4.join(worktreeRoot, "repo");
5455
+ const worktreeRoot = await mkdtemp(path5.join(os.tmpdir(), "agentv-wip-"));
5456
+ const worktreeDir = path5.join(worktreeRoot, "repo");
4547
5457
  await runGit(["worktree", "add", "-B", params.wipBranch, worktreeDir, baseRef], {
4548
5458
  cwd: cloneDir
4549
5459
  });
@@ -4562,7 +5472,7 @@ async function setupWipWorktree(params) {
4562
5472
  };
4563
5473
  }
4564
5474
  async function pushWipCheckpoint(params) {
4565
- const destinationDir = path4.join(
5475
+ const destinationDir = path5.join(
4566
5476
  params.handle.worktreeDir,
4567
5477
  RESULTS_REPO_RUNS_DIR,
4568
5478
  params.destinationPath
@@ -4625,40 +5535,74 @@ async function listGitRuns(repoDir, ref = "origin/main") {
4625
5535
  }
4626
5536
  throw error;
4627
5537
  }
4628
- const benchmarkPaths = treeOut.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.endsWith("/benchmark.json"));
4629
- if (benchmarkPaths.length === 0) {
5538
+ const treePaths = treeOut.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5539
+ const indexPaths = treePaths.filter((line) => line.endsWith("/index.jsonl"));
5540
+ if (indexPaths.length === 0) {
4630
5541
  return [];
4631
5542
  }
4632
- const batchInput = `${benchmarkPaths.map((benchmarkPath) => `${ref}:${benchmarkPath}`).join("\n")}
5543
+ const batchInput = `${indexPaths.map((indexPath) => `${ref}:${indexPath}`).join("\n")}
4633
5544
  `;
4634
5545
  const blobs = parseGitBatchBlobs(await runGitBatch(repoDir, batchInput));
4635
- if (blobs.length !== benchmarkPaths.length) {
5546
+ if (blobs.length !== indexPaths.length) {
4636
5547
  throw new Error(
4637
- `Expected ${benchmarkPaths.length} git blobs but received ${blobs.length} while listing results runs`
5548
+ `Expected ${indexPaths.length} git blobs but received ${blobs.length} while listing results runs`
4638
5549
  );
4639
5550
  }
5551
+ const summaryPaths = indexPaths.map((indexPath) => path5.posix.join(path5.posix.dirname(indexPath), "summary.json")).filter((summaryPath) => treePaths.includes(summaryPath));
5552
+ const summaryByPath = /* @__PURE__ */ new Map();
5553
+ if (summaryPaths.length > 0) {
5554
+ const summaryBatchInput = `${summaryPaths.map((summaryPath) => `${ref}:${summaryPath}`).join("\n")}
5555
+ `;
5556
+ const summaryBlobs = parseGitBatchBlobs(await runGitBatch(repoDir, summaryBatchInput));
5557
+ for (let i = 0; i < summaryBlobs.length; i++) {
5558
+ const summaryPath = summaryPaths[i];
5559
+ const blob = summaryBlobs[i];
5560
+ if (!summaryPath || !blob) continue;
5561
+ summaryByPath.set(summaryPath, JSON.parse(blob.content.toString("utf8")));
5562
+ }
5563
+ }
4640
5564
  const runs = blobs.flatMap((blob, index) => {
4641
- const benchmarkPath = benchmarkPaths[index];
4642
- const benchmark = JSON.parse(blob.content.toString("utf8"));
4643
- const runDir = path4.posix.dirname(benchmarkPath);
4644
- const relativeRunPath = path4.posix.relative(RESULTS_REPO_RUNS_DIR, runDir);
5565
+ const manifestPath = indexPaths[index];
5566
+ const runDir = path5.posix.dirname(manifestPath);
5567
+ const summaryPath = path5.posix.join(runDir, "summary.json");
5568
+ const summary = summaryByPath.get(summaryPath);
5569
+ const relativeRunPath = path5.posix.relative(RESULTS_REPO_RUNS_DIR, runDir);
4645
5570
  const runId = buildGitRunId(relativeRunPath);
4646
- const timestamp = benchmark.metadata?.timestamp?.trim() || path4.posix.basename(runDir);
4647
- const displayName = benchmark.metadata?.display_name?.trim() || path4.posix.basename(runDir);
4648
- const targets = benchmark.metadata?.targets ?? [];
4649
- const passRate = computeAveragePassRate(benchmark.run_summary);
5571
+ const rows = blob.content.toString("utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).flatMap(
5572
+ (line) => {
5573
+ try {
5574
+ return [
5575
+ JSON.parse(line)
5576
+ ];
5577
+ } catch {
5578
+ return [];
5579
+ }
5580
+ }
5581
+ );
5582
+ const rowTargets = [
5583
+ ...new Set(rows.map((row) => row.target).filter((target) => !!target))
5584
+ ];
5585
+ const rowTestIds = [
5586
+ ...new Set(rows.map((row) => row.test_id).filter((testId) => !!testId))
5587
+ ];
5588
+ const rowScores = rows.map((row) => row.score).filter((score) => typeof score === "number" && Number.isFinite(score));
5589
+ const avgScore = rowScores.length > 0 ? rowScores.reduce((sum, score) => sum + score, 0) / rowScores.length : 0;
5590
+ const timestamp = summary?.metadata?.timestamp?.trim() || rows[0]?.timestamp || path5.posix.basename(runDir);
5591
+ const displayName = summary?.metadata?.display_name?.trim() || path5.posix.basename(runDir);
5592
+ const targets = summary?.metadata?.targets ?? rowTargets;
5593
+ const passRate = computeAveragePassRate(summary?.run_summary) ?? avgScore;
4650
5594
  return [
4651
5595
  {
4652
5596
  run_id: runId,
4653
- experiment: getRunExperiment(runId, benchmark),
5597
+ experiment: getRunExperiment(runId, summary),
4654
5598
  timestamp,
4655
5599
  ...passRate !== void 0 && { pass_rate: passRate },
4656
5600
  ...targets.length === 1 && targets[0] ? { target: targets[0] } : {},
4657
- manifest_path: path4.posix.join(runDir, "index.jsonl"),
4658
- benchmark_path: benchmarkPath,
5601
+ manifest_path: manifestPath,
5602
+ ...summaryByPath.has(summaryPath) && { summary_path: summaryPath },
4659
5603
  display_name: displayName,
4660
- test_count: benchmark.metadata?.tests_run?.length ?? 0,
4661
- avg_score: 0,
5604
+ test_count: summary?.metadata?.tests_run?.length ?? rowTestIds.length,
5605
+ avg_score: avgScore,
4662
5606
  size_bytes: blob.size
4663
5607
  }
4664
5608
  ];
@@ -4667,9 +5611,9 @@ async function listGitRuns(repoDir, ref = "origin/main") {
4667
5611
  return runs;
4668
5612
  }
4669
5613
  async function materializeGitRun(repoDir, relativeRunPath, ref = "origin/main") {
4670
- const normalizedRunPath = relativeRunPath.split(path4.sep).join("/");
4671
- const runTreePath = path4.posix.join(RESULTS_REPO_RUNS_DIR, normalizedRunPath);
4672
- const targetRunDir = path4.join(repoDir, ...runTreePath.split("/"));
5614
+ const normalizedRunPath = relativeRunPath.split(path5.sep).join("/");
5615
+ const runTreePath = path5.posix.join(RESULTS_REPO_RUNS_DIR, normalizedRunPath);
5616
+ const targetRunDir = path5.join(repoDir, ...runTreePath.split("/"));
4673
5617
  const { stdout: treeOut } = await runGit(["ls-tree", "-r", "--name-only", ref, runTreePath], {
4674
5618
  cwd: repoDir
4675
5619
  });
@@ -4685,16 +5629,16 @@ async function materializeGitRun(repoDir, relativeRunPath, ref = "origin/main")
4685
5629
  `Expected ${filePaths.length} git blobs but received ${blobs.length} while materializing results run`
4686
5630
  );
4687
5631
  }
4688
- const tempRoot = mkdtempSync(path4.join(repoDir, ".agentv-run-"));
4689
- const tempRunDir = path4.join(tempRoot, "run");
5632
+ const tempRoot = mkdtempSync(path5.join(repoDir, ".agentv-run-"));
5633
+ const tempRunDir = path5.join(tempRoot, "run");
4690
5634
  try {
4691
5635
  for (const [index, filePath] of filePaths.entries()) {
4692
- const relativeFilePath = path4.posix.relative(runTreePath, filePath);
4693
- const absolutePath = path4.join(tempRunDir, ...relativeFilePath.split("/"));
4694
- mkdirSync(path4.dirname(absolutePath), { recursive: true });
5636
+ const relativeFilePath = path5.posix.relative(runTreePath, filePath);
5637
+ const absolutePath = path5.join(tempRunDir, ...relativeFilePath.split("/"));
5638
+ mkdirSync(path5.dirname(absolutePath), { recursive: true });
4695
5639
  writeFileSync(absolutePath, blobs[index].content);
4696
5640
  }
4697
- mkdirSync(path4.dirname(targetRunDir), { recursive: true });
5641
+ mkdirSync(path5.dirname(targetRunDir), { recursive: true });
4698
5642
  try {
4699
5643
  renameSync(tempRunDir, targetRunDir);
4700
5644
  } catch (error) {
@@ -5679,7 +6623,7 @@ function extractResponseItemContent(content) {
5679
6623
  }
5680
6624
  return parts.length > 0 ? parts.join("") : void 0;
5681
6625
  }
5682
- var DEFAULT_SESSIONS_DIR = () => path5.join(homedir(), ".codex", "sessions");
6626
+ var DEFAULT_SESSIONS_DIR = () => path6.join(homedir(), ".codex", "sessions");
5683
6627
  async function discoverCodexSessions(opts) {
5684
6628
  const sessionsDir = opts?.sessionsDir ?? DEFAULT_SESSIONS_DIR();
5685
6629
  const limit = opts?.latest ? 1 : opts?.limit ?? 10;
@@ -5691,7 +6635,7 @@ async function discoverCodexSessions(opts) {
5691
6635
  return [];
5692
6636
  }
5693
6637
  for (const year of yearDirs) {
5694
- const yearPath = path5.join(sessionsDir, year);
6638
+ const yearPath = path6.join(sessionsDir, year);
5695
6639
  let monthDirs;
5696
6640
  try {
5697
6641
  monthDirs = await readdir2(yearPath);
@@ -5699,7 +6643,7 @@ async function discoverCodexSessions(opts) {
5699
6643
  continue;
5700
6644
  }
5701
6645
  for (const month of monthDirs) {
5702
- const monthPath = path5.join(yearPath, month);
6646
+ const monthPath = path6.join(yearPath, month);
5703
6647
  let dayDirs;
5704
6648
  try {
5705
6649
  dayDirs = await readdir2(monthPath);
@@ -5711,7 +6655,7 @@ async function discoverCodexSessions(opts) {
5711
6655
  const dirDate = `${year}-${month}-${day}`;
5712
6656
  if (dirDate !== opts.date) continue;
5713
6657
  }
5714
- const dayPath = path5.join(monthPath, day);
6658
+ const dayPath = path6.join(monthPath, day);
5715
6659
  let files;
5716
6660
  try {
5717
6661
  files = await readdir2(dayPath);
@@ -5720,7 +6664,7 @@ async function discoverCodexSessions(opts) {
5720
6664
  }
5721
6665
  for (const file of files) {
5722
6666
  if (!file.startsWith("rollout-") || !file.endsWith(".jsonl")) continue;
5723
- const filePath = path5.join(dayPath, file);
6667
+ const filePath = path6.join(dayPath, file);
5724
6668
  const nameWithoutExt = file.replace(/\.jsonl$/, "");
5725
6669
  const parts = nameWithoutExt.split("-");
5726
6670
  const sessionId = parts.length >= 6 ? parts.slice(-5).join("-") : nameWithoutExt;
@@ -5739,7 +6683,7 @@ async function discoverCodexSessions(opts) {
5739
6683
  sessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
5740
6684
  return sessions.slice(0, limit);
5741
6685
  }
5742
- var DEFAULT_PROJECTS_DIR = () => path6.join(homedir2(), ".claude", "projects");
6686
+ var DEFAULT_PROJECTS_DIR = () => path7.join(homedir2(), ".claude", "projects");
5743
6687
  function encodeProjectPath(projectPath) {
5744
6688
  return projectPath.replace(/\//g, "-");
5745
6689
  }
@@ -5758,7 +6702,7 @@ async function discoverClaudeSessions(opts) {
5758
6702
  }
5759
6703
  const sessions = [];
5760
6704
  for (const projectDir of projectDirs) {
5761
- const dirPath = path6.join(projectsDir, projectDir);
6705
+ const dirPath = path7.join(projectsDir, projectDir);
5762
6706
  let entries;
5763
6707
  try {
5764
6708
  entries = await readdir3(dirPath);
@@ -5769,7 +6713,7 @@ async function discoverClaudeSessions(opts) {
5769
6713
  if (!entry.endsWith(".jsonl")) continue;
5770
6714
  const sessionId = entry.replace(/\.jsonl$/, "");
5771
6715
  if (opts?.sessionId && sessionId !== opts.sessionId) continue;
5772
- const filePath = path6.join(dirPath, entry);
6716
+ const filePath = path7.join(dirPath, entry);
5773
6717
  let updatedAt;
5774
6718
  try {
5775
6719
  const fileStat = await stat3(filePath);
@@ -5844,6 +6788,12 @@ export {
5844
6788
  traceEnvelopeToTraceSessionResponse,
5845
6789
  buildTraceSpanTree,
5846
6790
  normalizeTraceArtifactToTraceSessionResponse,
6791
+ isExperimentFileReference,
6792
+ deriveExperimentNameFromPath,
6793
+ loadExperimentConfig,
6794
+ normalizeExperimentConfig,
6795
+ fingerprintExperimentConfig,
6796
+ buildExperimentArtifactMetadata,
5847
6797
  transpileEvalYaml,
5848
6798
  transpileEvalYamlFile,
5849
6799
  getOutputFilenames,
@@ -5857,9 +6807,11 @@ export {
5857
6807
  getResultsRepoLocalPaths,
5858
6808
  ensureResultsRepoClone,
5859
6809
  getResultsRepoStatus,
6810
+ buildResultsCompareUrl,
5860
6811
  getResultsRepoSyncStatus,
5861
6812
  syncResultsRepo,
5862
6813
  syncResultsRepoForProject,
6814
+ confirmResultsMergeAndPull,
5863
6815
  checkoutResultsRepoBranch,
5864
6816
  prepareResultsRepoBranch,
5865
6817
  stageResultsArtifacts,
@@ -5894,4 +6846,4 @@ export {
5894
6846
  TranscriptProvider,
5895
6847
  createAgentKernel
5896
6848
  };
5897
- //# sourceMappingURL=chunk-NCNDA3F6.js.map
6849
+ //# sourceMappingURL=chunk-UG4JHZYM.js.map