@jonit-dev/night-watch-cli 1.8.8-beta.9 → 1.8.10-beta.0

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.
Files changed (44) hide show
  1. package/dist/cli.js +1086 -519
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/audit.d.ts.map +1 -1
  4. package/dist/commands/audit.js +10 -2
  5. package/dist/commands/audit.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +1 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/install.d.ts +4 -0
  10. package/dist/commands/install.d.ts.map +1 -1
  11. package/dist/commands/install.js +25 -0
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/logs.d.ts +1 -1
  14. package/dist/commands/logs.d.ts.map +1 -1
  15. package/dist/commands/logs.js +25 -5
  16. package/dist/commands/logs.js.map +1 -1
  17. package/dist/commands/merge.d.ts +26 -0
  18. package/dist/commands/merge.d.ts.map +1 -0
  19. package/dist/commands/merge.js +159 -0
  20. package/dist/commands/merge.js.map +1 -0
  21. package/dist/commands/review.d.ts +0 -1
  22. package/dist/commands/review.d.ts.map +1 -1
  23. package/dist/commands/review.js +0 -13
  24. package/dist/commands/review.js.map +1 -1
  25. package/dist/commands/slice.d.ts +1 -0
  26. package/dist/commands/slice.d.ts.map +1 -1
  27. package/dist/commands/slice.js +19 -19
  28. package/dist/commands/slice.js.map +1 -1
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +54 -0
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/scripts/night-watch-merger-cron.sh +321 -0
  33. package/dist/scripts/night-watch-pr-reviewer-cron.sh +1 -137
  34. package/dist/templates/slicer.md +54 -64
  35. package/dist/web/assets/index-B1BnOpiO.css +1 -0
  36. package/dist/web/assets/index-CPQbZ1BL.css +1 -0
  37. package/dist/web/assets/index-CiRJZI4z.js +386 -0
  38. package/dist/web/assets/index-DGpU39Cp.css +1 -0
  39. package/dist/web/assets/index-DcgNAi4A.js +386 -0
  40. package/dist/web/assets/index-SQlBKu_s.js +386 -0
  41. package/dist/web/assets/index-ZE5lOeJp.js +386 -0
  42. package/dist/web/assets/index-rfU713Zm.js +386 -0
  43. package/dist/web/index.html +2 -2
  44. package/package.json +1 -1
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Night Watch Merge Orchestrator Cron Runner
5
+ # Usage: night-watch-merger-cron.sh /path/to/project
6
+ #
7
+ # Scans all open PRs, filters eligible ones, and merges them in FIFO order
8
+ # (oldest PR first by creation date). Rebases remaining PRs after each merge.
9
+ #
10
+ # Required env vars (with defaults shown):
11
+ # NW_MERGER_MAX_RUNTIME=1800 - Maximum runtime in seconds (30 min)
12
+ # NW_MERGER_MERGE_METHOD=squash - Merge method: squash|merge|rebase
13
+ # NW_MERGER_MIN_REVIEW_SCORE=80 - Minimum review score threshold
14
+ # NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
15
+ # NW_MERGER_REBASE_BEFORE_MERGE=1 - Set to 1 to rebase before merging
16
+ # NW_MERGER_MAX_PRS_PER_RUN=0 - Max PRs to merge per run (0 = unlimited)
17
+ # NW_DRY_RUN=0 - Set to 1 for dry-run mode
18
+
19
+ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
20
+ PROJECT_NAME=$(basename "${PROJECT_DIR}")
21
+ LOG_DIR="${PROJECT_DIR}/logs"
22
+ LOG_FILE="${LOG_DIR}/merger.log"
23
+ MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-1800}"
24
+ MAX_LOG_SIZE="524288" # 512 KB
25
+ MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
26
+ MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
27
+ REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
28
+ MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
29
+ BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
30
+ SCRIPT_START_TIME=$(date +%s)
31
+ DRY_RUN="${NW_DRY_RUN:-0}"
32
+ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
33
+ PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
34
+
35
+ # Normalize numeric settings
36
+ if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
37
+ MAX_PRS_PER_RUN="0"
38
+ fi
39
+ if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
40
+ MIN_REVIEW_SCORE="80"
41
+ fi
42
+ # Clamp merge method to valid values
43
+ case "${MERGE_METHOD}" in
44
+ squash|merge|rebase) ;;
45
+ *) MERGE_METHOD="squash" ;;
46
+ esac
47
+
48
+ mkdir -p "${LOG_DIR}"
49
+
50
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
+ # shellcheck source=night-watch-helpers.sh
52
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
53
+
54
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
55
+ # NOTE: Lock file path must match mergerLockPath() in src/utils/status-data.ts
56
+ LOCK_FILE="/tmp/night-watch-merger-${PROJECT_RUNTIME_KEY}.lock"
57
+ SCRIPT_TYPE="merger"
58
+
59
+ MERGED_PRS=0
60
+ FAILED_PRS=0
61
+ MERGED_PR_LIST=""
62
+
63
+ emit_result() {
64
+ local status="${1:?status required}"
65
+ local details="${2:-}"
66
+ if [ -n "${details}" ]; then
67
+ echo "NIGHT_WATCH_RESULT:${status}|${details}"
68
+ else
69
+ echo "NIGHT_WATCH_RESULT:${status}"
70
+ fi
71
+ }
72
+
73
+ # ── Global Job Queue Gate ────────────────────────────────────────────────────
74
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
75
+ if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
76
+ if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
77
+ arm_global_queue_cleanup
78
+ else
79
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
80
+ fi
81
+ fi
82
+ # ──────────────────────────────────────────────────────────────────────────────
83
+
84
+ # Check if branch matches configured patterns
85
+ matches_branch_patterns() {
86
+ local branch="${1}"
87
+ if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
88
+ return 0 # No filter = match all
89
+ fi
90
+ IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
91
+ for pattern in "${patterns[@]}"; do
92
+ pattern="${pattern# }" # trim leading space
93
+ if [ -n "${pattern}" ] && [[ "${branch}" == "${pattern}"* ]]; then
94
+ return 0
95
+ fi
96
+ done
97
+ return 1
98
+ }
99
+
100
+ # Get review score from PR labels/comments
101
+ get_review_score() {
102
+ local pr_number="${1}"
103
+ # Look for review score comment from night-watch
104
+ local score
105
+ score=$(gh pr view "${pr_number}" --json comments \
106
+ --jq '[.comments[].body | select(test("review score|score:? [0-9]+/100"; "i")) | capture("(?i)score:? *(?<s>[0-9]+)/100") | .s] | last | tonumber // -1' \
107
+ 2>/dev/null || echo "-1")
108
+ echo "${score}"
109
+ }
110
+
111
+ # Check if CI is passing for a PR (all checks must be complete and none failing)
112
+ ci_passing() {
113
+ local pr_number="${1}"
114
+ local checks_json
115
+ checks_json=$(gh pr checks "${pr_number}" --json name,state,conclusion 2>/dev/null || echo "[]")
116
+ # Fail if any checks have explicit failures
117
+ local fail_count
118
+ fail_count=$(echo "${checks_json}" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED" or .state == "FAILURE")] | length' 2>/dev/null || echo "999")
119
+ if [ "${fail_count}" != "0" ]; then
120
+ return 1
121
+ fi
122
+ # Fail if any checks are still pending/in-progress (not yet concluded)
123
+ local pending_count
124
+ pending_count=$(echo "${checks_json}" | jq '[.[] | select(.state == "PENDING" or .state == "IN_PROGRESS" or (.conclusion == null and .state != "SUCCESS"))] | length' 2>/dev/null || echo "999")
125
+ if [ "${pending_count}" != "0" ]; then
126
+ return 1
127
+ fi
128
+ return 0
129
+ }
130
+
131
+ # Rebase a PR against its base branch
132
+ rebase_pr() {
133
+ local pr_number="${1}"
134
+ log "INFO: Rebasing PR #${pr_number} against base branch"
135
+ if [ "${DRY_RUN}" = "1" ]; then
136
+ log "INFO: [DRY RUN] Would rebase PR #${pr_number}"
137
+ return 0
138
+ fi
139
+ gh pr update-branch --rebase "${pr_number}" 2>/dev/null
140
+ return $?
141
+ }
142
+
143
+ log() {
144
+ echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "${LOG_FILE}"
145
+ }
146
+
147
+ rotate_log() {
148
+ if [ -f "${LOG_FILE}" ] && [ "$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0)" -ge "${MAX_LOG_SIZE}" ]; then
149
+ mv "${LOG_FILE}" "${LOG_FILE}.bak" 2>/dev/null || true
150
+ fi
151
+ }
152
+
153
+ # ── Log rotation ──────────────────────────────────────────────────────────────
154
+ rotate_log
155
+ # ─────────────────────────────────────────────────────────────────────────────
156
+
157
+ cd "${PROJECT_DIR}"
158
+
159
+ log "========================================"
160
+ log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
161
+ log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
162
+ log "========================================"
163
+
164
+ if ! acquire_lock "${LOCK_FILE}"; then
165
+ emit_result "skip_locked"
166
+ exit 0
167
+ fi
168
+
169
+ # ── Dry-run mode ────────────────────────────────────────────────────────────
170
+ if [ "${DRY_RUN}" = "1" ]; then
171
+ echo "=== Dry Run: Merge Orchestrator ==="
172
+ echo "Merge Method: ${MERGE_METHOD}"
173
+ echo "Min Review Score: ${MIN_REVIEW_SCORE}/100"
174
+ echo "Rebase Before Merge: ${REBASE_BEFORE_MERGE}"
175
+ echo "Max PRs Per Run: ${MAX_PRS_PER_RUN} (0=unlimited)"
176
+ echo "Max Runtime: ${MAX_RUNTIME}s"
177
+ echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
178
+ log "INFO: Dry run mode — exiting without processing"
179
+ emit_result "skip_dry_run"
180
+ exit 0
181
+ fi
182
+
183
+ # Timeout watchdog
184
+ (
185
+ sleep "${MAX_RUNTIME}"
186
+ log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
187
+ kill -TERM $$ 2>/dev/null || true
188
+ ) &
189
+ WATCHDOG_PID=$!
190
+ trap 'kill ${WATCHDOG_PID} 2>/dev/null || true; rm -f "${LOCK_FILE}"' EXIT
191
+
192
+ # Discover open PRs sorted by creation date (oldest first = FIFO)
193
+ log "INFO: Scanning open PRs..."
194
+ PR_LIST_JSON=$(gh pr list --state open \
195
+ --json number,headRefName,createdAt,isDraft \
196
+ --jq 'sort_by(.createdAt)' \
197
+ 2>/dev/null || echo "[]")
198
+
199
+ PR_COUNT=$(echo "${PR_LIST_JSON}" | jq 'length')
200
+ log "INFO: Found ${PR_COUNT} open PRs"
201
+
202
+ if [ "${PR_COUNT}" = "0" ]; then
203
+ log "SKIP: No open PRs found. Exiting."
204
+ emit_result "skip_no_prs"
205
+ exit 0
206
+ fi
207
+
208
+ # Process each PR in FIFO order
209
+ PROCESSED=0
210
+ while IFS= read -r pr_json; do
211
+ pr_number=$(echo "${pr_json}" | jq -r '.number')
212
+ pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
213
+ is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
214
+
215
+ # Skip drafts
216
+ if [ "${is_draft}" = "true" ]; then
217
+ log "INFO: PR #${pr_number} (${pr_branch}): Skipping draft"
218
+ continue
219
+ fi
220
+
221
+ # Check branch pattern filter
222
+ if ! matches_branch_patterns "${pr_branch}"; then
223
+ log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
224
+ continue
225
+ fi
226
+
227
+ # Check CI status
228
+ if ! ci_passing "${pr_number}"; then
229
+ log "INFO: PR #${pr_number} (${pr_branch}): CI not passing, skipping"
230
+ continue
231
+ fi
232
+
233
+ # Check review score
234
+ if [ "${MIN_REVIEW_SCORE}" -gt "0" ]; then
235
+ score=$(get_review_score "${pr_number}")
236
+ if [ "${score}" -lt "0" ] || [ "${score}" -lt "${MIN_REVIEW_SCORE}" ]; then
237
+ log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), skipping"
238
+ continue
239
+ fi
240
+ fi
241
+
242
+ log "INFO: PR #${pr_number} (${pr_branch}): Eligible for merge"
243
+
244
+ # Rebase before merge if configured
245
+ if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
246
+ if ! rebase_pr "${pr_number}"; then
247
+ log "WARN: PR #${pr_number}: Rebase failed, skipping"
248
+ FAILED_PRS=$((FAILED_PRS + 1))
249
+ continue
250
+ fi
251
+ log "INFO: PR #${pr_number}: Rebase successful"
252
+
253
+ # Poll CI until all checks complete after rebase (up to 5 minutes)
254
+ local ci_max_wait=300
255
+ local ci_waited=0
256
+ local ci_poll=15
257
+ while [ "${ci_waited}" -lt "${ci_max_wait}" ]; do
258
+ sleep "${ci_poll}"
259
+ ci_waited=$((ci_waited + ci_poll))
260
+ if ci_passing "${pr_number}"; then
261
+ break
262
+ fi
263
+ log "INFO: PR #${pr_number}: Waiting for CI after rebase (${ci_waited}s/${ci_max_wait}s)..."
264
+ done
265
+ if ! ci_passing "${pr_number}"; then
266
+ log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${ci_waited}s), skipping"
267
+ continue
268
+ fi
269
+ fi
270
+
271
+ # Merge the PR
272
+ log "INFO: Merging PR #${pr_number} with method: ${MERGE_METHOD}..."
273
+ if gh pr merge "${pr_number}" "--${MERGE_METHOD}" --delete-branch 2>&1 | tee -a "${LOG_FILE}"; then
274
+ log "INFO: PR #${pr_number}: Merged successfully"
275
+ MERGED_PRS=$((MERGED_PRS + 1))
276
+ MERGED_PR_LIST="${MERGED_PR_LIST}${pr_number},"
277
+
278
+ # Rebase remaining PRs after each successful merge
279
+ log "INFO: Rebasing remaining open PRs after merging #${pr_number}..."
280
+ REMAINING_JSON=$(gh pr list --state open \
281
+ --json number,headRefName \
282
+ 2>/dev/null || echo "[]")
283
+ while IFS= read -r remaining_pr; do
284
+ remaining_number=$(echo "${remaining_pr}" | jq -r '.number')
285
+ remaining_branch=$(echo "${remaining_pr}" | jq -r '.headRefName')
286
+ if [ "${remaining_number}" != "${pr_number}" ]; then
287
+ log "INFO: Rebasing remaining PR #${remaining_number} (${remaining_branch})"
288
+ gh pr update-branch --rebase "${remaining_number}" 2>/dev/null || \
289
+ log "WARN: PR #${remaining_number}: Rebase failed (continuing)"
290
+ fi
291
+ done < <(echo "${REMAINING_JSON}" | jq -c '.[]')
292
+ else
293
+ log "WARN: PR #${pr_number}: Merge failed"
294
+ FAILED_PRS=$((FAILED_PRS + 1))
295
+ fi
296
+
297
+ PROCESSED=$((PROCESSED + 1))
298
+
299
+ # Check max PRs per run limit
300
+ if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${PROCESSED}" -ge "${MAX_PRS_PER_RUN}" ]; then
301
+ log "INFO: Reached max PRs per run limit (${MAX_PRS_PER_RUN}). Stopping."
302
+ break
303
+ fi
304
+
305
+ # Enforce global timeout
306
+ elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
307
+ if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
308
+ log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
309
+ break
310
+ fi
311
+ done < <(echo "${PR_LIST_JSON}" | jq -c '.[]')
312
+
313
+ # Trim trailing comma from PR list
314
+ MERGED_PR_LIST="${MERGED_PR_LIST%,}"
315
+
316
+ log "========================================"
317
+ log "RUN-END: merger complete merged=${MERGED_PRS} failed=${FAILED_PRS}"
318
+ log "========================================"
319
+
320
+ emit_result "success" "merged=${MERGED_PRS}|failed=${FAILED_PRS}|prs=${MERGED_PR_LIST}"
321
+ exit 0
@@ -10,8 +10,6 @@ set -euo pipefail
10
10
  # NW_REVIEWER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
