@jonit-dev/night-watch-cli 1.8.12-beta.9 → 1.8.14-beta.0

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 +376 -201
  2. package/dist/commands/audit.d.ts.map +1 -1
  3. package/dist/commands/audit.js +5 -1
  4. package/dist/commands/audit.js.map +1 -1
  5. package/dist/commands/board.d.ts.map +1 -1
  6. package/dist/commands/board.js +2 -0
  7. package/dist/commands/board.js.map +1 -1
  8. package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
  9. package/dist/commands/dashboard/tab-config.js +1 -11
  10. package/dist/commands/dashboard/tab-config.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +1 -6
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +2 -59
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +1 -0
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/notify.d.ts.map +1 -1
  19. package/dist/commands/notify.js +3 -13
  20. package/dist/commands/notify.js.map +1 -1
  21. package/dist/commands/run.d.ts +14 -1
  22. package/dist/commands/run.d.ts.map +1 -1
  23. package/dist/commands/run.js +97 -40
  24. package/dist/commands/run.js.map +1 -1
  25. package/dist/scripts/night-watch-audit-cron.sh +11 -1
  26. package/dist/scripts/night-watch-cron.sh +4 -2
  27. package/dist/scripts/night-watch-merger-cron.sh +177 -32
  28. package/dist/scripts/night-watch-pr-reviewer-cron.sh +107 -2
  29. package/dist/templates/audit.md +64 -30
  30. package/dist/templates/night-watch-audit.md +71 -30
  31. package/dist/templates/night-watch-pr-reviewer.md +7 -6
  32. package/dist/templates/pr-reviewer.md +7 -6
  33. package/dist/web/assets/index-CL3Q-KB4.css +1 -0
  34. package/dist/web/assets/index-FDOCfjkP.js +442 -0
  35. package/dist/web/index.html +2 -2
  36. package/package.json +1 -1
@@ -14,6 +14,8 @@ set -euo pipefail
14
14
  # NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
15
15
  # NW_MERGER_REBASE_BEFORE_MERGE=1 - Set to 1 to rebase before merging
16
16
  # NW_MERGER_MAX_PRS_PER_RUN=0 - Max PRs to merge per run (0 = unlimited)
17
+ # NW_MERGER_CI_MAX_WAIT=300 - Max seconds to wait for checks after rebase
18
+ # NW_MERGER_CI_POLL_INTERVAL=15 - Seconds between check polls after rebase
17
19
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode
18
20
 
19
21
  PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
@@ -26,6 +28,8 @@ MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
26
28
  MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
27
29
  REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
28
30
  MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
31
+ CI_MAX_WAIT="${NW_MERGER_CI_MAX_WAIT:-300}"
32
+ CI_POLL_INTERVAL="${NW_MERGER_CI_POLL_INTERVAL:-15}"
29
33
  BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
30
34
  READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
31
35
  SCRIPT_START_TIME=$(date +%s)
@@ -40,6 +44,12 @@ fi
40
44
  if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
41
45
  MIN_REVIEW_SCORE="80"
42
46
  fi
47
+ if ! [[ "${CI_MAX_WAIT}" =~ ^[0-9]+$ ]]; then
48
+ CI_MAX_WAIT="300"
49
+ fi
50
+ if ! [[ "${CI_POLL_INTERVAL}" =~ ^[0-9]+$ ]] || [ "${CI_POLL_INTERVAL}" = "0" ]; then
51
+ CI_POLL_INTERVAL="15"
52
+ fi
43
53
  # Clamp merge method to valid values
44
54
  case "${MERGE_METHOD}" in
45
55
  squash|merge|rebase) ;;
@@ -110,24 +120,145 @@ get_review_score() {
110
120
  echo "${score}"
111
121
  }
112
122
 
