@jonit-dev/night-watch-cli 1.8.8-beta.1 → 1.8.8-beta.11

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 (52) hide show
  1. package/dist/cli.js +981 -78
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +39 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.d.ts +8 -0
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +50 -0
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/merge.d.ts +26 -0
  11. package/dist/commands/merge.d.ts.map +1 -0
  12. package/dist/commands/merge.js +159 -0
  13. package/dist/commands/merge.js.map +1 -0
  14. package/dist/commands/qa.d.ts.map +1 -1
  15. package/dist/commands/qa.js +2 -0
  16. package/dist/commands/qa.js.map +1 -1
  17. package/dist/commands/queue.d.ts.map +1 -1
  18. package/dist/commands/queue.js +27 -4
  19. package/dist/commands/queue.js.map +1 -1
  20. package/dist/commands/resolve.d.ts +26 -0
  21. package/dist/commands/resolve.d.ts.map +1 -0
  22. package/dist/commands/resolve.js +186 -0
  23. package/dist/commands/resolve.js.map +1 -0
  24. package/dist/commands/review.d.ts +5 -1
  25. package/dist/commands/review.d.ts.map +1 -1
  26. package/dist/commands/review.js +18 -18
  27. package/dist/commands/review.js.map +1 -1
  28. package/dist/commands/slice.d.ts +1 -0
  29. package/dist/commands/slice.d.ts.map +1 -1
  30. package/dist/commands/slice.js +19 -19
  31. package/dist/commands/slice.js.map +1 -1
  32. package/dist/commands/summary.d.ts +14 -0
  33. package/dist/commands/summary.d.ts.map +1 -0
  34. package/dist/commands/summary.js +193 -0
  35. package/dist/commands/summary.js.map +1 -0
  36. package/dist/commands/uninstall.d.ts.map +1 -1
  37. package/dist/commands/uninstall.js +14 -2
  38. package/dist/commands/uninstall.js.map +1 -1
  39. package/dist/scripts/night-watch-helpers.sh +10 -1
  40. package/dist/scripts/night-watch-merger-cron.sh +321 -0
  41. package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
  42. package/dist/scripts/night-watch-pr-reviewer-cron.sh +23 -142
  43. package/dist/scripts/night-watch-qa-cron.sh +30 -4
  44. package/dist/scripts/test-helpers.bats +45 -0
  45. package/dist/templates/night-watch-pr-reviewer.md +2 -1
  46. package/dist/templates/pr-reviewer.md +2 -1
  47. package/dist/templates/slicer.md +54 -64
  48. package/dist/web/assets/index-CPQbZ1BL.css +1 -0
  49. package/dist/web/assets/index-CiRJZI4z.js +386 -0
  50. package/dist/web/assets/index-ZE5lOeJp.js +386 -0
  51. package/dist/web/index.html +2 -2
  52. package/package.json +1 -1
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Night Watch PR Resolver Cron Runner (project-agnostic)
5
+ # Usage: night-watch-pr-resolver-cron.sh /path/to/project
6
+ #
7
+ # NOTE: This script expects environment variables to be set by the caller.
8
+ # The Node.js CLI will inject config values via environment variables.
9
+ # Required env vars (with defaults shown):
10
+ # NW_PR_RESOLVER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
11
+ # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
+ # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
13
+ # NW_PR_RESOLVER_MAX_PRS_PER_RUN=0 - Max PRs to process per run (0 = unlimited)
14
+ # NW_PR_RESOLVER_PER_PR_TIMEOUT=600 - Per-PR AI timeout in seconds
15
+ # NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION=1 - Set to 1 to use AI for conflict resolution
16
+ # NW_PR_RESOLVER_AI_REVIEW_RESOLUTION=0 - Set to 1 to also address review comments
17
+ # NW_PR_RESOLVER_READY_LABEL=ready-to-merge - Label to add when PR is conflict-free
18
+ # NW_PR_RESOLVER_BRANCH_PATTERNS= - Comma-separated branch prefixes to filter (empty = all)
19
+
20
+ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
21
+ PROJECT_NAME=$(basename "${PROJECT_DIR}")
22
+ LOG_DIR="${PROJECT_DIR}/logs"
23
+ LOG_FILE="${LOG_DIR}/pr-resolver.log"
24
+ MAX_RUNTIME="${NW_PR_RESOLVER_MAX_RUNTIME:-3600}" # 1 hour
25
+ MAX_LOG_SIZE="524288" # 512 KB
26
+ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
27
+ PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
28
+ MAX_PRS_PER_RUN="${NW_PR_RESOLVER_MAX_PRS_PER_RUN:-0}"
29
+ PER_PR_TIMEOUT="${NW_PR_RESOLVER_PER_PR_TIMEOUT:-600}"
30
+ AI_CONFLICT_RESOLUTION="${NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION:-1}"
31
+ AI_REVIEW_RESOLUTION="${NW_PR_RESOLVER_AI_REVIEW_RESOLUTION:-0}"
32
+ READY_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
33
+ BRANCH_PATTERNS_RAW="${NW_PR_RESOLVER_BRANCH_PATTERNS:-}"
34
+ SCRIPT_START_TIME=$(date +%s)
35
+
36
+ # Normalize numeric settings to safe ranges
37
+ if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
38
+ MAX_PRS_PER_RUN="0"
39
+ fi
40
+ if ! [[ "${PER_PR_TIMEOUT}" =~ ^[0-9]+$ ]]; then
41
+ PER_PR_TIMEOUT="600"
42
+ fi
43
+ if [ "${MAX_PRS_PER_RUN}" -gt 100 ]; then
44
+ MAX_PRS_PER_RUN="100"
45
+ fi
46
+ if [ "${PER_PR_TIMEOUT}" -gt 3600 ]; then
47
+ PER_PR_TIMEOUT="3600"
48
+ fi
49
+
50
+ mkdir -p "${LOG_DIR}"
51
+
52
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
53
+ # shellcheck source=night-watch-helpers.sh
54
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
55
+
56
+ # Ensure provider CLI is on PATH (nvm, fnm, volta, common bin dirs)
57
+ if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
58
+ echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
59
+ exit 127
60
+ fi
61
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
62
+ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
63
+ # NOTE: Lock file path must match resolverLockPath() in src/utils/status-data.ts
64
+ LOCK_FILE="/tmp/night-watch-pr-resolver-${PROJECT_RUNTIME_KEY}.lock"
65
+ SCRIPT_TYPE="pr-resolver"
66
+
67
+ emit_result() {
68
+ local status="${1:?status required}"
69
+ local details="${2:-}"
70
+ if [ -n "${details}" ]; then
71
+ echo "NIGHT_WATCH_RESULT:${status}|${details}"
72
+ else
73
+ echo "NIGHT_WATCH_RESULT:${status}"
74
+ fi
75
+ }
76
+
77
+ # ── Global Job Queue Gate ────────────────────────────────────────────────────
78
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
79
+ if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
80
+ if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
81
+ arm_global_queue_cleanup
82
+ else
83
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
84
+ fi
85
+ fi
86
+ # ──────────────────────────────────────────────────────────────────────────────
87
+
88
+ # PR discovery: returns JSON array of open PRs with required fields
89
+ discover_open_prs() {
90
+ gh pr list --state open \
91
+ --json number,title,headRefName,mergeable,isDraft,labels \
92
+ 2>/dev/null || echo "[]"
93
+ }
94
+
95
+ # Check if a branch matches any configured branch prefix patterns.
96
+ # Returns 0 (match/pass) or 1 (no match, skip PR).
97
+ matches_branch_patterns() {
98
+ local branch="${1}"
99
+ if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
100
+ return 0 # No filter configured = match all
101
+ fi
102
+ IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
103
+ for pattern in "${patterns[@]}"; do
104
+ pattern="${pattern# }" # trim leading space
105
+ if [[ "${branch}" == ${pattern}* ]]; then
106
+ return 0
107
+ fi
108
+ done
109
+ return 1
110
+ }
111
+
112
+ # Process a single PR: resolve conflicts and/or review comments, then label.
113
+ # Echoes "ready" if the PR ends up conflict-free, "conflicted" otherwise.
114
+ # Returns 0 on success, 1 on unrecoverable failure.
115
+ process_pr() {
116
+ local pr_number="${1:?pr_number required}"
117
+ local pr_branch="${2:?pr_branch required}"
118
+ local pr_title="${3:-}"
119
+ local worktree_dir="/tmp/nw-resolver-pr${pr_number}-$$"
120
+
121
+ log "INFO: Processing PR #${pr_number}: ${pr_title}" "branch=${pr_branch}"
122
+
123
+ # Inner cleanup for worktree created during this PR's processing
124
+ cleanup_pr_worktree() {
125
+ if git -C "${PROJECT_DIR}" worktree list --porcelain 2>/dev/null \
126
+ | grep -qF "worktree ${worktree_dir}"; then
127
+ git -C "${PROJECT_DIR}" worktree remove --force "${worktree_dir}" 2>/dev/null || true
128
+ fi
129
+ rm -rf "${worktree_dir}" 2>/dev/null || true
130
+ }
131
+
132
+ # ── Determine default branch ─────────────────────────────────────────────
133
+ local default_branch
134
+ default_branch="${NW_DEFAULT_BRANCH:-}"
135
+ if [ -z "${default_branch}" ]; then
136
+ default_branch=$(detect_default_branch "${PROJECT_DIR}")
137
+ fi
138
+
139
+ # ── Check current mergeable status ──────────────────────────────────────
140
+ local mergeable
141
+ mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
142
+
143
+ if [ "${mergeable}" = "CONFLICTING" ]; then
144
+ log "INFO: PR #${pr_number} has conflicts, attempting resolution" "branch=${pr_branch}"
145
+
146
+ # Fetch the PR branch so we have an up-to-date ref
147
+ git -C "${PROJECT_DIR}" fetch --quiet origin "${pr_branch}" 2>/dev/null || true
148
+
149
+ # Create an isolated worktree on the PR branch
150
+ if ! prepare_branch_worktree "${PROJECT_DIR}" "${worktree_dir}" "${pr_branch}" "${default_branch}" "${LOG_FILE}"; then
151
+ log "WARN: Failed to create worktree for PR #${pr_number}" "branch=${pr_branch}"
152
+ cleanup_pr_worktree
153
+ return 1
154
+ fi
155
+
156
+ local rebase_success=0
157
+
158
+ # Attempt a clean rebase first (no AI needed if it auto-resolves)
159
+ if git -C "${worktree_dir}" rebase "origin/${default_branch}" --quiet 2>/dev/null; then
160
+ rebase_success=1
161
+ log "INFO: PR #${pr_number} rebased cleanly (no conflicts)" "branch=${pr_branch}"
162
+ else
163
+ # Clean up the failed rebase state
164
+ git -C "${worktree_dir}" rebase --abort 2>/dev/null || true
165
+
166
+ if [ "${AI_CONFLICT_RESOLUTION}" = "1" ]; then
167
+ log "INFO: Invoking AI to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
168
+
169
+ local ai_prompt
170
+ ai_prompt="You are working in a git repository at ${worktree_dir}. \
171
+ Branch '${pr_branch}' has merge conflicts with '${default_branch}'. \
172
+ Please resolve the merge conflicts by: \
173
+ 1) Running: git rebase origin/${default_branch} \
174
+ 2) Resolving any conflict markers in the affected files \
175
+ 3) Staging resolved files with: git add <files> \
176
+ 4) Continuing the rebase with: git rebase --continue \
177
+ 5) Finally pushing with: git push --force-with-lease origin ${pr_branch} \
178
+ Work exclusively in the directory: ${worktree_dir}"
179
+
180
+ local -a cmd_parts
181
+ mapfile -d '' -t cmd_parts < <(build_provider_cmd "${worktree_dir}" "${ai_prompt}")
182
+
183
+ if timeout "${PER_PR_TIMEOUT}" "${cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
184
+ rebase_success=1
185
+ log "INFO: AI resolved conflicts for PR #${pr_number}" "branch=${pr_branch}"
186
+ else
187
+ log "WARN: AI failed to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
188
+ cleanup_pr_worktree
189
+ return 1
190
+ fi
191
+ else
192
+ log "WARN: Skipping PR #${pr_number} — conflicts exist and AI resolution is disabled" "branch=${pr_branch}"
193
+ cleanup_pr_worktree
194
+ return 1
195
+ fi
196
+ fi
197
+
198
+ if [ "${rebase_success}" = "1" ]; then
199
+ # Safety: never force-push to the default branch
200
+ if [ "${pr_branch}" = "${default_branch}" ]; then
201
+ log "WARN: Refusing to force-push to default branch ${default_branch} for PR #${pr_number}"
202
+ cleanup_pr_worktree
203
+ return 1
204
+ fi
205
+ # Push the rebased branch (AI may have already pushed; --force-with-lease is idempotent)
206
+ git -C "${worktree_dir}" push --force-with-lease origin "${pr_branch}" >> "${LOG_FILE}" 2>&1 || {
207
+ log "WARN: Push after rebase failed for PR #${pr_number}" "branch=${pr_branch}"
208
+ }
209
+ fi
210
+ fi
211
+
212
+ # ── Secondary: AI review comment resolution (opt-in) ────────────────────
213
+ if [ "${AI_REVIEW_RESOLUTION}" = "1" ]; then
214
+ local unresolved_count
215
+ unresolved_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
216
+ --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
217
+
218
+ if [ "${unresolved_count}" -gt "0" ]; then
219
+ log "INFO: PR #${pr_number} has ${unresolved_count} change request(s), invoking AI" "branch=${pr_branch}"
220
+
221
+ local review_workdir="${worktree_dir}"
222
+ if [ ! -d "${review_workdir}" ]; then
223
+ review_workdir="${PROJECT_DIR}"
224
+ fi
225
+
226
+ local review_prompt
227
+ review_prompt="You are working in the git repository at ${review_workdir}. \
228
+ PR #${pr_number} on branch '${pr_branch}' has unresolved review comments requesting changes. \
229
+ Please: \
230
+ 1) Run 'gh pr view ${pr_number} --comments' to read the review comments \
231
+ 2) Implement the requested changes \
232
+ 3) Commit the changes with a descriptive message \
233
+ 4) Push with: git push origin ${pr_branch} \
234
+ Work in the directory: ${review_workdir}"
235
+
236
+ local -a review_cmd_parts
237
+ mapfile -d '' -t review_cmd_parts < <(build_provider_cmd "${review_workdir}" "${review_prompt}")
238
+
239
+ if timeout "${PER_PR_TIMEOUT}" "${review_cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
240
+ log "INFO: AI addressed review comments for PR #${pr_number}" "branch=${pr_branch}"
241
+ else
242
+ log "WARN: AI failed to address review comments for PR #${pr_number}" "branch=${pr_branch}"
243
+ fi
244
+ fi
245
+ fi
246
+
247
+ # ── Re-check mergeable status after processing ──────────────────────────
248
+ # Brief wait for GitHub to propagate the push and recompute mergeability
249
+ sleep 3
250
+ local final_mergeable
251
+ final_mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
252
+
253
+ # ── Labeling ─────────────────────────────────────────────────────────────
254
+ local result
255
+ if [ "${final_mergeable}" != "CONFLICTING" ]; then
256
+ # Ensure the ready label exists in the repo (idempotent)
257
+ gh label create "${READY_LABEL}" \
258
+ --color "0075ca" \
259
+ --description "PR is conflict-free and ready to merge" \
260
+ 2>/dev/null || true
261
+ gh pr edit "${pr_number}" --add-label "${READY_LABEL}" 2>/dev/null || true
262
+ log "INFO: PR #${pr_number} marked as '${READY_LABEL}'" "branch=${pr_branch}"
263
+ result="ready"
264
+ else
265
+ gh pr edit "${pr_number}" --remove-label "${READY_LABEL}" 2>/dev/null || true
266
+ log "WARN: PR #${pr_number} still has conflicts after processing" "branch=${pr_branch}"
267
+ result="conflicted"
268
+ fi
269
+
270
+ cleanup_pr_worktree
271
+ echo "${result}"
272
+ }
273
+
274
+ # ── Validate provider ────────────────────────────────────────────────────────
275
+ if ! validate_provider "${PROVIDER_CMD}"; then
276
+ echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
277
+ exit 1
278
+ fi
279
+
280
+ rotate_log
281
+ log_separator
282
+ log "RUN-START: pr-resolver invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
283
+ log "CONFIG: max_runtime=${MAX_RUNTIME}s max_prs=${MAX_PRS_PER_RUN} per_pr_timeout=${PER_PR_TIMEOUT}s ai_conflict=${AI_CONFLICT_RESOLUTION} ai_review=${AI_REVIEW_RESOLUTION} ready_label=${READY_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
284
+
285
+ if ! acquire_lock "${LOCK_FILE}"; then
286
+ emit_result "skip_locked"
287
+ exit 0
288
+ fi
289
+
290
+ cd "${PROJECT_DIR}"
291
+
292
+ # ── Dry-run mode ────────────────────────────────────────────────────────────
293
+ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
294
+ echo "=== Dry Run: PR Resolver ==="
295
+ echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
296
+ echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
297
+ echo "Max PRs Per Run: ${MAX_PRS_PER_RUN}"
298
+ echo "Per-PR Timeout: ${PER_PR_TIMEOUT}s"
299
+ echo "AI Conflict Resolution: ${AI_CONFLICT_RESOLUTION}"
300
+ echo "AI Review Resolution: ${AI_REVIEW_RESOLUTION}"
301
+ echo "Ready Label: ${READY_LABEL}"
302
+ echo "Max Runtime: ${MAX_RUNTIME}s"
303
+ log "INFO: Dry run mode — exiting without processing"
304
+ emit_result "skip_dry_run"
305
+ exit 0
306
+ fi
307
+
308
+ send_telegram_status_message "Night Watch PR Resolver: started" "Project: ${PROJECT_NAME}
309
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
310
+ Branch patterns: ${BRANCH_PATTERNS_RAW:-all}
311
+ Action: scanning open PRs for merge conflicts."
312
+
313
+ # ── Discover open PRs ────────────────────────────────────────────────────────
314
+ pr_json=$(discover_open_prs)
315
+
316
+ if [ -z "${pr_json}" ] || [ "${pr_json}" = "[]" ]; then
317
+ log "SKIP: No open PRs found"
318
+ send_telegram_status_message "Night Watch PR Resolver: nothing to do" "Project: ${PROJECT_NAME}
319
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
320
+ Result: no open PRs found."
321
+ emit_result "skip_no_open_prs"
322
+ exit 0
323
+ fi
324
+
325
+ pr_count=$(printf '%s' "${pr_json}" | jq 'length' 2>/dev/null || echo "0")
326
+ log "INFO: Found ${pr_count} open PR(s) to evaluate"
327
+
328
+ # ── Main processing loop ─────────────────────────────────────────────────────
329
+ processed=0
330
+ conflicts_resolved=0
331
+ reviews_addressed=0
332
+ prs_ready=0
333
+ prs_failed=0
334
+
335
+ while IFS= read -r pr_line; do
336
+ [ -z "${pr_line}" ] && continue
337
+
338
+ pr_number=$(printf '%s' "${pr_line}" | jq -r '.number')
339
+ pr_branch=$(printf '%s' "${pr_line}" | jq -r '.headRefName')
340
+ pr_title=$(printf '%s' "${pr_line}" | jq -r '.title')
341
+ is_draft=$(printf '%s' "${pr_line}" | jq -r '.isDraft')
342
+ labels=$(printf '%s' "${pr_line}" | jq -r '[.labels[].name] | join(",")')
343
+
344
+ [ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
345
+
346
+ # Skip draft PRs
347
+ if [ "${is_draft}" = "true" ]; then
348
+ log "INFO: Skipping draft PR #${pr_number}" "branch=${pr_branch}"
349
+ continue
350
+ fi
351
+
352
+ # Skip PRs labelled skip-resolver
353
+ if [[ "${labels}" == *"skip-resolver"* ]]; then
354
+ log "INFO: Skipping PR #${pr_number} (skip-resolver label)" "branch=${pr_branch}"
355
+ continue
356
+ fi
357
+
358
+ # Apply branch pattern filter
359
+ if ! matches_branch_patterns "${pr_branch}"; then
360
+ log "DEBUG: Skipping PR #${pr_number} — branch '${pr_branch}' does not match patterns" "patterns=${BRANCH_PATTERNS_RAW}"
361
+ continue
362
+ fi
363
+
364
+ # Enforce max PRs per run
365
+ if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${processed}" -ge "${MAX_PRS_PER_RUN}" ]; then
366
+ log "INFO: Reached max PRs per run (${MAX_PRS_PER_RUN}), stopping"
367
+ break
368
+ fi
369
+
370
+ # Enforce global timeout
371
+ elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
372
+ if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
373
+ log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
374
+ break
375
+ fi
376
+
377
+ processed=$(( processed + 1 ))
378
+
379
+ result=""
380
+ if result=$(process_pr "${pr_number}" "${pr_branch}" "${pr_title}" 2>&1); then
381
+ # process_pr echoes "ready" or "conflicted" on the last line; extract it
382
+ last_line=$(printf '%s' "${result}" | tail -1)
383
+ if [ "${last_line}" = "ready" ]; then
384
+ prs_ready=$(( prs_ready + 1 ))
385
+ conflicts_resolved=$(( conflicts_resolved + 1 ))
386
+ fi
387
+ else
388
+ prs_failed=$(( prs_failed + 1 ))
389
+ fi
390
+
391
+ done < <(printf '%s' "${pr_json}" | jq -c '.[]')
392
+
393
+ log "RUN-END: pr-resolver complete processed=${processed} conflicts_resolved=${conflicts_resolved} prs_ready=${prs_ready} prs_failed=${prs_failed}"
394
+
395
+ send_telegram_status_message "Night Watch PR Resolver: completed" "Project: ${PROJECT_NAME}
396
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
397
+ PRs processed: ${processed}
398
+ Conflicts resolved: ${conflicts_resolved}
399
+ PRs marked '${READY_LABEL}': ${prs_ready}
400
+ PRs failed: ${prs_failed}"
401
+
402
+ emit_result "success" "prs_processed=${processed}|conflicts_resolved=${conflicts_resolved}|reviews_addressed=${reviews_addressed}|prs_ready=${prs_ready}|prs_failed=${prs_failed}"
@@ -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}"
@@ -105,7 +101,9 @@ extract_review_score_from_text() {
105
101
  # ── Global Job Queue Gate ────────────────────────────────────────────────────
106
102
  # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
107
103
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
108
- if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
104
+ if [ "${NW_QUEUE_INHERITED_SLOT:-0}" = "1" ]; then
105
+ :
106
+ elif [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
109
107
  arm_global_queue_cleanup
110
108
  else
111
109
  claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
@@ -550,7 +548,7 @@ fi
550
548
  rotate_log
551
549
  log_separator
552
550
  log "RUN-START: reviewer invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} worker=${WORKER_MODE} target_pr=${TARGET_PR:-all} parallel=${PARALLEL_ENABLED}"
553
- 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}"
554
552
 
555
553
  if ! acquire_lock "${LOCK_FILE}"; then
556
554
  emit_result "skip_locked"
@@ -664,7 +662,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
664
662
  } | awk '!seen[$0]++'