11
11
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
12
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
13
- # NW_AUTO_MERGE=0 - Set to 1 to enable auto-merge
14
- # NW_AUTO_MERGE_METHOD=squash - Merge method: squash, merge, or rebase
15
13
 
16
14
  PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
17
15
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
@@ -23,8 +21,6 @@ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
23
21
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
24
22
  MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
25
23
  BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
26
- AUTO_MERGE="${NW_AUTO_MERGE:-0}"
27
- AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
28
24
  TARGET_PR="${NW_TARGET_PR:-}"
29
25
  PARALLEL_ENABLED="${NW_REVIEWER_PARALLEL:-1}"
30
26
  WORKER_MODE="${NW_REVIEWER_WORKER_MODE:-0}"
@@ -552,7 +548,7 @@ fi
552
548
  rotate_log
553
549
  log_separator
554
550
  log "RUN-START: reviewer invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} worker=${WORKER_MODE} target_pr=${TARGET_PR:-all} parallel=${PARALLEL_ENABLED}"
555
- log "CONFIG: max_runtime=${MAX_RUNTIME}s min_review_score=${MIN_REVIEW_SCORE} auto_merge=${AUTO_MERGE} branch_patterns=${BRANCH_PATTERNS_RAW}"
551
+ log "CONFIG: max_runtime=${MAX_RUNTIME}s min_review_score=${MIN_REVIEW_SCORE} branch_patterns=${BRANCH_PATTERNS_RAW}"
556
552
 
