Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions backend/druks/contrib/software_factory/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from druks.apps import App, AppSettings, Choices
from druks.contrib.software_factory.contracts import (
ContractRevisionOutput,
EvaluationOutput,
ImplementationOutput,
PlanOutput,
RepoProfilerOutput,
Expand Down Expand Up @@ -214,11 +213,6 @@ async def get_tracker(cls, source: str | None = None) -> Tracker | None:
prompt="software_factory/build/implement.md",
contract=ImplementationOutput,
)
evaluate_implementation = Agent(
description="verification + code review of the diff, one verdict",
prompt="software_factory/build/evaluate_implementation.md",
contract=EvaluationOutput,
)
triage_human_feedback = Agent(
description="routes a human's PR feedback back into the workflow",
prompt="software_factory/build/triage_human_feedback.md",
Expand Down
2 changes: 2 additions & 0 deletions backend/druks/contrib/software_factory/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
"software_factory_update_ticket",
"software_factory_create_ticket",
)
# A review takes minutes. A build still waiting after this long was never answered.
REVIEW_TTL_SECONDS = 2 * 60 * 60
44 changes: 1 addition & 43 deletions backend/druks/contrib/software_factory/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
from pydantic import BaseModel, Field, model_validator

from druks.agents import AgentOutput
from druks.contrib.software_factory.enums import (
EvaluationVerdict,
HumanFeedbackAction,
ReviewDecision,
)
from druks.contrib.software_factory.enums import HumanFeedbackAction, ReviewDecision
from druks.workflows import Gate, OperatorReply, Workflow

if TYPE_CHECKING:
Expand Down Expand Up @@ -304,41 +300,3 @@ def to_artifact(self) -> dict[str, str]:

def to_event(self) -> dict[str, str]:
return {"topic": "review.completed", "summary": self.summary}


class EvalCheckOutput(AgentOutput):
name: str
status: Literal["pass", "fail", "not_run"]
evidence: str


class AcceptanceResultOutput(AgentOutput):
criterion_id: str
status: Literal["pass", "fail", "not_run"]
evidence: str


class EvaluationOutput(AgentOutput):
verdict: EvaluationVerdict
body: str
review_notes: str
findings: list[FindingOutput]
checks: list[EvalCheckOutput]
acceptance_results: list[AcceptanceResultOutput]

def to_artifact(self) -> dict[str, str]:
# A blocked verdict says the checks could not decide, not that the work has
# a blocker, so the headline names the checks that failed or did not run.
if self.verdict == EvaluationVerdict.BLOCKED:
sections = ["Verification blocked"]
sections.extend(
f"- {check.name}: {check.evidence}"
for check in self.checks
if check.status != "pass"
)
else:
sections = [f"Verdict: {self.verdict}"]
sections.append(self.body)
for finding in self.findings:
sections.extend(finding.to_sections())
return {"kind": "markdown", "title": "Evaluation", "content": "\n\n".join(sections)}
6 changes: 0 additions & 6 deletions backend/druks/contrib/software_factory/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ class ReviewDecision(StrEnum):
COMMENT = "COMMENT"


class EvaluationVerdict(StrEnum):
PASS = "pass"
FAIL = "fail"
BLOCKED = "blocked"


class HumanFeedbackAction(StrEnum):
NO_CHANGE = "no_change"
CHANGE_REQUIRED = "change_required"
Expand Down
12 changes: 6 additions & 6 deletions backend/druks/contrib/software_factory/journal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from typing import Any

from druks.contrib.software_factory.contracts import (
EvaluationOutput,
ImplementationOutput,
PlanData,
ReviewOutput,
ReviewReport,
ReviewWork,
TriageOutput,
)
Expand Down Expand Up @@ -42,8 +42,8 @@ def implementation_revision(self) -> int:
return len(self.implementations)

@property
def evaluations(self) -> list[EvaluationOutput]:
return self.filter(EvaluationOutput)
def reviews(self) -> list[ReviewReport]:
return self.filter(ReviewReport)

