Compare commits

..

1 Commits

Author SHA1 Message Date
e3d0e57ae7 agent: implement issue #1 - Doc: request CN version README 2026-05-06 16:00:44 +08:00
5 changed files with 22 additions and 70 deletions

View File

@@ -34,8 +34,7 @@ Issue URL: {issue.html_url}
Implement the requested change in this workspace. Keep the change scoped to this issue. Implement the requested change in this workspace. Keep the change scoped to this issue.
Run the relevant tests before finishing. Run the relevant tests before finishing.
Write `.agent-output/AGENT_IMPLEMENTATION_REPORT.md` using this exact section contract. Write `AGENT_IMPLEMENTATION_REPORT.md` in the workspace root using this exact section contract:
Keep the section headings exactly as written below, but write the section content in Chinese:
- Summary - Summary
- Files changed - Files changed
@@ -56,9 +55,7 @@ Issue: #{issue.issue_number} {issue.title}
Review the implementation currently checked out in this workspace. Focus on correctness, Review the implementation currently checked out in this workspace. Focus on correctness,
scope control, test evidence, and human risks. Do not modify code. scope control, test evidence, and human risks. Do not modify code.
Write `.agent-output/AGENT_REVIEW_REPORT.md` using this exact section contract. Write `AGENT_REVIEW_REPORT.md` in the workspace root 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 - Verdict: APPROVE, REQUEST_CHANGES, or NEEDS_HUMAN_DECISION
- Summary - Summary
@@ -72,16 +69,16 @@ but write the section content and Suggested PR Comment in Chinese:
def render_pr_body(issue: IssueRecord, implementation_report: str) -> str: def render_pr_body(issue: IssueRecord, implementation_report: str) -> str:
return f"""关联 Issue#{issue.issue_number} return f"""Closes #{issue.issue_number}
## 代理实现报告 ## Agent Implementation Report
{implementation_report.strip()} {implementation_report.strip()}
## 人工审核 ## Human Review Gate
此 PR 由本地 agent-manager 自动创建,但不会自动合并。 This PR was opened by the local agent manager. It has not been auto-merged.
请维护者人工审核、决策并手动合并。 Human maintainers must review, decide, and merge manually.
""" """
@@ -105,13 +102,13 @@ def extract_section(raw: str, title: str) -> str:
def render_human_review_summary(review: ReviewReport) -> str: def render_human_review_summary(review: ReviewReport) -> str:
return f"""## 代理评审摘要 return f"""## Agent Review Summary
结论:`{review.verdict}` Verdict: `{review.verdict}`
{review.suggested_pr_comment.strip()} {review.suggested_pr_comment.strip()}
## 需要人工处理 ## Human Action Required
请人工审核该 PR。agent-manager 不会自动合并、关闭 PR 或提交变更请求。 Please review the PR manually. The agent manager will not merge, close, or request changes automatically.
""" """

View File

