@jonit-dev/night-watch-cli 1.7.51 → 1.7.52

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.
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
12
  # NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
13
13
  # NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
14
- # NW_QA_ARTIFACTS=both - Artifact mode (both, tests, report)
14
+ # NW_QA_ARTIFACTS=both - Artifact mode (screenshot, video, both)
15
15
  # NW_QA_AUTO_INSTALL_PLAYWRIGHT=1 - Auto-install Playwright browsers
16
16
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
17
17
 
@@ -22,6 +22,7 @@ LOG_FILE="${LOG_DIR}/night-watch-qa.log"
22
22
  MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
23
23
  MAX_LOG_SIZE="524288" # 512 KB
24
24
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
+ PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
25
26
  BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
26
27
  SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
27
28
  QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
@@ -65,6 +66,147 @@ decode_base64_value() {
65
66
  fi
66
67
  }
67
68
 
69
+ append_csv() {
70
+ local current="${1:-}"
71
+ local incoming="${2:-}"
72
+ if [ -z "${incoming}" ]; then
73
+ printf "%s" "${current}"
74
+ return 0
75
+ fi
76
+ if [ -z "${current}" ]; then
77
+ printf "%s" "${incoming}"
78
+ else
79
+ printf "%s,%s" "${current}" "${incoming}"
80
+ fi
81
+ }
82
+
83
+ csv_or_none() {
84
+ local value="${1:-}"
85
+ if [ -n "${value}" ]; then
86
+ printf "%s" "${value}"
87
+ else
88
+ printf "none"
89
+ fi
90
+ }
91
+
92
+ describe_qa_artifacts() {
93
+ local mode="${1:-both}"
94
+ case "${mode}" in
95
+ screenshot)
96
+ printf "screenshots only"
97
+ ;;
98
+ video)
99
+ printf "videos only"
100
+ ;;
101
+ both)
102
+ printf "screenshots + videos"
103
+ ;;
104
+ *)
105
+ printf "custom (%s)" "${mode}"
106
+ ;;
107
+ esac
108
+ }
109
+
110
+ normalize_qa_screenshot_url() {
111
+ local raw_url="${1:-}"
112
+ if [ -z "${raw_url}" ]; then
113
+ return 0
114
+ fi
115
+
116
+ if printf '%s' "${raw_url}" | grep -Eq '^https?://'; then
117
+ printf '%s' "${raw_url}"
118
+ return 0
119
+ fi
120
+
121
+ if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^\.\./blob/'; then
122
+ printf 'https://github.com/%s/%s' "${REPO}" "${raw_url#../}"
123
+ return 0
124
+ fi
125
+
126
+ if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^blob/'; then
127
+ printf 'https://github.com/%s/%s' "${REPO}" "${raw_url}"
128
+ return 0
129
+ fi
130
+
131
+ printf '%s' "${raw_url}"
132
+ }
133
+
134
+ extract_url_host() {
135
+ local raw_url="${1:-}"
136
+ if [ -z "${raw_url}" ]; then
137
+ return 0
138
+ fi
139
+ printf '%s' "${raw_url}" | sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://##; s#/.*$##'
140
+ }
141
+
142
+ resolve_claude_model_hint() {
143
+ local sonnet="${ANTHROPIC_DEFAULT_SONNET_MODEL:-}"
144
+ local opus="${ANTHROPIC_DEFAULT_OPUS_MODEL:-}"
145
+ local native_model="${NW_CLAUDE_MODEL_ID:-}"
146
+
147
+ if [ -n "${sonnet}" ] && [ -n "${opus}" ]; then
148
+ if [ "${sonnet}" = "${opus}" ]; then
149
+ printf "%s" "${sonnet}"
150
+ else
151
+ printf "sonnet=%s, opus=%s" "${sonnet}" "${opus}"
152
+ fi
153
+ return 0
154
+ fi
155
+ if [ -n "${sonnet}" ]; then
156
+ printf "%s" "${sonnet}"
157
+ return 0
158
+ fi
159
+ if [ -n "${opus}" ]; then
160
+ printf "%s" "${opus}"
161
+ return 0
162
+ fi
163
+ if [ -n "${native_model}" ]; then
164
+ printf "%s" "${native_model}"
165
+ return 0
166
+ fi
167
+ printf "default"
168
+ }
169
+
170
+ resolve_provider_model_display() {
171
+ local provider_cmd="${1:?provider command required}"
172
+ local provider_label="${2:-}"
173
+ local label_trimmed=""
174
+ local model_hint=""
175
+ local endpoint_host=""
176
+ local details=""
177
+
178
+ label_trimmed=$(printf '%s' "${provider_label}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
179
+
180
+ case "${provider_cmd}" in
181
+ claude)
182
+ model_hint=$(resolve_claude_model_hint)
183
+ endpoint_host=$(extract_url_host "${ANTHROPIC_BASE_URL:-}")
184
+ details="${model_hint}"
185
+ if [ -n "${endpoint_host}" ]; then
186
+ details="${details} via ${endpoint_host}"
187
+ fi
188
+ if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Claude" ] && [ "${label_trimmed}" != "Claude (proxy)" ]; then
189
+ details="${label_trimmed}; ${details}"
190
+ fi
191
+ printf "%s (%s)" "${provider_cmd}" "${details}"
192
+ ;;
193
+ codex)
194
+ if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Codex" ]; then
195
+ printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
196
+ else
197
+ printf "%s" "${provider_cmd}"
198
+ fi
199
+ ;;
200
+ *)
201
+ if [ -n "${label_trimmed}" ]; then
202
+ printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
203
+ else
204
+ printf "%s" "${provider_cmd}"
205
+ fi
206
+ ;;
207
+ esac
208
+ }
209
+
68
210
  get_pr_comment_bodies_base64() {
69
211
  local pr_number="${1:?PR number required}"
70
212
  gh pr view "${pr_number}" --json comments --jq '.comments[]?.body | @base64' 2>/dev/null || true
@@ -90,6 +232,61 @@ get_latest_qa_comment_body() {
90
232
  printf "%s" "${latest}"
91
233
  }
92
234
 
235
+ get_qa_screenshot_links() {
236
+ local pr_number="${1:?PR number required}"
237
+ local qa_comment=""
238
+
239
+ qa_comment=$(get_latest_qa_comment_body "${pr_number}")
240
+ if [ -z "${qa_comment}" ]; then
241
+ return 0
242
+ fi
243
+
244
+ printf '%s' "${qa_comment}" \
245
+ | { grep -Eo '!\[[^]]*\]\(([^)]*qa-artifacts/[^)]*)\)' || true; } \
246
+ | sed -E 's/^!\[[^]]*\]\(([^)]*)\)$/\1/' \
247
+ | while IFS= read -r raw_url; do
248
+ [ -z "${raw_url}" ] && continue
249
+ normalize_qa_screenshot_url "${raw_url}"
250
+ printf '\n'
251
+ done \
252
+ | awk 'NF && !seen[$0]++'
253
+ }
254
+
255
+ classify_qa_comment_outcome() {
256
+ local pr_number="${1:?PR number required}"
257
+ local qa_comment=""
258
+ local status_lines=""
259
+
260
+ qa_comment=$(get_latest_qa_comment_body "${pr_number}")
261
+ if [ -z "${qa_comment}" ]; then
262
+ printf "unclassified"
263
+ return 0
264
+ fi
265
+
266
+ if printf '%s' "${qa_comment}" | grep -Eqi 'QA: No tests needed for this PR|No tests needed'; then
267
+ printf "no_tests_needed"
268
+ return 0
269
+ fi
270
+
271
+ status_lines=$(printf '%s' "${qa_comment}" | grep -E '^- \*\*Status\*\*:' || true)
272
+ if [ -z "${status_lines}" ]; then
273
+ printf "unclassified"
274
+ return 0
275
+ fi
276
+
277
+ if printf '%s' "${status_lines}" | grep -Eqi 'failing|failed|error|timed out|timeout'; then
278
+ printf "issues_found"
279
+ return 0
280
+ fi
281
+
282
+ if printf '%s' "${status_lines}" | grep -Eqi 'all passing'; then
283
+ printf "passing"
284
+ return 0
285
+ fi
286
+
287
+ printf "unclassified"
288
+ }
289
+
93
290
  pr_has_qa_generated_files() {
94
291
  local pr_number="${1:?PR number required}"
95
292
  gh pr view "${pr_number}" --json files --jq '.files[]?.path' 2>/dev/null \
@@ -152,8 +349,13 @@ fi
152
349
 
153
350
  cd "${PROJECT_DIR}"
154
351
 
352
+ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
353
+ QA_ARTIFACTS_DESC=$(describe_qa_artifacts "${QA_ARTIFACTS}")
354
+
155
355
  send_telegram_status_message "🧪 Night Watch QA: started" "Project: ${PROJECT_NAME}
156
- Provider: ${PROVIDER_CMD}
356
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
357
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
358
+ Branch patterns: ${BRANCH_PATTERNS_RAW}
157
359
  Scanning open PRs for QA candidates."
158
360
 
159
361
  # Convert comma-separated branch prefixes into a regex that matches branch starts.
@@ -185,7 +387,9 @@ OPEN_PRS=$(
185
387
  if [ "${OPEN_PRS}" -eq 0 ]; then
186
388
  log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
187
389
  send_telegram_status_message "🧪 Night Watch QA: no matching PRs" "Project: ${PROJECT_NAME}
188
- Branch patterns: ${BRANCH_PATTERNS_RAW}"
390
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
391
+ Branch patterns: ${BRANCH_PATTERNS_RAW}
392
+ Result: 0 open PRs matched."
189
393
  emit_result "skip_no_open_prs"
190
394
  exit 0
191
395
  fi
@@ -242,7 +446,9 @@ done < <(
242
446
  if [ "${QA_NEEDED}" -eq 0 ]; then
243
447
  log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
244
448
  send_telegram_status_message "🧪 Night Watch QA: nothing to do" "Project: ${PROJECT_NAME}
245
- All matching PRs already have QA results."
449
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
450
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
451
+ Result: All matching PRs already have QA results."
246
452
  emit_result "skip_all_qa_done"
247
453
  exit 0
248
454
  fi
@@ -265,10 +471,10 @@ cleanup_worktrees "${PROJECT_DIR}"
265
471
  # Dry-run mode: print diagnostics and exit
266
472
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
267
473
  echo "=== Dry Run: QA Runner ==="
268
- echo "Provider: ${PROVIDER_CMD}"
474
+ echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
269
475
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
270
476
  echo "Skip Label: ${SKIP_LABEL}"
271
- echo "QA Artifacts: ${QA_ARTIFACTS}"
477
+ echo "QA Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})"
272
478
  echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
273
479
  echo "Open PRs needing QA:${PRS_NEEDING_QA}"
274
480
  echo "Default Branch: ${DEFAULT_BRANCH}"
@@ -278,17 +484,31 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
278
484
  fi
279
485
 
280
486
  EXIT_CODE=0
487
+ PROCESSED_PRS_CSV=""
488
+ PASSING_PRS_CSV=""
489
+ ISSUES_FOUND_PRS_CSV=""
490
+ NO_TESTS_PRS_CSV=""
491
+ UNCLASSIFIED_PRS_CSV=""
492
+ FAILED_AUTOMATION_PRS_CSV=""
493
+ FAILED_PR=""
494
+ FAILED_REASON="unknown"
495
+ QA_SCREENSHOT_SUMMARY=""
281
496
 
282
497
  # Process each PR that needs QA
283
498
  for pr_ref in ${PRS_NEEDING_QA}; do
284
499
  pr_num="${pr_ref#\#}"
500
+ PROCESSED_PRS_CSV=$(append_csv "${PROCESSED_PRS_CSV}" "#${pr_num}")
285
501
  send_telegram_status_message "🧪 Night Watch QA: processing PR #${pr_num}" "Project: ${PROJECT_NAME}
286
- Provider: ${PROVIDER_CMD}
287
- Artifacts: ${QA_ARTIFACTS}"
502
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
503
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
504
+ Action: generating QA tests and evidence."
288
505
 
289
506
  cleanup_worktrees "${PROJECT_DIR}"
290
507
  if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
291
508
  log "FAIL: Unable to create isolated QA worktree ${QA_WORKTREE_DIR} for PR #${pr_num}"
509
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
510
+ FAILED_PR="#${pr_num}"
511
+ FAILED_REASON="worktree_setup_failed"
292
512
  EXIT_CODE=1
293
513
  break
294
514
  fi
@@ -296,6 +516,9 @@ Artifacts: ${QA_ARTIFACTS}"
296
516
  log "QA: Checking out PR #${pr_num} in worktree"
297
517
  if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
298
518
  log "WARN: Failed to checkout PR #${pr_num}, skipping"
519
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
520
+ FAILED_PR="#${pr_num}"
521
+ FAILED_REASON="checkout_failed"
299
522
  EXIT_CODE=1
300
523
  cleanup_worktrees "${PROJECT_DIR}"
301
524
  continue
@@ -304,6 +527,9 @@ Artifacts: ${QA_ARTIFACTS}"
304
527
  QA_PROMPT_PATH=$(resolve_instruction_path_with_fallback "${QA_WORKTREE_DIR}" "qa.md" "night-watch-qa.md" || true)
305
528
  if [ -z "${QA_PROMPT_PATH}" ]; then
306
529
  log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked qa.md/night-watch-qa.md in instructions/, .claude/commands/, and bundled templates/"
530
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
531
+ FAILED_PR="#${pr_num}"
532
+ FAILED_REASON="missing_prompt"
307
533
  EXIT_CODE=1
308
534
  break
309
535
  fi
@@ -316,6 +542,10 @@ Artifacts: ${QA_ARTIFACTS}"
316
542
  QA_PROMPT_REF=$(instruction_ref_for_prompt "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}")
317
543
  log "QA: PR #${pr_num} — using prompt from ${QA_PROMPT_REF}"
318
544
 
545
+ # Inject provider attribution requirement into the QA prompt.
546
+ QA_PROVIDER_LABEL="${NW_PROVIDER_LABEL:-${PROVIDER_CMD}}"
547
+ QA_PROMPT="${QA_PROMPT}"$'\n\n'"## QA Attribution (Required)"$'\n'"At the very end of each QA result comment you post, add this footer on its own line:"$'\n'"> 🧪 QA run by ${QA_PROVIDER_LABEL}"
548
+
319
549
  LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
320
550
  PROVIDER_OK=0
321
551
  case "${PROVIDER_CMD}" in
@@ -331,9 +561,15 @@ Artifacts: ${QA_ARTIFACTS}"
331
561
  local_exit=$?
332
562
  log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
333
563
  if [ ${local_exit} -eq 124 ]; then
564
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
565
+ FAILED_PR="#${pr_num}"
566
+ FAILED_REASON="timeout"
334
567
  EXIT_CODE=124
335
568
  break
336
569
  fi
570
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
571
+ FAILED_PR="#${pr_num}"
572
+ FAILED_REASON="provider_exit_${local_exit}"
337
573
  EXIT_CODE=${local_exit}
338
574
  fi
339
575
  ;;
@@ -350,9 +586,15 @@ Artifacts: ${QA_ARTIFACTS}"
350
586
  local_exit=$?
351
587
  log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
352
588
  if [ ${local_exit} -eq 124 ]; then
589
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
590
+ FAILED_PR="#${pr_num}"
591
+ FAILED_REASON="timeout"
353
592
  EXIT_CODE=124
354
593
  break
355
594
  fi
595
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
596
+ FAILED_PR="#${pr_num}"
597
+ FAILED_REASON="provider_exit_${local_exit}"
356
598
  EXIT_CODE=${local_exit}
357
599
  fi
358
600
  ;;
@@ -365,10 +607,37 @@ Artifacts: ${QA_ARTIFACTS}"
365
607
  if [ "${PROVIDER_OK}" -eq 1 ]; then
366
608
  if provider_output_looks_invalid "${LOG_LINE_BEFORE}"; then
367
609
  log "FAIL-QA-EVIDENCE: PR #${pr_num} provider output indicates an invalid automation run"
610
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
611
+ FAILED_PR="#${pr_num}"
612
+ FAILED_REASON="invalid_provider_output"
368
613
  EXIT_CODE=1
369
614
  elif ! validate_qa_evidence "${pr_num}"; then
615
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
616
+ FAILED_PR="#${pr_num}"
617
+ FAILED_REASON="qa_evidence_validation_failed"
370
618
  EXIT_CODE=1
371
619
  else
620
+ QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
621
+ case "${QA_OUTCOME}" in
622
+ passing)
623
+ PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
624
+ ;;
625
+ issues_found)
626
+ ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
627
+ ;;
628
+ no_tests_needed)
629
+ NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
630
+ ;;
631
+ *)
632
+ UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
633
+ ;;
634
+ esac
635
+
636
+ PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
637
+ if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
638
+ QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
639
+ fi
640
+
372
641
  log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
