diff --git a/devloop b/devloop index 3dc1432..e8d5b76 100755 --- a/devloop +++ b/devloop @@ -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" @@ -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" @@ -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" @@ -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() { @@ -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 < "$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 @@ -3580,17 +3668,19 @@ 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 @@ -3598,7 +3688,6 @@ round_review_comment_body() { # Devloop Review Round $pass - Verdict: $verdict -- Local cache: \`.codex/reviews/$slug-r$pass.md\` $(cat "$review_file") EOF @@ -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" @@ -3653,10 +3741,8 @@ 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 @@ -3664,10 +3750,6 @@ final_pr_report_body() { 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." ;; @@ -3709,10 +3791,6 @@ $(commit_lines) ## Residual Risk - $risk - -## Local Report - -- HTML report path: $html_path EOF } @@ -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" diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh index 85af3aa..b5afe14 100755 --- a/tests/devloop_test.sh +++ b/tests/devloop_test.sh @@ -603,12 +603,17 @@ 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 @@ -616,6 +621,9 @@ case "${1:-}" in 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" @@ -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 @@ -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")" @@ -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"