@jonit-dev/night-watch-cli 1.7.9 → 1.7.10

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 (57) hide show
  1. package/dist/cli.js +3 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
  4. package/dist/commands/dashboard/tab-config.js +1 -0
  5. package/dist/commands/dashboard/tab-config.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +131 -19
  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 +24 -0
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/qa.d.ts +30 -0
  14. package/dist/commands/qa.d.ts.map +1 -0
  15. package/dist/commands/qa.js +159 -0
  16. package/dist/commands/qa.js.map +1 -0
  17. package/dist/commands/review.d.ts +1 -0
  18. package/dist/commands/review.d.ts.map +1 -1
  19. package/dist/commands/review.js +10 -0
  20. package/dist/commands/review.js.map +1 -1
  21. package/dist/commands/run.d.ts.map +1 -1
  22. package/dist/commands/run.js +16 -1
  23. package/dist/commands/run.js.map +1 -1
  24. package/dist/commands/status.d.ts.map +1 -1
  25. package/dist/commands/status.js +3 -0
  26. package/dist/commands/status.js.map +1 -1
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +155 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/constants.d.ts +20 -1
  31. package/dist/constants.d.ts.map +1 -1
  32. package/dist/constants.js +40 -0
  33. package/dist/constants.js.map +1 -1
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +4 -2
  36. package/dist/server/index.js.map +1 -1
  37. package/dist/types.d.ts +43 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/utils/notify.d.ts.map +1 -1
  40. package/dist/utils/notify.js +18 -0
  41. package/dist/utils/notify.js.map +1 -1
  42. package/dist/utils/status-data.d.ts +4 -0
  43. package/dist/utils/status-data.d.ts.map +1 -1
  44. package/dist/utils/status-data.js +13 -3
  45. package/dist/utils/status-data.js.map +1 -1
  46. package/package.json +1 -1
  47. package/scripts/night-watch-cron.sh +43 -2
  48. package/scripts/night-watch-helpers.sh +25 -2
  49. package/scripts/night-watch-pr-reviewer-cron.sh +70 -0
  50. package/scripts/night-watch-qa-cron.sh +269 -0
  51. package/templates/night-watch-qa.md +157 -0
  52. package/templates/night-watch.config.json +14 -1
  53. package/web/dist/assets/index-BPW-7_1C.js +380 -0
  54. package/web/dist/assets/index-DVqjjJEO.css +1 -0
  55. package/web/dist/index.html +2 -2
  56. package/web/dist/assets/index-C64sy08d.js +0 -360
  57. package/web/dist/assets/index-DzoZeo_Y.css +0 -1
@@ -290,9 +290,14 @@ fi
290
290
  BACKOFF_BASE=300 # 5 minutes in seconds
291
291
  EXIT_CODE=0
292
292
  ATTEMPT=0
293
+ RATE_LIMIT_FALLBACK_TRIGGERED=0
293
294
 
294
295
  while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
295
296
  EXIT_CODE=0
297
+ # Capture log position before this attempt so check_rate_limited only
298
+ # scans lines written by the current invocation (not leftover 429s from
299
+ # previous runs that would cause false-positive rate-limit retries).
300
+ LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
296
301
 
297
302
  case "${PROVIDER_CMD}" in
298
303
  claude)
@@ -331,8 +336,14 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
331
336
  break
332
337
  fi
333
338
 
334
- # Check if this was a rate limit (429) error
335
- if check_rate_limited "${LOG_FILE}"; then
339
+ # Check if this was a rate limit (429) error (only in lines from this attempt)
340
+ if check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
341
+ # If fallback is enabled, skip proxy retries and switch to native Claude immediately
342
+ if [ "${NW_FALLBACK_ON_RATE_LIMIT:-}" = "true" ] && [ "${PROVIDER_CMD}" = "claude" ]; then
343
+ log "RATE-LIMITED: Proxy quota exhausted — triggering native Claude fallback"
344
+ RATE_LIMIT_FALLBACK_TRIGGERED=1
345
+ break
346
+ fi
336
347
  ATTEMPT=$((ATTEMPT + 1))
337
348
  if [ "${ATTEMPT}" -ge "${MAX_RETRIES}" ]; then
338
349
  log "RATE-LIMITED: All ${MAX_RETRIES} attempts exhausted for ${ELIGIBLE_PRD}"
