From 8791f7d076951b0d726ac5cb98f6e2e06a3fa88c Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Thu, 25 Jun 2026 02:59:06 +0200 Subject: [PATCH 1/2] docs: describe no-force-push results sync in public docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The conflict-free results sync (epic av-raf) replaced the force-push push conflict path with an auto-merge loop plus a human GitHub-merge fallback, but the public docs still described `backup_and_force_push` as creating a backup ref and force-pushing. Update results.mdx and dashboard.mdx to state that non-fast-forward pushes auto-merge with artifact-aware drivers and push as a fast-forward (never force), that `backup_and_force_push` is deprecated and no longer force-pushes, and that genuine overlay conflicts surface a Pending merge card with a GitHub compare link + "I merged it — resync". Add needs-human-merge to the documented sync statuses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/web/src/content/docs/docs/tools/dashboard.mdx | 7 ++++--- apps/web/src/content/docs/docs/tools/results.mdx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/src/content/docs/docs/tools/dashboard.mdx b/apps/web/src/content/docs/docs/tools/dashboard.mdx index 903ef4436..e96125b39 100644 --- a/apps/web/src/content/docs/docs/tools/dashboard.mdx +++ b/apps/web/src/content/docs/docs/tools/dashboard.mdx @@ -295,7 +295,7 @@ projects: push_conflict_policy: block ``` -`results.repo.remote` is the Git remote URL AgentV fetches and pushes. `results.repo.path: .` stores completed run artifacts on a dedicated branch of the source repository without checking out that branch in the source worktree. AgentV manages the local Git remote alias for that URL, so the normal config stays portable across machines. When `results.repo.remote` is omitted, `results.repo.path` means an existing local Git checkout whose object database and refs AgentV should write to, and the branch defaults to `agentv/results/v1`. AgentV creates the branch automatically on first publish and commits only AgentV result paths into it. `sync.auto_push: false` keeps the result commit local; set it to `true` to push the branch best-effort after each completed run. `sync.require_push: true` is for CI workflows where a push failure should fail the command after local artifacts are written. `sync.push_conflict_policy` defaults to `block`; set it to `backup_and_force_push` only for a single-writer Dashboard/server that should replace the remote results branch after creating a remote backup ref. +`results.repo.remote` is the Git remote URL AgentV fetches and pushes. `results.repo.path: .` stores completed run artifacts on a dedicated branch of the source repository without checking out that branch in the source worktree. AgentV manages the local Git remote alias for that URL, so the normal config stays portable across machines. When `results.repo.remote` is omitted, `results.repo.path` means an existing local Git checkout whose object database and refs AgentV should write to, and the branch defaults to `agentv/results/v1`. AgentV creates the branch automatically on first publish and commits only AgentV result paths into it. `sync.auto_push: false` keeps the result commit local; set it to `true` to push the branch best-effort after each completed run. `sync.require_push: true` is for CI workflows where a push failure should fail the command after local artifacts are written. `sync.push_conflict_policy` defaults to `block`; the `backup_and_force_push` value is deprecated and no longer force-pushes. Non-fast-forward result branch pushes are auto-merged with artifact-aware Git merge drivers and pushed as a fast-forward, so the canonical results branch is never force-pushed or rewritten. Genuine overlay conflicts route to a timestamped temp branch plus a GitHub compare link for a human merge instead. For a separate results repository, use `results.repo.remote` and an optional managed clone `results.repo.path`: @@ -407,7 +407,7 @@ Automation can use the same API that Dashboard uses: Single-project sessions also expose `GET /api/remote/status` and `POST /api/remote/sync`. -In the default multi-project flow, open a project card first, then use **Sync Project** in that project's toolbar. The toolbar shows the project display name, sync state, last synced time, configured repo, and remote run count. Statuses include clean, unavailable, behind, ahead, dirty, diverged, conflicted, and syncing. +In the default multi-project flow, open a project card first, then use **Sync Project** in that project's toolbar. The toolbar shows the project display name, sync state, last synced time, configured repo, and remote run count. Statuses include clean, unavailable, behind, ahead, dirty, diverged, conflicted, needs human merge, and syncing. Use the **All Sources / Local Only / Remote Only** filter to narrow the run list by origin. @@ -425,6 +425,7 @@ After sync, newly fetched remote runs appear in the list with a **remote** sourc - Safe uncommitted changes under the configured results repo's owned result and metadata paths, such as remote tag overlays under `metadata/runs/**`, are committed and pushed when `sync.auto_push: true`. - A local results repo that is ahead is pushed when `sync.auto_push: true` and the committed paths are all under `.agentv/results/**`. - Dirty non-results files, dirty metadata plus remote changes, unresolved conflicts, missing upstream branches, non-results commits ahead, and rejected pushes are blocked instead of reset. -- Non-fast-forward result branch pushes use `sync.push_conflict_policy`. The default `block` policy reports a `push_conflict` with the target branch and local/remote commits. The explicit `backup_and_force_push` policy fetches the target branch, creates `agentv/backups/--` from the current remote commit, then force-pushes with a lease tied to that same commit. Backup refs are siblings under `agentv/backups/`, not nested below the target branch. +- Non-fast-forward result branch pushes never force-push. AgentV runs a bounded fetch → merge → push loop that absorbs concurrent remote writes with a real merge commit using artifact-aware Git merge drivers (union for the append-only `index.jsonl`, a JSON-union driver for tag and feedback overlays), so the common append-mostly case auto-merges and pushes as a fast-forward. The `sync.push_conflict_policy: backup_and_force_push` value is deprecated and no longer force-pushes; it now auto-merges like the default and emits a one-time deprecation notice. +- When a genuine overlay conflict cannot be auto-merged, AgentV does not touch the canonical branch. It pushes the local work to a fresh timestamped `agentv/results-sync/--` branch and reports `needs_human_merge` with a `pending_merge` block (temp branch, target branch, and a GitHub compare URL when the remote is on GitHub). The toolbar shows a **Pending merge** card: open the link to merge the branch into the canonical target on GitHub (GitHub's pull request is the conflict surface — AgentV builds no merge UI), then click **I merged it — resync**. That resumes canonical sync by fast-forward-pulling the merged target. A premature click is a safe no-op — local work stays intact and the next sync re-creates a temp branch. When sync is blocked, Dashboard keeps the local clone intact and shows the `block_reason`, `dirty_paths` or `conflicted_paths`, `git_status`, and a compact `git_diff_summary` so you can resolve the results repo manually before syncing again. diff --git a/apps/web/src/content/docs/docs/tools/results.mdx b/apps/web/src/content/docs/docs/tools/results.mdx index 63eee4867..9d6bfed72 100644 --- a/apps/web/src/content/docs/docs/tools/results.mdx +++ b/apps/web/src/content/docs/docs/tools/results.mdx @@ -239,7 +239,7 @@ The CLI contract is deliberately narrow: `agentv results` manages local result a Use these supported remote workflows instead: -- **Automatic publishing:** configure `projects[].results` or top-level `results`; new `agentv eval` and `agentv pipeline bench` runs publish completed artifacts after the run completes. Use `repo.remote` with `repo.path: .` and `repo.branch: agentv/results/v1` to store primary result records on a dedicated branch of the source repo without requiring a machine-local Git remote name. AgentV reserves `agentv/results/v1` for primary results and `agentv/artifacts/v1` for heavy artifact payloads. When `index.jsonl` rows point trace or transcript payloads at `agentv/artifacts/v1`, automatic publishing stores those bytes on that artifact branch in the same remote and publishes pointer keys such as `runs//`. The configured results branch remains the metadata/control plane (`index.jsonl`, `benchmark.json`, tags, and pointers) instead of duplicating canonical trace/transcript payload bodies. Local pre-publish run workspaces can still contain those files beside the manifest so local tools keep working. Mutable run tags are stored as `tags.json` with a `tag_revision`; there is no tag event log in the normal results layout. `results.repo.path` without `results.repo.remote` means an existing local Git checkout, distinct from `workspace.repos[].repo`, which is a portable repository identity. AgentV manages any local Git remote alias internally. Set `sync.auto_push: true` to push after publish, or `sync.require_push: true` in CI to fail when that push fails. Non-fast-forward result branch pushes block by default with `sync.push_conflict_policy: block`; `backup_and_force_push` is an explicit single-writer opt-in that first creates an `agentv/backups/--` remote backup branch and then force-pushes with a lease. While an eval is still running, [WIP checkpoints](/docs/tools/wip-checkpoints/) can keep partial run output durable on `agentv/wip/...` branches when auto-push is enabled. +- **Automatic publishing:** configure `projects[].results` or top-level `results`; new `agentv eval` and `agentv pipeline bench` runs publish completed artifacts after the run completes. Use `repo.remote` with `repo.path: .` and `repo.branch: agentv/results/v1` to store primary result records on a dedicated branch of the source repo without requiring a machine-local Git remote name. AgentV reserves `agentv/results/v1` for primary results and `agentv/artifacts/v1` for heavy artifact payloads. When `index.jsonl` rows point trace or transcript payloads at `agentv/artifacts/v1`, automatic publishing stores those bytes on that artifact branch in the same remote and publishes pointer keys such as `runs//`. The configured results branch remains the metadata/control plane (`index.jsonl`, `benchmark.json`, tags, and pointers) instead of duplicating canonical trace/transcript payload bodies. Local pre-publish run workspaces can still contain those files beside the manifest so local tools keep working. Mutable run tags are stored as `tags.json` with a `tag_revision`; there is no tag event log in the normal results layout. `results.repo.path` without `results.repo.remote` means an existing local Git checkout, distinct from `workspace.repos[].repo`, which is a portable repository identity. AgentV manages any local Git remote alias internally. Set `sync.auto_push: true` to push after publish, or `sync.require_push: true` in CI to fail when that push fails. Non-fast-forward result branch pushes never force-push: AgentV auto-merges concurrent remote writes with artifact-aware Git merge drivers (a union driver for the append-only `index.jsonl`, a JSON-union driver for tag and feedback overlays) and pushes the merge as a fast-forward, and routes a genuine overlay conflict to a timestamped `agentv/results-sync/...` branch plus a GitHub compare/PR link for a human merge. `sync.push_conflict_policy: backup_and_force_push` is deprecated and no longer force-pushes — it now auto-merges like the default `block` and emits a one-time deprecation notice. While an eval is still running, [WIP checkpoints](/docs/tools/wip-checkpoints/) can keep partial run output durable on `agentv/wip/...` branches when auto-push is enabled. - **Manual Dashboard sync:** run `agentv dashboard`, open the project, and use **Sync Project**. - **Manual API sync:** while Dashboard is running, call `GET /api/projects/:projectId/remote/status` or `POST /api/projects/:projectId/remote/sync` for project-scoped automation. Single-project sessions also expose `GET /api/remote/status` and `POST /api/remote/sync`. - **Git escape hatch:** for advanced recovery, inspect or repair the configured `projects[].results.repo.path` clone with `git` directly, then sync again. From 65e5228ed67530d9f1246a9b573d99bb84ba6443 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Thu, 25 Jun 2026 03:08:32 +0200 Subject: [PATCH 2/2] docs: promote conflict-free results sync plan to an ADR The implementation plan was a temporary working artifact (per .agents/workflow.md, plans are deleted once their details land in official docs). Now that all phases (av-raf) are merged, distill the durable decision into a proper ADR and remove the plan doc. Repoint the results-repo.ts comment at the ADR. - add docs/adr/2026-06-24-no-force-push-results-sync.md (Status: Accepted) - remove docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md - results-repo.ts: update the plan-doc reference to the ADR path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-06-24-no-force-push-results-sync.md | 130 +++++++ ...01-feat-conflict-free-results-sync-plan.md | 339 ------------------ packages/core/src/evaluation/results-repo.ts | 2 +- 3 files changed, 131 insertions(+), 340 deletions(-) create mode 100644 docs/adr/2026-06-24-no-force-push-results-sync.md delete mode 100644 docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md diff --git a/docs/adr/2026-06-24-no-force-push-results-sync.md b/docs/adr/2026-06-24-no-force-push-results-sync.md new file mode 100644 index 000000000..3ed7d98cc --- /dev/null +++ b/docs/adr/2026-06-24-no-force-push-results-sync.md @@ -0,0 +1,130 @@ +# ADR: Conflict-free results sync without force push + +Date: 2026-06-24 + +Status: Accepted + +Bead: av-raf + +## Context + +AgentV publishes portable run artifacts to a shared results branch (for example +`agentv/results/v1` or `main`) and the Dashboard can sync them. When the local +results branch diverged from the remote, the previous +`sync.push_conflict_policy: backup_and_force_push` policy created a remote backup +ref and then force-pushed the local ref over the canonical branch with a lease. + +Even leased and backed up, that path **rewrites shared history**: remote commits +that landed between fetch and push survive only on a backup ref, and recovery +depends on someone noticing it and re-merging. That violates the repo safety +norms in `.agents/workflow.md` ("Never force-push", "Never rewrite shared +history"). + +The key observation is that results artifacts are almost never in genuine +conflict: + +| Family | Path | Mutability | Conflict shape | +| --- | --- | --- | --- | +| Run bundles | `runs///**` | Immutable, write-once | Unique timestamped dirs; writers never overlap | +| Run index | `index.jsonl` | Append-only | Concurrent appends; line-union resolves cleanly | +| Mutable overlay | `metadata/runs/**/tags.json`, `feedback.json` | Editable | Only this can truly conflict (two writers retag one run) | + +So a non-fast-forward push is overwhelmingly something a merge resolves +automatically, and force push was being used as a blunt instrument. + +The Dashboard must also stay zero-infra per `.agents/product-boundary.md`: no +Phoenix, hosted DB, or inbound webhook server at runtime. + +## Decision + +Replace the force-push path with a two-layer, no-force-push design. + +**Layer 1 — auto-merge the common case.** On a non-fast-forward results push, run +a bounded `fetch → merge → push` loop using artifact-aware Git merge drivers: +`merge=union` for the append-only `index.jsonl` and a small `agentv-json` +JSON-union driver for the mutable tag/feedback overlay (registered once in the +AgentV-owned results checkout config; `.gitattributes` mirrored into the git dir +so drivers apply on both working-tree and detached `merge-tree` paths). Every +push is a fast-forward of canonical — a plain FF, or a FF onto a merge commit +that already contains the remote tip — so shared history is never rewritten. +A bounded optimistic retry absorbs the benign race where another writer pushes +between our fetch and push. + +**Layer 2 — human merge via a GitHub PR on a true conflict.** When `git merge` +(with the drivers) reports a genuine conflict, abort it, leave the canonical +branch untouched, and push the local work to a fresh **flat** timestamped branch +`agentv/results-sync/--` (create-only; the flat +`agentv/results-sync/` namespace avoids a directory/file ref conflict with the +canonical `agentv/results/v1`). The Dashboard surfaces a **Pending merge** card +with a GitHub compare/PR link. The user merges that branch into the target on +GitHub — **GitHub's PR is the conflict surface; AgentV builds no merge/diff +editor** — then clicks **OK** ("I merged it — resync"). AgentV then +fast-forward-pulls the target and resumes normal sync. + +**Resume is an explicit OK, not auto-detection.** Branch deletion is not a merge +signal (a user can delete without merging, merge without deleting, or be blocked +by branch protection), and the repo's required squash merge gives the merge a new +SHA so ancestor checks fail despite the content merging. An explicit OK avoids +all of it and is safe: a premature OK just pulls a target lacking the local work, +re-diverges on the next push, and re-creates a temp branch — no data loss, no +force push. + +`backup_and_force_push` is **deprecated, not removed**: the config value still +validates but now auto-merges like the default and emits a one-time deprecation +notice, so shipped surfaces referencing it keep working. + +## Consequences + +- The canonical results branch advances only by real merges; history is never + rewritten and no push is ever forced. +- The common append-mostly case syncs with no human action. +- True overlay conflicts are routed to GitHub's PR UI plus a one-click resync, + with no AgentV-built conflict editor. +- Zero-infra holds: local-git fetch/merge/push for Layer 1; a plain push to a new + ref plus a URL string for Layer 2. `gh`/GitHub compare URLs are optional + enrichment, never required. +- Temp-branch cleanup is out of AgentV scope — the user owns the GitHub merge, so + deletion is GitHub auto-delete-on-merge or manual cleanup. + +## Alternatives Considered + +- **Auto-detect the merge (tree-equality / ancestor) instead of an OK button.** + Must be squash-safe across every contributed run bundle and must distinguish + merge from deletion — meaningfully more code for a signal the user gives in one + click. Rejected. +- **Backup + force-with-lease (the previous policy).** Rewrites shared history; + concurrent remote commits survive only on a backup ref. Rejected/removed. +- **Per-file conflict-resolution UI (av-xwm).** Duplicates GitHub's PR UI; heavy + to build and maintain. Rejected — GitHub's PR is the conflict surface. The + av-xwm optimistic-concurrency guard for stale tag writes remains independently + useful, but its merge UI is not a dependency here. +- **Rebase/replay local commits onto the remote tip.** Rewrites local SHAs and + reintroduces a history-rewrite hazard if those commits were ever shared (e.g. + on a temp branch); linear history is not valued on the results branch. + Rejected in favor of merge. +- **Append-only / CRDT overlay (per-writer tag event files).** Makes overlay + conflicts structurally impossible but requires a layout migration. Deferred as + a potential end-state only if overlay conflicts prove common; the JSON-union + driver already gets most of the benefit since add/remove commute. + +## Implementation + +Delivered in phases under epic av-raf (all non-breaking): + +- Phase 0 — `.gitattributes` + `agentv-json` merge driver registration (#1506). +- Phase 1 — bounded `fetch → merge → push` loop replacing the force-push path; + `backup_and_force_push` deprecated (#1506). +- Phase 2 — temp-branch fallback + `confirm-merge` (OK-to-resync) API (#1507). +- Phase 3 — Dashboard **Pending merge** card with the GitHub link + resync button + (#1508). +- Phase 4 (deferred) — append-only overlay layout, only if overlay conflicts + prove common in practice. + +## Non-Goals + +- Force push (blind or leased) or any rewrite of shared history. +- A webhook server, hosted DB, or Phoenix dependency for sync. +- An AgentV merge/diff/conflict-editor UI. +- Automatic merge detection (tree-equality / ancestor / deletion watching). +- Temp-branch deletion/cleanup by AgentV. +- A CLI command family for conflict resolution (stays Dashboard/API-owned). diff --git a/docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md b/docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md deleted file mode 100644 index e3b94bc45..000000000 --- a/docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: "feat: Conflict-free results sync without force push" -type: feat -date: 2026-06-24 -bead: av-raf -related: - - docs/plans/2026-06-23-002-feat-remote-result-metadata-conflicts-plan.md - - docs/plans/2026-06-10-remote-results-cli-contract.md - - docs/plans/git-native-results.md - - docs/plans/results-branch-layout.md ---- - -# feat: Conflict-free results sync without force push - -## Summary - -Design a results-sync workflow for AgentV that never force-pushes a shared branch -and never rewrites shared history, while keeping the Dashboard zero-infra -(local-git-first; no Phoenix, no hosted DB, no inbound webhook server). - -Two layers, deliberately small: - -1. **Auto-merge the common case.** Most result writes are append-mostly and - line-orthogonal (immutable run bundles under `runs///`, append-only - index JSONL). On a non-fast-forward push, run a bounded **fetch → merge → push** - loop using artifact-aware merge drivers (`union` for the index, a small - JSON-union driver for the mutable overlay). This commits a real merge instead of - overwriting the remote, so force push is never needed for the overwhelming - majority of pushes. -2. **Human merge via a GitHub PR only on a true conflict.** When a real content - conflict cannot be auto-resolved, push the local work to a **new timestamped - branch** (never the canonical branch) and surface a compare/PR link. The user - merges that branch into the target branch (`main` or `agentv/results/v1`) using - **GitHub's own PR/merge UI** — AgentV builds **no merge UI of its own**. When the - user has merged, they click **OK** in the Dashboard; AgentV pulls the target - branch and resumes normal sync. - -The resume signal is an **explicit user confirmation ("OK")**, not branch deletion -and not automatic merge detection. This keeps the design tiny: no squash-safe -ancestor detection, no merge-state polling, no per-file conflict editor. - -This supersedes the `backup_and_force_push` policy as the recommended default. It -does **not** depend on building the per-file conflict-resolution UI from -`docs/plans/2026-06-23-002-feat-remote-result-metadata-conflicts-plan.md` (av-xwm); -GitHub's PR is the conflict surface instead. - -This is a design artifact only. It does not implement a broad change. - ---- - -## Current Behavior - -### Results sync + push - -- Config: `results.sync.{auto_push, require_push, push_conflict_policy}` where - `push_conflict_policy: 'block' | 'backup_and_force_push'` - (`packages/core/src/evaluation/loaders/config-loader.ts:40`, - `packages/core/src/evaluation/results-repo.ts:230-249`). -- Sync status can be `clean | ahead | behind | diverged | conflicted | - push_conflict | ...` (`results-repo.ts:80-83`). -- On a non-fast-forward push, `resolveResultBranchPushConflict()` runs - (`results-repo.ts:1380-1468`): - - `block` → return `sync_status: 'push_conflict'`, blocked, with a message telling - the user to switch to `backup_and_force_push` - (`buildBlockedPushConflictReason`, `results-repo.ts:1374-1378`). - - `backup_and_force_push` → create a remote backup ref - `agentv/backups/--` from the **current remote - commit** (`buildResultsBackupRef`, `results-repo.ts:1341-1345`), then - `git push --force-with-lease=refs/heads/:` the local ref - over the canonical branch (`results-repo.ts:1440-1459`). -- The force push is leased and backed up, so it is not a blind `--force`. But it - still **rewrites shared history**: remote commits that landed between fetch and - push survive only on the backup ref, not on the canonical branch, and recovery - requires someone to notice that ref and re-merge it. This is exactly what the repo - safety norms (`.agents/workflow.md`: "Never force-push", "Never rewrite shared - history") want to avoid. - -### Dashboard / API surface - -- `POST /api/remote/sync` and `POST /api/projects/:projectId/remote/sync` call - `syncRemoteResults()` (`apps/cli/src/commands/results/serve.ts:2942-2999`, - `apps/cli/src/commands/results/remote.ts:356-385`). -- `syncRemoteResults()` delegates to `syncResultsRepoForProject()` and reports - status; on error it returns `blocked: true` with the message. - -### Artifact families (what "merge" means here) - -From `docs/plans/results-branch-layout.md` and `results-repo.ts`: - -| Family | Path | Mutability | Conflict shape | -| --- | --- | --- | --- | -| Run bundles | `runs///...` | Immutable, write-once | New files in unique timestamped dirs; **never** overlap between writers | -| Run index | append-only JSONL | Append-only | Concurrent appends → both-modified on the tail; line-union resolves cleanly | -| Mutable overlay | `metadata/runs///tags.json`, `feedback.json` | Editable | Genuine content conflict possible (two writers retag the same run) | - -The crucial observation: **only the small editable overlay can truly conflict.** -Run bundles are uniquely pathed by timestamp, so two agents pushing different runs -never touch the same path. Index appends are line-orthogonal. So a force push is -almost never *necessary* — it is being used as a blunt instrument for a -non-fast-forward that a merge resolves automatically. - ---- - -## Recommended Design - -### Layer 1 — Auto-merge push loop (replaces the force-push path) - -``` -push_results(local_ref, canonical): - for attempt in 1..N: # bounded optimistic retry - git fetch origin canonical - if local_ref is ancestor of origin/canonical: return up_to_date - if origin/canonical is ancestor of local_ref: # fast-forward - git push origin local_ref:canonical - if ok: return pushed else: continue # raced; retry - # diverged → try a real merge with artifact-aware drivers - git merge -m "chore(results): merge remote results" origin/canonical - if merge clean: # union/json drivers resolved it - git push origin HEAD:canonical - if ok: return merged_pushed else: continue - else: # TRUE conflict (overlay only) - git merge --abort - return needs_human_merge # → Layer 2 - return needs_human_merge -``` - -- **No force push anywhere.** Every push is a fast-forward of canonical (a plain FF, - or a FF onto a merge commit that already contains the remote tip). -- Bounded retry handles the benign race where another writer pushes between our - fetch and push. - -#### Merge strategy per artifact family - -| Family | Strategy | Mechanism | -| --- | --- | --- | -| Run bundles `runs///**` | Always auto (no overlap) | Disjoint paths ⇒ standard 3-way merge never conflicts. | -| Append-only index JSONL | Union merge | `.gitattributes`: `index.jsonl merge=union`. Index is a rebuildable projection (see the SQLite index epic / `results-storage-retention-oplog-plan.md`), so worst case it is regenerated. | -| Mutable overlay `tags.json`, `feedback.json` | JSON-union driver; else human path | `merge=agentv-json` does a 3-way set/field union for tags (add/remove are commutative); if it cannot reconcile a genuine scalar conflict, it leaves the file conflicted → Layer 2. | - -`.gitattributes` lives on the results branch; the `agentv-json` driver is registered -once in the AgentV-owned results checkout config when AgentV initializes it (it -already manages a dedicated checkout / storage-branch worktree, so this is a one-time -`git config`, not user-facing infra). - -### Layer 2 — Human merge via GitHub PR + explicit OK (only on true conflict) - -When Layer 1 returns `needs_human_merge`: - -1. **Push to a new timestamped temp branch**, never canonical: - `agentv/results-sync/--` (create-only push; - `` is the slugified target branch and `` avoids same-second - collisions between concurrent writers). The name is deliberately **flat** under - a dedicated `agentv/results-sync/` namespace: a nested - `agentv/results/v1/sync-...` ref would D/F-conflict with the canonical - `agentv/results/v1` branch (git cannot store one ref as both a file and a - directory). -2. **Surface a link** in the Dashboard: - - A **compare/PR URL**. With a GitHub remote and `gh`, build - `https://github.com///compare/...?expand=1` - (or `gh pr create --web`). Without `gh`, show the branch name + compare path. - - Status chip: `Pending merge` with the temp branch name and a copy line: - "Merge this branch into `` on GitHub, then click OK." -3. **The user merges the PR on GitHub.** GitHub's PR/merge/conflict UI is the - resolution surface; AgentV renders **no diff/merge editor**. -4. **The user clicks OK** in the Dashboard. -5. AgentV **pulls the target branch** (`git fetch` + fast-forward / merge of - `origin/` into the local results checkout) and resumes normal sync. - -#### Why an explicit OK instead of auto-detecting the merge - -Auto-detecting "the temp branch was merged" is surprisingly hard and was the main -complexity in earlier drafts: - -- **Branch deletion is not a merge signal.** A user can delete without merging - (loses work), merge without deleting (never resumes), or be blocked from deleting - by branch protection. -- **Squash merge** (the repo's required style — `.agents/workflow.md` uses - `gh pr merge --squash`) gives the merge a *new* SHA, so the temp tip is not an - ancestor of the target even though the content merged. Detecting it requires - tree-equality comparison of every contributed run bundle — extra machinery for a - signal the user can simply give us. - -An explicit OK sidesteps all of it. It is also **safe**: if the user clicks OK -*without* having merged, AgentV just pulls the target (which lacks their work), -re-diverges on the next push, and re-creates a temp branch. Local run artifacts are -never lost, so a premature OK only costs one extra loop — no data loss, no force -push. - -#### Concurrency - -Each writer uses a unique `agentv/results-sync/--` branch, so temp pushes never collide, -and the runs they carry live in disjoint `runs///` dirs. The target branch -absorbs N temp PRs through N normal merges. The only true contention is the mutable -overlay, which Layer 1's JSON-union driver already handles for add/remove; a genuine -scalar overlay conflict is the rare case that reaches a PR. - -### Dashboard UX states - -`Clean | Ahead | Behind | Syncing | Merged remote (auto) | Pending merge (link) | -Unavailable` - -- `Merged remote (auto)`: transient toast after Layer 1 committed a real merge — the - user's push absorbed remote changes with no action. -- `Pending merge (link)`: Layer 2 card with the temp branch name, the compare/PR - link, and a single **OK** button ("I merged it — resync"). Optionally an - `gh`-enriched label showing the PR is merged/closed, as a convenience only; the OK - button remains the trigger. -- No per-file conflict view, no inline diffs, no accept-incoming/outgoing buttons. - -### Detecting "true conflict" vs auto-mergeable - -The split is purely whatever `git merge` (with the configured drivers) decides: -clean merge ⇒ Layer 1 pushes; conflicted merge ⇒ Layer 2. AgentV does not classify -conflicts itself, which keeps the core tiny. - -### Rationale against the product boundary (`.agents/product-boundary.md`) - -- **Zero-infra local to CI:** local-git fetch/merge/push for Layer 1; a plain - `git push` to a new ref + a URL string for Layer 2. `gh` is an optional - enrichment, never required. No webhook, no Phoenix, no hosted DB. -- **Portable artifacts as source of truth:** canonical branch advances only by real - merges; history is never rewritten. -- **Small composable core / narrow adapters:** stock git `union` + one tiny JSON - driver; the human path is "push a branch, open a PR on GitHub, click OK." -- **YAGNI:** no merge UI, no squash-safe detection, no event log/CRDT. The heaviest - earlier idea (per-file conflict editor from av-xwm) is explicitly **not** built. -- **Industry alignment:** "push a branch, open a PR, merge it on GitHub" is the - lowest-common-denominator flow; `merge=union` for append-only data is a standard - git idiom. - ---- - -## Alternatives Considered - -### A. Auto-detect merge (tree-equality / ancestor) instead of an OK button - -- Pros: no human click; could auto-resume. -- Cons: must be squash-safe (tree-equality across every contributed run bundle) and - must distinguish merge from deletion; meaningfully more code and edge cases for a - signal the user can give in one click. **Rejected** in favor of explicit OK. - -### B. Backup + force-with-lease (current `backup_and_force_push`) - -- Pros: no human step. -- Cons: rewrites shared history; concurrent remote commits survive only on a backup - ref. Violates repo safety norms. **Removed** by this design. - -### C. Per-file conflict-resolution UI (av-xwm) - -- Build incoming/outgoing accept buttons + inline diffs in the Dashboard. -- Cons: heavy UI to build and maintain; duplicates what GitHub's PR UI already does. - **Rejected** — GitHub's PR is the conflict surface. The av-xwm design's optimistic - concurrency for *stale tag writes* remains independently useful, but its conflict - *merge UI* is not a dependency here. - -### D. Rebase/replay local commits onto the remote tip - -- Cons: rewrites local commit SHAs; if those were ever shared (e.g. on a temp - branch) it reintroduces a history-rewrite hazard, and we do not care about linear - history on the results branch. **Rejected** in favor of merge. - -### E. Append-only / CRDT overlay (per-writer tag event files) - -- Makes even overlay conflicts structurally impossible. -- Cons: a layout migration the prior design explicitly declined (KTD6). The JSON - union driver already gets most of the benefit (add/remove commute). **Deferred** - as a potential end-state only if overlay conflicts prove common. - ---- - -## Phased, Non-Breaking Implementation Plan - -### Phase 0 — Merge drivers + `.gitattributes` - -- Add `.gitattributes` (`merge=union` for the index) to the results branch; register - the `agentv-json` driver in the AgentV-owned results checkout config. -- Non-breaking: drivers only affect merges AgentV performs. - -### Phase 1 — Auto-merge push loop (removes the force-push need) - -- Replace the `backup_and_force_push` branch in `resolveResultBranchPushConflict` - (`results-repo.ts:1380-1468`) with the bounded fetch → merge → push loop. -- Keep `push_conflict_policy` for back-compat but deprecate `backup_and_force_push`: - treat it as `'block'` + route true conflicts to Layer 2. Same-week/unreleased - latitude (`product-boundary.md` §6) may allow hard removal — confirm release state. -- Tests (temp-remote integration): FF push, auto-merge of disjoint run bundles, - union index merge, benign push-race retry. - -### Phase 2 — Temp-branch + OK-to-resync - -- Core helpers: `pushResultsSyncBranch()` (create-only push to - `agentv/results-sync/--`) and `pullResultsTargetBranch()` (fetch + FF/merge target into - the local checkout, invoked on OK). -- API: extend `POST /api/remote/sync` to return a `pending_merge` block - (`temp_branch`, `compare_url`, `contributed_run_count`); add - `POST /api/remote/sync/confirm-merge` (the OK action) that pulls the target and - returns refreshed status. -- Tests: true overlay conflict produces a temp branch + pending_merge payload; OK - pulls the target and clears pending state; premature OK (target not actually - merged) re-diverges without data loss. - -### Phase 3 — Dashboard UX - -- `RunSourceToolbar` / `project-sync-status`: `Merged remote (auto)` toast and a - `Pending merge` card with the compare/PR link and an **OK** button. -- Optional `gh` enrichment to label the PR state (convenience only). -- Browser UAT per `.agents/verification.md` (evidence to `agentv-private`). - -### Phase 4 — (Deferred) Append-only overlay - -- Only if overlay conflicts prove common in practice (Alternative E). - ---- - -## Non-Goals - -- Force push, blind or leased, anywhere. -- Rewriting shared history (no rebase-and-force of shared branches). -- A webhook server, hosted DB, or Phoenix dependency for sync. -- An AgentV merge/diff/conflict-editor UI — GitHub's PR is the conflict surface. -- Automatic merge detection (tree-equality/ancestor/deletion watching) — replaced by - an explicit OK. -- **Temp-branch deletion/cleanup.** The user owns the merge on GitHub, so deleting - the merged `agentv/results-sync/--` branch belongs to that same GitHub flow - (auto-delete-on-merge, or the user's manual cleanup). AgentV does not delete temp - branches and does not track them for cleanup. AgentV only creates the temp branch - and reads the target on OK. -- A CRDT or operation-log overlay layout in v1 (deferred to Phase 4). -- A CLI command family for conflict resolution (stays Dashboard/API-owned). - ---- - -## Open Questions - -- Release state of `backup_and_force_push`: hard-remove (same-week, unshipped) or - deprecate with a compatibility window? -- Keep `merge=union` on the index permanently, or rely on the rebuildable SQLite - index and treat the on-branch JSONL as best-effort? -- Default retry count `N` and backoff for the optimistic loop. diff --git a/packages/core/src/evaluation/results-repo.ts b/packages/core/src/evaluation/results-repo.ts index 65f3130b9..b07a0e58e 100644 --- a/packages/core/src/evaluation/results-repo.ts +++ b/packages/core/src/evaluation/results-repo.ts @@ -1906,7 +1906,7 @@ async function mergeRemoteIntoCheckedOutBranch( } // Layer 1 of the no-force-push results sync (see -// docs/plans/2026-06-24-001-feat-conflict-free-results-sync-plan.md). A bounded +// docs/adr/2026-06-24-no-force-push-results-sync.md). A bounded // fetch → merge → push loop: fast-forward when possible, otherwise commit a real // 3-way merge using the artifact-aware drivers (union index, agentv-json // overlay) and fast-forward the canonical branch onto it. A genuine overlay