@jonit-dev/night-watch-cli 1.7.50 โ†’ 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.
Files changed (36) hide show
  1. package/dist/cli.js +393 -229
  2. package/dist/commands/audit.d.ts.map +1 -1
  3. package/dist/commands/audit.js +6 -24
  4. package/dist/commands/audit.js.map +1 -1
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +20 -23
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/qa.d.ts.map +1 -1
  9. package/dist/commands/qa.js +16 -4
  10. package/dist/commands/qa.js.map +1 -1
  11. package/dist/commands/review.d.ts.map +1 -1
  12. package/dist/commands/review.js +6 -4
  13. package/dist/commands/review.js.map +1 -1
  14. package/dist/commands/shared/env-builder.d.ts +5 -0
  15. package/dist/commands/shared/env-builder.d.ts.map +1 -1
  16. package/dist/commands/shared/env-builder.js +32 -0
  17. package/dist/commands/shared/env-builder.js.map +1 -1
  18. package/dist/commands/slice.d.ts +8 -0
  19. package/dist/commands/slice.d.ts.map +1 -1
  20. package/dist/commands/slice.js +90 -2
  21. package/dist/commands/slice.js.map +1 -1
  22. package/dist/scripts/night-watch-audit-cron.sh +17 -4
  23. package/dist/scripts/night-watch-cron.sh +19 -5
  24. package/dist/scripts/night-watch-helpers.sh +137 -0
  25. package/dist/scripts/night-watch-pr-reviewer-cron.sh +268 -5
  26. package/dist/scripts/night-watch-qa-cron.sh +427 -22
  27. package/dist/scripts/night-watch-slicer-cron.sh +14 -3
  28. package/dist/templates/audit.md +87 -0
  29. package/dist/templates/executor.md +67 -0
  30. package/dist/templates/night-watch-pr-reviewer.md +33 -0
  31. package/dist/templates/night-watch.config.json +31 -1
  32. package/dist/templates/night-watch.md +31 -0
  33. package/dist/templates/pr-reviewer.md +203 -0
  34. package/dist/templates/qa.md +157 -0
  35. package/dist/templates/slicer.md +234 -0
  36. package/package.json +1 -1
@@ -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}"
@@ -53,6 +54,286 @@ emit_result() {
53
54
  fi
54
55
  }
55
56
 
