@jonit-dev/night-watch-cli 1.8.8-beta.0 → 1.8.8-beta.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 (38) hide show
  1. package/dist/cli.js +640 -24
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +38 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.d.ts +4 -0
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +25 -0
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/qa.d.ts.map +1 -1
  11. package/dist/commands/qa.js +5 -0
  12. package/dist/commands/qa.js.map +1 -1
  13. package/dist/commands/queue.d.ts.map +1 -1
  14. package/dist/commands/queue.js +27 -4
  15. package/dist/commands/queue.js.map +1 -1
  16. package/dist/commands/resolve.d.ts +26 -0
  17. package/dist/commands/resolve.d.ts.map +1 -0
  18. package/dist/commands/resolve.js +186 -0
  19. package/dist/commands/resolve.js.map +1 -0
  20. package/dist/commands/review.d.ts +5 -0
  21. package/dist/commands/review.d.ts.map +1 -1
  22. package/dist/commands/review.js +18 -5
  23. package/dist/commands/review.js.map +1 -1
  24. package/dist/commands/summary.d.ts +14 -0
  25. package/dist/commands/summary.d.ts.map +1 -0
  26. package/dist/commands/summary.js +193 -0
  27. package/dist/commands/summary.js.map +1 -0
  28. package/dist/commands/uninstall.d.ts.map +1 -1
  29. package/dist/commands/uninstall.js +14 -2
  30. package/dist/commands/uninstall.js.map +1 -1
  31. package/dist/scripts/night-watch-helpers.sh +10 -1
  32. package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
  33. package/dist/scripts/night-watch-pr-reviewer-cron.sh +22 -5
  34. package/dist/scripts/night-watch-qa-cron.sh +107 -38
  35. package/dist/scripts/test-helpers.bats +45 -0
  36. package/dist/templates/night-watch-pr-reviewer.md +2 -1
  37. package/dist/templates/pr-reviewer.md +2 -1
  38. package/package.json +1 -1
@@ -25,6 +25,7 @@ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
25
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
26
26
  BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
27
27
  SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
28
+ VALIDATED_LABEL="${NW_QA_VALIDATED_LABEL:-e2e-validated}"
28
29
  QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
29
30
  QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
30
31
  SCRIPT_START_TIME=$(date +%s)
@@ -55,6 +56,16 @@ emit_result() {
55
56
  fi
56
57
  }
57
58
 
59
+ LABEL_ENSURED=0
60
+ ensure_validated_label() {
61
+ if [ "${LABEL_ENSURED}" -eq 1 ]; then return 0; fi
62
+ gh label create "${VALIDATED_LABEL}" \
63
+ --description "PR acceptance requirements validated by e2e/integration tests" \
64
+ --color "0e8a16" \
65
+ --force 2>/dev/null || true
66
+ LABEL_ENSURED=1
67
+ }
68
+
58
69
  # ── Global Job Queue Gate ────────────────────────────────────────────────────
59
70
  # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