373
642
  fi
374
643
  fi
@@ -378,34 +647,68 @@ done
378
647
 
379
648
  cleanup_worktrees "${PROJECT_DIR}"
380
649
 
650
+ FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
651
+ PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
652
+ ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
653
+ NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
654
+ UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
655
+ FAILED_AUTOMATION_PRS_SUMMARY=$(csv_or_none "${FAILED_AUTOMATION_PRS_CSV}")
656
+ FAILED_PR_SUMMARY=$(csv_or_none "${FAILED_PR}")
657
+
381
658
  if [ ${EXIT_CODE} -eq 0 ]; then
382
659
  log "DONE: QA runner completed successfully"
383
- send_telegram_status_message "🧪 Night Watch QA: completed" "Project: ${PROJECT_NAME}
384
- Processed PRs: ${PRS_NEEDING_QA_CSV}"
660
+ TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
661
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
662
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
663
+ Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
664
+ Passing tests: ${PASSING_PRS_SUMMARY}
665
+ Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
666
+ No tests needed: ${NO_TESTS_PRS_SUMMARY}
667
+ Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
668
+ if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
669
+ TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
670
+ Screenshot links:
671
+ ${QA_SCREENSHOT_SUMMARY}"
672
+ fi
673
+ send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
385
674
  if [ -n "${REPO}" ]; then