@property
def plan_reviews(self) -> list[ReviewOutput]:
Expand All @@ -59,13 +59,13 @@ def assignee_github_login(self) -> str | None:
@property
def revision_request(self) -> str | None:
# The newest triage's instructions drive exactly one implement round.
# An evaluation recorded after that triage means the round already ran —
# the evaluator's findings are the current request, not these.
# A review recorded after that triage means the round already ran —
# the review's findings are the current request, not these.
triage = self.latest(TriageOutput)
if (
triage
and triage.implementation_instructions
and not self.filter(EvaluationOutput, after=triage)
and not self.filter(ReviewReport, after=triage)
):
return triage.implementation_instructions
return
Expand Down
5 changes: 0 additions & 5 deletions backend/druks/contrib/software_factory/prompt_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,4 @@ class BuildPromptContext:
assignee_email: str | None
related_repos: list[ProjectRepo]
skills: list[Skill]
review_code: bool
# How the github MCP identity may post reviews: "approve" (distinct
# review identity) or "comment" (the operator, barred from approving its
# own pull requests).
review_mode: str
journal: BuildJournal
23 changes: 22 additions & 1 deletion backend/druks/contrib/software_factory/subscribers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from druks.contrib.software_factory.app import SoftwareFactory
from druks.contrib.software_factory.contracts import ReviewWork
from druks.contrib.software_factory.datastructures import PullRequest
from druks.contrib.software_factory.enums import Resolution, Status
from druks.contrib.software_factory.models import ProjectRepo, WorkItem
from druks.contrib.software_factory.ticketing.enums import TicketStatus
from druks.contrib.software_factory.workflows import Build, Profile
from druks.contrib.software_factory.workflows import Build, Profile, PullRequestReview
from druks.models import Base
from druks.signals import publish, subscribe
from druks.workflows import WorkflowEvent
Expand Down Expand Up @@ -50,6 +51,26 @@ async def review_park_marks_ticket_in_review(*, subject: WorkItem, **_: object)
await subject.set_ticket_status(TicketStatus.IN_REVIEW)


@subscribe(WorkflowEvent.FINISHED, workflow=PullRequestReview)
async def finished_review_answers_the_build(
*, subject: PullRequest, result: dict, **_: object
) -> None:
"""The build that handed over the pull request waits for the decision. A review a person
asked for has no build waiting, and answers no one."""
if item := await WorkItem.get_for_pr(repo=subject.repo, pr_number=subject.number):
await Build.answer(item, "review.completed", **result)


@subscribe(WorkflowEvent.FAILED, workflow=PullRequestReview)
@subscribe(WorkflowEvent.CANCELLED, workflow=PullRequestReview)
async def stopped_review_fails_the_build(
*, subject: PullRequest, failure: str = "", **_: object
) -> None:
# The build fails with the review's reason instead of waiting out the TTL.
if item := await WorkItem.get_for_pr(repo=subject.repo, pr_number=subject.number):
await Build.answer(item, "review.completed", failure=failure or "the review stopped")


@subscribe("repo.pushed", to_default_branch=True)
async def policy_push_reprofiles_the_repo(*, repo: str, paths: list, **_: object) -> None:
# The operator edited the repo's build policy — re-apply it over the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,17 @@
{% endfor %}

{% endif %}
{% if build.journal.evaluations %}
{% if build.journal.reviews %}
## Prior implementation review

{% for review in build.journal.evaluations %}
### Round {{ loop.index }} — verdict: {{ review.verdict.value if review.verdict.value is defined else review.verdict }}
{% for review in build.journal.reviews %}
### Round {{ loop.index }} — decision: {{ review.decision }}

{% if review.body %}
{{ review.body }}
{{ review.summary }}

{% endif %}
{% if review.review_notes %}
#### Review notes

{{ review.review_notes }}
{% for finding in review.findings %}
- **{{ finding.severity }}** {{ finding.summary }}{% if finding.path %} (`{{ finding.path }}{% if finding.line %}:{{ finding.line }}{% endif %}`){% endif %}: {{ finding.evidence }}
{% endfor %}

{% endif %}
{% endfor %}
{% endif %}

This file was deleted.

Loading
Loading