60
71
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
@@ -330,8 +341,8 @@ validate_qa_evidence() {
330
341
  fi
331
342
 
332
343
  if ! pr_has_qa_generated_files "${pr_number}"; then
333
- log "FAIL-QA-EVIDENCE: PR #${pr_number} has QA marker comment but no qa-artifacts/ or tests/*/qa/ files"
334
- return 1
344
+ log "WARN-QA-EVIDENCE: PR #${pr_number} has QA marker comment but no qa-artifacts/ or tests/*/qa/ files"
345
+ return 2
335
346
  fi
336
347
 
337
348
  if [ "${QA_ARTIFACTS}" = "screenshot" ] || [ "${QA_ARTIFACTS}" = "both" ]; then
@@ -487,13 +498,16 @@ fi
487
498
  EXIT_CODE=0
488
499
  PROCESSED_PRS_CSV=""
489
500
  PASSING_PRS_CSV=""
501
+ VALIDATED_PRS_CSV=""
490
502
  ISSUES_FOUND_PRS_CSV=""
491
503
  NO_TESTS_PRS_CSV=""
492
504
  UNCLASSIFIED_PRS_CSV=""
505
+ WARNING_PRS_CSV=""
493
506
  FAILED_AUTOMATION_PRS_CSV=""
494
507
  FAILED_PR=""
495
508
  FAILED_REASON="unknown"
496
509
  QA_SCREENSHOT_SUMMARY=""
510
+ QA_WARNING_SUMMARY=""
497
511
 
498
512
  # Process each PR that needs QA
499
513
  for pr_ref in ${PRS_NEEDING_QA}; do
@@ -595,34 +609,56 @@ for pr_ref in ${PRS_NEEDING_QA}; do
595
609
  FAILED_PR="#${pr_num}"
596
610
  FAILED_REASON="invalid_provider_output"
597
611
  EXIT_CODE=1
598
- elif ! validate_qa_evidence "${pr_num}"; then
599
- FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
600
- FAILED_PR="#${pr_num}"
601
- FAILED_REASON="qa_evidence_validation_failed"
602
- EXIT_CODE=1
603
612
  else
604
- QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
605
- case "${QA_OUTCOME}" in
606
- passing)
607
- PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
608
- ;;
609
- issues_found)
610
- ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
611
- ;;
612
- no_tests_needed)
613
- NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
614
- ;;
615
- *)
616
- UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
617
- ;;
618
- esac
619
-
620
- PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
621
- if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
622
- QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
613
+ if validate_qa_evidence "${pr_num}"; then
614
+ QA_EVIDENCE_STATUS=0
615
+ else
616
+ QA_EVIDENCE_STATUS=$?
617
+ fi
618
+ if [ ${QA_EVIDENCE_STATUS} -eq 2 ]; then
619
+ WARNING_PRS_CSV=$(append_csv "${WARNING_PRS_CSV}" "#${pr_num}")
620
+ QA_WARNING_SUMMARY="${QA_WARNING_SUMMARY}${QA_WARNING_SUMMARY:+$'\n'}#${pr_num}: no qa-artifacts/ or tests/*/qa/ files"
621
+ log "QA: PR #${pr_num} — provider completed with warning-only QA evidence"
622
+ elif [ ${QA_EVIDENCE_STATUS} -ne 0 ]; then
623
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
624
+ FAILED_PR="#${pr_num}"
625
+ FAILED_REASON="qa_evidence_validation_failed"
626
+ EXIT_CODE=1
627
+ else
628
+ QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
629
+ case "${QA_OUTCOME}" in
630
+ passing)
631
+ PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
632
+ # Apply e2e-validated label
633
+ ensure_validated_label
634
+ gh pr edit "${pr_num}" --add-label "${VALIDATED_LABEL}" 2>/dev/null || true
635
+ VALIDATED_PRS_CSV=$(append_csv "${VALIDATED_PRS_CSV}" "#${pr_num}")
636
+ log "QA: PR #${pr_num} — added '${VALIDATED_LABEL}' label (tests passing)"
637
+ ;;
638
+ issues_found)
639
+ ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
640
+ # Remove e2e-validated label if present
641
+ gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
642
+ log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (issues found)"
643
+ ;;
644
+ no_tests_needed)
645
+ NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
646
+ # Remove e2e-validated label — no tests doesn't prove acceptance
647
+ gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
648
+ log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (no tests needed)"
649
+ ;;
650
+ *)
651
+ UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
652
+ ;;
653
+ esac
654
+
655
+ PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
656
+ if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
657
+ QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
658
+ fi
659
+
660
+ log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
623
661
  fi
624
-
625
- log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
626
662
  fi
627
663
  fi
628
664
 
@@ -633,9 +669,11 @@ cleanup_worktrees "${PROJECT_DIR}"
633
669
 
634
670
  FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
635
671
  PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
672
+ VALIDATED_PRS_SUMMARY=$(csv_or_none "${VALIDATED_PRS_CSV}")
636
673
  ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
637
674
  NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
638
675
  UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
676
+ WARNING_PRS_SUMMARY=$(csv_or_none "${WARNING_PRS_CSV}")
639
677
  FAILED_AUTOMATION_PRS_SUMMARY=$(csv_or_none "${FAILED_AUTOMATION_PRS_CSV}")
640
678
  FAILED_PR_SUMMARY=$(csv_or_none "${FAILED_PR}")
641
679
 
@@ -643,25 +681,56 @@ QA_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
643
681
  log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${QA_TOTAL_ELAPSED}s processed_prs=${FINAL_PROCESSED_PRS_CSV:-none}"
644
682
 
645
683
  if [ ${EXIT_CODE} -eq 0 ]; then
646
- log "DONE: QA runner completed successfully"
647
- TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
684
+ if [ -n "${WARNING_PRS_CSV}" ]; then
685
+ log "DONE-WARN: QA runner completed with warnings"
686
+ TELEGRAM_WARNING_BODY="Project: ${PROJECT_NAME}
648
687
  Provider (model): ${PROVIDER_MODEL_DISPLAY}
649
688
  Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
650
689
  Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
651
690
  Passing tests: ${PASSING_PRS_SUMMARY}
691
+ E2E validated: ${VALIDATED_PRS_SUMMARY}
652
692
  Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
653
693
  No tests needed: ${NO_TESTS_PRS_SUMMARY}
654
- Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
655
- if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
656
- TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
694
+ Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}
695
+ Warnings: ${WARNING_PRS_SUMMARY}"
696
+ if [ -n "${QA_WARNING_SUMMARY}" ]; then
697
+ TELEGRAM_WARNING_BODY="${TELEGRAM_WARNING_BODY}
698
+ Warning details:
699
+ ${QA_WARNING_SUMMARY}"
700
+ fi
701
+ if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
702
+ TELEGRAM_WARNING_BODY="${TELEGRAM_WARNING_BODY}
657
703
  Screenshot links:
658
704
  ${QA_SCREENSHOT_SUMMARY}"