@@ -349,6 +360,36 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
349
360
  fi
350
361
  done
351
362
 
363
+ # ── Native Claude fallback ────────────────────────────────────────────────────
364
+ # When the proxy returns 429 and fallbackOnRateLimit is enabled, re-run the
365
+ # same prompt with native Claude (OAuth), bypassing the proxy entirely.
366
+ if [ "${RATE_LIMIT_FALLBACK_TRIGGERED}" = "1" ]; then
367
+ FALLBACK_MODEL="${NW_CLAUDE_MODEL_ID:-claude-sonnet-4-6}"
368
+ log "RATE-LIMIT-FALLBACK: Running native Claude (${FALLBACK_MODEL})"
369
+
370
+ # Send immediate Telegram warning (fire-and-forget)
371
+ send_rate_limit_fallback_warning "${FALLBACK_MODEL}" "$(basename "${PROJECT_DIR}")"
372
+
373
+ LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
374
+
375
+ if (
376
+ cd "${WORKTREE_DIR}" && \
377
+ unset ANTHROPIC_BASE_URL ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN \
378
+ ANTHROPIC_DEFAULT_SONNET_MODEL ANTHROPIC_DEFAULT_OPUS_MODEL && \
379
+ timeout "${MAX_RUNTIME}" \
380
+ claude -p "${PROMPT}" \
381
+ --dangerously-skip-permissions \
382
+ --model "${FALLBACK_MODEL}" \
383
+ >> "${LOG_FILE}" 2>&1
384
+ ); then
385
+ EXIT_CODE=0
386
+ else
387
+ EXIT_CODE=$?
388
+ fi
389
+
390
+ log "RATE-LIMIT-FALLBACK: Native Claude exited with code ${EXIT_CODE}"
391
+ fi
392
+
352
393
  if [ ${EXIT_CODE} -eq 0 ]; then
353
394
  OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
354
395
  if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