386
- emit_result "success_qa" "prs=${PRS_NEEDING_QA_CSV}|repo=${REPO}"
675
+ emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
387
676
  else
388
- emit_result "success_qa" "prs=${PRS_NEEDING_QA_CSV}"
677
+ emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
389
678
  fi
390
679
  elif [ ${EXIT_CODE} -eq 124 ]; then
391
680
  log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
392
681
  send_telegram_status_message "🧪 Night Watch QA: timeout" "Project: ${PROJECT_NAME}
682
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
393
683
  Timeout: ${MAX_RUNTIME}s
394
- Processed PRs: ${PRS_NEEDING_QA_CSV}"
684
+ Failed PR: ${FAILED_PR_SUMMARY}
685
+ Failure reason: ${FAILED_REASON}
686
+ Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
687
+ Passing tests: ${PASSING_PRS_SUMMARY}
688
+ Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
689
+ No tests needed: ${NO_TESTS_PRS_SUMMARY}
690
+ Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
395
691
  if [ -n "${REPO}" ]; then
396
- emit_result "timeout" "prs=${PRS_NEEDING_QA_CSV}|repo=${REPO}"
692
+ emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
397
693
  else
398
- emit_result "timeout" "prs=${PRS_NEEDING_QA_CSV}"
694
+ emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
399
695
  fi