@@ -132,15 +132,12 @@ class TaskRunner:
error_message="implementer produced no diff", error_message="implementer produced no diff",
clear_lease=True, 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) self.workspace_manager.push_branch(workspace, branch_name)
pr_body = render_pr_body(issue, implementation_report) pr_body = render_pr_body(issue, implementation_report)
pr = self.gitea.create_pull_request( pr = self.gitea.create_pull_request(
owner=repo.owner, owner=repo.owner,
name=repo.name, name=repo.name,
title=f"代理实现:{issue.title}", title=f"Agent: {issue.title}",
body=pr_body, body=pr_body,
head=branch_name, head=branch_name,
base=repo.default_branch, base=repo.default_branch,
@@ -194,9 +191,7 @@ class TaskRunner:
workspace: Path, workspace: Path,
) -> str: ) -> str:
prompt = render_implementer_prompt(repo, issue, branch_name) prompt = render_implementer_prompt(repo, issue, branch_name)
output_dir = workspace / ".agent-output" prompt_path = workspace / "AGENT_IMPLEMENTER_PROMPT.md"
output_dir.mkdir(exist_ok=True)
prompt_path = output_dir / "AGENT_IMPLEMENTER_PROMPT.md"
write_prompt(prompt_path, prompt) write_prompt(prompt_path, prompt)
command = render_command( command = render_command(
self.config.agents.implementer.command, self.config.agents.implementer.command,
@@ -207,7 +202,7 @@ class TaskRunner:
branch_name=branch_name, branch_name=branch_name,
) )
result = self.command_runner.run(command, workspace, stdin=prompt) result = self.command_runner.run(command, workspace, stdin=prompt)
report = read_report(output_dir / "AGENT_IMPLEMENTATION_REPORT.md") report = read_report(workspace / "AGENT_IMPLEMENTATION_REPORT.md")
self.db.add_agent_run( self.db.add_agent_run(
task_id=task.id, task_id=task.id,
role="implementer", role="implementer",
@@ -231,9 +226,7 @@ class TaskRunner:
workspace: Path, workspace: Path,
) -> str: ) -> str:
prompt = render_reviewer_prompt(repo, issue, pr_number) prompt = render_reviewer_prompt(repo, issue, pr_number)
output_dir = workspace / ".agent-output" prompt_path = workspace / "AGENT_REVIEWER_PROMPT.md"
output_dir.mkdir(exist_ok=True)
prompt_path = output_dir / "AGENT_REVIEWER_PROMPT.md"
write_prompt(prompt_path, prompt) write_prompt(prompt_path, prompt)
command = render_command( command = render_command(
self.config.agents.reviewer.command, self.config.agents.reviewer.command,
@@ -244,7 +237,7 @@ class TaskRunner:
pr_number=pr_number, pr_number=pr_number,
) )
result = self.command_runner.run(command, workspace, stdin=prompt) result = self.command_runner.run(command, workspace, stdin=prompt)
report = read_report(output_dir / "AGENT_REVIEW_REPORT.md") report = read_report(workspace / "AGENT_REVIEW_REPORT.md")
self.db.add_agent_run( self.db.add_agent_run(
task_id=task.id, task_id=task.id,
role="reviewer", role="reviewer",

View File

@@ -32,22 +32,8 @@ class WorkspaceManager:
self._git(["clone", repo.clone_url, str(path)], Path.cwd()) self._git(["clone", repo.clone_url, str(path)], Path.cwd())
self._git(["checkout", repo.default_branch], path) self._git(["checkout", repo.default_branch], path)
self._git(["checkout", "-B", branch_name], path) self._git(["checkout", "-B", branch_name], path)
self.exclude_runtime_artifacts(path)
return 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: def has_diff(self, workspace: str | Path, base_ref: str = "origin/HEAD") -> bool:
result = self._git(["status", "--porcelain"], Path(workspace), check=False) result = self._git(["status", "--porcelain"], Path(workspace), check=False)
if result.stdout.strip(): if result.stdout.strip():
@@ -55,16 +41,6 @@ class WorkspaceManager:
diff = self._git(["diff", "--quiet", f"{base_ref}...HEAD"], Path(workspace), check=False) diff = self._git(["diff", "--quiet", f"{base_ref}...HEAD"], Path(workspace), check=False)
return diff.returncode == 1 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: def push_branch(self, workspace: str | Path, branch_name: str) -> None:
self._git(["push", "-u", "origin", branch_name], Path(workspace)) self._git(["push", "-u", "origin", branch_name], Path(workspace))

View File

@@ -136,10 +136,6 @@ class FakeWorkspaceManager:
def push_branch(self, workspace, branch_name): def push_branch(self, workspace, branch_name):
self.pushed.append(branch_name) self.pushed.append(branch_name)
def commit_changes(self, workspace, message):
self.commit_message = message
return "abc1234"
def cleanup(self, workspace): def cleanup(self, workspace):
pass pass
@@ -154,16 +150,12 @@ class FakeRunner:
if role == self.fail_role: if role == self.fail_role:
return AgentResult(exit_code=1, stdout="", stderr="failed") return AgentResult(exit_code=1, stdout="", stderr="failed")
if role == "implementer": if role == "implementer":
output_dir = Path(cwd) / ".agent-output" (Path(cwd) / "AGENT_IMPLEMENTATION_REPORT.md").write_text(
output_dir.mkdir(exist_ok=True)
(output_dir / "AGENT_IMPLEMENTATION_REPORT.md").write_text(
"## Summary\nImplemented\n\n## Test commands run\npytest\n", "## Summary\nImplemented\n\n## Test commands run\npytest\n",
encoding="utf-8", encoding="utf-8",
) )
if role == "reviewer": if role == "reviewer":
output_dir = Path(cwd) / ".agent-output" (Path(cwd) / "AGENT_REVIEW_REPORT.md").write_text(
output_dir.mkdir(exist_ok=True)
(output_dir / "AGENT_REVIEW_REPORT.md").write_text(
"## Verdict\nAPPROVE\n\n## Suggested PR Comment\nLooks good.\n", "## Verdict\nAPPROVE\n\n## Suggested PR Comment\nLooks good.\n",
encoding="utf-8", encoding="utf-8",
) )
@@ -204,12 +196,11 @@ def test_run_task_success_posts_review_comments(db, tmp_path):
return httpx.Response(201, json={"id": 1}) return httpx.Response(201, json={"id": 1})
return httpx.Response(404) return httpx.Response(404)
workspace_manager = FakeWorkspaceManager(tmp_path / "work")
task = TaskRunner( task = TaskRunner(
db=db, db=db,
config=config, config=config,
gitea=make_client(handler), gitea=make_client(handler),
workspace_manager=workspace_manager, workspace_manager=FakeWorkspaceManager(tmp_path / "work"),
command_runner=FakeRunner(), command_runner=FakeRunner(),
worker_id="worker", worker_id="worker",
).run_once() ).run_once()
@@ -217,11 +208,6 @@ def test_run_task_success_posts_review_comments(db, tmp_path):
assert task is not None assert task is not None
assert task.state == TaskState.HUMAN_REVIEW_READY assert task.state == TaskState.HUMAN_REVIEW_READY
assert task.pr_number == 5 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"]) command = json.loads(db.list_agent_runs(task.id)[0]["command_json"])
assert command[1] == "--cd" assert command[1] == "--cd"
assert Path(command[2]).is_absolute() assert Path(command[2]).is_absolute()

View File

@@ -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") prompt = render_implementer_prompt(repo, issue, "agent/issue-7-add-thing")
body = render_pr_body(issue, "## Summary\nDone") body = render_pr_body(issue, "## Summary\nDone")
assert ".agent-output/AGENT_IMPLEMENTATION_REPORT.md" in prompt assert "AGENT_IMPLEMENTATION_REPORT.md" in prompt
assert "关联 Issue#7" in body assert "Closes #7" in body
assert "人工审核" in body assert "Human Review Gate" in body
def test_review_report_parsing_extracts_verdict_and_comment(): def test_review_report_parsing_extracts_verdict_and_comment():