665
663
  )
666
664
  LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
667
- if [ -n "${LATEST_SCORE}" ] && [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
665
+ if [ -z "${LATEST_SCORE}" ]; then
666
+ log "INFO: PR #${pr_number} (${pr_branch}) has no review score yet — needs initial review"
667
+ NEEDS_WORK=1
668
+ PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
669
+ elif [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
668
670
  log "INFO: PR #${pr_number} (${pr_branch}) has review score ${LATEST_SCORE}/100 (threshold: ${MIN_REVIEW_SCORE})"
669
671
  NEEDS_WORK=1
670
672
  PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
@@ -675,62 +677,7 @@ done < <(
675
677
  )
676
678
 
677
679
  if [ "${NEEDS_WORK}" -eq 0 ]; then
678
- log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE} (or no score yet)"
679
-
680
- # ── Auto-merge eligible PRs ───────────────────────────────
681
- if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
682
- AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
683
- AUTO_MERGED_COUNT=0
684
-
685
- log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
686
-
687
- while IFS=$'\t' read -r pr_number pr_branch; do
688
- [ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
689
- printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
690
-
691
- # Check CI status - must have ALL checks passing (not just "no failures")
692
- # gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
693
- if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
694
- log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
695
- continue
696
- fi
697
-
698
- # Check review score
699
- PR_COMMENTS=$(
700
- {
701
- gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
702
- if [ -n "${REPO}" ]; then
703
- gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
704
- fi
705
- } | awk '!seen[$0]++'
706
- )
707
- PR_SCORE=$(extract_review_score_from_text "${PR_COMMENTS}")
708
-
709
- # Skip PRs without a score or with score below threshold
710
- [ -z "${PR_SCORE}" ] && continue
711
- [ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
712
-
713
- # PR is merge-ready
714
- log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
715
-
716
- # Dry-run mode: show what would be merged
717
- if [ "${NW_DRY_RUN:-0}" = "1" ]; then
718
- log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
719
- continue
720
- fi
721
-
722
- if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
723
- log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
724
- AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
725
- else
726
- log "WARN: Auto-merge failed for PR #${pr_number}"
727
- fi
728
- done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
729
-
730
- if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
731
- log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
732
- fi
733
- fi
680
+ log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}"
734
681
 
735
682
  if [ "${WORKER_MODE}" != "1" ]; then
736
683
  send_telegram_status_message "🔍 Night Watch Reviewer: nothing to do" "Project: ${PROJECT_NAME}
@@ -780,10 +727,6 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
780
727
  echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
781
728
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
782
729
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
783
- echo "Auto-merge: ${AUTO_MERGE}"
784
- if [ "${AUTO_MERGE}" = "1" ]; then
785
- echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
786
- fi
787
730
  echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
788
731
  echo "Open PRs needing work:${PRS_NEEDING_WORK}"
789
732
  echo "Default Branch: ${DEFAULT_BRANCH}"
@@ -814,6 +757,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
814
757
  NW_TARGET_PR="${pr_number}" \
815
758
  NW_REVIEWER_WORKER_MODE="1" \
816
759
  NW_REVIEWER_PARALLEL="0" \
760
+ NW_QUEUE_INHERITED_SLOT="1" \
817
761
  bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}" > "${worker_output}" 2>&1
818
762
  ) &