400
696
  else
401
697
  log "FAIL: QA runner exited with code ${EXIT_CODE}"
402
698
  send_telegram_status_message "🧪 Night Watch QA: failed" "Project: ${PROJECT_NAME}
699
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
403
700
  Exit code: ${EXIT_CODE}
404
- Processed PRs: ${PRS_NEEDING_QA_CSV}"
701
+ Failed PR: ${FAILED_PR_SUMMARY}
702
+ Failure reason: ${FAILED_REASON}
703
+ Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
704
+ Passing tests: ${PASSING_PRS_SUMMARY}
705
+ Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
706
+ No tests needed: ${NO_TESTS_PRS_SUMMARY}
707
+ Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
405
708
  if [ -n "${REPO}" ]; then
406
- emit_result "failure" "prs=${PRS_NEEDING_QA_CSV}|repo=${REPO}"
709
+ emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
407
710
  else
408
- emit_result "failure" "prs=${PRS_NEEDING_QA_CSV}"
711
+ emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
409
712
  fi
410
713
  fi
411
714
 
@@ -22,6 +22,7 @@ LOCK_FILE=""
22
22
  MAX_RUNTIME="${NW_SLICER_MAX_RUNTIME:-600}" # 10 minutes
23
23
  MAX_LOG_SIZE="524288" # 512 KB
