fix: commit agent changes before opening PR
This commit is contained in:
@@ -34,7 +34,8 @@ Issue URL: {issue.html_url}
|
||||
Implement the requested change in this workspace. Keep the change scoped to this issue.
|
||||
Run the relevant tests before finishing.
|
||||
|
||||
Write `AGENT_IMPLEMENTATION_REPORT.md` in the workspace root using this exact section contract:
|
||||
Write `.agent-output/AGENT_IMPLEMENTATION_REPORT.md` using this exact section contract.
|
||||
Keep the section headings exactly as written below, but write the section content in Chinese:
|
||||
|
||||
- Summary
|
||||
- Files changed
|
||||
@@ -55,7 +56,9 @@ Issue: #{issue.issue_number} {issue.title}
|
||||
Review the implementation currently checked out in this workspace. Focus on correctness,
|
||||
scope control, test evidence, and human risks. Do not modify code.
|
||||
|
||||
Write `AGENT_REVIEW_REPORT.md` in the workspace root using this exact section contract:
|
||||
Write `.agent-output/AGENT_REVIEW_REPORT.md` using this exact section contract.
|
||||
Keep the section headings exactly as written below. Keep the Verdict token in English,
|
||||
but write the section content and Suggested PR Comment in Chinese:
|
||||
|
||||
- Verdict: APPROVE, REQUEST_CHANGES, or NEEDS_HUMAN_DECISION
|
||||
- Summary
|
||||
@@ -69,16 +72,16 @@ Write `AGENT_REVIEW_REPORT.md` in the workspace root using this exact section co
|
||||
|
||||
|
||||
def render_pr_body(issue: IssueRecord, implementation_report: str) -> str:
|
||||
return f"""Closes #{issue.issue_number}
|
||||
return f"""关联 Issue:#{issue.issue_number}
|
||||
|
||||
## Agent Implementation Report
|
||||
## 代理实现报告
|
||||
|
||||
{implementation_report.strip()}
|
||||
|
||||
## Human Review Gate
|
||||
## 人工审核
|
||||
|
||||
This PR was opened by the local agent manager. It has not been auto-merged.
|
||||
Human maintainers must review, decide, and merge manually.
|
||||
此 PR 由本地 agent-manager 自动创建,但不会自动合并。
|
||||
请维护者人工审核、决策并手动合并。
|
||||
"""
|
||||
|
||||
|
||||
@@ -102,13 +105,13 @@ def extract_section(raw: str, title: str) -> str:
|
||||
|
||||
|
||||
def render_human_review_summary(review: ReviewReport) -> str:
|
||||
return f"""## Agent Review Summary
|
||||
return f"""## 代理评审摘要
|
||||
|
||||
Verdict: `{review.verdict}`
|
||||
结论:`{review.verdict}`
|
||||
|
||||
{review.suggested_pr_comment.strip()}
|
||||
|
||||
## Human Action Required
|
||||
## 需要人工处理
|
||||
|
||||
Please review the PR manually. The agent manager will not merge, close, or request changes automatically.
|
||||
请人工审核该 PR。agent-manager 不会自动合并、关闭 PR 或提交变更请求。
|
||||
"""
|
||||
|
||||
@@ -132,12 +132,15 @@ class TaskRunner:
|
||||
error_message="implementer produced no diff",
|
||||
clear_lease=True,
|
||||
)
|
||||
commit_message = f"agent: implement issue #{issue.issue_number} - {issue.title}"
|
||||
commit_id = self.workspace_manager.commit_changes(workspace, commit_message)
|
||||
self.db.add_event(task.id, TaskState.TESTING, TaskState.TESTING, f"committed implementation {commit_id}")
|
||||
self.workspace_manager.push_branch(workspace, branch_name)
|
||||
pr_body = render_pr_body(issue, implementation_report)
|
||||
pr = self.gitea.create_pull_request(
|
||||
owner=repo.owner,
|
||||
name=repo.name,
|
||||
title=f"Agent: {issue.title}",
|
||||
title=f"代理实现:{issue.title}",
|
||||
body=pr_body,
|
||||
head=branch_name,
|
||||
base=repo.default_branch,
|
||||
@@ -191,7 +194,9 @@ class TaskRunner:
|
||||
workspace: Path,
|
||||
) -> str:
|
||||
prompt = render_implementer_prompt(repo, issue, branch_name)
|
||||
prompt_path = workspace / "AGENT_IMPLEMENTER_PROMPT.md"
|
||||
output_dir = workspace / ".agent-output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
prompt_path = output_dir / "AGENT_IMPLEMENTER_PROMPT.md"
|
||||
write_prompt(prompt_path, prompt)
|
||||
command = render_command(
|
||||
self.config.agents.implementer.command,
|
||||
@@ -202,7 +207,7 @@ class TaskRunner:
|
||||
branch_name=branch_name,
|
||||
)
|
||||
result = self.command_runner.run(command, workspace, stdin=prompt)
|
||||
report = read_report(workspace / "AGENT_IMPLEMENTATION_REPORT.md")
|
||||
report = read_report(output_dir / "AGENT_IMPLEMENTATION_REPORT.md")
|
||||
self.db.add_agent_run(
|
||||
task_id=task.id,
|
||||
role="implementer",
|
||||
@@ -226,7 +231,9 @@ class TaskRunner:
|
||||
workspace: Path,
|
||||
) -> str:
|
||||
prompt = render_reviewer_prompt(repo, issue, pr_number)
|
||||
prompt_path = workspace / "AGENT_REVIEWER_PROMPT.md"
|
||||
output_dir = workspace / ".agent-output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
prompt_path = output_dir / "AGENT_REVIEWER_PROMPT.md"
|
||||
write_prompt(prompt_path, prompt)
|
||||
command = render_command(
|
||||
self.config.agents.reviewer.command,
|
||||
@@ -237,7 +244,7 @@ class TaskRunner:
|
||||
pr_number=pr_number,
|
||||
)
|
||||
result = self.command_runner.run(command, workspace, stdin=prompt)
|
||||
report = read_report(workspace / "AGENT_REVIEW_REPORT.md")
|
||||
report = read_report(output_dir / "AGENT_REVIEW_REPORT.md")
|
||||
self.db.add_agent_run(
|
||||
task_id=task.id,
|
||||
role="reviewer",
|
||||
|
||||
@@ -32,8 +32,22 @@ class WorkspaceManager:
|
||||
self._git(["clone", repo.clone_url, str(path)], Path.cwd())
|
||||
self._git(["checkout", repo.default_branch], path)
|
||||
self._git(["checkout", "-B", branch_name], path)
|
||||
self.exclude_runtime_artifacts(path)
|
||||
return path
|
||||
|
||||
def exclude_runtime_artifacts(self, workspace: str | Path) -> None:
|
||||
exclude_path = Path(workspace) / ".git" / "info" / "exclude"
|
||||
entries = {
|
||||
".agent-output/",
|
||||
"AGENT_IMPLEMENTER_PROMPT.md",
|
||||
"AGENT_REVIEWER_PROMPT.md",
|
||||
}
|
||||
existing = exclude_path.read_text(encoding="utf-8") if exclude_path.exists() else ""
|
||||
with exclude_path.open("a", encoding="utf-8") as handle:
|
||||
for entry in sorted(entries):
|
||||
if entry not in existing:
|
||||
handle.write(f"\n{entry}\n")
|
||||
|
||||
def has_diff(self, workspace: str | Path, base_ref: str = "origin/HEAD") -> bool:
|
||||
result = self._git(["status", "--porcelain"], Path(workspace), check=False)
|
||||
if result.stdout.strip():
|
||||
@@ -41,6 +55,16 @@ class WorkspaceManager:
|
||||
diff = self._git(["diff", "--quiet", f"{base_ref}...HEAD"], Path(workspace), check=False)
|
||||
return diff.returncode == 1
|
||||
|
||||
def commit_changes(self, workspace: str | Path, message: str) -> str:
|
||||
workspace_path = Path(workspace)
|
||||
self._git(["add", "-A"], workspace_path)
|
||||
result = self._git(["diff", "--cached", "--quiet"], workspace_path, check=False)
|
||||
if result.returncode == 0:
|
||||
raise RuntimeError("no staged implementation changes to commit")
|
||||
self._git(["commit", "-m", message], workspace_path)
|
||||
commit = self._git(["rev-parse", "--short", "HEAD"], workspace_path)
|
||||
return commit.stdout.strip()
|
||||
|
||||
def push_branch(self, workspace: str | Path, branch_name: str) -> None:
|
||||
self._git(["push", "-u", "origin", branch_name], Path(workspace))
|
||||
|
||||
|
||||
@@ -136,6 +136,10 @@ class FakeWorkspaceManager:
|
||||
def push_branch(self, workspace, branch_name):
|
||||
self.pushed.append(branch_name)
|
||||
|
||||
def commit_changes(self, workspace, message):
|
||||
self.commit_message = message
|
||||
return "abc1234"
|
||||
|
||||
def cleanup(self, workspace):
|
||||
pass
|
||||
|
||||
@@ -150,12 +154,16 @@ class FakeRunner:
|
||||
if role == self.fail_role:
|
||||
return AgentResult(exit_code=1, stdout="", stderr="failed")
|
||||
if role == "implementer":
|
||||
(Path(cwd) / "AGENT_IMPLEMENTATION_REPORT.md").write_text(
|
||||
output_dir = Path(cwd) / ".agent-output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
(output_dir / "AGENT_IMPLEMENTATION_REPORT.md").write_text(
|
||||
"## Summary\nImplemented\n\n## Test commands run\npytest\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if role == "reviewer":
|
||||
(Path(cwd) / "AGENT_REVIEW_REPORT.md").write_text(
|
||||
output_dir = Path(cwd) / ".agent-output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
(output_dir / "AGENT_REVIEW_REPORT.md").write_text(
|
||||
"## Verdict\nAPPROVE\n\n## Suggested PR Comment\nLooks good.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -196,11 +204,12 @@ def test_run_task_success_posts_review_comments(db, tmp_path):
|
||||
return httpx.Response(201, json={"id": 1})
|
||||
return httpx.Response(404)
|
||||
|
||||
workspace_manager = FakeWorkspaceManager(tmp_path / "work")
|
||||
task = TaskRunner(
|
||||
db=db,
|
||||
config=config,
|
||||
gitea=make_client(handler),
|
||||
workspace_manager=FakeWorkspaceManager(tmp_path / "work"),
|
||||
workspace_manager=workspace_manager,
|
||||
command_runner=FakeRunner(),
|
||||
worker_id="worker",
|
||||
).run_once()
|
||||
@@ -208,6 +217,11 @@ def test_run_task_success_posts_review_comments(db, tmp_path):
|
||||
assert task is not None
|
||||
assert task.state == TaskState.HUMAN_REVIEW_READY
|
||||
assert task.pr_number == 5
|
||||
assert workspace_manager.pushed == ["agent/issue-1-ready-issue"]
|
||||
assert workspace_manager.commit_message == "agent: implement issue #1 - Ready issue"
|
||||
pull_requests = [payload for _, path, payload in requests if path == "/api/v1/repos/acme/service/pulls"]
|
||||
assert pull_requests[0]["title"] == "代理实现:Ready issue"
|
||||
assert "代理实现报告" in pull_requests[0]["body"]
|
||||
command = json.loads(db.list_agent_runs(task.id)[0]["command_json"])
|
||||
assert command[1] == "--cd"
|
||||
assert Path(command[2]).is_absolute()
|
||||
|
||||
@@ -38,9 +38,9 @@ def test_prompt_and_pr_body_include_contract_sections(db):
|
||||
prompt = render_implementer_prompt(repo, issue, "agent/issue-7-add-thing")
|
||||
body = render_pr_body(issue, "## Summary\nDone")
|
||||
|
||||
assert "AGENT_IMPLEMENTATION_REPORT.md" in prompt
|
||||
assert "Closes #7" in body
|
||||
assert "Human Review Gate" in body
|
||||
assert ".agent-output/AGENT_IMPLEMENTATION_REPORT.md" in prompt
|
||||
assert "关联 Issue:#7" in body
|
||||
assert "人工审核" in body
|
||||
|
||||
|
||||
def test_review_report_parsing_extracts_verdict_and_comment():
|
||||
|
||||
Reference in New Issue
Block a user