diff --git a/apps/cli/src/commands/results/remote.ts b/apps/cli/src/commands/results/remote.ts index 907926df9..222552098 100644 --- a/apps/cli/src/commands/results/remote.ts +++ b/apps/cli/src/commands/results/remote.ts @@ -233,7 +233,6 @@ export async function loadNormalizedResultsConfig( }), ...(project.results.repoPath !== undefined && { repo_path: project.results.repoPath }), ...(project.results.branch !== undefined && { branch: project.results.branch }), - ...(project.results.remote !== undefined && { remote: project.results.remote }), ...(project.results.path !== undefined && { path: project.results.path }), ...((project.results.sync?.autoPush !== undefined || project.results.sync?.pushConflictPolicy !== undefined) && { diff --git a/apps/web/src/content/docs/docs/tools/dashboard.mdx b/apps/web/src/content/docs/docs/tools/dashboard.mdx index 8ebdb3ae6..21776a35b 100644 --- a/apps/web/src/content/docs/docs/tools/dashboard.mdx +++ b/apps/web/src/content/docs/docs/tools/dashboard.mdx @@ -291,10 +291,9 @@ projects: branch: agentv/results/v1 sync: auto_push: false - push_conflict_policy: block ``` -`results.repo.remote` is the Git remote URL used when AgentV creates a fresh results checkout, and the intended remote URL for portable project config. `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 does not add or rewrite remotes inside an existing checkout; the checkout's existing `origin` must already point at the repository you want to fetch and push. 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. For CI workflows where a push failure should fail the command after local artifacts are written, invoke the run with `agentv eval run --results-require-push`. `sync.push_conflict_policy` defaults to `block`; the removed `backup_and_force_push` value is rejected with migration guidance because AgentV never force-pushes result branches. 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. +`results.repo.remote` is the Git remote URL used when AgentV creates a fresh results checkout, and the intended remote URL for portable project config. `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 does not add or rewrite remotes inside an existing checkout; the checkout's existing `origin` must already point at the repository you want to fetch and push. 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. For CI workflows where a push failure should fail the command after local artifacts are written, invoke the run with `agentv eval run --results-require-push`. The default conflict behavior is block-and-ask; the removed `backup_and_force_push` value is rejected with migration guidance because AgentV never force-pushes result branches. 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`: @@ -311,7 +310,6 @@ projects: path: /home/entity/projects/EntityProcess/agentv-examples-eval-results sync: auto_push: true - push_conflict_policy: block ``` `results.repo.remote` is the Git remote URL used for clone and push operations, so use HTTPS when credentials are HTTP-token based and SSH when the runtime has SSH keys configured. When `results.repo.remote` is set and `results.repo.path` is missing or empty, AgentV creates that filesystem location with `git clone`. If `results.repo.path` already points at a Git checkout, AgentV treats that checkout's remotes as user-owned state: it fetches and pushes using the existing configured remote name (`origin` by default), but it does not run `git remote add` or `git remote set-url`. Omit `results.repo.remote` only when `results.repo.path` points at an already-existing local checkout such as `.`. @@ -326,7 +324,6 @@ results: branch: agentv/results/v1 sync: auto_push: false - push_conflict_policy: block ``` Project-local `.agentv/config.yaml` is for portable eval defaults such as `execution`, `eval_patterns`, and `dashboard`. Do not put `projects` in project-local config; AgentV warns and ignores it there. `results_by_project` is deprecated; use `projects[].results` in `$AGENTV_HOME/config.yaml`. @@ -374,7 +371,7 @@ projects: auto_push: true ``` -Current flat fields (`path`, `repo_url`, `ref`, `results.repo_url`, `results.repo_path`, `results.branch`, `results.remote`, and `results.path`) still load with migration warnings and are written back in nested form the next time AgentV saves the project registry. Older removed fields (`source`, `repository`, `results.mode`, `results.repo` as a string, `results.repository`, `results.local_path`, and `results.auto_push`) fail validation with migration guidance. +Current flat fields (`path`, `repo_url`, `ref`, `results.repo_url`, `results.repo_path`, `results.branch`, and `results.path`) still load with migration warnings and are written back in nested form the next time AgentV saves the project registry. Removed fields (`source`, `repository`, `results.mode`, `results.repo` as a string, `results.remote`, `results.repository`, `results.local_path`, and `results.auto_push`) fail validation with migration guidance. Use project-level **Sync Project** as the results exchange workflow. It handles pulled remote runs, locally edited metadata, dirty state, and blocked conflict feedback in one project-scoped action. diff --git a/apps/web/src/content/docs/docs/tools/results.mdx b/apps/web/src/content/docs/docs/tools/results.mdx index 7e8cf8f29..f4b41c55b 100644 --- a/apps/web/src/content/docs/docs/tools/results.mdx +++ b/apps/web/src/content/docs/docs/tools/results.mdx @@ -240,7 +240,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. AgentV never adds or rewrites remotes in an existing checkout; that checkout's `origin` must already point at the repository you want to fetch and push. 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`, `summary.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. Set `sync.auto_push: true` to push after publish. In CI, use `agentv eval run --results-require-push` when push failures should fail that invocation after local artifacts are written. 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. The removed `sync.push_conflict_policy: backup_and_force_push` value is rejected with migration guidance; remove the field or set it to `block`. 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. AgentV never adds or rewrites remotes in an existing checkout; that checkout's `origin` must already point at the repository you want to fetch and push. 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`, `summary.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. Set `sync.auto_push: true` to push after publish. In CI, use `agentv eval run --results-require-push` when push failures should fail that invocation after local artifacts are written. 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. The removed `sync.push_conflict_policy: backup_and_force_push` value is rejected with migration guidance; remove the field and rely on the default block-and-ask behavior. 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. diff --git a/docs/adr/0007-conflict-free-results-sync-without-force-push.md b/docs/adr/0007-conflict-free-results-sync-without-force-push.md index 0b606498d..1170be2b0 100644 --- a/docs/adr/0007-conflict-free-results-sync-without-force-push.md +++ b/docs/adr/0007-conflict-free-results-sync-without-force-push.md @@ -74,8 +74,8 @@ force push. `backup_and_force_push` is **hard-deprecated/removed** from supported config: the value shipped only on the `next` npm tag before stable release, so AgentV now rejects it with migration guidance instead of preserving a compatibility -alias. Remove the field or set `sync.push_conflict_policy: block`; AgentV never -force-pushes result branches. +alias. Remove the field and rely on the default block-and-ask behavior; AgentV +never force-pushes result branches. ## Consequences diff --git a/docs/plans/git-native-results.md b/docs/plans/git-native-results.md index bb573409c..9b8f9fca9 100644 --- a/docs/plans/git-native-results.md +++ b/docs/plans/git-native-results.md @@ -20,30 +20,32 @@ The configured results branch tree IS the index. `git ls-tree -r - ### Storage -- `results.repo_path` points at an existing local Git checkout whose object database and refs AgentV may write to. Use `repo_path: .` to store results in a dedicated branch of the source repo without checking that branch out in the source worktree. -- `results.repo_url` points at a remote results repository. AgentV manages a local clone at `results.path`; omit `path` to use the default AgentV data dir. -- `results.branch` is the storage branch. `repo_path` configs default to `agentv/results/v1`. +- `results.repo.path` points at an existing local Git checkout whose object database and refs AgentV may write to. Use `path: .` to store results in a dedicated branch of the source repo without checking that branch out in the source worktree. +- `results.repo.remote` points at the portable Git endpoint URL. AgentV manages a local clone at `results.repo.path` when the path is separate or omitted. +- `results.repo.branch` is the storage branch. Existing-checkout configs default to `agentv/results/v1`. - Local `.agentv/results/runs/` remains the active run workspace for local Dashboard, resume, and rerun flows. Publishing copies completed run artifacts into the branch-backed store under `runs/**` (the branch name already namespaces results, so no redundant `.agentv/results/` prefix on the branch). Editable tag overlays live alongside under `metadata/runs/**`. ```yaml # Existing checkout, usually the eval source repo. results: - repo_path: . - branch: agentv/results/v1 - remote: origin + repo: + remote: https://github.com/OWNER/REPO.git + path: . + branch: agentv/results/v1 sync: - auto_push: true + auto_push: false # Separate results repository. results: - repo_url: git@github.com:myorg/eval-results.git - branch: agentv/results/v1 - path: ~/data/agentv-results + repo: + remote: git@github.com:myorg/eval-results.git + branch: agentv/results/v1 + path: ~/data/agentv-results sync: auto_push: true ``` -The field is intentionally `repo_path`, not `repo`: `workspace.repos[].repo` is a portable repository identity, while `results.repo_path` is a filesystem path to an already-existing local checkout. +The nested `results.repo.path` field is a filesystem path to an existing local checkout or managed clone location; `workspace.repos[].repo` remains a separate portable repository identity. ### Writes diff --git a/docs/solutions/best-practices/name-portable-config-endpoints-by-user-intent.md b/docs/solutions/best-practices/name-portable-config-endpoints-by-user-intent.md index 40719bceb..7efb9b354 100644 --- a/docs/solutions/best-practices/name-portable-config-endpoints-by-user-intent.md +++ b/docs/solutions/best-practices/name-portable-config-endpoints-by-user-intent.md @@ -74,15 +74,7 @@ results: **Avoid making a checked-in config depend on a local alias:** -```yaml -results: - repo: - remote: origin - path: .agentv/remotes/agentv-private - branch: agentv/results/v1 -``` - -If a local alias override must exist for backward compatibility, keep it on an explicitly advanced or legacy option, not on the ordinary project config contract. +Do not set `results.repo.remote` to a local alias such as `origin`. If AgentV is operating on an existing checkout, it may still use that checkout's local alias internally, but the persisted config should either use the endpoint URL or omit `results.repo.remote`. ## Related diff --git a/docs/solutions/conventions/hard-correct-next-tag-only-surfaces.md b/docs/solutions/conventions/hard-correct-next-tag-only-surfaces.md index 09ad7c96d..b1cc4ccc4 100644 --- a/docs/solutions/conventions/hard-correct-next-tag-only-surfaces.md +++ b/docs/solutions/conventions/hard-correct-next-tag-only-surfaces.md @@ -33,14 +33,8 @@ release channels: especially when preserving the surface would encode a dangerous or misleading contract. -For removed config values, make the correction explicit: - -```yaml -results: - sync: - # Remove unsupported aliases and use the stable default. - push_conflict_policy: block -``` +For removed config values, make the correction explicit: delete the unsupported +field and rely on the stable default behavior when no replacement is needed. If existing local registries or generated config may contain the removed value, either reject it with migration guidance or drop it during a registry migration diff --git a/packages/core/src/evaluation/loaders/config-loader.ts b/packages/core/src/evaluation/loaders/config-loader.ts index 5ec57f9a8..c6d894765 100644 --- a/packages/core/src/evaluation/loaders/config-loader.ts +++ b/packages/core/src/evaluation/loaders/config-loader.ts @@ -57,6 +57,7 @@ export type ResultsConfig = { readonly repo_path?: string; /** Optional remote branch used as the canonical git-backed results store. */ readonly branch?: string; + /** Runtime-only local Git remote-name override. Persistent config should not set this. */ readonly remote?: string; /** Local filesystem path for the results clone. Optional; defaults to ~/.agentv/results//. */ readonly path?: string; @@ -685,7 +686,6 @@ type NestedResultsRepoConfig = { readonly repo_url?: string; readonly repo_path?: string; readonly branch?: string; - readonly remote?: string; readonly path?: string; }; @@ -811,15 +811,13 @@ export function parseResultsConfig(raw: unknown, configPath: string): ResultsCon branch = obj.branch.trim(); } - let remote: string | undefined; - if (nestedRepo?.remote !== undefined) { - remote = nestedRepo.remote; - } else if (obj.remote !== undefined) { - if (typeof obj.remote !== 'string' || obj.remote.trim().length === 0) { - logWarning(`Invalid results.remote in ${configPath}, expected non-empty string`); + if (obj.remote !== undefined) { + if (!hasNestedRepo) { + logWarning( + `results.remote in ${configPath} is no longer supported in persistent config. Use results.repo.remote for a portable Git endpoint URL, or omit it and let AgentV use the local checkout remote alias internally.`, + ); return undefined; } - remote = obj.remote.trim(); } let resultsPath: string | undefined; @@ -895,7 +893,6 @@ export function parseResultsConfig(raw: unknown, configPath: string): ResultsCon ...(repoUrl && { repo_url: repoUrl }), ...(repoPath && { repo_path: repoPath }), ...(branch !== undefined && { branch }), - ...(remote !== undefined && { remote }), ...(resultsPath !== undefined && { path: resultsPath }), ...(typeof obj.auto_push === 'boolean' && { auto_push: obj.auto_push }), ...(sync && { sync }), diff --git a/packages/core/src/evaluation/validation/config-validator.ts b/packages/core/src/evaluation/validation/config-validator.ts index fa1dc8d6f..972ba5507 100644 --- a/packages/core/src/evaluation/validation/config-validator.ts +++ b/packages/core/src/evaluation/validation/config-validator.ts @@ -530,15 +530,12 @@ function validateFlatResultsRepoConfig( ); } - if ( - resultsRecord.remote !== undefined && - (typeof resultsRecord.remote !== 'string' || resultsRecord.remote.trim().length === 0) - ) { + if (resultsRecord.remote !== undefined) { addError( errors, filePath, `${location}.remote`, - `Field '${location}.remote' must be a non-empty string`, + `Field '${location}.remote' was removed from persistent config because it was a local Git remote-name alias. Use '${location}.repo.remote' for the portable Git endpoint URL, or omit it and let AgentV use the checkout remote alias internally.`, ); } @@ -590,15 +587,6 @@ function warnFlatResultsMigration( ); } } - - if (resultsRecord.remote !== undefined) { - addWarning( - errors, - filePath, - `${location}.remote`, - `Field '${location}.remote' is a legacy local Git remote-name override. Prefer omitting it; nested '${location}.repo.remote' is the portable Git remote URL and AgentV manages local aliases automatically.`, - ); - } } function validateResultsConfigBody( diff --git a/packages/core/src/projects.ts b/packages/core/src/projects.ts index 9cf0f8fca..e87ba28b5 100644 --- a/packages/core/src/projects.ts +++ b/packages/core/src/projects.ts @@ -76,7 +76,6 @@ export interface ProjectResultsConfig { repoUrl?: string; repoPath?: string; branch?: string; - remote?: string; path?: string; sync?: ProjectResultsSyncConfig; branchPrefix?: string; @@ -157,6 +156,7 @@ function readTrimmedString(value: unknown): string | undefined { let warnedRemovedBackupAndForcePushPolicy = false; let warnedRemovedRequirePushConfig = false; +let warnedRejectedFlatResultsRemoteConfig = false; function warnRemovedBackupAndForcePushPolicy(): void { if (warnedRemovedBackupAndForcePushPolicy) { @@ -178,6 +178,16 @@ function warnRemovedRequirePushConfig(): void { ); } +function warnRejectedFlatResultsRemoteConfig(): void { + if (warnedRejectedFlatResultsRemoteConfig) { + return; + } + warnedRejectedFlatResultsRemoteConfig = true; + console.warn( + '[agentv] projects[].results.remote is no longer supported in persistent config, so that results block was ignored while loading the project registry. Use projects[].results.repo.remote for a portable Git endpoint URL, or omit it and let AgentV use the local checkout remote alias internally.', + ); +} + function fromYaml(raw: unknown): ProjectEntry | null { if (!raw || typeof raw !== 'object') return null; const e = raw as Partial; @@ -203,6 +213,10 @@ function fromYaml(raw: unknown): ProjectEntry | null { } if (e.results && typeof e.results === 'object') { const r = e.results as Partial; + if (r.remote !== undefined) { + warnRejectedFlatResultsRemoteConfig(); + return entry; + } const resultsRepo = r.repo && typeof r.repo === 'object' && !Array.isArray(r.repo) ? r.repo : undefined; const repoUrl = @@ -216,14 +230,12 @@ function fromYaml(raw: unknown): ProjectEntry | null { ? (readTrimmedString(resultsRepo?.path) ?? readTrimmedString(r.path)) : readTrimmedString(r.path); const resultsBranch = readTrimmedString(resultsRepo?.branch) ?? readTrimmedString(r.branch); - const resultsRemote = resultsRepo ? undefined : readTrimmedString(r.remote); if (repoUrl || repoPath) { const sync = r.sync && typeof r.sync === 'object' ? r.sync : undefined; entry.results = { ...(repoUrl ? { repoUrl } : {}), ...(repoPath ? { repoPath } : {}), ...(resultsBranch ? { branch: resultsBranch } : {}), - ...(resultsRemote ? { remote: resultsRemote } : {}), ...(clonePath ? { path: clonePath } : {}), ...(sync && (typeof sync.auto_push === 'boolean' || @@ -283,19 +295,6 @@ function toYaml(entry: ProjectEntry): ProjectEntryYaml { const branchPrefix = entry.results.branchPrefix !== undefined ? { branch_prefix: entry.results.branchPrefix } : {}; - if (entry.results.remote !== undefined) { - yaml.results = { - ...(entry.results.repoUrl !== undefined && { repo_url: entry.results.repoUrl }), - ...(entry.results.repoPath !== undefined && { repo_path: entry.results.repoPath }), - ...(entry.results.branch !== undefined && { branch: entry.results.branch }), - remote: entry.results.remote, - ...(entry.results.path !== undefined && { path: entry.results.path }), - ...resultsSync, - ...branchPrefix, - }; - return yaml; - } - const resultsRepo: ProjectResultsRepoYaml = { ...(entry.results.repoUrl !== undefined && { remote: entry.results.repoUrl }), ...(entry.results.branch !== undefined && { branch: entry.results.branch }), diff --git a/packages/core/test/evaluation/loaders/config-loader.test.ts b/packages/core/test/evaluation/loaders/config-loader.test.ts index 436cedeb3..9ff7e3dad 100644 --- a/packages/core/test/evaluation/loaders/config-loader.test.ts +++ b/packages/core/test/evaluation/loaders/config-loader.test.ts @@ -381,7 +381,6 @@ describe('parseResultsConfig', () => { { repo_path: '.', branch: 'agentv/results/v1', - remote: 'upstream', sync: { auto_push: false, push_conflict_policy: 'block', @@ -394,7 +393,6 @@ describe('parseResultsConfig', () => { mode: 'github', repo_path: '.', branch: 'agentv/results/v1', - remote: 'upstream', sync: { auto_push: false, push_conflict_policy: 'block', @@ -402,6 +400,26 @@ describe('parseResultsConfig', () => { }); }); + it('rejects flat results.remote in persistent config', () => { + const warn = spyOn(console, 'warn').mockImplementation(() => undefined); + try { + const result = parseResultsConfig( + { + repo_path: '.', + branch: 'agentv/results/v1', + remote: 'origin', + }, + '/tmp/.agentv/config.yaml', + ); + + expect(result).toBeUndefined(); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('results.remote')); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('results.repo.remote')); + } finally { + warn.mockRestore(); + } + }); + it('rejects require_push in persistent results sync config', () => { const warn = spyOn(console, 'warn').mockImplementation(() => undefined); try { diff --git a/packages/core/test/evaluation/validation/config-validator.test.ts b/packages/core/test/evaluation/validation/config-validator.test.ts index 5b8e0ed2f..2b577e047 100644 --- a/packages/core/test/evaluation/validation/config-validator.test.ts +++ b/packages/core/test/evaluation/validation/config-validator.test.ts @@ -190,6 +190,31 @@ describe('validateConfigFile', () => { ); }); + it('errors on removed flat results.remote persistent config', async () => { + const filePath = path.join(tempDir, 'removed-flat-results-remote.yaml'); + await writeFile( + filePath, + `results: + repo_path: . + branch: agentv/results/v1 + remote: origin +`, + ); + + const result = await validateConfigFile(filePath); + + expect(result.valid).toBe(false); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + severity: 'error', + location: 'results.remote', + message: expect.stringContaining('results.repo.remote'), + }), + ]), + ); + }); + it('keeps flat project repo fields compatible with migration warnings', async () => { const filePath = path.join(tempDir, 'global-config-flat-project.yaml'); await writeFile( @@ -394,6 +419,15 @@ describe('validateConfigFile', () => { migration: 'repo.remote', }, + { + field: 'results.remote', + yaml: `results: + repo_path: . + branch: agentv/results/v1 + remote: origin`, + location: 'projects[0].results.remote', + migration: 'repo.remote', + }, { field: 'results.repository', yaml: `results: diff --git a/packages/core/test/projects.test.ts b/packages/core/test/projects.test.ts index 378a653df..2ea0b7400 100644 --- a/packages/core/test/projects.test.ts +++ b/packages/core/test/projects.test.ts @@ -236,7 +236,6 @@ describe('projects registry', () => { branch: agentv/results/v1 sync: auto_push: false - push_conflict_policy: block added_at: "2026-01-01T00:00:00Z" last_opened_at: "2026-01-01T00:00:00Z" `, @@ -248,7 +247,7 @@ describe('projects registry', () => { repoUrl: 'git@github.com:example/source.git', path: '.', branch: 'agentv/results/v1', - sync: { autoPush: false, pushConflictPolicy: 'block' }, + sync: { autoPush: false }, }); saveProjectRegistry(registry); @@ -258,15 +257,16 @@ describe('projects registry', () => { expect(yamlOnDisk).toContain('path: .'); expect(yamlOnDisk).toContain('branch: agentv/results/v1'); expect(yamlOnDisk).toContain('auto_push: false'); - expect(yamlOnDisk).toContain('push_conflict_policy: block'); + expect(yamlOnDisk).not.toContain('push_conflict_policy:'); expect(yamlOnDisk).not.toContain('repo_path:'); expect(yamlOnDisk).not.toContain('repoPath:'); expect(yamlOnDisk).not.toContain('require_push:'); }); - it('preserves legacy flat results remote aliases through YAML', () => { + it('warns and rejects results blocks with legacy flat results remote aliases', () => { const registryPath = getProjectsRegistryPath(); mkdirSync(path.dirname(registryPath), { recursive: true }); + const warnSpy = spyOn(console, 'warn').mockImplementation(() => undefined); writeFileSync( registryPath, `projects: @@ -283,19 +283,20 @@ describe('projects registry', () => { 'utf-8', ); - const registry = loadProjectRegistry(); - expect(registry.projects[0].results).toEqual({ - repoPath: '.', - branch: 'agentv/results/v1', - remote: 'upstream', - }); - - saveProjectRegistry(registry); - const yamlOnDisk = readFileSync(registryPath, 'utf-8'); - expect(yamlOnDisk).toContain('repo_path: .'); - expect(yamlOnDisk).toContain('branch: agentv/results/v1'); - expect(yamlOnDisk).toContain('remote: upstream'); - expect(yamlOnDisk).not.toContain('results:\n repo:\n'); + try { + const registry = loadProjectRegistry(); + expect(registry.projects[0].results).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('projects[].results.remote')); + + saveProjectRegistry(registry); + const yamlOnDisk = readFileSync(registryPath, 'utf-8'); + expect(yamlOnDisk).not.toContain('results:'); + expect(yamlOnDisk).toContain('repo:'); + expect(yamlOnDisk).not.toContain('remote: upstream'); + expect(yamlOnDisk).not.toContain('repo_path:'); + } finally { + warnSpy.mockRestore(); + } }); it('preserves unrelated global config keys when saving projects', () => {