guard reviews against issue rediscovery
This commit is contained in:
+11
-1
@@ -1,3 +1,5 @@
|
|||||||
|
ARG TEA_VERSION=0.14.2
|
||||||
|
|
||||||
FROM node:24-bookworm-slim AS codex
|
FROM node:24-bookworm-slim AS codex
|
||||||
|
|
||||||
ARG CODEX_VERSION=0.144.6
|
ARG CODEX_VERSION=0.144.6
|
||||||
@@ -8,6 +10,8 @@ RUN npm install --global \
|
|||||||
|
|
||||||
FROM ghcr.io/astral-sh/uv:0.8.14 AS uv
|
FROM ghcr.io/astral-sh/uv:0.8.14 AS uv
|
||||||
|
|
||||||
|
FROM gitea/tea:${TEA_VERSION} AS tea
|
||||||
|
|
||||||
FROM python:3.13-slim-bookworm
|
FROM python:3.13-slim-bookworm
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
@@ -16,15 +20,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
UV_NO_DEV=1 \
|
UV_NO_DEV=1 \
|
||||||
CODEGRAPH_TELEMETRY=0 \
|
CODEGRAPH_TELEMETRY=0 \
|
||||||
CODEX_HOME=/var/lib/codex \
|
CODEX_HOME=/var/lib/codex \
|
||||||
|
XDG_CONFIG_HOME=/run/agentci \
|
||||||
PATH=/opt/agentci/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/opt/agentci/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --yes --no-install-recommends adduser ca-certificates git libstdc++6 \
|
&& apt-get install --yes --no-install-recommends adduser ca-certificates git libstdc++6 \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \
|
&& /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \
|
||||||
&& mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex
|
&& mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex /run/agentci \
|
||||||
|
&& chown agentci:agentci /run/agentci
|
||||||
|
|
||||||
COPY --from=uv /uv /uvx /usr/local/bin/
|
COPY --from=uv /uv /uvx /usr/local/bin/
|
||||||
|
COPY --from=tea /bin/tea /usr/local/bin/tea
|
||||||
COPY --from=codex /usr/local/bin/node /usr/local/bin/node
|
COPY --from=codex /usr/local/bin/node /usr/local/bin/node
|
||||||
COPY --from=codex /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex
|
COPY --from=codex /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex
|
||||||
COPY --from=codex /usr/local/lib/node_modules/@colbymchenry /usr/local/lib/node_modules/@colbymchenry
|
COPY --from=codex /usr/local/lib/node_modules/@colbymchenry /usr/local/lib/node_modules/@colbymchenry
|
||||||
@@ -39,7 +46,9 @@ COPY scripts ./scripts
|
|||||||
COPY codex/config.toml /etc/codex/config.toml
|
COPY codex/config.toml /etc/codex/config.toml
|
||||||
|
|
||||||
RUN chmod 0755 \
|
RUN chmod 0755 \
|
||||||
|
/opt/agentci/scripts/entrypoint.sh \
|
||||||
/opt/agentci/scripts/gitea-askpass.sh \
|
/opt/agentci/scripts/gitea-askpass.sh \
|
||||||
|
/usr/local/bin/tea \
|
||||||
/usr/local/bin/codex \
|
/usr/local/bin/codex \
|
||||||
/usr/local/bin/codegraph \
|
/usr/local/bin/codegraph \
|
||||||
&& uv sync --frozen --no-dev \
|
&& uv sync --frozen --no-dev \
|
||||||
@@ -51,4 +60,5 @@ EXPOSE 8080
|
|||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live')"]
|
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live')"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["/opt/agentci/scripts/entrypoint.sh"]
|
||||||
CMD ["agentci"]
|
CMD ["agentci"]
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ glob_scan_max_depth = 5
|
|||||||
".git" = "read"
|
".git" = "read"
|
||||||
"**/*.env" = "deny"
|
"**/*.env" = "deny"
|
||||||
|
|
||||||
|
[permissions.agentci-review]
|
||||||
|
description = "Review scoped workflow material and query Gitea with tea without editing files."
|
||||||
|
|
||||||
|
[permissions.agentci-review.filesystem]
|
||||||
|
":minimal" = "read"
|
||||||
|
glob_scan_max_depth = 5
|
||||||
|
|
||||||
|
[permissions.agentci-review.filesystem.":workspace_roots"]
|
||||||
|
"." = "read"
|
||||||
|
".git" = "read"
|
||||||
|
"**/*.env" = "deny"
|
||||||
|
|
||||||
|
[permissions.agentci-review.network]
|
||||||
|
enabled = true
|
||||||
|
allow_local_binding = false
|
||||||
|
|
||||||
|
[permissions.agentci-review.network.domains]
|
||||||
|
"*" = "allow"
|
||||||
|
|
||||||
[permissions.agentci-write]
|
[permissions.agentci-write]
|
||||||
description = "Edit a workflow repository without changing Git metadata."
|
description = "Edit a workflow repository without changing Git metadata."
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
CODEX_VERSION: ${CODEX_VERSION:-0.144.6}
|
CODEX_VERSION: ${CODEX_VERSION:-0.144.6}
|
||||||
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
|
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
|
||||||
|
TEA_VERSION: ${TEA_VERSION:-0.14.2}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000}
|
AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
python -c '
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
config_dir = Path(os.environ["XDG_CONFIG_HOME"]) / "tea"
|
||||||
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_path = config_dir / "config.yml"
|
||||||
|
token_path = Path(
|
||||||
|
os.environ.get("AGENTCI_GITEA_TOKEN_FILE", "/run/secrets/gitea_token")
|
||||||
|
)
|
||||||
|
login = {
|
||||||
|
"name": "agentci",
|
||||||
|
"url": os.environ.get("AGENTCI_GITEA_URL", "http://gitea:3000"),
|
||||||
|
"token": token_path.read_text().strip(),
|
||||||
|
"default": True,
|
||||||
|
"version_check": False,
|
||||||
|
}
|
||||||
|
config_path.write_text(json.dumps({"logins": [login], "preferences": {}}))
|
||||||
|
config_path.chmod(0o600)
|
||||||
|
'
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -183,7 +183,14 @@ class CodexClient:
|
|||||||
output_path.unlink(missing_ok=True) # noqa: ASYNC240
|
output_path.unlink(missing_ok=True) # noqa: ASYNC240
|
||||||
|
|
||||||
def _environment(self) -> dict[str, str]:
|
def _environment(self) -> dict[str, str]:
|
||||||
allowed = {"PATH", "LANG", "LC_ALL", "SSL_CERT_FILE", "CODEX_CA_CERTIFICATE"}
|
allowed = {
|
||||||
|
"PATH",
|
||||||
|
"LANG",
|
||||||
|
"LC_ALL",
|
||||||
|
"SSL_CERT_FILE",
|
||||||
|
"CODEX_CA_CERTIFICATE",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
|
}
|
||||||
environment = {key: value for key, value in os.environ.items() if key in allowed}
|
environment = {key: value for key, value in os.environ.items() if key in allowed}
|
||||||
environment["CODEX_HOME"] = str(self.codex_home)
|
environment["CODEX_HOME"] = str(self.codex_home)
|
||||||
if self.context7_api_key:
|
if self.context7_api_key:
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
Independently review the current uncommitted implementation against the issue context. Inspect the
|
Independently review only the proposed pull request, the issue it fixes, and the canonical plan
|
||||||
working tree and diff yourself. Do not edit files. Focus on correctness, regressions, security,
|
from that issue. Inspect the pull request diff yourself, but do not inspect or review unrelated
|
||||||
missing tests, and whether the requested behavior is actually complete.
|
repository files, history, branches, issues, or other repository-wide concerns. You may read a
|
||||||
|
changed file only as needed to understand a changed diff hunk. Do not edit files. Focus on concrete
|
||||||
|
correctness, regressions, security, missing tests, and whether the requested behavior is complete.
|
||||||
|
|
||||||
<issue_context>
|
<issue_context>
|
||||||
$context
|
$issue_context
|
||||||
</issue_context>
|
</issue_context>
|
||||||
|
|
||||||
|
<canonical_plan>
|
||||||
|
$artifact
|
||||||
|
</canonical_plan>
|
||||||
|
|
||||||
|
<pull_request_context>
|
||||||
|
$pull_context
|
||||||
|
</pull_request_context>
|
||||||
|
|
||||||
|
Before reporting any finding, run
|
||||||
|
`tea issues list --repo OWNER/REPO --state all --keyword "FOCUSED KEYWORDS"` for the repository
|
||||||
|
named by the issue context. This duplicate search is the only permitted access to other issues. If
|
||||||
|
a matching issue already tracks the finding, cite its number in the finding. Do not use `tea` to
|
||||||
|
create, edit, close, reopen, comment on, or otherwise mutate issues or pull requests.
|
||||||
|
|
||||||
Return a structured review. Use `blocking` only when the result cannot safely be proposed, `major`
|
Return a structured review. Use `blocking` only when the result cannot safely be proposed, `major`
|
||||||
for a material defect, and `minor` for a non-blocking improvement.
|
for a material defect, and `minor` for a non-blocking improvement.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
You are the independent reviewer for a proposed implementation plan. Inspect the repository
|
You are the independent reviewer for a proposed implementation plan. Review only the proposed
|
||||||
yourself and compare the plan to the issue. Do not edit files. Identify concrete correctness,
|
plan against the issue context below. Do not inspect the repository, working tree, Git history,
|
||||||
security, compatibility, missing-decision, and testing problems. Do not invent speculative work.
|
or any other repository material, and do not edit files. Identify concrete correctness, security,
|
||||||
|
compatibility, missing-decision, and testing problems. Do not invent speculative work.
|
||||||
|
|
||||||
|
Before reporting any finding, run
|
||||||
|
`tea issues list --repo OWNER/REPO --state all --keyword "FOCUSED KEYWORDS"` for the repository
|
||||||
|
named by the issue context. This duplicate search is the only permitted access to repository
|
||||||
|
material outside the issue context and plan. If a matching issue already tracks the finding, cite
|
||||||
|
its number in the finding. Do not use `tea` to create, edit, close, reopen, comment on, or otherwise
|
||||||
|
mutate issues or pull requests.
|
||||||
|
|
||||||
<issue_context>
|
<issue_context>
|
||||||
$context
|
$context
|
||||||
@@ -12,4 +20,3 @@ $artifact
|
|||||||
|
|
||||||
Return a structured review. Use `blocking` only when work cannot safely proceed, `major` for a
|
Return a structured review. Use `blocking` only when work cannot safely proceed, `major` for a
|
||||||
material defect or unresolved implementation decision, and `minor` for a non-blocking improvement.
|
material defect or unresolved implementation decision, and `minor` for a non-blocking improvement.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class CodeReviewLoop:
|
|||||||
self,
|
self,
|
||||||
job: Job,
|
job: Job,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
context: str,
|
issue_context: str,
|
||||||
|
plan: str,
|
||||||
result: AgentResult,
|
result: AgentResult,
|
||||||
) -> tuple[AgentResult, ReviewReport]:
|
) -> tuple[AgentResult, ReviewReport]:
|
||||||
report = ReviewReport(summary="", findings=[])
|
report = ReviewReport(summary="", findings=[])
|
||||||
@@ -22,7 +23,15 @@ class CodeReviewLoop:
|
|||||||
stage=f"reviewing implementation {round_index + 1}/"
|
stage=f"reviewing implementation {round_index + 1}/"
|
||||||
f"{self.deps.settings.implement_review_rounds}",
|
f"{self.deps.settings.implement_review_rounds}",
|
||||||
)
|
)
|
||||||
report = await self.once(workflow, context)
|
report = await self.once(
|
||||||
|
workflow,
|
||||||
|
issue_context=issue_context,
|
||||||
|
plan=plan,
|
||||||
|
pull_context=(
|
||||||
|
"The proposed pull request is the current uncommitted working-tree diff. "
|
||||||
|
"Review only that diff."
|
||||||
|
),
|
||||||
|
)
|
||||||
workflow.artifact = result.model_dump_json()
|
workflow.artifact = result.model_dump_json()
|
||||||
workflow.review_json = report_json(report)
|
workflow.review_json = report_json(report)
|
||||||
await self.deps.storage.update_workflow(workflow)
|
await self.deps.storage.update_workflow(workflow)
|
||||||
@@ -46,15 +55,27 @@ class CodeReviewLoop:
|
|||||||
)
|
)
|
||||||
return result, report
|
return result, report
|
||||||
|
|
||||||
async def once(self, workflow: Workflow, context: str) -> ReviewReport:
|
async def once(
|
||||||
prompt = self.deps.prompts.render("implementation_review", context=context)
|
self,
|
||||||
|
workflow: Workflow,
|
||||||
|
*,
|
||||||
|
issue_context: str,
|
||||||
|
plan: str,
|
||||||
|
pull_context: str,
|
||||||
|
) -> ReviewReport:
|
||||||
|
prompt = self.deps.prompts.render(
|
||||||
|
"implementation_review",
|
||||||
|
issue_context=issue_context,
|
||||||
|
artifact=plan,
|
||||||
|
pull_context=pull_context,
|
||||||
|
)
|
||||||
if workflow.reviewer_session_id:
|
if workflow.reviewer_session_id:
|
||||||
return await self.deps.codex.resume(
|
return await self.deps.codex.resume(
|
||||||
session_id=workflow.reviewer_session_id,
|
session_id=workflow.reviewer_session_id,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=self.deps.settings.implement_model,
|
model=self.deps.settings.implement_model,
|
||||||
reasoning=self.deps.settings.implement_reasoning,
|
reasoning=self.deps.settings.implement_reasoning,
|
||||||
permission="agentci-read",
|
permission="agentci-review",
|
||||||
workspace=workflow.workspace_path,
|
workspace=workflow.workspace_path,
|
||||||
schema_name="review.json",
|
schema_name="review.json",
|
||||||
result_type=ReviewReport,
|
result_type=ReviewReport,
|
||||||
@@ -64,7 +85,7 @@ class CodeReviewLoop:
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=self.deps.settings.implement_model,
|
model=self.deps.settings.implement_model,
|
||||||
reasoning=self.deps.settings.implement_reasoning,
|
reasoning=self.deps.settings.implement_reasoning,
|
||||||
permission="agentci-read",
|
permission="agentci-review",
|
||||||
schema_name="review.json",
|
schema_name="review.json",
|
||||||
result_type=ReviewReport,
|
result_type=ReviewReport,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,7 +83,13 @@ class ImplementWorkflow:
|
|||||||
workflow.primary_session_id = session_id
|
workflow.primary_session_id = session_id
|
||||||
workflow.artifact = result.model_dump_json()
|
workflow.artifact = result.model_dump_json()
|
||||||
await self.deps.storage.update_workflow(workflow)
|
await self.deps.storage.update_workflow(workflow)
|
||||||
result, report = await self.review.run(job, workflow, context, result)
|
result, report = await self.review.run(
|
||||||
|
job,
|
||||||
|
workflow,
|
||||||
|
context,
|
||||||
|
plan.artifact if plan and plan.artifact else "(no canonical plan)",
|
||||||
|
result,
|
||||||
|
)
|
||||||
sha = await self.changes.commit_and_push(
|
sha = await self.changes.commit_and_push(
|
||||||
job,
|
job,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class PlanWorkflow:
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=self.deps.settings.plan_model,
|
model=self.deps.settings.plan_model,
|
||||||
reasoning=self.deps.settings.plan_reasoning,
|
reasoning=self.deps.settings.plan_reasoning,
|
||||||
permission="agentci-read",
|
permission="agentci-review",
|
||||||
workspace=workflow.workspace_path,
|
workspace=workflow.workspace_path,
|
||||||
schema_name="review.json",
|
schema_name="review.json",
|
||||||
result_type=ReviewReport,
|
result_type=ReviewReport,
|
||||||
@@ -186,7 +186,7 @@ class PlanWorkflow:
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=self.deps.settings.plan_model,
|
model=self.deps.settings.plan_model,
|
||||||
reasoning=self.deps.settings.plan_reasoning,
|
reasoning=self.deps.settings.plan_reasoning,
|
||||||
permission="agentci-read",
|
permission="agentci-review",
|
||||||
schema_name="review.json",
|
schema_name="review.json",
|
||||||
result_type=ReviewReport,
|
result_type=ReviewReport,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agentci.domain.models import AgentResult, Job, WorkflowStatus
|
from agentci.domain.models import AgentResult, Job, WorkflowKind, WorkflowStatus
|
||||||
from agentci.workflows.change_set import ChangeSet, result_comment
|
from agentci.workflows.change_set import ChangeSet, result_comment
|
||||||
from agentci.workflows.code_review import CodeReviewLoop
|
from agentci.workflows.code_review import CodeReviewLoop
|
||||||
from agentci.workflows.common import (
|
from agentci.workflows.common import (
|
||||||
@@ -58,7 +58,18 @@ class PullRequestWorkflow:
|
|||||||
schema_name="agent_result.json",
|
schema_name="agent_result.json",
|
||||||
result_type=AgentResult,
|
result_type=AgentResult,
|
||||||
)
|
)
|
||||||
report = await self.review.once(workflow, context)
|
issue_context = await self.deps.context.issue_context(
|
||||||
|
job.repo_owner, job.repo_name, workflow.issue_number
|
||||||
|
)
|
||||||
|
plan = await self.deps.storage.latest_workflow(
|
||||||
|
job.repo_owner, job.repo_name, workflow.issue_number, WorkflowKind.PLAN
|
||||||
|
)
|
||||||
|
report = await self.review.once(
|
||||||
|
workflow,
|
||||||
|
issue_context=issue_context,
|
||||||
|
plan=plan.artifact if plan and plan.artifact else "(no canonical plan)",
|
||||||
|
pull_context=context,
|
||||||
|
)
|
||||||
sha = await self.changes.commit_and_push(
|
sha = await self.changes.commit_and_push(
|
||||||
job,
|
job,
|
||||||
workflow.workspace_path,
|
workflow.workspace_path,
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ async def test_stops_after_clean_second_review() -> None:
|
|||||||
clean = ReviewReport(summary="Ready", findings=[])
|
clean = ReviewReport(summary="Ready", findings=[])
|
||||||
loop, codex, workflow, job = objects(4, [serious_report(), clean])
|
loop, codex, workflow, job = objects(4, [serious_report(), clean])
|
||||||
_, report = await loop.run(
|
_, report = await loop.run(
|
||||||
job, workflow, "context", AgentResult(summary_markdown="initial", tests=[])
|
job,
|
||||||
|
workflow,
|
||||||
|
"issue context",
|
||||||
|
"canonical plan",
|
||||||
|
AgentResult(summary_markdown="initial", tests=[]),
|
||||||
)
|
)
|
||||||
assert not report.has_serious_findings
|
assert not report.has_serious_findings
|
||||||
assert codex.reviews == 2
|
assert codex.reviews == 2
|
||||||
@@ -113,9 +117,12 @@ async def test_does_not_make_unreviewed_final_revision() -> None:
|
|||||||
3, [serious_report(), serious_report(), serious_report()]
|
3, [serious_report(), serious_report(), serious_report()]
|
||||||
)
|
)
|
||||||
_, report = await loop.run(
|
_, report = await loop.run(
|
||||||
job, workflow, "context", AgentResult(summary_markdown="initial", tests=[])
|
job,
|
||||||
|
workflow,
|
||||||
|
"issue context",
|
||||||
|
"canonical plan",
|
||||||
|
AgentResult(summary_markdown="initial", tests=[]),
|
||||||
)
|
)
|
||||||
assert report.has_serious_findings
|
assert report.has_serious_findings
|
||||||
assert codex.reviews == 3
|
assert codex.reviews == 3
|
||||||
assert codex.revisions == 2
|
assert codex.revisions == 2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user