557
553
  if ! acquire_lock "${LOCK_FILE}"; then
558
554
  emit_result "skip_locked"
@@ -683,61 +679,6 @@ done < <(
683
679
  if [ "${NEEDS_WORK}" -eq 0 ]; then
684
680
  log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}"
685
681
 
686
- # ── Auto-merge eligible PRs ───────────────────────────────
687
- if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
688
- AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
689
- AUTO_MERGED_COUNT=0
690
-
691
- log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
692
-
693
- while IFS=$'\t' read -r pr_number pr_branch; do
694
- [ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
695
- printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
696
-
697
- # Check CI status - must have ALL checks passing (not just "no failures")
698
- # gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
699
- if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
700
- log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
701
- continue
702
- fi
703
-
704
- # Check review score
705
- PR_COMMENTS=$(
706
- {
707
- gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
708
- if [ -n "${REPO}" ]; then
709
- gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
710
- fi
711
- } | awk '!seen[$0]++'
712
- )
713
- PR_SCORE=$(extract_review_score_from_text "${PR_COMMENTS}")
714
-
715
- # Skip PRs without a score or with score below threshold
716
- [ -z "${PR_SCORE}" ] && continue
717
- [ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
718
-
719
- # PR is merge-ready
720
- log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
721
-
722
- # Dry-run mode: show what would be merged
723
- if [ "${NW_DRY_RUN:-0}" = "1" ]; then
724
- log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
725
- continue
726
- fi
727
-
728
- if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
729
- log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
730
- AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
731
- else
732
- log "WARN: Auto-merge failed for PR #${pr_number}"
733
- fi
734
- done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
735
-
736
- if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
737
- log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
738
- fi
739
- fi
740
-
741
682
  if [ "${WORKER_MODE}" != "1" ]; then
742
683
  send_telegram_status_message "🔍 Night Watch Reviewer: nothing to do" "Project: ${PROJECT_NAME}
743
684
  Provider (model): ${PROVIDER_MODEL_DISPLAY}
@@ -786,10 +727,6 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
786
727
  echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
787
728
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
788
729
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
789
- echo "Auto-merge: ${AUTO_MERGE}"
790
- if [ "${AUTO_MERGE}" = "1" ]; then
791
- echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
792
- fi
793
730
  echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
794
731
  echo "Open PRs needing work:${PRS_NEEDING_WORK}"
795
732
  echo "Default Branch: ${DEFAULT_BRANCH}"
@@ -947,10 +884,6 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
947
884
  echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
948
885
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
949
886
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
950
- echo "Auto-merge: ${AUTO_MERGE}"
951
- if [ "${AUTO_MERGE}" = "1" ]; then
952
- echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
953
- fi
954
887
  echo "Max Retries: ${REVIEWER_MAX_RETRIES}"
955
888
  echo "Retry Delay: ${REVIEWER_RETRY_DELAY}s"
956
889
  echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
@@ -1257,78 +1190,9 @@ if [ "${EXIT_CODE}" -eq 0 ] && [ -n "${TARGET_PR}" ] && [ -n "${PR_BRANCH_HEAD_B
1257
1190
  fi
1258
1191
  fi
1259
1192
 
1260
- # ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
1261
- # After the reviewer completes, check for PRs that are merge-ready and queue them
1262
- # for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
1263
1193
  AUTO_MERGED_PRS=""
1264
1194
  AUTO_MERGE_FAILED_PRS=""
1265
1195
 
1266
- if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
1267
- log "AUTO-MERGE: Checking for merge-ready PRs..."
1268
-
1269
- while IFS=$'\t' read -r pr_number pr_branch; do
1270
- if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
1271
- continue
1272
- fi
1273
-
1274
- if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
1275
- continue
1276
- fi
1277
-
1278
- # Only process PRs matching branch patterns
1279
- if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
1280
- continue
1281
- fi
1282
-
1283
- # Check CI status - must have ALL checks passing (not just "no failures")
1284
- # gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
1285
- if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
1286
- log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
1287
- continue
1288
- fi
1289
-
1290
- # Check review score - must have score >= threshold
1291
- ALL_COMMENTS=$(
1292
- {
1293
- gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
1294
- if [ -n "${REPO}" ]; then
1295
- gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
1296
- fi
1297
- } | awk '!seen[$0]++'
1298
- )
1299
- LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
1300
-
1301
- # Skip PRs without a score
1302
- if [ -z "${LATEST_SCORE}" ]; then
1303
- continue
1304
- fi
1305
-
1306
- # Skip PRs with score below threshold
1307
- if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
1308
- continue
1309
- fi
1310
-
1311
- # PR is merge-ready - queue for auto-merge
1312
- log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
1313
-
1314
- if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
1315
- log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
1316
- if [ -z "${AUTO_MERGED_PRS}" ]; then
1317
- AUTO_MERGED_PRS="#${pr_number}"
1318
- else
1319
- AUTO_MERGED_PRS="${AUTO_MERGED_PRS},#${pr_number}"
1320
- fi
1321
- else
1322
- log "WARN: Auto-merge failed for PR #${pr_number}"
1323
- if [ -z "${AUTO_MERGE_FAILED_PRS}" ]; then
1324
- AUTO_MERGE_FAILED_PRS="#${pr_number}"
1325
- else
1326
- AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS},#${pr_number}"
1327
- fi
1328
- fi
1329
- done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
1330
- fi
1331
-
1332
1196
  REVIEWER_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
1333
1197
  log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${REVIEWER_TOTAL_ELAPSED}s prs=${PRS_NEEDING_WORK_CSV:-none} attempts=${ATTEMPTS_MADE}"
1334
1198
  emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${ATTEMPTS_MADE}" "${FINAL_SCORE}" "${NO_CHANGES_NEEDED}" "${NO_CHANGES_PRS}"