@jonit-dev/night-watch-cli 1.7.47 → 1.7.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,24 @@ TARGET_PR="${NW_TARGET_PR:-}"
28
28
  PARALLEL_ENABLED="${NW_REVIEWER_PARALLEL:-1}"
29
29
  WORKER_MODE="${NW_REVIEWER_WORKER_MODE:-0}"
30
30
 
31
+ # Retry configuration
32
+ REVIEWER_MAX_RETRIES="${NW_REVIEWER_MAX_RETRIES:-2}"
33
+ REVIEWER_RETRY_DELAY="${NW_REVIEWER_RETRY_DELAY:-30}"
34
+
35
+ # Normalize retry settings to safe numeric ranges
36
+ if ! [[ "${REVIEWER_MAX_RETRIES}" =~ ^[0-9]+$ ]]; then
37
+ REVIEWER_MAX_RETRIES="2"
38
+ fi
39
+ if ! [[ "${REVIEWER_RETRY_DELAY}" =~ ^[0-9]+$ ]]; then
40
+ REVIEWER_RETRY_DELAY="30"
41
+ fi
42
+ if [ "${REVIEWER_MAX_RETRIES}" -gt 10 ]; then
43
+ REVIEWER_MAX_RETRIES="10"
44
+ fi
45
+ if [ "${REVIEWER_RETRY_DELAY}" -gt 300 ]; then
46
+ REVIEWER_RETRY_DELAY="300"
47
+ fi
48
+
31
49
  # Ensure NVM / Node / Claude are on PATH
32
50
  export NVM_DIR="${HOME}/.nvm"
33
51
  [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
@@ -64,16 +82,31 @@ emit_final_status() {
64
82
  local prs_csv="${2:-}"
65
83
  local auto_merged="${3:-}"
66
84
  local auto_merge_failed="${4:-}"
85
+ local attempts="${5:-1}"
86
+ local final_score="${6:-}"
87
+ local details=""
67
88
 
68
89
  if [ "${exit_code}" -eq 0 ]; then
90
+ details="prs=${prs_csv}|auto_merged=${auto_merged}|auto_merge_failed=${auto_merge_failed}|attempts=${attempts}"
91
+ if [ -n "${final_score}" ]; then
92
+ details="${details}|final_score=${final_score}"
93
+ fi
69
94
  log "DONE: PR reviewer completed successfully"
70
- emit_result "success_reviewed" "prs=${prs_csv}|auto_merged=${auto_merged}|auto_merge_failed=${auto_merge_failed}"
95
+ emit_result "success_reviewed" "${details}"
71
96
  elif [ "${exit_code}" -eq 124 ]; then
97
+ details="prs=${prs_csv}|attempts=${attempts}"
98
+ if [ -n "${final_score}" ]; then
99
+ details="${details}|final_score=${final_score}"
100
+ fi
72
101
  log "TIMEOUT: PR reviewer killed after ${MAX_RUNTIME}s"
73
- emit_result "timeout" "prs=${prs_csv}"
102
+ emit_result "timeout" "${details}"
74
103
  else
104
+ details="prs=${prs_csv}|attempts=${attempts}"
105
+ if [ -n "${final_score}" ]; then
106
+ details="${details}|final_score=${final_score}"
107
+ fi
75
108
  log "FAIL: PR reviewer exited with code ${exit_code}"
76
- emit_result "failure" "prs=${prs_csv}"
109
+ emit_result "failure" "${details}"
77
110
  fi
78
111
  }
79
112
 
@@ -91,6 +124,25 @@ append_csv() {
91
124
  fi
92
125
  }
93
126
 
127
+ # Extract the latest review score from PR comments
128
+ # Returns empty string if no score found
129
+ get_pr_score() {
130
+ local pr_number="${1:?PR number required}"
131
+ local all_comments
132
+ all_comments=$(
133
+ {
134
+ gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
135
+ if [ -n "${REPO:-}" ]; then
136
+ gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
137
+ fi
138
+ } | sort -u
139
+ )
140
+ echo "${all_comments}" \
141
+ | grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
142
+ | tail -1 \
143
+ | grep -oP '\d+(?=/100)' || echo ""
144
+ }
145
+
94
146
  # Validate provider
95
147
  if ! validate_provider "${PROVIDER_CMD}"; then
96
148
  echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
@@ -323,6 +375,8 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
323
375
  EXIT_CODE=0
324
376
  AUTO_MERGED_PRS=""
325
377
  AUTO_MERGE_FAILED_PRS=""
378
+ MAX_WORKER_ATTEMPTS=1
379
+ MAX_WORKER_FINAL_SCORE=""
326
380
 
327
381
  for idx in "${!WORKER_PIDS[@]}"; do
328
382
  worker_pid="${WORKER_PIDS[$idx]}"
@@ -344,10 +398,21 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
344
398
  worker_status=$(printf '%s' "${worker_result}" | sed -n 's/^NIGHT_WATCH_RESULT:\([^|]*\).*$/\1/p')
345
399
  worker_auto_merged=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merged=)[^|]+' || true)
