continue
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from hashlib import sha256
|
||||
@@ -34,6 +35,19 @@ from cross_eval.runtime_env import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_current_head(cwd: Path) -> str | None:
|
||||
"""Return the current HEAD SHA for an existing repository."""
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return result.stdout.strip() or None
|
||||
|
||||
|
||||
def run_pipeline(
|
||||
config: PipelineConfig,
|
||||
cwd: Path | None = None,
|
||||
@@ -124,8 +138,6 @@ def _copy_inputs_to_worktree(
|
||||
Updates ``config.inputs`` in-place so subsequent reference refreshes use
|
||||
worktree-local paths.
|
||||
"""
|
||||
import shutil
|
||||
|
||||
base_root = base_cwd.resolve()
|
||||
track_external_inputs = config.preset_name == "plan-review"
|
||||
inputs_dir = worktree_path / ".cross-eval-inputs"
|
||||
@@ -134,7 +146,7 @@ def _copy_inputs_to_worktree(
|
||||
# Exclude read-only input copies from git so they don't pollute code diffs.
|
||||
(inputs_dir / ".gitignore").write_text("*\n", encoding="utf-8")
|
||||
for key, val in list(config.inputs.items()):
|
||||
if key.endswith("_ref") or not isinstance(val, Path):
|
||||
if not isinstance(val, Path):
|
||||
continue
|
||||
if not val.exists():
|
||||
continue
|
||||
@@ -143,17 +155,71 @@ def _copy_inputs_to_worktree(
|
||||
rel_path = resolved.relative_to(base_root)
|
||||
except ValueError:
|
||||
dest = inputs_dir / val.name
|
||||
shutil.copy2(resolved, dest)
|
||||
_copy_path(resolved, dest)
|
||||
config.inputs[key] = dest
|
||||
continue
|
||||
|
||||
worktree_target = worktree_path / rel_path
|
||||
if not worktree_target.exists():
|
||||
worktree_target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(resolved, worktree_target)
|
||||
_copy_path(resolved, worktree_target)
|
||||
config.inputs[key] = worktree_target
|
||||
|
||||
|
||||
def _snapshot_input_paths(config: PipelineConfig) -> dict[str, Path]:
|
||||
"""Capture original on-disk input paths before remapping into a worktree."""
|
||||
return {
|
||||
key: val
|
||||
for key, val in config.inputs.items()
|
||||
if isinstance(val, Path)
|
||||
}
|
||||
|
||||
|
||||
def _apply_worktree_inputs_to_base(
|
||||
config: PipelineConfig,
|
||||
original_inputs: dict[str, Path],
|
||||
*,
|
||||
cwd: Path,
|
||||
) -> list[Path]:
|
||||
"""Copy the final worktree-edited inputs back onto the user-provided paths."""
|
||||
restored: list[Path] = []
|
||||
for key, original_path in original_inputs.items():
|
||||
current_path = config.inputs.get(key)
|
||||
if not isinstance(current_path, Path) or not current_path.exists():
|
||||
continue
|
||||
if current_path.resolve() == original_path.resolve():
|
||||
continue
|
||||
_copy_path(current_path, original_path)
|
||||
restored.append(original_path)
|
||||
return restored
|
||||
|
||||
|
||||
def _commit_base_repo_paths(cwd: Path, paths: list[Path], message: str) -> bool:
|
||||
"""Commit changed input paths in the base repository when they live under cwd."""
|
||||
rel_paths: list[str] = []
|
||||
for path in paths:
|
||||
try:
|
||||
rel_paths.append(str(path.resolve().relative_to(cwd.resolve())))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if not rel_paths:
|
||||
return False
|
||||
|
||||
subprocess.run(
|
||||
["git", "add", "--", *rel_paths],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
result = subprocess.run(
|
||||
["git", "commit", "-m", message],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def _snapshot_repo_state(cwd: Path) -> dict[str, str]:
|
||||
"""Capture the base repository working-tree state.
|
||||
|
||||
@@ -344,18 +410,26 @@ def _run_simple_pipeline(
|
||||
|
||||
# Setup shared worktree for agentic mode
|
||||
worktree_path: Path | None = None
|
||||
agent_execution_path: Path | None = None
|
||||
agentic_branch_name: str | None = None
|
||||
agentic_base_commit: str | None = None
|
||||
original_input_paths: dict[str, Path] = {}
|
||||
base_repo_state: dict[str, str] | None = None
|
||||
base_repo_status: str | None = None
|
||||
if not dry_run and _has_agentic_steps(config, config.pipeline):
|
||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||
cwd, run_dir, config.preset_name,
|
||||
)
|
||||
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||
_refresh_input_references(config, input_contents)
|
||||
base_repo_state = _snapshot_repo_state(cwd)
|
||||
base_repo_status = _snapshot_repo_status(cwd)
|
||||
if config.use_worktree:
|
||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||
cwd, run_dir, config.preset_name,
|
||||
)
|
||||
original_input_paths = _snapshot_input_paths(config)
|
||||
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||
_refresh_input_references(config, input_contents)
|
||||
base_repo_state = _snapshot_repo_state(cwd)
|
||||
base_repo_status = _snapshot_repo_status(cwd)
|
||||
agent_execution_path = worktree_path
|
||||
else:
|
||||
agent_execution_path = cwd
|
||||
agentic_base_commit = _get_current_head(cwd)
|
||||
|
||||
feedback = "(no feedback — first iteration)"
|
||||
iterations: list[IterationResult] = []
|
||||
@@ -381,7 +455,7 @@ def _run_simple_pipeline(
|
||||
config.pipeline, config, input_contents, feedback,
|
||||
i, config.max_iterations, cwd, timeout, dry_run,
|
||||
run_dir=run_dir, output_iter=i,
|
||||
worktree_path=worktree_path,
|
||||
worktree_path=agent_execution_path,
|
||||
runtime_env=runtime_env,
|
||||
base_repo_state=base_repo_state,
|
||||
base_repo_status=base_repo_status,
|
||||
@@ -389,7 +463,7 @@ def _run_simple_pipeline(
|
||||
)
|
||||
|
||||
# Intermediate commit so next iteration's diff only shows new changes
|
||||
if worktree_path is not None:
|
||||
if config.use_worktree and worktree_path is not None:
|
||||
agentic_base_commit = _commit_iteration(worktree_path, config.preset_name, i, verdict)
|
||||
|
||||
iter_result = IterationResult(
|
||||
@@ -480,8 +554,25 @@ def _run_simple_pipeline(
|
||||
break
|
||||
|
||||
finally:
|
||||
if config.use_worktree and worktree_path is not None and original_input_paths:
|
||||
restored_paths = _apply_worktree_inputs_to_base(
|
||||
config, original_input_paths, cwd=cwd,
|
||||
)
|
||||
if restored_paths:
|
||||
try:
|
||||
committed = _commit_base_repo_paths(
|
||||
cwd,
|
||||
restored_paths,
|
||||
f"cross-eval: {config.preset_name} ({final_verdict})",
|
||||
)
|
||||
if committed:
|
||||
logger.info(" Applied and committed final input changes in base repo.")
|
||||
else:
|
||||
logger.info(" Applied final input changes in base repo (no commit created).")
|
||||
except Exception:
|
||||
logger.warning(" Failed to commit final input changes in base repo", exc_info=True)
|
||||
agentic_branch: str | None = None
|
||||
if worktree_path is not None and agentic_branch_name is not None:
|
||||
if config.use_worktree and worktree_path is not None and agentic_branch_name is not None:
|
||||
agentic_branch = _finalize_worktree(
|
||||
cwd, worktree_path, agentic_branch_name,
|
||||
config.preset_name, final_verdict,
|
||||
@@ -523,18 +614,26 @@ def _run_phased_pipeline(
|
||||
# Setup shared worktree for agentic mode
|
||||
all_phase_steps = [s for p in config.phases for s in p.steps]
|
||||
worktree_path: Path | None = None
|
||||
agent_execution_path: Path | None = None
|
||||
agentic_branch_name: str | None = None
|
||||
agentic_base_commit: str | None = None
|
||||
original_input_paths: dict[str, Path] = {}
|
||||
base_repo_state: dict[str, str] | None = None
|
||||
base_repo_status: str | None = None
|
||||
if not dry_run and _has_agentic_steps(config, all_phase_steps):
|
||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||
cwd, run_dir, config.preset_name,
|
||||
)
|
||||
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||
_refresh_input_references(config, input_contents)
|
||||
base_repo_state = _snapshot_repo_state(cwd)
|
||||
base_repo_status = _snapshot_repo_status(cwd)
|
||||
if config.use_worktree:
|
||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||
cwd, run_dir, config.preset_name,
|
||||
)
|
||||
original_input_paths = _snapshot_input_paths(config)
|
||||
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||
_refresh_input_references(config, input_contents)
|
||||
base_repo_state = _snapshot_repo_state(cwd)
|
||||
base_repo_status = _snapshot_repo_status(cwd)
|
||||
agent_execution_path = worktree_path
|
||||
else:
|
||||
agent_execution_path = cwd
|
||||
agentic_base_commit = _get_current_head(cwd)
|
||||
|
||||
iterations: list[IterationResult] = []
|
||||
feedback = "(no feedback — first iteration)"
|
||||
@@ -581,7 +680,7 @@ def _run_phased_pipeline(
|
||||
phase.steps, config, input_contents, feedback,
|
||||
pi, phase.max_iterations, cwd, timeout, dry_run,
|
||||
run_dir=run_dir, output_iter=global_iter, phase_name=phase.name,
|
||||
worktree_path=worktree_path,
|
||||
worktree_path=agent_execution_path,
|
||||
runtime_env=runtime_env,
|
||||
base_repo_state=base_repo_state,
|
||||
base_repo_status=base_repo_status,
|
||||
@@ -589,7 +688,7 @@ def _run_phased_pipeline(
|
||||
)
|
||||
|
||||
# Intermediate commit so next iteration's diff only shows new changes
|
||||
if worktree_path is not None:
|
||||
if config.use_worktree and worktree_path is not None:
|
||||
agentic_base_commit = _commit_iteration(
|
||||
worktree_path, f"{config.preset_name}/{phase.name}",
|
||||
global_iter, verdict,
|
||||
@@ -717,8 +816,25 @@ def _run_phased_pipeline(
|
||||
final_verdict = "PASS" if phase_converged else "MAX_ITERATIONS_REACHED"
|
||||
|
||||
finally:
|
||||
if config.use_worktree and worktree_path is not None and original_input_paths:
|
||||
restored_paths = _apply_worktree_inputs_to_base(
|
||||
config, original_input_paths, cwd=cwd,
|
||||
)
|
||||
if restored_paths:
|
||||
try:
|
||||
committed = _commit_base_repo_paths(
|
||||
cwd,
|
||||
restored_paths,
|
||||
f"cross-eval: {config.preset_name} ({final_verdict})",
|
||||
)
|
||||
if committed:
|
||||
logger.info(" Applied and committed final input changes in base repo.")
|
||||
else:
|
||||
logger.info(" Applied final input changes in base repo (no commit created).")
|
||||
except Exception:
|
||||
logger.warning(" Failed to commit final input changes in base repo", exc_info=True)
|
||||
agentic_branch: str | None = None
|
||||
if worktree_path is not None and agentic_branch_name is not None:
|
||||
if config.use_worktree and worktree_path is not None and agentic_branch_name is not None:
|
||||
agentic_branch = _finalize_worktree(
|
||||
cwd, worktree_path, agentic_branch_name,
|
||||
config.preset_name, final_verdict,
|
||||
@@ -752,6 +868,8 @@ def _load_inputs(config: PipelineConfig) -> dict[str, str]:
|
||||
for key, val in config.inputs.items():
|
||||
if key.endswith("_ref"):
|
||||
input_contents[key] = str(val)
|
||||
elif key == "docs":
|
||||
input_contents[key] = _load_docs_input(config, current_value=val)
|
||||
elif isinstance(val, str):
|
||||
input_contents[key] = val
|
||||
else:
|
||||
@@ -767,6 +885,8 @@ def _refresh_inputs(
|
||||
for key, val in config.inputs.items():
|
||||
if key.endswith("_ref"):
|
||||
input_contents[key] = str(val)
|
||||
elif key == "docs":
|
||||
input_contents[key] = _load_docs_input(config, current_value=val)
|
||||
elif isinstance(val, str):
|
||||
input_contents[key] = val
|
||||
elif isinstance(val, Path) and val.exists():
|
||||
@@ -774,6 +894,40 @@ def _refresh_inputs(
|
||||
_refresh_input_references(config, input_contents)
|
||||
|
||||
|
||||
def _load_docs_input(config: PipelineConfig, *, current_value: Path | str) -> str:
|
||||
"""Load docs content from docs_ref when available so edits are visible next iteration."""
|
||||
docs_ref = config.inputs.get("docs_ref")
|
||||
docs_path = docs_ref if isinstance(docs_ref, Path) else None
|
||||
if docs_path is not None and docs_path.exists():
|
||||
if docs_path.is_dir():
|
||||
return _read_docs_tree(docs_path)
|
||||
try:
|
||||
return docs_path.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
return ""
|
||||
if isinstance(current_value, str):
|
||||
return current_value
|
||||
if current_value.exists() and current_value.is_file():
|
||||
return current_value.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
def _read_docs_tree(docs_dir: Path) -> str:
|
||||
"""Read all visible text files under a docs tree and concatenate them."""
|
||||
parts: list[str] = []
|
||||
for f in sorted(
|
||||
path for path in docs_dir.rglob("*")
|
||||
if path.is_file() and not any(part.startswith(".") for part in path.relative_to(docs_dir).parts)
|
||||
):
|
||||
try:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
rel_path = f.relative_to(docs_dir).as_posix()
|
||||
parts.append(f"### {rel_path}\n{content}")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def _refresh_input_references(
|
||||
config: PipelineConfig,
|
||||
input_contents: dict[str, str],
|
||||
@@ -1703,3 +1857,12 @@ def _save_report(run_dir: Path, config: PipelineConfig, result: PipelineResult)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
logger.info("Report saved: %s", report_path)
|
||||
|
||||
|
||||
def _copy_path(src: Path, dest: Path) -> None:
|
||||
"""Copy a file or directory into the worktree, preserving structure."""
|
||||
if src.is_dir():
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
return
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
Reference in New Issue
Block a user