Fix plan-review worktree document tracking
This commit is contained in:
@@ -34,6 +34,12 @@ _NO_CHANGE_ACK_MARKERS = (
|
|||||||
"code is correct as-is",
|
"code is correct as-is",
|
||||||
"already correct",
|
"already correct",
|
||||||
"no action required",
|
"no action required",
|
||||||
|
"변경 없음",
|
||||||
|
"수정 없음",
|
||||||
|
"수정할 필요 없음",
|
||||||
|
"변경할 필요 없음",
|
||||||
|
"이미 올바름",
|
||||||
|
"조치 불필요",
|
||||||
)
|
)
|
||||||
_CHANGE_CLAIM_MARKERS = (
|
_CHANGE_CLAIM_MARKERS = (
|
||||||
"summary of all changes made",
|
"summary of all changes made",
|
||||||
@@ -73,6 +79,15 @@ _CHANGE_CLAIM_MARKERS = (
|
|||||||
"completed the implementation",
|
"completed the implementation",
|
||||||
"all changes have been made",
|
"all changes have been made",
|
||||||
"changes are complete",
|
"changes are complete",
|
||||||
|
"수정 완료",
|
||||||
|
"모든 수정이 완료",
|
||||||
|
"변경 요약",
|
||||||
|
"변경 파일",
|
||||||
|
"신규 생성",
|
||||||
|
"기획서 수정",
|
||||||
|
"체크리스트 수정",
|
||||||
|
"문서를 수정",
|
||||||
|
"문서 수정",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,27 +108,48 @@ def _setup_worktree(cwd: Path, run_dir: Path, preset_name: str) -> tuple[Path, s
|
|||||||
def _copy_inputs_to_worktree(
|
def _copy_inputs_to_worktree(
|
||||||
config: PipelineConfig,
|
config: PipelineConfig,
|
||||||
worktree_path: Path,
|
worktree_path: Path,
|
||||||
|
*,
|
||||||
|
base_cwd: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Copy input files (plan, checklist, etc.) into the worktree.
|
"""Copy input files (plan, checklist, etc.) into the worktree.
|
||||||
|
|
||||||
This ensures agents running in plan/read-only mode within the worktree
|
Repo-local inputs are remapped to the corresponding path inside the worktree
|
||||||
can access these files, even though the originals live in the base repo.
|
so agentic edits produce a real git diff. External inputs are copied into a
|
||||||
Updates config.inputs in-place so subsequent reference refreshes use
|
dedicated inputs directory. For ``plan-review`` these external copies remain
|
||||||
|
tracked so document edits can survive on the branch; other presets keep them
|
||||||
|
ignored to avoid polluting code diffs.
|
||||||
|
|
||||||
|
Updates ``config.inputs`` in-place so subsequent reference refreshes use
|
||||||
worktree-local paths.
|
worktree-local paths.
|
||||||
"""
|
"""
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
base_root = base_cwd.resolve()
|
||||||
|
track_external_inputs = config.preset_name == "plan-review"
|
||||||
inputs_dir = worktree_path / ".cross-eval-inputs"
|
inputs_dir = worktree_path / ".cross-eval-inputs"
|
||||||
inputs_dir.mkdir(exist_ok=True)
|
inputs_dir.mkdir(exist_ok=True)
|
||||||
# Exclude from git so these don't pollute agentic diffs
|
if not track_external_inputs:
|
||||||
(inputs_dir / ".gitignore").write_text("*\n", encoding="utf-8")
|
# 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()):
|
for key, val in list(config.inputs.items()):
|
||||||
if key.endswith("_ref") or not isinstance(val, Path):
|
if key.endswith("_ref") or not isinstance(val, Path):
|
||||||
continue
|
continue
|
||||||
if not val.exists():
|
if not val.exists():
|
||||||
continue
|
continue
|
||||||
dest = inputs_dir / val.name
|
resolved = val.resolve()
|
||||||
shutil.copy2(val, dest)
|
try:
|
||||||
config.inputs[key] = dest
|
rel_path = resolved.relative_to(base_root)
|
||||||
|
except ValueError:
|
||||||
|
dest = inputs_dir / val.name
|
||||||
|
shutil.copy2(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)
|
||||||
|
config.inputs[key] = worktree_target
|
||||||
|
|
||||||
|
|
||||||
def _snapshot_repo_state(cwd: Path) -> dict[str, str]:
|
def _snapshot_repo_state(cwd: Path) -> dict[str, str]:
|
||||||
@@ -329,7 +350,7 @@ def _run_simple_pipeline(
|
|||||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||||
cwd, run_dir, config.preset_name,
|
cwd, run_dir, config.preset_name,
|
||||||
)
|
)
|
||||||
_copy_inputs_to_worktree(config, worktree_path)
|
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||||
_refresh_input_references(config, input_contents)
|
_refresh_input_references(config, input_contents)
|
||||||
base_repo_state = _snapshot_repo_state(cwd)
|
base_repo_state = _snapshot_repo_state(cwd)
|
||||||
base_repo_status = _snapshot_repo_status(cwd)
|
base_repo_status = _snapshot_repo_status(cwd)
|
||||||
@@ -508,7 +529,7 @@ def _run_phased_pipeline(
|
|||||||
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
worktree_path, agentic_branch_name, agentic_base_commit = _setup_worktree(
|
||||||
cwd, run_dir, config.preset_name,
|
cwd, run_dir, config.preset_name,
|
||||||
)
|
)
|
||||||
_copy_inputs_to_worktree(config, worktree_path)
|
_copy_inputs_to_worktree(config, worktree_path, base_cwd=cwd)
|
||||||
_refresh_input_references(config, input_contents)
|
_refresh_input_references(config, input_contents)
|
||||||
base_repo_state = _snapshot_repo_state(cwd)
|
base_repo_state = _snapshot_repo_state(cwd)
|
||||||
base_repo_status = _snapshot_repo_status(cwd)
|
base_repo_status = _snapshot_repo_status(cwd)
|
||||||
|
|||||||
@@ -465,6 +465,9 @@ class TestExpandedClaimMarkers(unittest.TestCase):
|
|||||||
def test_changes_are_complete(self) -> None:
|
def test_changes_are_complete(self) -> None:
|
||||||
self.assertTrue(_claims_file_changes("All changes are complete"))
|
self.assertTrue(_claims_file_changes("All changes are complete"))
|
||||||
|
|
||||||
|
def test_korean_change_summary_triggers(self) -> None:
|
||||||
|
self.assertTrue(_claims_file_changes("모든 수정이 완료되었습니다. 아래는 변경 요약입니다."))
|
||||||
|
|
||||||
|
|
||||||
class TestExpandedNoChangeMarkers(unittest.TestCase):
|
class TestExpandedNoChangeMarkers(unittest.TestCase):
|
||||||
"""New no-change markers prevent false positives."""
|
"""New no-change markers prevent false positives."""
|
||||||
@@ -484,6 +487,9 @@ class TestExpandedNoChangeMarkers(unittest.TestCase):
|
|||||||
def test_no_action_required(self) -> None:
|
def test_no_action_required(self) -> None:
|
||||||
self.assertFalse(_claims_file_changes("No action required"))
|
self.assertFalse(_claims_file_changes("No action required"))
|
||||||
|
|
||||||
|
def test_korean_no_change_marker(self) -> None:
|
||||||
|
self.assertFalse(_claims_file_changes("변경할 필요 없음"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 6. Cross-iteration evidence propagation
|
# 6. Cross-iteration evidence propagation
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from cross_eval.agent import (
|
|||||||
)
|
)
|
||||||
from cross_eval.models import AgentConfig, AgentResult, ExecutionConfig, PipelineConfig, StepConfig
|
from cross_eval.models import AgentConfig, AgentResult, ExecutionConfig, PipelineConfig, StepConfig
|
||||||
from cross_eval.pipeline import (
|
from cross_eval.pipeline import (
|
||||||
|
_copy_inputs_to_worktree,
|
||||||
_commit_iteration,
|
_commit_iteration,
|
||||||
_execute_parallel_batch,
|
_execute_parallel_batch,
|
||||||
_execute_step,
|
_execute_step,
|
||||||
@@ -118,6 +119,42 @@ class TestInvokeAgentRuntime(unittest.TestCase):
|
|||||||
self.assertEqual(ctx.exception.failure_type, "API_ERROR")
|
self.assertEqual(ctx.exception.failure_type, "API_ERROR")
|
||||||
self.assertIn("backend down", ctx.exception.raw_error)
|
self.assertIn("backend down", ctx.exception.raw_error)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorktreeInputMapping(unittest.TestCase):
|
||||||
|
def test_repo_local_plan_input_maps_to_tracked_worktree_path(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
repo = Path(tmpdir) / "repo"
|
||||||
|
repo.mkdir()
|
||||||
|
_init_git_repo(repo)
|
||||||
|
(repo / "plan.md").write_text("plan v1\n", encoding="utf-8")
|
||||||
|
subprocess.run(["git", "add", "plan.md"], cwd=repo, capture_output=True, check=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "commit", "-m", "add plan"],
|
||||||
|
cwd=repo,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
worktree_dir = Path(tmpdir) / "wt"
|
||||||
|
branch = "cross-eval/test-plan-review"
|
||||||
|
worktree_path, _ = create_worktree(repo, worktree_dir, branch)
|
||||||
|
try:
|
||||||
|
config = PipelineConfig(
|
||||||
|
inputs={"plan": repo / "plan.md"},
|
||||||
|
preset_name="plan-review",
|
||||||
|
)
|
||||||
|
|
||||||
|
_copy_inputs_to_worktree(config, worktree_path, base_cwd=repo)
|
||||||
|
|
||||||
|
self.assertEqual(config.inputs["plan"], worktree_path / "plan.md")
|
||||||
|
finally:
|
||||||
|
remove_worktree(base_cwd=repo, work_dir=worktree_path)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "branch", "-D", branch],
|
||||||
|
cwd=repo,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
def test_classify_unknown_failure(self) -> None:
|
def test_classify_unknown_failure(self) -> None:
|
||||||
failure_type, suggested_action = _classify_agent_failure("weird crash")
|
failure_type, suggested_action = _classify_agent_failure("weird crash")
|
||||||
self.assertEqual(failure_type, "UNKNOWN")
|
self.assertEqual(failure_type, "UNKNOWN")
|
||||||
|
|||||||
Reference in New Issue
Block a user