819
763
 
@@ -922,7 +866,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
922
866
  cleanup_reviewer_worktrees
923
867
 
924
868
  emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
925
- exit 0
869
+ exit "${EXIT_CODE}"
926
870
  fi
927
871
 
928
872
  REVIEW_RUN_TOKEN="${PROJECT_RUNTIME_KEY}-$$"
@@ -940,10 +884,6 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
940
884
  echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
941
885
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
942
886
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
943
- echo "Auto-merge: ${AUTO_MERGE}"
944
- if [ "${AUTO_MERGE}" = "1" ]; then
945
- echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
946
- fi
947
887
  echo "Max Retries: ${REVIEWER_MAX_RETRIES}"
948
888
  echo "Retry Delay: ${REVIEWER_RETRY_DELAY}s"
949
889
  echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
@@ -1015,8 +955,16 @@ if [ -n "${TARGET_PR}" ]; then
1015
955
  fi
1016
956
  if [ -n "${TARGET_SCORE}" ]; then
1017
957
  TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
958
+ TARGET_SCOPE_PROMPT+=$'- action: fix\n'
959
+ # Inject the latest review comment body for the fix prompt
960
+ REVIEW_BODY=$(gh api "repos/$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)/issues/${TARGET_PR}/comments" --jq '[.[] | select(.body | test("Overall Score|Score:.*[0-9]+/100"))] | last | .body // ""' 2>/dev/null || echo "")
961
+ if [ -n "${REVIEW_BODY}" ]; then
962
+ TRUNCATED_REVIEW=$(printf '%s' "${REVIEW_BODY}" | head -c 6000)
963
+ TARGET_SCOPE_PROMPT+=$'\n## Latest Review Feedback\n'"${TRUNCATED_REVIEW}"$'\n'
964
+ fi
1018
965
  else