113
- # Check if CI is passing for a PR (all checks must be complete and none failing)
114
- ci_passing() {
123
+ # Count pending review feedback that should send the PR back through reviewer
124
+ # instead of allowing the merger to land it. We combine GitHub's review decision
125
+ # with unresolved review threads because some providers leave actionable feedback
126
+ # as thread comments without lowering the score.
127
+ get_pending_review_feedback_count() {
115
128
  local pr_number="${1}"
116
- local checks_json
117
- checks_json=$(gh pr checks "${pr_number}" --json name,state,conclusion 2>/dev/null || echo "[]")
118
- # Fail if any checks have explicit failures
119
- local fail_count
120
- fail_count=$(echo "${checks_json}" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED" or .state == "FAILURE")] | length' 2>/dev/null || echo "999")
121
- if [ "${fail_count}" != "0" ]; then
122
- return 1
129
+ local review_decision=""
130
+ local change_request_count="0"
131
+ local unresolved_thread_count="0"
132
+ local repo=""
133
+ local owner=""
134
+ local name=""
135
+
136
+ review_decision=$(gh pr view "${pr_number}" --json reviewDecision --jq '.reviewDecision // ""' 2>/dev/null || echo "")
137
+
138
+ change_request_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
139
+ --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
140
+ if ! [[ "${change_request_count}" =~ ^[0-9]+$ ]]; then
141
+ change_request_count="0"
142
+ fi
143
+
144
+ repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
145
+ if [[ "${repo}" == */* ]]; then
146
+ owner="${repo%%/*}"
147
+ name="${repo#*/}"
148
+ unresolved_thread_count=$(gh api graphql \
149
+ -F owner="${owner}" \
150
+ -F name="${name}" \
151
+ -F number="${pr_number}" \
152
+ -f query='
153
+ query($owner: String!, $name: String!, $number: Int!) {
154
+ repository(owner: $owner, name: $name) {
155
+ pullRequest(number: $number) {
156
+ reviewThreads(first: 100) {
157
+ nodes {
158
+ isResolved
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ ' \
165
+ --jq '[.data.repository.pullRequest.reviewThreads.nodes[]? | select(.isResolved == false)] | length' \
166
+ 2>/dev/null || echo "0")
167
+ if ! [[ "${unresolved_thread_count}" =~ ^[0-9]+$ ]]; then
168
+ unresolved_thread_count="0"
169
+ fi
170
+ fi
171
+
172
+ if [ "${unresolved_thread_count}" -gt 0 ]; then
173
+ echo "${unresolved_thread_count}"
174
+ return 0
175
+ fi
176
+
177
+ if [ "${change_request_count}" -gt 0 ]; then
178
+ echo "${change_request_count}"
179
+ return 0
123
180
  fi
124
- # Fail if any checks are still pending/in-progress (not yet concluded)
125
- local pending_count
126
- pending_count=$(echo "${checks_json}" | jq '[.[] | select(.state == "PENDING" or .state == "IN_PROGRESS" or (.conclusion == null and .state != "SUCCESS"))] | length' 2>/dev/null || echo "999")
127
- if [ "${pending_count}" != "0" ]; then
128
- return 1
181
+
182
+ if [ "${review_decision}" = "CHANGES_REQUESTED" ]; then
183
+ echo "1"
184
+ return 0
129
185
  fi
130
- return 0
186
+
187
+ echo "0"
188
+ }
189
+
190
+ # Get the current head OID for a PR.
191
+ get_pr_head_oid() {
192
+ local pr_number="${1}"
193
+ gh pr view "${pr_number}" --json headRefOid --jq '.headRefOid // ""' 2>/dev/null || echo ""
194
+ }
195
+
196
+ # Return the CI state for the PR's status rollup on the expected head OID.
197
+ ci_status_for_head() {
198
+ local pr_number="${1}"
199
+ local expected_head="${2:-}"
200
+ local status_json
201
+
202
+ status_json=$(gh pr view "${pr_number}" --json headRefOid,statusCheckRollup 2>/dev/null || echo "")
203
+ if [ -z "${status_json}" ]; then
204
+ echo "unknown"
205
+ return 0
206
+ fi
207
+
208
+ echo "${status_json}" | jq -r --arg expected_head "${expected_head}" '
209
+ def in_list($values): . as $value | $values | index($value);
210
+ def check_run_failure:
211
+ .__typename == "CheckRun"
212
+ and ((.conclusion // "") | in_list(["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STALE", "STARTUP_FAILURE"]));
213
+ def status_context_failure:
214
+ .__typename == "StatusContext"
215
+ and ((.state // "") | in_list(["FAILURE", "ERROR"]));
216
+ def check_run_pending:
217
+ .__typename == "CheckRun"
218
+ and ((.status // "") != "COMPLETED" or .conclusion == null);
219
+ def status_context_pending:
220
+ .__typename == "StatusContext"
221
+ and ((.state // "") | in_list(["PENDING", "EXPECTED"]));
222
+ def check_run_nonpassing:
223
+ .__typename == "CheckRun"
224
+ and ((.conclusion // "") | in_list(["SUCCESS", "NEUTRAL", "SKIPPED"]) | not);
225
+ def status_context_nonpassing:
226
+ .__typename == "StatusContext"
227
+ and (.state // "") != "SUCCESS";
228
+ if ($expected_head != "" and (.headRefOid // "") != $expected_head) then
229
+ "head_mismatch"
230
+ elif ((.statusCheckRollup // []) | length) == 0 then
231
+ "absent"
232
+ elif any((.statusCheckRollup // [])[]; check_run_failure or status_context_failure) then
233
+ "failed"
234
+ elif any((.statusCheckRollup // [])[]; check_run_pending or status_context_pending) then
235
+ "pending"
236
+ elif any((.statusCheckRollup // [])[]; check_run_nonpassing or status_context_nonpassing) then
237
+ "failed"
238
+ else
239
+ "passing"
240
+ end
241
+ ' 2>/dev/null || echo "unknown"
242
+ }
243
+
244
+ wait_for_ci_passing_on_head() {
245
+ local pr_number="${1}"
246
+ local expected_head="${2}"
247
+ local ci_waited=0
248
+ LAST_CI_STATUS="unknown"
249
+
250
+ while [ "${ci_waited}" -lt "${CI_MAX_WAIT}" ]; do
251
+ LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
252
+ if [ "${LAST_CI_STATUS}" = "passing" ]; then
253
+ return 0
254
+ fi
255
+ log "INFO: PR #${pr_number}: Waiting for fresh CI on head ${expected_head} (${LAST_CI_STATUS}, ${ci_waited}s/${CI_MAX_WAIT}s)..."
256
+ sleep "${CI_POLL_INTERVAL}"
257
+ ci_waited=$((ci_waited + CI_POLL_INTERVAL))
258
+ done
259
+
260
+ LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
261
+ [ "${LAST_CI_STATUS}" = "passing" ]
131
262
  }
132
263
 
133
264
  # Rebase a PR against its base branch
@@ -254,9 +385,16 @@ while IFS= read -r pr_json; do
254
385
  continue
255
386
  fi
256
387
 
388
+ pr_head_oid=$(get_pr_head_oid "${pr_number}")
389
+ if [ -z "${pr_head_oid}" ]; then
390
+ log "INFO: PR #${pr_number} (${pr_branch}): Unable to determine PR head, skipping"
391
+ continue
392
+ fi
393
+
257
394
  # Check CI status
258
- if ! ci_passing "${pr_number}"; then
259
- log "INFO: PR #${pr_number} (${pr_branch}): CI not passing, skipping"
395
+ ci_status=$(ci_status_for_head "${pr_number}" "${pr_head_oid}")
396
+ if [ "${ci_status}" != "passing" ]; then
397
+ log "INFO: PR #${pr_number} (${pr_branch}): CI not passing on head ${pr_head_oid} (${ci_status}), skipping"
260
398
  continue
261
399
  fi
262
400
 
@@ -264,15 +402,22 @@ while IFS= read -r pr_json; do
264
402
  if [ "${MIN_REVIEW_SCORE}" -gt "0" ]; then
265
403
  score=$(get_review_score "${pr_number}")
266
404
  if [ "${score}" -lt "0" ] || [ "${score}" -lt "${MIN_REVIEW_SCORE}" ]; then
267
- log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), skipping"
405
+ log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), reviewer job required before merge"
268
406
  continue
269
407
  fi
270
408
  fi
271
409
 
410
+ pending_review_feedback_count=$(get_pending_review_feedback_count "${pr_number}")
411
+ if [ "${pending_review_feedback_count}" -gt "0" ]; then
412
+ log "INFO: PR #${pr_number} (${pr_branch}): ${pending_review_feedback_count} pending review feedback item(s), reviewer job required before merge"
413
+ continue
414
+ fi
415
+
272
416
  log "INFO: PR #${pr_number} (${pr_branch}): Eligible for merge"
273
417
 
274
418
  # Rebase before merge if configured
275
419
  if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
420
+ pr_head_before_rebase="${pr_head_oid}"
276
421
  if ! rebase_pr "${pr_number}"; then
277
422
  log "WARN: PR #${pr_number}: Rebase failed, skipping"
278
423
  FAILED_PRS=$((FAILED_PRS + 1))
@@ -280,20 +425,20 @@ while IFS= read -r pr_json; do
280
425
  fi
281
426
  log "INFO: PR #${pr_number}: Rebase successful"
282
427
 
283
- # Poll CI until all checks complete after rebase (up to 5 minutes)
284
- ci_max_wait=300
285
- ci_waited=0
286
- ci_poll=15
287
- while [ "${ci_waited}" -lt "${ci_max_wait}" ]; do
288
- sleep "${ci_poll}"
289
- ci_waited=$((ci_waited + ci_poll))
290
- if ci_passing "${pr_number}"; then
291
- break
292
- fi
293
- log "INFO: PR #${pr_number}: Waiting for CI after rebase (${ci_waited}s/${ci_max_wait}s)..."
294
- done
295
- if ! ci_passing "${pr_number}"; then
296
- log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${ci_waited}s), skipping"
428
+ pr_head_after_rebase=$(get_pr_head_oid "${pr_number}")
429
+ if [ -z "${pr_head_after_rebase}" ]; then
430
+ log "INFO: PR #${pr_number}: Unable to determine PR head after rebase, skipping"
431
+ continue
432
+ fi
433
+ if [ "${pr_head_after_rebase}" != "${pr_head_before_rebase}" ]; then
434
+ log "INFO: PR #${pr_number}: Head changed after rebase ${pr_head_before_rebase} -> ${pr_head_after_rebase}; waiting for fresh CI"
435
+ else
436
+ log "INFO: PR #${pr_number}: Head unchanged after rebase (${pr_head_after_rebase}); confirming CI"
437
+ fi
438
+
439
+ # Poll CI until all checks attached to the post-rebase head are complete and passing.
440
+ if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
441
+ log "INFO: PR #${pr_number}: Fresh CI not passing on head ${pr_head_after_rebase} after rebase (${LAST_CI_STATUS}, waited ${CI_MAX_WAIT}s), skipping"
297
442
  continue
298
443
  fi
299
444
  fi
@@ -89,6 +89,26 @@ emit_result() {
89
89
  fi
90
90
  }
91
91
 
92
+ is_pr_open() {
93
+ local pr_number="${1:?PR number required}"
94
+ local pr_state=""
95
+
96
+ pr_state=$(gh pr view "${pr_number}" --json state --jq '.state // ""' 2>/dev/null || echo "")
97
+ case "${pr_state}" in
98
+ OPEN)
99
+ return 0
100
+ ;;
101
+ MERGED|CLOSED)
102
+ return 1
103
+ ;;
104
+ esac
105
+
106
+ # Backward-compatible fallback for older gh versions or tests that do not
107
+ # support the state field. Treat an unknown/empty state as open only when the
108
+ # PR is still viewable by number.
109
+ gh pr view "${pr_number}" --json number >/dev/null 2>&1
110
+ }
111
+
92
112
  require_provider_on_path() {
93
113
  if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
94
114
  echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
@@ -132,6 +152,72 @@ get_pr_comments() {
132
152
  } | awk '!seen[$0]++'
133
153
  }
134
154
 
155
+ get_pr_pending_review_feedback_count() {
156
+ local pr_number="${1:?PR number required}"
157
+ local review_decision=""
158
+ local change_request_count="0"
159
+ local unresolved_thread_count="0"
160
+ local repo="${REPO:-}"
161
+ local owner=""
162
+ local name=""
163
+
164
+ review_decision=$(gh pr view "${pr_number}" --json reviewDecision --jq '.reviewDecision // ""' 2>/dev/null || echo "")
165
+
166
+ change_request_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
167
+ --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
168
+ if ! [[ "${change_request_count}" =~ ^[0-9]+$ ]]; then
169
+ change_request_count="0"
170
+ fi
171
+
172
+ if [ -z "${repo}" ]; then
173
+ repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
174
+ fi
175
+
176
+ if [[ "${repo}" == */* ]]; then
177
+ owner="${repo%%/*}"
178
+ name="${repo#*/}"
179
+ unresolved_thread_count=$(gh api graphql \
180
+ -F owner="${owner}" \
181
+ -F name="${name}" \
182
+ -F number="${pr_number}" \
183
+ -f query='
184
+ query($owner: String!, $name: String!, $number: Int!) {
185
+ repository(owner: $owner, name: $name) {
186
+ pullRequest(number: $number) {
187
+ reviewThreads(first: 100) {
188
+ nodes {
189
+ isResolved
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ' \
196
+ --jq '[.data.repository.pullRequest.reviewThreads.nodes[]? | select(.isResolved == false)] | length' \
197
+ 2>/dev/null || echo "0")
198
+ if ! [[ "${unresolved_thread_count}" =~ ^[0-9]+$ ]]; then
199
+ unresolved_thread_count="0"
200
+ fi
201
+ fi
202
+
203
+ if [ "${unresolved_thread_count}" -gt 0 ]; then
204
+ echo "${unresolved_thread_count}"
205
+ return 0
206
+ fi
207
+
208
+ if [ "${change_request_count}" -gt 0 ]; then
209
+ echo "${change_request_count}"
210
+ return 0
211
+ fi
212
+
213
+ if [ "${review_decision}" = "CHANGES_REQUESTED" ]; then
214
+ echo "1"
215
+ return 0
216
+ fi
217
+
218
+ echo "0"
219
+ }
220
+
135
221
  get_pr_head_ref_oid() {
136
222
  local pr_number="${1:?PR number required}"
137
223
  gh pr view "${pr_number}" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo ""
@@ -654,7 +740,7 @@ fi
654
740
 
655
741
  if [ -n "${TARGET_PR}" ]; then
656
742
  OPEN_PRS=$(
657
- if gh pr view "${TARGET_PR}" --json number >/dev/null 2>&1; then
743
+ if is_pr_open "${TARGET_PR}"; then
658
744
  echo "1"
659
745
  else
660
746
  echo "0"
@@ -760,6 +846,18 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
760
846
  continue
761
847
  fi
762
848
 
849
+ PENDING_REVIEW_FEEDBACK_COUNT=$(get_pr_pending_review_feedback_count "${pr_number}")
850
+ if [ "${PENDING_REVIEW_FEEDBACK_COUNT}" -gt 0 ]; then
851
+ if [ "${local_ready_for_review_label_present}" -eq 1 ]; then
852
+ log "INFO: PR #${pr_number} (${pr_branch}) has pending review feedback; removing stale ${READY_FOR_REVIEW_LABEL} label"
853
+ clear_ready_for_human_review_label "${pr_number}"
854
+ fi
855
+ log "INFO: PR #${pr_number} (${pr_branch}) has ${PENDING_REVIEW_FEEDBACK_COUNT} pending review feedback item(s)"
856
+ NEEDS_WORK=1
857
+ PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
858
+ continue
859
+ fi
860
+
763
861
  if has_ready_for_human_review_marker "${all_comments}" "${current_head_sha}"; then
764
862
  SKIPPED_ALREADY_REVIEWED_CURRENT_HEAD=1
765
863
  log "INFO: PR #${pr_number} (${pr_branch}) is already marked ready for human review at head ${current_head_sha:0:12}; skipping repeat automated review"
@@ -893,6 +991,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
893
991
  EXIT_CODE=0
894
992
  AUTO_MERGED_PRS=""
895
993
  AUTO_MERGE_FAILED_PRS=""
994
+ ACTUAL_REVIEWED_PRS=""
896
995
  NO_CHANGES_PRS=""
897
996
  MAX_WORKER_ATTEMPTS=1
898
997
  MAX_WORKER_FINAL_SCORE=""
@@ -937,6 +1036,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
937
1036
  worker_status=$(printf '%s' "${worker_result}" | sed -n 's/^NIGHT_WATCH_RESULT:\([^|]*\).*$/\1/p')
938
1037
  worker_auto_merged=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merged=)[^|]+' || true)
939
1038
  worker_auto_merge_failed=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merge_failed=)[^|]+' || true)
1039
+ worker_reviewed_prs=$(printf '%s' "${worker_result}" | grep -oP '(?<=prs=)[^|]+' || true)
940
1040
  worker_attempts=$(printf '%s' "${worker_result}" | grep -oP '(?<=attempts=)[^|]+' || true)
941
1041
  worker_final_score=$(printf '%s' "${worker_result}" | grep -oP '(?<=final_score=)[^|]+' || true)
942
1042
  worker_no_changes=$(printf '%s' "${worker_result}" | grep -oP '(?<=no_changes_needed=)[^|]+' || true)
@@ -944,6 +1044,9 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
944
1044
 
945
1045
  AUTO_MERGED_PRS=$(append_csv "${AUTO_MERGED_PRS}" "${worker_auto_merged}")
946
1046
  AUTO_MERGE_FAILED_PRS=$(append_csv "${AUTO_MERGE_FAILED_PRS}" "${worker_auto_merge_failed}")
1047
+ if [ "${worker_status}" = "success_reviewed" ]; then
1048
+ ACTUAL_REVIEWED_PRS=$(append_csv "${ACTUAL_REVIEWED_PRS}" "${worker_reviewed_prs}")
1049
+ fi
947
1050
  NO_CHANGES_PRS=$(append_csv "${NO_CHANGES_PRS}" "${worker_no_changes_prs}")
948
1051
  if [ -z "${worker_no_changes_prs}" ] && [ "${worker_no_changes}" = "1" ]; then
949
1052
  NO_CHANGES_PRS=$(append_csv "${NO_CHANGES_PRS}" "#${worker_pr}")
@@ -988,7 +1091,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
988
1091
  # worker runs may have left behind.
989
1092
  cleanup_reviewer_worktrees
990
1093
 
991
- emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
1094
+ emit_final_status "${EXIT_CODE}" "${ACTUAL_REVIEWED_PRS}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
992
1095
  exit "${EXIT_CODE}"
993
1096
  fi
994
1097
 
@@ -1077,6 +1180,7 @@ if [ -n "${TARGET_PR}" ]; then
1077
1180
  TARGET_MERGE_STATE=$(gh pr view "${TARGET_PR}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "UNKNOWN")
1078
1181
  TARGET_FAILED_CHECKS=$(get_pr_failed_ci_summary "${TARGET_PR}")
1079
1182
  TARGET_SCORE=$(get_pr_score "${TARGET_PR}")
1183
+ TARGET_PENDING_REVIEW_FEEDBACK=$(get_pr_pending_review_feedback_count "${TARGET_PR}")
1080
1184
 
1081
1185
  TARGET_SCOPE_PROMPT+=$'\n## Preflight Data (from CLI)\n- mergeStateStatus: '"${TARGET_MERGE_STATE}"$'\n'
1082
1186
  if [ -n "${TARGET_FAILED_CHECKS}" ]; then
@@ -1084,6 +1188,7 @@ if [ -n "${TARGET_PR}" ]; then
1084
1188
  else
1085
1189
  TARGET_SCOPE_PROMPT+=$'- failing checks: none detected\n'
1086
1190
  fi
1191
+ TARGET_SCOPE_PROMPT+=$'- pending review feedback: '"${TARGET_PENDING_REVIEW_FEEDBACK}"$' item(s)\n'
1087
1192
  if [ -n "${TARGET_SCORE}" ]; then
1088
1193
  TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
1089
1194
  TARGET_SCOPE_PROMPT+=$'- action: fix\n'
@@ -1,4 +1,6 @@
1
- You are the Night Watch Code Auditor. Your job is to scan the codebase for real engineering risks and write a structured, high-signal report.
1
+ You are the Night Watch Code Auditor. Your job is to scan the codebase for systemic engineering risks and write a consolidated architecture/code-quality audit report for human prioritization.
2
+
3
+ The default output is a broad report, not executor fodder. Do not create or recommend one board issue per finding unless an explicit "Board Issue Mode" section is appended to this prompt.
2
4
 
3
5
  ## What to look for
4
6
 
@@ -30,65 +32,97 @@ You are the Night Watch Code Auditor. Your job is to scan the codebase for real
30
32
 
31
33
  - `node_modules/`, `dist/`, `.git/`, `coverage/`, generated files.
32
34
  - Test files (`*.test.ts`, `*.spec.ts`, `__tests__/`) unless they expose production design flaws.
33
- - Intentional no-op catches in file walkers/read-only probing paths (e.g., `catch { continue }`, `catch { return null }` when clearly harmless).
35
+ - Intentional no-op catches in file walkers/read-only probing paths (for example, `catch { continue }`, `catch { return null }` when clearly harmless).
34
36
  - Cosmetic style-only nits (formatting, naming preference, import order).
35
- - Hypothetical principle violations without concrete impact.
37
+ - Hypothetical principle violations without concrete code evidence and impact.
36
38
 
37
39
  ## How to scan
38
40
 
39
41
  Use file-reading/search tools and scan systematically, prioritizing:
40
42
 
41
- - `src/` (core TypeScript implementation)
42
- - `scripts/` (automation and shell execution paths)
43
+ - `src/` and package implementation directories.
44
+ - `scripts/` and automation/runtime shell paths.
45
+ - Shared configuration, scheduler, queue, provider, board, and command flows.
43
46
 
44
47
  For each potential issue, verify:
45
48
 
46
49
  1. It is real and actionable.
47
- 2. It has concrete impact (correctness, security, scalability, operability, maintainability).
48
- 3. The fix direction is clear.
50
+ 2. It has concrete impact on correctness, security, scalability, operability, or maintainability.
51
+ 3. The affected locations show a pattern or systemic design problem, not just a tiny isolated nit.
52
+ 4. The fix direction is useful for human planning.
53
+
54
+ ## Priority model
55
+
56
+ Use an Effort x Impact priority model:
49
57
 
50
- ## Severity model
58
+ - **Impact**: critical, high, medium, low.
59
+ - **Effort**: small, medium, large.
60
+ - **Priority**: P0, P1, P2, P3.
51
61
 
52
- - **critical**: likely production outage/data loss/security exposure or severe architectural risk.
53
- - **high**: significant bug/risk with near-term impact.
54
- - **medium**: clear risk/smell that should be addressed soon.
55
- - **low**: valid but lower urgency.
62
+ Assign P0/P1 only when the issue is urgent or unlocks significant risk reduction. Be selective.
56
63
 
57
64
  ## Report format
58
65
 
59
- Write findings to `logs/audit-report.md` using this exact format:
66
+ Write `logs/audit-report.md` using this format:
60
67
 
61
68
  ```markdown
62
- # Code Audit Report
69
+ # Architecture and Code Quality Audit
63
70
 
64
71
  Generated: <ISO timestamp>
65
72
 
66
- ## Findings
73
+ ## Executive Summary
67
74
 
68
- ### Finding 1
75
+ One to three concise paragraphs covering the highest-risk themes, the likely cost of leaving them alone, and the recommended order of attack.
69
76
 
70
- - **Location**: `src/path/to/file.ts:42`
71
- - **Severity**: critical | high | medium | low
72
- - **Category**: empty_catch | critical_todo | hardcoded_secret | unhandled_promise | unsafe_assertion | scalability_hotspot | architecture_violation | srp_violation | dry_violation | kiss_violation | solid_violation | yagni_violation
73
- - **Description**: What the issue is, why it matters, and concrete impact
74
- - **Snippet**: `the offending code`
75
- - **Suggested Fix**: Specific fix direction (minimal, pragmatic)
77
+ ## Priority Matrix
76
78
 
77
- ### Finding 2
79
+ | Priority | Theme | Impact | Effort | Why now |
80
+ | -------- | ------------ | ------ | ------ | -------------- |
81
+ | P1 | <theme name> | high | medium | <short reason> |
78
82
 
79
- ...
80
- ```
83
+ ## Findings by Theme
81
84
 
82
- If you find **no actionable issues**, write exactly this to `logs/audit-report.md`:
85
+ ### <Theme Name>
86
+
87
+ Impact: critical | high | medium | low
88
+ Effort: small | medium | large
89
+ Priority: P0 | P1 | P2 | P3
90
+
91
+ #### Evidence
92
+
93
+ - `<path>:<line>` - what is happening and why it matters.
94
+ - `<path>:<line>` - related evidence showing this is systemic.
95
+
96
+ #### Architecture or Quality Rule Violated
97
+
98
+ Name the concrete rule or boundary being violated.
99
+
100
+ #### Recommended Direction
101
+
102
+ Describe the pragmatic remediation path. Prefer grouped fixes and sequencing over tiny task breakdowns.
103
+
104
+ #### Full Violation List
105
+
106
+ - `<path>:<line>` - concise violation.
107
+ - `<path>:<line>` - concise violation.
108
+
109
+ ## Cross-Cutting Recommendations
110
+
111
+ - <Recommendation that helps multiple themes>
112
+
113
+ ## No-Issue Result
114
+
115
+ If there are no actionable systemic issues, write exactly:
83
116
 
84
- ```
85
117
  NO_ISSUES_FOUND
86
118
  ```
87
119
 
88
120
  ## Rules
89
121
 
90
- - Prioritize high-impact findings over volume. 3 strong findings beat 15 weak ones.
91
- - Report principle violations (SRP/DRY/KISS/SOLID/YAGNI) only when they create concrete risk.
122
+ - Favor grouped systemic findings over granular one-off findings.
123
+ - Include a full violation list under each theme so humans can size and prioritize the work.
124
+ - Do not use `### Finding N` headings in default report mode.
125
+ - Do not create one issue per finding, and do not optimize the report for automatic execution.
126
+ - Report principle violations only when they create concrete risk.
92
127
  - Avoid theoretical architecture criticism without code evidence.
93
- - Be decisive: skip noisy false positives.
94
128
  - After writing the report, stop. Do NOT open PRs, push code, or make changes.