57
+ decode_base64_value() {
58
+ local value="${1:-}"
59
+ if [ -z "${value}" ]; then
60
+ return 0
61
+ fi
62
+ if printf '%s' "${value}" | base64 --decode >/dev/null 2>&1; then
63
+ printf '%s' "${value}" | base64 --decode
64
+ else
65
+ printf '%s' "${value}" | base64 -d 2>/dev/null || true
66
+ fi
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
+
210
+ get_pr_comment_bodies_base64() {
211
+ local pr_number="${1:?PR number required}"
212
+ gh pr view "${pr_number}" --json comments --jq '.comments[]?.body | @base64' 2>/dev/null || true
213
+ if [ -n "${REPO:-}" ]; then
214
+ gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body | @base64' 2>/dev/null || true
215
+ fi
216
+ }
217
+
218
+ get_latest_qa_comment_body() {
219
+ local pr_number="${1:?PR number required}"
220
+ local latest=""
221
+ local encoded=""
222
+ local decoded=""
223
+
224
+ while IFS= read -r encoded; do
225
+ [ -z "${encoded}" ] && continue
226
+ decoded=$(decode_base64_value "${encoded}")
227
+ if printf '%s' "${decoded}" | grep -q '<!-- night-watch-qa-marker -->'; then
228
+ latest="${decoded}"
229
+ fi
230
+ done < <(get_pr_comment_bodies_base64 "${pr_number}")
231
+
232
+ printf "%s" "${latest}"
233
+ }
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
+
290
+ pr_has_qa_generated_files() {
291
+ local pr_number="${1:?PR number required}"
292
+ gh pr view "${pr_number}" --json files --jq '.files[]?.path' 2>/dev/null \
293
+ | grep -Eq '^(qa-artifacts/|tests/.*/qa/)'
294
+ }
295
+
296
+ provider_output_looks_invalid() {
297
+ local from_line="${1:-0}"
298
+ if [ ! -f "${LOG_FILE}" ]; then
299
+ return 1
300
+ fi
301
+
302
+ tail -n "+$((from_line + 1))" "${LOG_FILE}" 2>/dev/null \
303
+ | grep -Eqi 'Unknown skill:|session is in a broken state|working directory .* no longer exists|Please restart this session'
304
+ }
305
+
306
+ validate_qa_evidence() {
307
+ local pr_number="${1:?PR number required}"
308
+ local qa_comment=""
309
+
310
+ qa_comment=$(get_latest_qa_comment_body "${pr_number}")
311
+ if [ -z "${qa_comment}" ]; then
312
+ log "FAIL-QA-EVIDENCE: PR #${pr_number} has no QA marker comment (<!-- night-watch-qa-marker -->)"
313
+ return 1
314
+ fi
315
+
316
+ if printf '%s' "${qa_comment}" | grep -Eqi 'QA: No tests needed for this PR|No tests needed'; then
317
+ return 0
318
+ fi
319
+
320
+ if ! pr_has_qa_generated_files "${pr_number}"; then
321
+ log "FAIL-QA-EVIDENCE: PR #${pr_number} has QA marker comment but no qa-artifacts/ or tests/*/qa/ files"
322
+ return 1
323
+ fi
324
+
325
+ if [ "${QA_ARTIFACTS}" = "screenshot" ] || [ "${QA_ARTIFACTS}" = "both" ]; then
326
+ if printf '%s' "${qa_comment}" | grep -q '#### UI Tests (Playwright)'; then
327
+ if ! printf '%s' "${qa_comment}" | grep -Eq '!\[[^]]*\]\([^)]*qa-artifacts/[^)]*\)'; then
328
+ log "FAIL-QA-EVIDENCE: PR #${pr_number} reports UI tests but comment lacks screenshot links to qa-artifacts/"
329
+ return 1
330
+ fi
331
+ fi
332
+ fi
333
+
334
+ return 0
335
+ }
336
+
56
337
  # Validate provider
57
338
  if ! validate_provider "${PROVIDER_CMD}"; then
58
339
  echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
@@ -68,8 +349,13 @@ fi
68
349
 
69
350
  cd "${PROJECT_DIR}"
70
351
 
352
+ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
353
+ QA_ARTIFACTS_DESC=$(describe_qa_artifacts "${QA_ARTIFACTS}")
354
+
71
355
  send_telegram_status_message "๐Ÿงช Night Watch QA: started" "Project: ${PROJECT_NAME}
72
- Provider: ${PROVIDER_CMD}
356
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
357
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
358
+ Branch patterns: ${BRANCH_PATTERNS_RAW}
73
359
  Scanning open PRs for QA candidates."
74
360
 
75
361
  # Convert comma-separated branch prefixes into a regex that matches branch starts.
@@ -101,7 +387,9 @@ OPEN_PRS=$(
101
387
  if [ "${OPEN_PRS}" -eq 0 ]; then
102
388
  log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
103
389
  send_telegram_status_message "๐Ÿงช Night Watch QA: no matching PRs" "Project: ${PROJECT_NAME}
104
- Branch patterns: ${BRANCH_PATTERNS_RAW}"
390
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
391
+ Branch patterns: ${BRANCH_PATTERNS_RAW}
392
+ Result: 0 open PRs matched."
105
393
  emit_result "skip_no_open_prs"
106
394
  exit 0
107
395
  fi
@@ -158,7 +446,9 @@ done < <(
158
446
  if [ "${QA_NEEDED}" -eq 0 ]; then
159
447
  log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
160
448
  send_telegram_status_message "๐Ÿงช Night Watch QA: nothing to do" "Project: ${PROJECT_NAME}
161
- 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."
162
452
  emit_result "skip_all_qa_done"
163
453
  exit 0
164
454
  fi
@@ -181,10 +471,10 @@ cleanup_worktrees "${PROJECT_DIR}"
181
471
  # Dry-run mode: print diagnostics and exit
182
472
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
183
473
  echo "=== Dry Run: QA Runner ==="
184
- echo "Provider: ${PROVIDER_CMD}"
474
+ echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
185
475
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
186
476
  echo "Skip Label: ${SKIP_LABEL}"
187
- echo "QA Artifacts: ${QA_ARTIFACTS}"
477
+ echo "QA Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})"
188
478
  echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
189
479
  echo "Open PRs needing QA:${PRS_NEEDING_QA}"
190
480
  echo "Default Branch: ${DEFAULT_BRANCH}"
@@ -194,17 +484,31 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
194
484
  fi
195
485
 
196
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=""
197
496
 
198
497
  # Process each PR that needs QA
199
498
  for pr_ref in ${PRS_NEEDING_QA}; do
200
499
  pr_num="${pr_ref#\#}"
500
+ PROCESSED_PRS_CSV=$(append_csv "${PROCESSED_PRS_CSV}" "#${pr_num}")
201
501
  send_telegram_status_message "๐Ÿงช Night Watch QA: processing PR #${pr_num}" "Project: ${PROJECT_NAME}
202
- Provider: ${PROVIDER_CMD}
203
- Artifacts: ${QA_ARTIFACTS}"
502
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
503
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
504
+ Action: generating QA tests and evidence."
204
505
 
205
506
  cleanup_worktrees "${PROJECT_DIR}"
206
507
  if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
207
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"
208
512
  EXIT_CODE=1
209
513
  break
210
514
  fi
@@ -212,21 +516,38 @@ Artifacts: ${QA_ARTIFACTS}"
212
516
  log "QA: Checking out PR #${pr_num} in worktree"
213
517
  if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
214
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"
215
522
  EXIT_CODE=1
216
523
  cleanup_worktrees "${PROJECT_DIR}"
217
524
  continue
218
525
  fi
219
526
 
220
- QA_PROMPT_PATH=$(resolve_instruction_path "${QA_WORKTREE_DIR}" "night-watch-qa.md" || true)
527
+ QA_PROMPT_PATH=$(resolve_instruction_path_with_fallback "${QA_WORKTREE_DIR}" "qa.md" "night-watch-qa.md" || true)
221
528
  if [ -z "${QA_PROMPT_PATH}" ]; then
222
- log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked instructions/, .claude/commands/, and bundled templates/"
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"
223
533
  EXIT_CODE=1
224
534
  break
225
535
  fi
536
+ QA_PROMPT_BUNDLED_NAME="qa.md"
537
+ if [[ "${QA_PROMPT_PATH}" == */night-watch-qa.md ]]; then
538
+ QA_PROMPT_BUNDLED_NAME="night-watch-qa.md"
539
+ fi
540
+ QA_PROMPT_PATH=$(prefer_bundled_prompt_if_legacy_command "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}" "${QA_PROMPT_BUNDLED_NAME}")
226
541
  QA_PROMPT=$(cat "${QA_PROMPT_PATH}")
227
542
  QA_PROMPT_REF=$(instruction_ref_for_prompt "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}")
228
543
  log "QA: PR #${pr_num} โ€” using prompt from ${QA_PROMPT_REF}"
229
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
+
549
+ LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
550
+ PROVIDER_OK=0
230
551
  case "${PROVIDER_CMD}" in
231
552
  claude)
232
553
  if (
@@ -235,14 +556,20 @@ Artifacts: ${QA_ARTIFACTS}"
235
556
  --dangerously-skip-permissions \
236
557
  >> "${LOG_FILE}" 2>&1
237
558
  ); then
238
- log "QA: PR #${pr_num} โ€” provider completed successfully"
559
+ PROVIDER_OK=1
239
560
  else
240
561
  local_exit=$?
241
562
  log "QA: PR #${pr_num} โ€” provider exited with code ${local_exit}"
242
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"
243
567
  EXIT_CODE=124
244
568
  break
245
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}"
246
573
  EXIT_CODE=${local_exit}
247
574
  fi
248
575
  ;;
@@ -254,14 +581,20 @@ Artifacts: ${QA_ARTIFACTS}"
254
581
  --prompt "${QA_PROMPT}" \
255
582
  >> "${LOG_FILE}" 2>&1
256
583
  ); then
