Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/web/src/content/docs/docs/tools/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down Expand Up @@ -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.

Expand All @@ -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/<timestamp>-<target_branch_slug>-<remote_short_sha>` 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/<timestamp>-<branch-slug>-<random>` 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.
2 changes: 1 addition & 1 deletion apps/web/src/content/docs/docs/tools/results.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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/<run-path>/<pointer.path>`. 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/<timestamp>-<target_branch_slug>-<remote_short_sha>` 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/<run-path>/<pointer.path>`. 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.
130 changes: 130 additions & 0 deletions docs/adr/2026-06-24-no-force-push-results-sync.md
Original file line number Diff line number Diff line change
@@ -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/<exp>/<ts>/**` | 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/<utc_ts>-<branch_slug>-<rand6>` (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).
Loading
Loading