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
126 changes: 101 additions & 25 deletions devloop
Original file line number Diff line number Diff line change
Expand Up @@ -2160,7 +2160,7 @@ run_devloop() {
if [ "$create_pr" = true ] && [ -n "$PASS_COMMIT" ]; then
local pr_id="pull-request-$pass"
event_step "$pr_id" "push branch and ensure draft PR"
if create_pull_request "$repo" "$FINAL_BRANCH" "$base"; then
if create_pull_request "$repo" "$FINAL_BRANCH" "$base" "$spec" "$criteria_file" "$FINAL_COMMIT"; then
event_done "$pr_id" true "$PULL_REQUEST"
else
STATUS="pr-error"
Expand Down Expand Up @@ -2212,7 +2212,7 @@ run_devloop() {
if [ "$create_pr" = true ] && [ -n "$PULL_REQUEST" ]; then
local pr_comment_id="pr-review-comment-$pass"
event_step "$pr_comment_id" "post round review to PR"
if post_pr_round_review "$repo" "$PULL_REQUEST" "$repo/$review" "$pass" "$slug"; then
if post_pr_round_review "$repo" "$PULL_REQUEST" "$repo/$review" "$pass"; then
event_done "$pr_comment_id" true "posted"
else
STATUS="pr-error"
Expand Down Expand Up @@ -2261,7 +2261,7 @@ run_devloop() {

if [ "$create_pr" = true ] && [ -n "$PULL_REQUEST" ] && [ "$STATUS" != "pr-error" ]; then
event_step "pr-final-report" "post final report to PR"
if post_pr_final_report "$repo" "$PULL_REQUEST" "$slug" "$STATUS" "$PASSES" "$max" "$FINAL_BRANCH" "$REPORT" "$report_format"; then
if post_pr_final_report "$repo" "$PULL_REQUEST" "$slug" "$STATUS" "$PASSES" "$max" "$FINAL_BRANCH"; then
event_done "pr-final-report" true "posted"
else
STATUS="pr-error"
Expand Down Expand Up @@ -3535,7 +3535,10 @@ create_pull_request() {
local repo="$1"
local branch="$2"
local base="$3"
ensure_pull_request "$repo" "$branch" "$base"
local spec="${4:-}"
local criteria_file="${5:-}"
local commit="${6:-}"
ensure_pull_request "$repo" "$branch" "$base" "$spec" "$criteria_file" "$commit"
}

push_pull_request_branch() {
Expand All @@ -3561,16 +3564,101 @@ lookup_pull_request() {
PULL_REQUEST="$(printf '%s\n' "$out" | sed '/^[[:space:]]*$/d' | head -n 1)"
}

spec_title() {
local spec="$1"
[ -f "$spec" ] || return 1
awk '
/^#[[:space:]]+/ {
sub(/^#[[:space:]]+/, "")
print
exit
}
' "$spec"
}

spec_section() {
local spec="$1"
local name="$2"
[ -f "$spec" ] || return 1
awk -v want="$name" '
/^[[:space:]]*##[[:space:]]+/ {
heading = $0
sub(/^[[:space:]]*##[[:space:]]+/, "", heading)
sub(/[[:space:]]+$/, "", heading)
if (tolower(heading) == tolower(want)) { inside = 1; started = 0; next }
if (inside) exit
}
inside {
if (!started && $0 ~ /^[[:space:]]*$/) next
started = 1
print
}
' "$spec"
}

draft_pull_request_body() {
local spec="$1"
local criteria_file="$2"
local commit="$3"
local title problem outcome
title="$(spec_title "$spec" 2>/dev/null || true)"
problem="$(spec_section "$spec" "Problem" 2>/dev/null || true)"
outcome="$(spec_section "$spec" "Outcome" 2>/dev/null || true)"
if [ -z "$title" ]; then title="Devloop change"; fi
if [ -z "$problem" ]; then problem="_No problem statement in the source spec._"; fi
if [ -z "$outcome" ]; then outcome="_No outcome described in the source spec._"; fi
if [ -z "$commit" ]; then commit="none"; fi
cat <<EOF
# $title

Generated by \`devloop --create-pr\`.

## Problem

$problem

## Outcome

$outcome

## Acceptance Criteria

$(if [ -n "$criteria_file" ] && [ -f "$criteria_file" ]; then criteria_block "$criteria_file"; else printf '%s\n' "No parsed acceptance criteria."; fi)

## Review Trail

Review rounds and the final report are posted as PR comments below.

---

Latest commit: $commit
EOF
}

create_draft_pull_request() {
local repo="$1"
local branch="$2"
local base="$3"
local out
if ! run_compact_command "$repo" "open draft pull request" gh pr create --draft --fill --base "$base" --head "$branch"; then
local spec="${4:-}"
local criteria_file="${5:-}"
local commit="${6:-}"
local body_file out
if ! body_file="$(mktemp "${TMPDIR:-/tmp}/devloop-pr-create.XXXXXX")"; then
PULL_REQUEST_ERROR="PR body file failed: mktemp failed"
return 1
fi
if ! draft_pull_request_body "$spec" "$criteria_file" "$commit" > "$body_file"; then
PULL_REQUEST_ERROR="PR body file failed"
rm -f "$body_file"
return 1
fi
if ! run_compact_command "$repo" "open draft pull request" gh pr create --draft --fill --base "$base" --head "$branch" --body-file "$body_file"; then
out="$RUN_OUTPUT"
PULL_REQUEST_ERROR="PR creation failed: $(gh_error_detail "$out")"
rm -f "$body_file"
return 1
fi
rm -f "$body_file"
out="$RUN_OUTPUT"
PULL_REQUEST="$(printf '%s\n' "$out" | awk '/^https?:\/\// { print; exit }')"
if [ -z "$PULL_REQUEST" ]; then PULL_REQUEST="$(printf '%s\n' "$out" | tail -n 1)"; fi
Expand All @@ -3580,25 +3668,26 @@ ensure_pull_request() {
local repo="$1"
local branch="$2"
local base="$3"
local spec="${4:-}"
local criteria_file="${5:-}"
local commit="${6:-}"
PULL_REQUEST_ERROR=""
if ! push_pull_request_branch "$repo" "$branch"; then return 1; fi
if ! lookup_pull_request "$repo" "$branch"; then return 1; fi
if [ -n "$PULL_REQUEST" ]; then return 0; fi
create_draft_pull_request "$repo" "$branch" "$base"
create_draft_pull_request "$repo" "$branch" "$base" "$spec" "$criteria_file" "$commit"
}

round_review_comment_body() {
local review_file="$1"
local pass="$2"
local slug="$3"
local verdict
verdict="$(parse_verdict "$review_file")"
if [ -z "$verdict" ]; then verdict="UNCLEAR"; fi
cat <<EOF
# Devloop Review Round $pass

- Verdict: $verdict
- Local cache: \`.codex/reviews/$slug-r$pass.md\`

$(cat "$review_file")
EOF
Expand All @@ -3609,10 +3698,9 @@ post_pr_round_review() {
local pr="$2"
local review_file="$3"
local pass="$4"
local slug="$5"
local body_file out
body_file="$(mktemp "${TMPDIR:-/tmp}/devloop-pr-review.XXXXXX")"
round_review_comment_body "$review_file" "$pass" "$slug" > "$body_file"
round_review_comment_body "$review_file" "$pass" > "$body_file"
if ! out="$(cd "$repo" >/dev/null 2>&1 && gh pr comment "$pr" --body-file "$body_file" 2>&1)"; then
PULL_REQUEST_ERROR="PR comment failed: $(gh_error_detail "$out")"
rm -f "$body_file"
Expand Down Expand Up @@ -3653,21 +3741,15 @@ final_pr_report_body() {
local max="$5"
local pr="$6"
local branch="$7"
local report="$8"
local report_format="$9"
local latest_review="$repo/.codex/reviews/$slug-r$pass.md"
local final_verdict acceptance engineering html_path risk
local final_verdict acceptance engineering risk
final_verdict=""
if [ -f "$latest_review" ]; then final_verdict="$(parse_verdict "$latest_review")"; fi
if [ -z "$final_verdict" ]; then final_verdict="none"; fi
acceptance="$(review_section "$latest_review" "Acceptance matrix" || true)"
engineering="$(review_section "$latest_review" "Engineering quality matrix" || true)"
if [ -z "$acceptance" ]; then acceptance="- Not available."; fi
if [ -z "$engineering" ]; then engineering="- Not available."; fi
html_path="Not generated."
if [ "$report_format" = "html" ] && [ -f "$repo/$report" ]; then
html_path="$repo/$report"
fi
case "$status" in
accepted) risk="No blocking residual risk was recorded by the final review." ;;
*) risk="The run ended as $status. Inspect the latest findings and missing tests before merging." ;;
Expand Down Expand Up @@ -3709,10 +3791,6 @@ $(commit_lines)
## Residual Risk

- $risk

## Local Report

- HTML report path: $html_path
EOF
}

Expand All @@ -3724,11 +3802,9 @@ post_pr_final_report() {
local pass="$5"
local max="$6"
local branch="$7"
local report="$8"
local report_format="$9"
local body_file out
body_file="$(mktemp "${TMPDIR:-/tmp}/devloop-pr-final.XXXXXX")"
final_pr_report_body "$repo" "$slug" "$status" "$pass" "$max" "$pr" "$branch" "$report" "$report_format" > "$body_file"
final_pr_report_body "$repo" "$slug" "$status" "$pass" "$max" "$pr" "$branch" > "$body_file"
if ! out="$(cd "$repo" >/dev/null 2>&1 && gh pr comment "$pr" --body-file "$body_file" 2>&1)"; then
PULL_REQUEST_ERROR="PR final report comment failed: $(gh_error_detail "$out")"
rm -f "$body_file"
Expand Down
31 changes: 30 additions & 1 deletion tests/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -603,19 +603,27 @@ case "${1:-}" in
exit 1
fi
head=""
body_file=""
while [ "$#" -gt 0 ]; do
case "$1" in
--head)
shift
head="${1:-}"
;;
--body-file)
shift
body_file="${1:-}"
;;
esac
shift || true
done
if [ -n "$head" ] && ! git ls-remote --heads origin "$head" | grep -q .; then
printf 'head branch was not pushed before PR creation: %s\n' "$head" >&2
exit 1
fi
if [ -n "$body_file" ]; then
cp "$body_file" "$state/pr-body.md"
fi
url="${DEVLOOP_GH_PR_URL:-https://github.com/satyaborg/devloop/pull/123}"
printf '%s\n' "$url" > "$state/pr_url"
printf '%s\n' "$url"
Expand Down Expand Up @@ -877,6 +885,14 @@ pr: null

# $title

## Problem

The result file is never written during the loop.

## Outcome

The loop writes the result file on accept.

## Acceptance criteria
1. Write the result file.
MARKDOWN
Expand Down Expand Up @@ -1114,6 +1130,18 @@ contains "$pr_accept_output" "pr:" "PR accept loop"
create_line="$(grep -n 'gh pr create' "$pr_log" | cut -d: -f1 | head -n 1)"
review_line="$(grep -n 'agent reviewer 1' "$pr_log" | cut -d: -f1 | head -n 1)"
[[ -n "$create_line" && -n "$review_line" && "$create_line" -lt "$review_line" ]] || fail "PR was not created before reviewer pass 1"
contains "$(cat "$pr_log")" "--body-file" "PR create body flag"
[[ -s "$pr_state/pr-body.md" ]] || fail "created PR body missing"
pr_body="$(cat "$pr_state/pr-body.md")"
contains "$pr_body" "E2E PR Accept" "created PR body"
contains "$pr_body" "## Problem" "created PR body"
contains "$pr_body" "The result file is never written during the loop." "created PR body"
contains "$pr_body" "## Outcome" "created PR body"
contains "$pr_body" "The loop writes the result file on accept." "created PR body"
contains "$pr_body" "Latest commit" "created PR body"
if ! printf '%s\n' "$pr_body" | grep -Eq 'Latest commit:[[:space:]]*[0-9a-f]{7,}'; then fail "created PR body missing commit hash"; fi
contains "$pr_body" "Write the result file." "created PR body"
if printf '%s\n' "$pr_body" | grep -q '/Users/'; then fail "created PR body leaked absolute local path"; fi
equals "$(find "$pr_state/comments" -name 'round-*.md' | wc -l | tr -d ' ')" "1" "one round PR comment"
equals "$(find "$pr_state/comments" -name 'final-*.md' | wc -l | tr -d ' ')" "1" "one final PR comment"
round_body="$(cat "$pr_state/comments/round-1.md")"
Expand Down Expand Up @@ -1141,7 +1169,8 @@ contains "$final_body" "Residual Risk" "final PR comment"
contains "$final_body" "PR URL" "final PR comment"
contains "$final_body" "Branch" "final PR comment"
contains "$final_body" "Commit References" "final PR comment"
contains "$final_body" ".codex/reports/e2e-pr-accept.html" "final PR comment"
if printf '%s\n' "$final_body" | grep -q '/Users/'; then fail "final PR comment leaked absolute local path"; fi
if printf '%s\n' "$round_body" | grep -q 'Local cache'; then fail "round PR comment leaked local cache path"; fi
if printf '%s\n' "$final_body" | grep -Eq '<(html|script|style)'; then fail "final PR comment embedded standalone HTML"; fi
unset DEVLOOP_GH_STATE DEVLOOP_GH_LOG DEVLOOP_AGENT_LOG
ok "PR-backed accept comments"
Expand Down