257
- log "QA: PR #${pr_num} โ€” provider completed successfully"
584
+ PROVIDER_OK=1
258
585
  else
259
586
  local_exit=$?
260
587
  log "QA: PR #${pr_num} โ€” provider exited with code ${local_exit}"
261
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"
262
592
  EXIT_CODE=124
263
593
  break
264
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}"
265
598
  EXIT_CODE=${local_exit}
266
599
  fi
267
600
  ;;
@@ -271,39 +604,111 @@ Artifacts: ${QA_ARTIFACTS}"
271
604
  ;;
272
605
  esac
273
606
 
607
+ if [ "${PROVIDER_OK}" -eq 1 ]; then
608
+ if provider_output_looks_invalid "${LOG_LINE_BEFORE}"; then
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"
613
+ EXIT_CODE=1
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"
618
+ EXIT_CODE=1
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
+
641
+ log "QA: PR #${pr_num} โ€” provider completed with verifiable QA evidence"
642
+ fi
643
+ fi
644
+
274
645
  cleanup_worktrees "${PROJECT_DIR}"
275
646
  done
276
647
 
277
648
  cleanup_worktrees "${PROJECT_DIR}"
278
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
+
279
658
  if [ ${EXIT_CODE} -eq 0 ]; then
280
659
  log "DONE: QA runner completed successfully"
281
- send_telegram_status_message "๐Ÿงช Night Watch QA: completed" "Project: ${PROJECT_NAME}
282
- 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}"
283
674
  if [ -n "${REPO}" ]; then
284
- 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}"
285
676
  else
286
- 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}"
287
678
  fi
288
679
  elif [ ${EXIT_CODE} -eq 124 ]; then
289
680
  log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
290
681
  send_telegram_status_message "๐Ÿงช Night Watch QA: timeout" "Project: ${PROJECT_NAME}
682
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
291
683
  Timeout: ${MAX_RUNTIME}s
292
- 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}"
293
691
  if [ -n "${REPO}" ]; then
294
- 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}"
295
693
  else
296
- 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}"
297
695
  fi
298
696
  else
299
697
  log "FAIL: QA runner exited with code ${EXIT_CODE}"
300
698
  send_telegram_status_message "๐Ÿงช Night Watch QA: failed" "Project: ${PROJECT_NAME}
699
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
301
700
  Exit code: ${EXIT_CODE}
302
- 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}"
303
708
  if [ -n "${REPO}" ]; then
304
- 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}"
305
710
  else
306
- 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}"
307
712
  fi
308
713
  fi
309
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