1019
966
  TARGET_SCOPE_PROMPT+=$'- latest review score: not found\n'
967
+ TARGET_SCOPE_PROMPT+=$'- action: review\n'
1020
968
  fi
1021
969
  fi
1022
970
 
@@ -1201,7 +1149,8 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1201
1149
  fi
1202
1150
  continue
1203
1151
  fi
1204
- log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; failing run."
1152
+ log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; labeling needs-human-review and failing run."
1153
+ gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
1205
1154
  EXIT_CODE=1
1206
1155
  break
1207
1156
  fi
@@ -1241,78 +1190,10 @@ if [ "${EXIT_CODE}" -eq 0 ] && [ -n "${TARGET_PR}" ] && [ -n "${PR_BRANCH_HEAD_B
1241
1190
  fi
1242
1191
  fi
1243
1192
 
1244
- # ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
1245
- # After the reviewer completes, check for PRs that are merge-ready and queue them
1246
- # for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
1247
1193
  AUTO_MERGED_PRS=""
1248
1194
  AUTO_MERGE_FAILED_PRS=""
1249
1195
 
1250
- if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
1251
- log "AUTO-MERGE: Checking for merge-ready PRs..."
1252
-
1253
- while IFS=$'\t' read -r pr_number pr_branch; do
1254
- if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
1255
- continue
1256
- fi
1257
-
1258
- if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
1259
- continue
1260
- fi
1261
-
1262
- # Only process PRs matching branch patterns
1263
- if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
1264
- continue
1265
- fi
1266
-
1267
- # Check CI status - must have ALL checks passing (not just "no failures")
1268
- # gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
1269
- if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
1270
- log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
1271
- continue
1272
- fi
1273
-
1274
- # Check review score - must have score >= threshold
1275
- ALL_COMMENTS=$(
1276
- {
1277
- gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
1278
- if [ -n "${REPO}" ]; then
1279
- gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
1280
- fi
1281
- } | awk '!seen[$0]++'
1282
- )
1283
- LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
1284
-
1285
- # Skip PRs without a score
1286
- if [ -z "${LATEST_SCORE}" ]; then
1287
- continue
1288
- fi
1289
-
1290
- # Skip PRs with score below threshold
1291
- if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
1292
- continue
1293
- fi
1294
-
1295
- # PR is merge-ready - queue for auto-merge
1296
- log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
1297
-
1298
- if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
1299
- log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
1300
- if [ -z "${AUTO_MERGED_PRS}" ]; then
1301
- AUTO_MERGED_PRS="#${pr_number}"
1302
- else
1303
- AUTO_MERGED_PRS="${AUTO_MERGED_PRS},#${pr_number}"
1304
- fi
1305
- else
1306
- log "WARN: Auto-merge failed for PR #${pr_number}"
1307
- if [ -z "${AUTO_MERGE_FAILED_PRS}" ]; then
1308
- AUTO_MERGE_FAILED_PRS="#${pr_number}"
1309
- else
1310
- AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS},#${pr_number}"
1311
- fi
1312
- fi
1313
- done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
1314
- fi
1315
-
1316
1196
  REVIEWER_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
1317
1197
  log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${REVIEWER_TOTAL_ELAPSED}s prs=${PRS_NEEDING_WORK_CSV:-none} attempts=${ATTEMPTS_MADE}"
1318
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}"
1199
+ exit "${EXIT_CODE}"