24
24
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
+ PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
25
26
 
26
27
  # Ensure NVM / Node / Night Watch CLI are on PATH
27
28
  export NVM_DIR="${HOME}/.nvm"
@@ -34,6 +35,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
35
  source "${SCRIPT_DIR}/night-watch-helpers.sh"
35
36
  PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
36
37
  LOCK_FILE="/tmp/night-watch-slicer-${PROJECT_RUNTIME_KEY}.lock"
38
+ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
37
39
 
38
40
  # Validate provider
39
41
  if ! validate_provider "${PROVIDER_CMD}"; then
@@ -55,14 +57,16 @@ trap cleanup_on_exit EXIT
55
57
 
56
58
  log "START: Running roadmap slicer for ${PROJECT_DIR}"
57
59
  send_telegram_status_message "📋 Night Watch Planner: started" "Project: ${PROJECT_NAME}
58
- Provider: ${PROVIDER_CMD}
59
- Planning next roadmap item into a PRD."
60
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
61
+ Roadmap path: ${NW_ROADMAP_PATH:-ROADMAP.md}
62
+ Action: planning next roadmap item into a PRD."
60
63
 
61
64
  # Dry-run mode: print diagnostics and exit
62
65
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
63
66
  echo "=== Dry Run: Roadmap Slicer ==="