659
- fi
660
- send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
661
- if [ -n "${REPO}" ]; then
662
- 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}"
705
+ fi
706
+ send_telegram_status_message "🧪 Night Watch QA: warning" "${TELEGRAM_WARNING_BODY}"
707
+ if [ -n "${REPO}" ]; then
708
+ emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}|repo=${REPO}"
709
+ else
710
+ emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}"
711
+ fi
663
712
  else
664
- 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}"
713
+ log "DONE: QA runner completed successfully"
714
+ TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
715
+ Provider (model): ${PROVIDER_MODEL_DISPLAY}
716
+ Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
717
+ Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
718
+ Passing tests: ${PASSING_PRS_SUMMARY}
719
+ E2E validated: ${VALIDATED_PRS_SUMMARY}
720
+ Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
721
+ No tests needed: ${NO_TESTS_PRS_SUMMARY}
722
+ Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
723
+ if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
724
+ TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
725
+ Screenshot links:
726
+ ${QA_SCREENSHOT_SUMMARY}"
727
+ fi
728
+ send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
729
+ if [ -n "${REPO}" ]; then
730
+ emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
731
+ else
732
+ emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
733
+ fi
665
734
  fi
666
735
  elif [ ${EXIT_CODE} -eq 124 ]; then
667
736
  log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
@@ -75,3 +75,48 @@ teardown() {
75
75
 
76
76
  [ "${result}" = "02-test-prd.md" ]
77
77
  }
78
+
79
+ # ── pr-resolver lock acquisition ─────────────────────────────────────────────
80
+
81
+ @test "pr-resolver lock acquisition: acquire_lock succeeds when no lock exists" {
82
+ local test_lock="/tmp/nw-test-resolver-$$.lock"
83
+
84
+ # Ensure clean state
85
+ rm -f "${test_lock}"
86
+
87
+ run acquire_lock "${test_lock}"
88
+ [ "$status" -eq 0 ]
89
+ [ -f "${test_lock}" ]
90
+
91
+ # PID written to lock file must be the current test process
92
+ local lock_pid
93
+ lock_pid=$(cat "${test_lock}")
94
+ [ -n "${lock_pid}" ]
95
+
96
+ rm -f "${test_lock}"
97
+ }
98
+
99
+ @test "pr-resolver lock acquisition: acquire_lock fails when active lock exists" {
100
+ local test_lock="/tmp/nw-test-resolver-active-$$.lock"
101
+
102
+ # Write current PID as an active lock holder
103
+ echo $$ > "${test_lock}"
104
+
105
+ run acquire_lock "${test_lock}"
106
+ [ "$status" -eq 1 ]
107
+
108
+ rm -f "${test_lock}"
109
+ }
110
+
111
+ @test "pr-resolver lock acquisition: acquire_lock removes stale lock and succeeds" {
112
+ local test_lock="/tmp/nw-test-resolver-stale-$$.lock"
113
+
114
+ # Write a PID that does not exist (use a very high number unlikely to be running)
115
+ echo "999999999" > "${test_lock}"
116
+
117
+ run acquire_lock "${test_lock}"
118
+ [ "$status" -eq 0 ]
119
+ [ -f "${test_lock}" ]
120
+
121
+ rm -f "${test_lock}"
122
+ }
@@ -21,7 +21,8 @@ If current PR code or review feedback conflicts with the PRD context, call out t
21
21
  ## Important: Early Exit
22
22
 
23
23
  - If there are **no open PRs** on `night-watch/` or `feat/` branches, **stop immediately** and report "No PRs to review."
24
- - If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80** (or no review score yet), **stop immediately** and report "All PRs are in good shape."
24
+ - If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80**, **stop immediately** and report "All PRs are in good shape."
25
+ - If a PR has no review score yet, it needs a first review — do NOT skip it.
25
26
  - Do **NOT** loop or retry. Process each PR **once** per run. After processing all PRs, stop.
26
27
  - Do **NOT** re-check PRs after pushing fixes -- the CI will re-run automatically on the next push.
27
28
 
@@ -21,7 +21,8 @@ If current PR code or review feedback conflicts with the PRD context, call out t
21
21
  ## Important: Early Exit
22
22
 
23
23
  - If there are **no open PRs** on `night-watch/` or `feat/` branches, **stop immediately** and report "No PRs to review."
24
- - If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80** (or no review score yet), **stop immediately** and report "All PRs are in good shape."
24
+ - If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80**, **stop immediately** and report "All PRs are in good shape."
25
+ - If a PR has no review score yet, it needs a first review — do NOT skip it.
25
26
  - Do **NOT** loop or retry. Process each PR **once** per run. After processing all PRs, stop.
26
27
  - Do **NOT** re-check PRs after pushing fixes -- the CI will re-run automatically on the next push.
27
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonit-dev/night-watch-cli",
3
- "version": "1.8.8-beta.0",
3
+ "version": "1.8.8-beta.10",
4
4
  "description": "AI agent that implements your specs, opens PRs, and reviews code overnight. Queue GitHub issues or PRDs, wake up to pull requests.",
5
5
  "type": "module",
6
6
  "bin": {