@@ -406,11 +406,34 @@ mark_prd_done() {
406
406
 
407
407
  # ── Rate limit detection ────────────────────────────────────────────────────
408
408
 
409
- # Check if the last N lines of the log contain a 429 rate limit error.
409
+ # Check if the log contains a 429 rate limit error since a given line number.
410
+ # Usage: check_rate_limited <log_file> [start_line]
411
+ # When start_line is provided, only lines after that position are checked,
412
+ # preventing false positives from 429 errors in previous runs.
410
413
  # Returns 0 if rate limited, 1 otherwise.
411
414
  check_rate_limited() {
412
415
  local log_file="${1:?log_file required}"
413
- tail -20 "${log_file}" 2>/dev/null | grep -q "429"
416
+ local start_line="${2:-0}"
417
+ if [ "${start_line}" -gt 0 ] 2>/dev/null; then
418
+ tail -n "+$((start_line + 1))" "${log_file}" 2>/dev/null | grep -q "429"
419
+ else
420
+ tail -20 "${log_file}" 2>/dev/null | grep -q "429"
421
+ fi
422
+ }
423
+
424
+ # Send an immediate Telegram warning when the rate-limit fallback is triggered.
425
+ # Uses NW_TELEGRAM_BOT_TOKEN and NW_TELEGRAM_CHAT_ID exported by the CLI runner.
426
+ # Falls back silently when credentials are absent.
427
+ # Usage: send_rate_limit_fallback_warning <model> <project_name>
428
+ send_rate_limit_fallback_warning() {
429
+ local model="${1:-native Claude}"
430
+ local project_name="${2:-unknown}"
431
+ if [ -z "${NW_TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${NW_TELEGRAM_CHAT_ID:-}" ]; then
432
+ return 0
433
+ fi
434
+ local msg="⚠️ Rate Limit Fallback%0A%0AProject: ${project_name}%0AProxy quota exhausted — falling back to native Claude (${model})"
435
+ curl -s -X POST "https://api.telegram.org/bot${NW_TELEGRAM_BOT_TOKEN}/sendMessage" \
436
+ -d "chat_id=${NW_TELEGRAM_CHAT_ID}&text=${msg}" > /dev/null 2>&1 || true
414
437
  }
415
438
 
416
439
  # ── Board mode issue discovery ────────────────────────────────────────────────
@@ -10,6 +10,8 @@ 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
13
15
 
14
16
  PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
15
17
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
@@ -20,6 +22,8 @@ MAX_LOG_SIZE="524288" # 512 KB
20
22
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
21
23
  MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
22
24
  BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
25
+ AUTO_MERGE="${NW_AUTO_MERGE:-0}"
26
+ AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
23
27
 
24
28
  # Ensure NVM / Node / Claude are on PATH
25
29
  export NVM_DIR="${HOME}/.nvm"
@@ -156,6 +160,10 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
156
160
  echo "Provider: ${PROVIDER_CMD}"
157
161
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
158
162
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
163
+ echo "Auto-merge: ${AUTO_MERGE}"
164
+ if [ "${AUTO_MERGE}" = "1" ]; then
165
+ echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
166
+ fi
159
167
  echo "Open PRs needing work:${PRS_NEEDING_WORK}"
160
168
  echo "Default Branch: ${DEFAULT_BRANCH}"
161
169
  echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
@@ -204,6 +212,68 @@ esac
204
212
 
205
213
  cleanup_worktrees "${PROJECT_DIR}"
206
214
 
215
+ # ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
216
+ # After the reviewer completes, check for PRs that are merge-ready and queue them
217
+ # for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
218
+ AUTO_MERGED_PRS=""
219
+ AUTO_MERGE_FAILED_PRS=""
220
+
221
+ if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
222
+ log "AUTO-MERGE: Checking for merge-ready PRs..."
223
+
224
+ while IFS=$'\t' read -r pr_number pr_branch; do
225
+ if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
226
+ continue
227
+ fi
228
+
229
+ # Only process PRs matching branch patterns
230
+ if ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
231
+ continue
232
+ fi
233
+
234
+ # Check CI status - must have no failures
235
+ FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
236
+ if [ "${FAILED_CHECKS}" -gt 0 ]; then
237
+ continue
238
+ fi
239
+
240
+ # Check review score - must have score >= threshold
241
+ ALL_COMMENTS=$(
242
+ {
243
+ gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
244
+ if [ -n "${REPO}" ]; then
245
+ gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
246
+ fi
247
+ } | sort -u
248
+ )
249
+ LATEST_SCORE=$(echo "${ALL_COMMENTS}" \
250
+ | grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
251
+ | tail -1 \
252
+ | grep -oP '\d+(?=/100)' || echo "")
253
+
254
+ # Skip PRs without a score
255
+ if [ -z "${LATEST_SCORE}" ]; then
256
+ continue
257
+ fi
258
+
259
+ # Skip PRs with score below threshold
260
+ if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
261
+ continue
262
+ fi
263
+
264
+ # PR is merge-ready - queue for auto-merge
265
+ log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
266
+
267
+ if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
268
+ log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
269
+ AUTO_MERGED_PRS="${AUTO_MERGED_PRS} #${pr_number}"
270
+ else
271
+ log "WARN: Auto-merge failed for PR #${pr_number}"
272
+ AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS} #${pr_number}"
273
+ fi
274
+ done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
275
+ fi
276
+
207
277
  if [ ${EXIT_CODE} -eq 0 ]; then
208
278
  log "DONE: PR reviewer completed successfully"