346
400
  worker_auto_merge_failed=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merge_failed=)[^|]+' || true)
401
+ worker_attempts=$(printf '%s' "${worker_result}" | grep -oP '(?<=attempts=)[^|]+' || true)
402
+ worker_final_score=$(printf '%s' "${worker_result}" | grep -oP '(?<=final_score=)[^|]+' || true)
347
403
 
348
404
  AUTO_MERGED_PRS=$(append_csv "${AUTO_MERGED_PRS}" "${worker_auto_merged}")
349
405
  AUTO_MERGE_FAILED_PRS=$(append_csv "${AUTO_MERGE_FAILED_PRS}" "${worker_auto_merge_failed}")
350
406
 
407
+ if [[ "${worker_attempts}" =~ ^[0-9]+$ ]] && [ "${worker_attempts}" -gt "${MAX_WORKER_ATTEMPTS}" ]; then
408
+ MAX_WORKER_ATTEMPTS="${worker_attempts}"
409
+ fi
410
+ if [[ "${worker_final_score}" =~ ^[0-9]+$ ]]; then
411
+ if [ -z "${MAX_WORKER_FINAL_SCORE}" ] || [ "${worker_final_score}" -gt "${MAX_WORKER_FINAL_SCORE}" ]; then
412
+ MAX_WORKER_FINAL_SCORE="${worker_final_score}"
413
+ fi
414
+ fi
415
+
351
416
  rm -f "${worker_output}"
352
417
 
353
418
  if [ "${worker_status}" = "failure" ] || { [ -n "${worker_status}" ] && [ "${worker_status}" != "success_reviewed" ] && [ "${worker_status}" != "timeout" ] && [ "${worker_status#skip_}" = "${worker_status}" ]; }; then
@@ -374,7 +439,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
374
439
  fi
375
440
  done
376
441
 
377
- emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
442
+ emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}"
378
443
  exit 0
379
444
  fi
380
445
 
@@ -396,6 +461,8 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
396
461
  if [ "${AUTO_MERGE}" = "1" ]; then
397
462
  echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
398
463
  fi
464
+ echo "Max Retries: ${REVIEWER_MAX_RETRIES}"
465
+ echo "Retry Delay: ${REVIEWER_RETRY_DELAY}s"
399
466
  echo "Open PRs needing work:${PRS_NEEDING_WORK}"
400
467
  echo "Default Branch: ${DEFAULT_BRANCH}"
401
468
  echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
@@ -413,44 +480,110 @@ if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFA
413
480
  fi
414
481
 
415
482
  EXIT_CODE=0
483
+ ATTEMPTS_MADE=1
484
+ FINAL_SCORE=""
416
485
  TARGET_SCOPE_PROMPT=""
417
486
  if [ -n "${TARGET_PR}" ]; then
418
487
  TARGET_SCOPE_PROMPT=$'\n\n## Target Scope\n- Only process PR #'"${TARGET_PR}"$'.\n- Ignore all other PRs.\n- If this PR no longer needs work, stop immediately.\n'
419
488
  fi
420
489
 
421
- case "${PROVIDER_CMD}" in
422
- claude)
423
- CLAUDE_PROMPT="/night-watch-pr-reviewer${TARGET_SCOPE_PROMPT}"
424
- if (
425
- cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
426
- claude -p "${CLAUDE_PROMPT}" \
427
- --dangerously-skip-permissions \
428
- >> "${LOG_FILE}" 2>&1
429
- ); then
430
- EXIT_CODE=0
431
- else
432
- EXIT_CODE=$?
490
+ # ── Retry Loop for Targeted PR Review ──────────────────────────────────────────
491
+ # Only retry when targeting a specific PR. Non-targeted mode handles all PRs in one shot.
492
+ TOTAL_ATTEMPTS=1
493
+ if [ -n "${TARGET_PR}" ]; then
494
+ TOTAL_ATTEMPTS=$((REVIEWER_MAX_RETRIES + 1))
495
+ fi
496
+ RUN_STARTED_AT=$(date +%s)
497
+
498
+ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
499
+ ATTEMPTS_MADE="${ATTEMPT}"
500
+
501
+ ATTEMPT_TIMEOUT="${MAX_RUNTIME}"
502
+ if [ -n "${TARGET_PR}" ]; then
503
+ # Calculate timeout from remaining runtime budget.
504
+ NOW_TS=$(date +%s)
505
+ ELAPSED=$((NOW_TS - RUN_STARTED_AT))
506
+ REMAINING_BUDGET=$((MAX_RUNTIME - ELAPSED))
507
+ if [ "${REMAINING_BUDGET}" -le 0 ]; then
508
+ EXIT_CODE=124
509
+ log "RETRY: Runtime budget exhausted before attempt ${ATTEMPT}"
510
+ break
433
511
  fi