64
- echo "Provider: ${PROVIDER_CMD}"
67
+ echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
65
68
  echo "Project Dir: ${PROJECT_DIR}"
69
+ echo "Roadmap Path: ${NW_ROADMAP_PATH:-ROADMAP.md}"
66
70
  echo "Timeout: ${MAX_RUNTIME}s"
67
71
  exit 0
68
72
  fi
@@ -71,6 +75,9 @@ fi
71
75
  CLI_BIN=""
72
76
  if ! CLI_BIN=$(resolve_night_watch_cli); then
73
77
  log "ERROR: Could not resolve night-watch CLI"
78
+ send_telegram_status_message "📋 Night Watch Planner: failed" "Project: ${PROJECT_NAME}
79
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
80
+ Failure reason: cli_not_found"
74
81
  exit 1
75
82
  fi
76
83
 
@@ -85,14 +92,18 @@ fi
85
92
  if [ ${EXIT_CODE} -eq 0 ]; then
86
93
  log "DONE: Slicer completed successfully"
87
94
  send_telegram_status_message "📋 Night Watch Planner: completed" "Project: ${PROJECT_NAME}
95
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
88
96
  PRD planning run finished successfully."
89
97
  elif [ ${EXIT_CODE} -eq 124 ]; then
90
98
  log "TIMEOUT: Slicer killed after ${MAX_RUNTIME}s"
91
99
  send_telegram_status_message "📋 Night Watch Planner: timeout" "Project: ${PROJECT_NAME}
100
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
92
101
  Timeout: ${MAX_RUNTIME}s"
93
102
  else
94
103
  log "FAIL: Slicer exited with code ${EXIT_CODE}"
95
104
  send_telegram_status_message "📋 Night Watch Planner: failed" "Project: ${PROJECT_NAME}
105
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
106
+ Failure reason: provider_exit_${EXIT_CODE}
96
107
  Exit code: ${EXIT_CODE}"
97
108
  fi
98
109
 
@@ -137,6 +137,16 @@ Parse the review score from the comment body. Look for patterns like:
137
137
  ```
138
138
  gh pr checks <number> --json name,state,conclusion
139
139
  ```
140
+ - First enumerate all checks/jobs from GitHub (source of truth):
141
+ ```
142
+ gh pr checks <number> --json name,state,conclusion --jq '.[] | [.name, .state, .conclusion] | @tsv'
143
+ ```
144
+ - To inspect the latest workflow run's job list in detail:
145
+ ```
146
+ RUN_ID=$(gh run list --branch <branch-name> --limit 1 --json databaseId --jq '.[0].databaseId')
147
+ gh run view "${RUN_ID}" --json jobs --jq '.jobs[] | [.name, .status, .conclusion] | @tsv'
148
+ gh run view "${RUN_ID}" --log-failed
149
+ ```
140
150
  - Read the failed job logs carefully to understand the root cause.
141
151
  - Fix checks based on their actual names and errors (for example: `typecheck`, `lint`, `test`, `build`, `verify`, `executor`, `qa`, `audit`).
142
152
  - Do not assume only a fixed set of CI job names.
@@ -201,3 +211,20 @@ Parse the review score from the comment body. Look for patterns like:
201
211
  6. When done, return to ${DEFAULT_BRANCH}: `git checkout ${DEFAULT_BRANCH}`
202
212
 
203
213
  Start now. Check for open PRs that need merge conflicts resolved, review feedback addressed, or CI failures fixed.