209
279
  emit_result "success_reviewed" "prs=${PRS_NEEDING_WORK_CSV}"
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Night Watch QA Cron Runner (project-agnostic)
5
+ # Usage: night-watch-qa-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_QA_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
11
+ # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
+ # NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
13
+ # NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
14
+ # NW_QA_ARTIFACTS=both - Artifact mode (both, tests, report)
15
+ # NW_QA_AUTO_INSTALL_PLAYWRIGHT=1 - Auto-install Playwright browsers
16
+ # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
17
+
18
+ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
19
+ PROJECT_NAME=$(basename "${PROJECT_DIR}")
20
+ LOG_DIR="${PROJECT_DIR}/logs"
21
+ LOG_FILE="${LOG_DIR}/night-watch-qa.log"
22
+ MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
23
+ MAX_LOG_SIZE="524288" # 512 KB
24
+ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
+ BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
26
+ SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
27
+ QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
28
+ QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
29
+
30
+ # Ensure NVM / Node / Claude are on PATH
31
+ export NVM_DIR="${HOME}/.nvm"
32
+ [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
33
+
34
+ # NOTE: Environment variables should be set by the caller (Node.js CLI).
35
+ # The .env.night-watch sourcing has been removed - config is now injected via env vars.
36
+
37
+ mkdir -p "${LOG_DIR}"
38
+
39
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+ # shellcheck source=night-watch-helpers.sh
41
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
42
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
43
+ # NOTE: Lock file path must match qaLockPath() in src/utils/status-data.ts
44
+ LOCK_FILE="/tmp/night-watch-qa-${PROJECT_RUNTIME_KEY}.lock"
45
+
46
+ emit_result() {
47
+ local status="${1:?status required}"
48
+ local details="${2:-}"
49
+ if [ -n "${details}" ]; then
50
+ echo "NIGHT_WATCH_RESULT:${status}|${details}"
51
+ else
52
+ echo "NIGHT_WATCH_RESULT:${status}"
53
+ fi
54
+ }
55
+
56
+ # Validate provider
57
+ if ! validate_provider "${PROVIDER_CMD}"; then
58
+ echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
59
+ exit 1
60
+ fi
61
+
62
+ rotate_log
63
+
64
+ if ! acquire_lock "${LOCK_FILE}"; then
65
+ emit_result "skip_locked"
66
+ exit 0
67
+ fi
68
+
69
+ cd "${PROJECT_DIR}"
70
+
71
+ # Convert comma-separated branch prefixes into a regex that matches branch starts.
72
+ BRANCH_REGEX=""
73
+ IFS=',' read -r -a BRANCH_PATTERNS <<< "${BRANCH_PATTERNS_RAW}"
74
+ for pattern in "${BRANCH_PATTERNS[@]}"; do
75
+ trimmed_pattern=$(printf '%s' "${pattern}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
76
+ if [ -n "${trimmed_pattern}" ]; then
77
+ BRANCH_REGEX="${BRANCH_REGEX}${BRANCH_REGEX:+|}^${trimmed_pattern}"
78
+ fi
79
+ done
80
+
81
+ if [ -z "${BRANCH_REGEX}" ]; then
82
+ BRANCH_REGEX='^(feat/|night-watch/)'
83
+ fi
84
+
85
+ # List open PRs with their details for filtering
86
+ PR_JSON=$(gh pr list --state open --json number,headRefName,title,labels 2>/dev/null || echo "[]")
87
+
88
+ # Count PRs matching branch patterns
89
+ OPEN_PRS=$(
90
+ echo "${PR_JSON}" \
91
+ | jq -r '.[].headRefName' 2>/dev/null \
92
+ | { grep -E "${BRANCH_REGEX}" || true; } \
93
+ | wc -l \
94
+ | tr -d '[:space:]'
95
+ )
96
+
97
+ if [ "${OPEN_PRS}" -eq 0 ]; then
98
+ log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
99
+ emit_result "skip_no_open_prs"
100
+ exit 0
101
+ fi
102
+
103
+ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
104
+
105
+ # Collect PRs that need QA
106
+ PRS_NEEDING_QA=""
107
+ QA_NEEDED=0
108
+
109
+ while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
110
+ if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
111
+ continue
112
+ fi
113
+
114
+ # Filter by branch pattern
115
+ if ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
116
+ continue
117
+ fi
118
+
119
+ # Skip PRs with the skip label
120
+ if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
121
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
122
+ continue
123
+ fi
124
+
125
+ # Skip PRs with [skip-qa] in their title
126
+ if echo "${pr_title}" | grep -qi '\[skip-qa\]'; then
127
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) has [skip-qa] in title"
128
+ continue
129
+ fi
130
+
131
+ # Skip PRs that already have a QA comment (idempotency)
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
+ if echo "${ALL_COMMENTS}" | grep -q '<!-- night-watch-qa-marker -->'; then
141
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) already has QA comment"
142
+ continue
143
+ fi
144
+
145
+ QA_NEEDED=1
146
+ PRS_NEEDING_QA="${PRS_NEEDING_QA} #${pr_number}"
147
+ done < <(
148
+ echo "${PR_JSON}" \
149
+ | jq -r '.[] | [.number, .headRefName, .title, ([.labels[].name] | join(","))] | @tsv' 2>/dev/null || true
150
+ )
151
+
152
+ if [ "${QA_NEEDED}" -eq 0 ]; then
153
+ log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
154
+ emit_result "skip_all_qa_done"
155
+ exit 0
156
+ fi
157
+
158
+ PRS_NEEDING_QA=$(echo "${PRS_NEEDING_QA}" \
159
+ | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]][[:space:]]*/ /g' -e 's/[[:space:]]*$//')
160
+ PRS_NEEDING_QA_CSV="${PRS_NEEDING_QA// /,}"
161
+
162
+ if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
163
+ DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
164
+ else
165
+ DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
166
+ fi
167
+ QA_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-qa-runner"
168
+
169
+ log "START: Found PR(s) needing QA:${PRS_NEEDING_QA}"
170
+
171
+ cleanup_worktrees "${PROJECT_DIR}"
172
+
173
+ # Dry-run mode: print diagnostics and exit
174
+ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
175
+ echo "=== Dry Run: QA Runner ==="
176
+ echo "Provider: ${PROVIDER_CMD}"
177
+ echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
178
+ echo "Skip Label: ${SKIP_LABEL}"
179
+ echo "QA Artifacts: ${QA_ARTIFACTS}"
180
+ echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
181
+ echo "Open PRs needing QA:${PRS_NEEDING_QA}"
182
+ echo "Default Branch: ${DEFAULT_BRANCH}"
183
+ echo "QA Worktree: ${QA_WORKTREE_DIR}"
184
+ echo "Timeout: ${MAX_RUNTIME}s"
185
+ exit 0
186
+ fi
187
+
188
+ EXIT_CODE=0
189
+
190
+ # Process each PR that needs QA
191
+ for pr_ref in ${PRS_NEEDING_QA}; do
192
+ pr_num="${pr_ref#\#}"
193
+
194
+ cleanup_worktrees "${PROJECT_DIR}"
195
+ if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
196
+ log "FAIL: Unable to create isolated QA worktree ${QA_WORKTREE_DIR} for PR #${pr_num}"
197
+ EXIT_CODE=1
198
+ break
199
+ fi
200
+
201
+ log "QA: Checking out PR #${pr_num} in worktree"
202
+ if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
203
+ log "WARN: Failed to checkout PR #${pr_num}, skipping"
204
+ EXIT_CODE=1
205
+ cleanup_worktrees "${PROJECT_DIR}"
206
+ continue
207
+ fi
208
+
209
+ case "${PROVIDER_CMD}" in
210
+ claude)
211
+ if (
212
+ cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
213
+ claude -p "/night-watch-qa" \
214
+ --dangerously-skip-permissions \
215
+ >> "${LOG_FILE}" 2>&1
216
+ ); then
217
+ log "QA: PR #${pr_num} — provider completed successfully"
218
+ else
219
+ local_exit=$?
220
+ log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
221
+ if [ ${local_exit} -eq 124 ]; then
222
+ EXIT_CODE=124
223
+ break
224
+ fi
225
+ EXIT_CODE=${local_exit}
226
+ fi
227
+ ;;
228
+ codex)
229
+ if (
230
+ cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
231
+ codex --quiet \
232
+ --yolo \
233
+ --prompt "$(cat "${QA_WORKTREE_DIR}/.claude/commands/night-watch-qa.md")" \
234
+ >> "${LOG_FILE}" 2>&1
235
+ ); then
236
+ log "QA: PR #${pr_num} — provider completed successfully"
237
+ else
238
+ local_exit=$?
239
+ log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
240
+ if [ ${local_exit} -eq 124 ]; then
241
+ EXIT_CODE=124
242
+ break
243
+ fi
244
+ EXIT_CODE=${local_exit}
245
+ fi
246
+ ;;
247
+ *)
248
+ log "ERROR: Unknown provider: ${PROVIDER_CMD}"
249
+ exit 1
250
+ ;;
251
+ esac
252
+
253
+ cleanup_worktrees "${PROJECT_DIR}"
254
+ done
255
+
256
+ cleanup_worktrees "${PROJECT_DIR}"
257
+
258
+ if [ ${EXIT_CODE} -eq 0 ]; then
259
+ log "DONE: QA runner completed successfully"
260
+ emit_result "success_qa" "prs=${PRS_NEEDING_QA_CSV}"
261
+ elif [ ${EXIT_CODE} -eq 124 ]; then
262
+ log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
263
+ emit_result "timeout" "prs=${PRS_NEEDING_QA_CSV}"
264
+ else
265
+ log "FAIL: QA runner exited with code ${EXIT_CODE}"
266
+ emit_result "failure" "prs=${PRS_NEEDING_QA_CSV}"
267
+ fi
268
+
269
+ exit "${EXIT_CODE}"
@@ -0,0 +1,157 @@
1
+ You are the Night Watch QA agent. Your job is to analyze open PRs, generate appropriate tests for the changes, run them, and report results with visual evidence.
2
+
3
+ ## Context
4
+
5
+ You are running inside a worktree checked out to a PR branch. Your goal is to:
6
+ 1. Analyze what changed in this PR compared to the base branch
7
+ 2. Determine if the changes are UI-related, API-related, or both
8
+ 3. Generate appropriate tests (Playwright e2e for UI, integration tests for API)
9
+ 4. Run the tests and capture artifacts (screenshots, videos for UI)
10
+ 5. Commit the tests and artifacts, then comment on the PR with results
11
+
12
+ ## Environment Variables Available
13
+ - `NW_QA_ARTIFACTS` — What to capture: "screenshot", "video", or "both" (default: "both")
14
+ - `NW_QA_AUTO_INSTALL_PLAYWRIGHT` — "1" to auto-install Playwright if missing
15
+
16
+ ## Instructions
17
+
18
+ ### Step 1: Analyze the PR diff
19
+
20
+ Get the diff against the base branch:
21
+ ```
22
+ git diff origin/${DEFAULT_BRANCH}...HEAD --name-only
23
+ git diff origin/${DEFAULT_BRANCH}...HEAD --stat
24
+ ```
25
+
26
+ Read the changed files to understand what the PR introduces.
27
+
28
+ ### Step 2: Classify and Decide
29
+
30
+ Based on the diff, determine:
31
+ - **UI changes**: New/modified components, pages, layouts, styles, client-side logic
32
+ - **API changes**: New/modified endpoints, controllers, services, middleware, database queries
33
+ - **Both**: PR touches both UI and API code
34
+ - **No tests needed**: Trivial changes (docs, config, comments only) — in this case, post a comment saying "QA: No tests needed for this PR" and stop
35
+
36
+ ### Step 3: Prepare Test Infrastructure
37
+
38
+ **For UI tests (Playwright):**
39
+ 1. Check if Playwright is available: `npx playwright --version`
40
+ 2. If not available and `NW_QA_AUTO_INSTALL_PLAYWRIGHT=1`:
41
+ - Run `npm install -D @playwright/test` (or yarn/pnpm equivalent based on lockfile)
42
+ - Run `npx playwright install chromium`
43
+ 3. If not available and auto-install is disabled, skip UI tests and note in the report
44
+
45
+ **For API tests:**
46
+ - Use the project's existing test framework (vitest, jest, or mocha — detect from package.json)
47
+ - If no test framework exists, use vitest
48
+
49
+ ### Step 4: Generate Tests
50
+
51
+ **UI Tests (Playwright):**
52
+ - Create test files in `tests/e2e/qa/` (or the project's existing e2e directory)
53
+ - Test the specific feature/page changed in the PR
54
+ - Configure Playwright for artifacts based on `NW_QA_ARTIFACTS`:
55
+ - `"screenshot"`: `screenshot: 'on'` only
56
+ - `"video"`: `video: { mode: 'on', size: { width: 1280, height: 720 } }` only
57
+ - `"both"`: Both screenshot and video enabled
58
+ - Name test files with a `qa-` prefix: `qa-<feature-name>.spec.ts`
59
+ - Include at minimum: navigation to the feature, interaction with key elements, visual assertions
60
+
61
+ **API Tests:**
62
+ - Create test files in `tests/integration/qa/` (or the project's existing test directory)
63
+ - Test the specific endpoints changed in the PR
64
+ - Include: happy path, error cases, validation checks
65
+ - Name test files with a `qa-` prefix: `qa-<endpoint-name>.test.ts`
66
+
67
+ ### Step 5: Run Tests
68
+
69
+ **UI Tests:**
70
+ ```bash
71
+ npx playwright test tests/e2e/qa/ --reporter=list
72
+ ```
73
+
74
+ **API Tests:**
75
+ ```bash
76
+ npx vitest run tests/integration/qa/ --reporter=verbose
77
+ # (or equivalent for the project's test runner)
78
+ ```
79
+
80
+ Capture the test output for the report.
81
+
82
+ ### Step 6: Collect Artifacts
83
+
84
+ Move Playwright artifacts (screenshots, videos) to `qa-artifacts/` in the project root:
85
+ ```bash
86
+ mkdir -p qa-artifacts
87
+ # Copy from playwright-report/ or test-results/ to qa-artifacts/
88
+ ```
89
+
90
+ ### Step 7: Commit and Push
91
+
92
+ ```bash
93
+ git add tests/e2e/qa/ tests/integration/qa/ qa-artifacts/ || true
94
+ git add -A tests/*/qa/ qa-artifacts/ || true
95
+ git commit -m "test(qa): add automated QA tests for PR changes
96
+
97
+ - Generated by Night Watch QA agent
98
+ - <UI tests: X passing, Y failing | No UI tests>
99
+ - <API tests: X passing, Y failing | No API tests>
100
+ - Artifacts: <screenshots, videos | screenshots | videos | none>
101
+
102
+ Co-Authored-By: Claude <noreply@anthropic.com>"
103
+ git push origin HEAD
104
+ ```
105
+
106
+ ### Step 8: Comment on PR
107
+
108
+ Post a comment on the PR with results. Use the `<!-- night-watch-qa-marker -->` HTML comment for idempotency detection.
109
+
110
+ ```bash
111
+ gh pr comment <PR_NUMBER> --body "<!-- night-watch-qa-marker -->
112
+ ## Night Watch QA Report
113
+
114
+ ### Changes Classification
115
+ - **Type**: <UI | API | UI + API>
116
+ - **Files changed**: <count>
117
+
118
+ ### Test Results
119
+
120
+ <If UI tests>
121
+ #### UI Tests (Playwright)
122
+ - **Status**: <All passing | X of Y failing>
123
+ - **Tests**: <count> test(s) in <count> file(s)
124
+
125
+ <If screenshots captured>
126
+ #### Screenshots
127
+ <For each screenshot>
128
+ ![<description>](../blob/<branch>/qa-artifacts/<filename>)
129
+ </For>
130
+ </If>
131
+
132
+ <If video captured>
133
+ #### Video Recording
134
+ Video artifact committed to \`qa-artifacts/\` — view in the PR's file changes.
135
+ </If>
136
+ </If>
137
+
138
+ <If API tests>
139
+ #### API Tests
140
+ - **Status**: <All passing | X of Y failing>
141
+ - **Tests**: <count> test(s) in <count> file(s)
142
+ </If>
143
+
144
+ <If no tests generated>
145
+ **QA: No tests needed for this PR** — changes are trivial (docs, config, comments).
146
+ </If>
147
+
148
+ ---
149
+ *Night Watch QA Agent*"
150
+ ```
151
+
152
+ ### Important Rules
153
+ - Process each PR **once** per run. Do NOT loop or retry after pushing.
154
+ - Do NOT modify existing project tests — only add new files in `qa/` subdirectories.
155
+ - If tests fail, still commit and report — the failures are useful information.
156
+ - Keep test files self-contained and independent from each other.
157
+ - Follow the project's existing code style and conventions (check CLAUDE.md, package.json scripts, tsconfig).
@@ -13,5 +13,18 @@
13
13
  "minReviewScore": 80,
14
14
  "maxLogSize": 524288,
15
15
  "cronSchedule": "0 0-21 * * *",
16
- "reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *"
16
+ "reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *",
17
+ "autoMerge": false,
18
+ "autoMergeMethod": "squash",
19
+ "fallbackOnRateLimit": false,
20
+ "claudeModel": "sonnet",
21
+ "qa": {
22
+ "enabled": true,
23
+ "schedule": "30 1,7,13,19 * * *",
24
+ "maxRuntime": 3600,
25
+ "branchPatterns": [],
26
+ "artifacts": "both",
27
+ "skipLabel": "skip-qa",
28
+ "autoInstallPlaywright": true
29
+ }
17
30
  }