434
- ;;
435
- codex)
436
- CODEX_PROMPT="$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")${TARGET_SCOPE_PROMPT}"
437
- if (
438
- cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
439
- codex --quiet \
440
- --yolo \
441
- --prompt "${CODEX_PROMPT}" \
442
- >> "${LOG_FILE}" 2>&1
443
- ); then
444
- EXIT_CODE=0
512
+
513
+ REMAINING_ATTEMPTS=$((TOTAL_ATTEMPTS - ATTEMPT + 1))
514
+ ATTEMPT_TIMEOUT=$((REMAINING_BUDGET / REMAINING_ATTEMPTS))
515
+ if [ "${ATTEMPT_TIMEOUT}" -lt 1 ]; then
516
+ ATTEMPT_TIMEOUT=1
517
+ fi
518
+ fi
519
+
520
+ log "RETRY: Starting attempt ${ATTEMPT}/${TOTAL_ATTEMPTS} (timeout: ${ATTEMPT_TIMEOUT}s)"
521
+
522
+ case "${PROVIDER_CMD}" in
523
+ claude)
524
+ CLAUDE_PROMPT="/night-watch-pr-reviewer${TARGET_SCOPE_PROMPT}"
525
+ if (
526
+ cd "${REVIEW_WORKTREE_DIR}" && timeout "${ATTEMPT_TIMEOUT}" \
527
+ claude -p "${CLAUDE_PROMPT}" \
528
+ --dangerously-skip-permissions \
529
+ >> "${LOG_FILE}" 2>&1
530
+ ); then
531
+ EXIT_CODE=0
532
+ else
533
+ EXIT_CODE=$?
534
+ fi
535
+ ;;
536
+ codex)
537
+ CODEX_PROMPT="$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")${TARGET_SCOPE_PROMPT}"
538
+ if (
539
+ cd "${REVIEW_WORKTREE_DIR}" && timeout "${ATTEMPT_TIMEOUT}" \
540
+ codex --quiet \
541
+ --yolo \
542
+ --prompt "${CODEX_PROMPT}" \
543
+ >> "${LOG_FILE}" 2>&1
544
+ ); then
545
+ EXIT_CODE=0
546
+ else
547
+ EXIT_CODE=$?
548
+ fi
549
+ ;;
550
+ *)
551
+ log "ERROR: Unknown provider: ${PROVIDER_CMD}"
552
+ exit 1
553
+ ;;
554
+ esac
555
+
556
+ # If provider failed (non-zero exit), don't retry
557
+ if [ "${EXIT_CODE}" -ne 0 ]; then
558
+ log "RETRY: Provider exited with code ${EXIT_CODE}, not retrying"
559
+ break
560
+ fi
561
+
562
+ # Re-check score for the target PR (only in targeted mode)
563
+ if [ -n "${TARGET_PR}" ]; then
564
+ CURRENT_SCORE=$(get_pr_score "${TARGET_PR}")
565
+ if [ -z "${CURRENT_SCORE}" ]; then
566
+ log "RETRY: No review score found for PR #${TARGET_PR} after attempt ${ATTEMPT}; not retrying"
567
+ break
568
+ fi
569
+
570
+ FINAL_SCORE="${CURRENT_SCORE}"
571
+ if [ "${CURRENT_SCORE}" -ge "${MIN_REVIEW_SCORE}" ]; then
572
+ log "RETRY: PR #${TARGET_PR} now scores ${CURRENT_SCORE}/100 (>= ${MIN_REVIEW_SCORE}) after attempt ${ATTEMPT}"
573
+ break
574
+ fi
575
+ if [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
576
+ log "RETRY: PR #${TARGET_PR} scores ${CURRENT_SCORE:-unknown}/100 after attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}, retrying in ${REVIEWER_RETRY_DELAY}s..."
577
+ sleep "${REVIEWER_RETRY_DELAY}"
445
578
  else
446
- EXIT_CODE=$?
579
+ log "RETRY: PR #${TARGET_PR} still at ${CURRENT_SCORE:-unknown}/100 after ${TOTAL_ATTEMPTS} attempts - giving up"
580
+ gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
447
581
  fi
448
- ;;
449
- *)
450
- log "ERROR: Unknown provider: ${PROVIDER_CMD}"
451
- exit 1
452
- ;;
453
- esac
582
+ else
583
+ # Non-targeted mode: no retry (reviewer handles all PRs in one shot)
584
+ break
585
+ fi
586
+ done
454
587
 
455
588
  cleanup_worktrees "${PROJECT_DIR}" "${REVIEW_WORKTREE_BASENAME}"
456
589
 
@@ -529,4 +662,4 @@ if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
529
662
  done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
530
663
  fi
531
664
 
532
- emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
665
+ emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${ATTEMPTS_MADE}" "${FINAL_SCORE}"