214
+
215
+ ---
216
+
217
+ ## Board Mode (when board provider is enabled)
218
+
219
+ When reviewing a PR that references a board issue (`Closes #N` in the body):
220
+
221
+ 1. After pushing review fixes, comment on the issue:
222
+
223
+ ```
224
+ night-watch board comment <N> --body "Review fixes pushed: <commit-sha>"
225
+ ```
226
+
227
+ 2. If review score >= threshold AND CI passes, move to Done:
228
+ ```
229
+ night-watch board move-issue <N> --column "Done"
230
+ ```
@@ -3,18 +3,43 @@
3
3
  "projectName": "",
4
4
  "defaultBranch": "",
5
5
  "provider": "claude",
6
+ "providerLabel": "",
6
7
  "executorEnabled": true,
7
8
  "reviewerEnabled": true,
9
+ "providerEnv": {},
8
10
  "prdDir": "docs/PRDs/night-watch",
9
11
  "templatesDir": ".night-watch/templates",
10
12
  "maxRuntime": 7200,
11
13
  "reviewerMaxRuntime": 3600,
12
14
  "branchPrefix": "night-watch",
13
- "branchPatterns": ["feat/", "night-watch/"],
15
+ "branchPatterns": [
16
+ "feat/",
17
+ "night-watch/"
18
+ ],
14
19
  "minReviewScore": 80,
15
20
  "maxLogSize": 524288,
16
21
  "cronSchedule": "0 0-21 * * *",
17
22
  "reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *",
23
+ "cronScheduleOffset": 0,
24
+ "maxRetries": 3,
25
+ "reviewerMaxRetries": 2,
26
+ "reviewerRetryDelay": 30,
27
+ "notifications": {
28
+ "webhooks": []
29
+ },
30
+ "prdPriority": [],
31
+ "roadmapScanner": {
32
+ "enabled": true,
33
+ "roadmapPath": "ROADMAP.md",
34
+ "autoScanInterval": 300,
35
+ "slicerSchedule": "0 */6 * * *",
36
+ "slicerMaxRuntime": 600
37
+ },
38
+ "boardProvider": {
39
+ "enabled": true,
40
+ "provider": "github"
41
+ },
42
+ "jobProviders": {},
18
43
  "autoMerge": false,
19
44
  "autoMergeMethod": "squash",
20
45
  "fallbackOnRateLimit": false,
@@ -27,5 +52,10 @@
27
52
  "artifacts": "both",
28
53
  "skipLabel": "skip-qa",
29
54
  "autoInstallPlaywright": true
55
+ },
56
+ "audit": {
57
+ "enabled": true,
58
+ "schedule": "0 3 * * *",
59
+ "maxRuntime": 1800
30
60
  }
31
61
  }
@@ -1,5 +1,36 @@
1
1
  You are the Night Watch agent. Your job is to autonomously pick up PRD tickets and implement them.
2
2
 
3
+ ## Board Mode (when `NW_BOARD_ENABLED=true` or board provider is configured)
4
+
5
+ If `NW_BOARD_ENABLED` is set to `true` in the environment, use board mode instead of filesystem scanning:
6
+
7
+ 1. **Get next task**: `night-watch board next-issue --column "Ready" --json`
8
+ - If no issues are in "Ready", STOP — nothing to do.
9
+
10
+ 2. **Claim the task**: `night-watch board move-issue <number> --column "In Progress"`
11
+
12
+ 3. **Read the spec**: The issue body IS the PRD. Parse it for phases and requirements.
13
+
14
+ 4. **Branch naming**: `night-watch/<issue-number>-<slugified-title>` (e.g., `night-watch/42-my-feature`)
15
+
16
+ 5. **Create worktree and implement** as normal (create branch, worktree, implement, test, commit).
17
+
18
+ 6. **Open PR**: Include `Closes #<issue-number>` in the PR body so the issue auto-closes when merged:
19
+
20
+ ```
21
+ gh pr create --title "feat: <short title>" --body "Closes #<number>\n\n<summary>"
22
+ ```
23
+
24
+ 7. **Move to Review**: `night-watch board move-issue <number> --column "Review"`
25
+
26
+ 8. **Comment on issue**: `night-watch board comment <number> --body "PR opened: <url>"`
27
+
28
+ 9. **Clean up** worktree and **STOP** — one task per run.
29
+
30
+ ---
31
+
32
+ ## Filesystem Mode (default, when board mode is not active)
33
+
3
34
  ## Instructions
4
35
 
5
36
  1. **Scan for PRDs**: Use `night-watch prd list --json` to get available PRDs. Each PRD is a ticket.