@jonit-dev/night-watch-cli 1.8.12-beta.1 → 1.8.12-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 (68) hide show
  1. package/dist/cli.js +3729 -2027
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/agent.d.ts +12 -0
  4. package/dist/commands/agent.d.ts.map +1 -0
  5. package/dist/commands/agent.js +307 -0
  6. package/dist/commands/agent.js.map +1 -0
  7. package/dist/commands/analytics.d.ts.map +1 -1
  8. package/dist/commands/analytics.js +60 -0
  9. package/dist/commands/analytics.js.map +1 -1
  10. package/dist/commands/audit.d.ts.map +1 -1
  11. package/dist/commands/audit.js +45 -0
  12. package/dist/commands/audit.js.map +1 -1
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +13 -0
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/merge.d.ts.map +1 -1
  17. package/dist/commands/merge.js +30 -3
  18. package/dist/commands/merge.js.map +1 -1
  19. package/dist/commands/plan.d.ts.map +1 -1
  20. package/dist/commands/plan.js +45 -0
  21. package/dist/commands/plan.js.map +1 -1
  22. package/dist/commands/qa.d.ts.map +1 -1
  23. package/dist/commands/qa.js +24 -0
  24. package/dist/commands/qa.js.map +1 -1
  25. package/dist/commands/queue.d.ts.map +1 -1
  26. package/dist/commands/queue.js +20 -0
  27. package/dist/commands/queue.js.map +1 -1
  28. package/dist/commands/resolve.d.ts.map +1 -1
  29. package/dist/commands/resolve.js +26 -0
  30. package/dist/commands/resolve.js.map +1 -1
  31. package/dist/commands/review.d.ts +2 -0
  32. package/dist/commands/review.d.ts.map +1 -1
  33. package/dist/commands/review.js +46 -1
  34. package/dist/commands/review.js.map +1 -1
  35. package/dist/commands/run.d.ts +16 -1
  36. package/dist/commands/run.d.ts.map +1 -1
  37. package/dist/commands/run.js +85 -1
  38. package/dist/commands/run.js.map +1 -1
  39. package/dist/commands/shared/env-builder.d.ts.map +1 -1
  40. package/dist/commands/shared/env-builder.js +1 -0
  41. package/dist/commands/shared/env-builder.js.map +1 -1
  42. package/dist/commands/shared/feedback.d.ts +24 -0
  43. package/dist/commands/shared/feedback.d.ts.map +1 -0
  44. package/dist/commands/shared/feedback.js +38 -0
  45. package/dist/commands/shared/feedback.js.map +1 -0
  46. package/dist/commands/slice.d.ts.map +1 -1
  47. package/dist/commands/slice.js +48 -1
  48. package/dist/commands/slice.js.map +1 -1
  49. package/dist/scripts/night-watch-audit-cron.sh +1 -0
  50. package/dist/scripts/night-watch-cron.sh +186 -23
  51. package/dist/scripts/night-watch-helpers.sh +65 -2
  52. package/dist/scripts/night-watch-merger-cron.sh +137 -36
  53. package/dist/scripts/night-watch-plan-cron.sh +2 -0
  54. package/dist/scripts/night-watch-pr-resolver-cron.sh +6 -0
  55. package/dist/scripts/night-watch-pr-reviewer-cron.sh +16 -5
  56. package/dist/scripts/night-watch-qa-cron.sh +8 -1
  57. package/dist/scripts/night-watch-slicer-cron.sh +3 -0
  58. package/dist/templates/night-watch.config.json +21 -0
  59. package/dist/templates/slicer.md +3 -0
  60. package/dist/web/assets/index-B6E6kOoR.js +406 -0
  61. package/dist/web/assets/index-C-xpWpS8.css +1 -0
  62. package/dist/web/assets/index-CEYe-290.js +412 -0
  63. package/dist/web/assets/index-DIMUXIP8.css +1 -0
  64. package/dist/web/assets/index-DpvzoXEv.js +442 -0
  65. package/dist/web/assets/index-DyME41HV.css +1 -0
  66. package/dist/web/assets/index-NR27JE3b.js +406 -0
  67. package/dist/web/index.html +2 -2
  68. package/package.json +3 -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,7 +28,10 @@ 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:-}"
34
+ READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
30
35
  SCRIPT_START_TIME=$(date +%s)
31
36
  DRY_RUN="${NW_DRY_RUN:-0}"
32
37
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
@@ -39,6 +44,12 @@ fi
39
44
  if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
40
45
  MIN_REVIEW_SCORE="80"
41
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
42
53
  # Clamp merge method to valid values
43
54
  case "${MERGE_METHOD}" in
44
55
  squash|merge|rebase) ;;
@@ -55,6 +66,7 @@ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
55
66
  # NOTE: Lock file path must match mergerLockPath() in src/utils/status-data.ts
56
67
  LOCK_FILE="/tmp/night-watch-merger-${PROJECT_RUNTIME_KEY}.lock"
57
68
  SCRIPT_TYPE="merger"
69
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
58
70
 
59
71
  MERGED_PRS=0
60
72
  FAILED_PRS=0
@@ -103,29 +115,83 @@ get_review_score() {
103
115
  # Look for review score comment from night-watch
104
116
  local score
105
117
  score=$(gh pr view "${pr_number}" --json comments \
106
- --jq '[.comments[].body | select(test("review score|score:? [0-9]+/100"; "i")) | capture("(?i)score:? *(?<s>[0-9]+)/100") | .s] | last | tonumber // -1' \
118
+ --jq '[.comments[].body | select(test("score[^0-9]{0,30}[0-9]+/100"; "i")) | capture("(?i)score[^0-9]{0,30}(?<s>[0-9]+)/100") | .s] | last | tonumber // -1' \
107
119
  2>/dev/null || echo "-1")
108
120
  echo "${score}"
109
121
  }
110
122
 
111
- # Check if CI is passing for a PR (all checks must be complete and none failing)
112
- ci_passing() {
123
+ # Get the current head OID for a PR.
124
+ get_pr_head_oid() {
113
125
  local pr_number="${1}"
114
- local checks_json
115
- checks_json=$(gh pr checks "${pr_number}" --json name,state,conclusion 2>/dev/null || echo "[]")
116
- # Fail if any checks have explicit failures
117
- local fail_count
118
- 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")
119
- if [ "${fail_count}" != "0" ]; then
120
- return 1
121
- fi
122
- # Fail if any checks are still pending/in-progress (not yet concluded)
123
- local pending_count
124
- 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")
125
- if [ "${pending_count}" != "0" ]; then
126
- return 1
126
+ gh pr view "${pr_number}" --json headRefOid --jq '.headRefOid // ""' 2>/dev/null || echo ""
127
+ }
128
+
129
+ # Return the CI state for the PR's status rollup on the expected head OID.
130
+ ci_status_for_head() {
131
+ local pr_number="${1}"
132
+ local expected_head="${2:-}"
133
+ local status_json
134
+
135
+ status_json=$(gh pr view "${pr_number}" --json headRefOid,statusCheckRollup 2>/dev/null || echo "")
136
+ if [ -z "${status_json}" ]; then
137
+ echo "unknown"
138
+ return 0
127
139
  fi
128
- return 0
140
+
141
+ echo "${status_json}" | jq -r --arg expected_head "${expected_head}" '
142
+ def in_list($values): . as $value | $values | index($value);
143
+ def check_run_failure:
144
+ .__typename == "CheckRun"
145
+ and ((.conclusion // "") | in_list(["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STALE", "STARTUP_FAILURE"]));
146
+ def status_context_failure:
147
+ .__typename == "StatusContext"
148
+ and ((.state // "") | in_list(["FAILURE", "ERROR"]));
149
+ def check_run_pending:
150
+ .__typename == "CheckRun"
151
+ and ((.status // "") != "COMPLETED" or .conclusion == null);
152
+ def status_context_pending:
153
+ .__typename == "StatusContext"
154
+ and ((.state // "") | in_list(["PENDING", "EXPECTED"]));
155
+ def check_run_nonpassing:
156
+ .__typename == "CheckRun"
157
+ and ((.conclusion // "") | in_list(["SUCCESS", "NEUTRAL", "SKIPPED"]) | not);
158
+ def status_context_nonpassing:
159
+ .__typename == "StatusContext"
160
+ and (.state // "") != "SUCCESS";
161
+ if ($expected_head != "" and (.headRefOid // "") != $expected_head) then
162
+ "head_mismatch"
163
+ elif ((.statusCheckRollup // []) | length) == 0 then
164
+ "absent"
165
+ elif any((.statusCheckRollup // [])[]; check_run_failure or status_context_failure) then
166
+ "failed"
167
+ elif any((.statusCheckRollup // [])[]; check_run_pending or status_context_pending) then
168
+ "pending"
169
+ elif any((.statusCheckRollup // [])[]; check_run_nonpassing or status_context_nonpassing) then
170
+ "failed"
171
+ else
172
+ "passing"
173
+ end
174
+ ' 2>/dev/null || echo "unknown"
175
+ }
176
+
177
+ wait_for_ci_passing_on_head() {
178
+ local pr_number="${1}"
179
+ local expected_head="${2}"
180
+ local ci_waited=0
181
+ LAST_CI_STATUS="unknown"
182
+
183
+ while [ "${ci_waited}" -lt "${CI_MAX_WAIT}" ]; do
184
+ LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
185
+ if [ "${LAST_CI_STATUS}" = "passing" ]; then
186
+ return 0
187
+ fi
188
+ log "INFO: PR #${pr_number}: Waiting for fresh CI on head ${expected_head} (${LAST_CI_STATUS}, ${ci_waited}s/${CI_MAX_WAIT}s)..."
189
+ sleep "${CI_POLL_INTERVAL}"
190
+ ci_waited=$((ci_waited + CI_POLL_INTERVAL))
191
+ done
192
+
193
+ LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
194
+ [ "${LAST_CI_STATUS}" = "passing" ]
129
195
  }
130
196
 
131
197
  # Rebase a PR against its base branch
@@ -140,6 +206,23 @@ rebase_pr() {
140
206
  return $?
141
207
  }
142
208
 
209
+ cleanup_watchdog() {
210
+ local pid="${1:-}"
211
+ local child_pids=""
212
+
213
+ if [ -z "${pid}" ]; then
214
+ return 0
215
+ fi
216
+
217
+ child_pids=$(pgrep -P "${pid}" 2>/dev/null || true)
218
+ if [ -n "${child_pids}" ]; then
219
+ kill ${child_pids} 2>/dev/null || true
220
+ fi
221
+
222
+ kill "${pid}" 2>/dev/null || true
223
+ wait "${pid}" 2>/dev/null || true
224
+ }
225
+
143
226
  log() {
144
227
  echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "${LOG_FILE}"
145
228
  }
@@ -158,7 +241,7 @@ cd "${PROJECT_DIR}"
158
241
 
159
242
  log "========================================"
160
243
  log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
161
- log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
244
+ log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s ready_label=${READY_TO_MERGE_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
162
245
  log "========================================"
163
246
 
164
247
  if ! acquire_lock "${LOCK_FILE}"; then
@@ -187,7 +270,7 @@ fi
187
270
  kill -TERM $$ 2>/dev/null || true
188
271
  ) &
189
272
  WATCHDOG_PID=$!
190
- append_exit_trap "kill ${WATCHDOG_PID} 2>/dev/null || true"
273
+ append_exit_trap "cleanup_watchdog ${WATCHDOG_PID}"
191
274
 
192
275
  # Discover open PRs sorted by creation date (oldest first = FIFO)
193
276
  log "INFO: Scanning open PRs..."
@@ -224,15 +307,27 @@ while IFS= read -r pr_json; do
224
307
  continue
225
308
  fi
226
309
 
310
+ if csv_has_label "${pr_labels:-}" "${READY_TO_MERGE_LABEL}"; then
311
+ log "INFO: PR #${pr_number} (${pr_branch}): Skipping PR labeled ${READY_TO_MERGE_LABEL}"
312
+ continue
313
+ fi
314
+
227
315
  # Check branch pattern filter
228
316
  if ! matches_branch_patterns "${pr_branch}"; then
229
317
  log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
230
318
  continue
231
319
  fi
232
320
 
321
+ pr_head_oid=$(get_pr_head_oid "${pr_number}")
322
+ if [ -z "${pr_head_oid}" ]; then
323
+ log "INFO: PR #${pr_number} (${pr_branch}): Unable to determine PR head, skipping"
324
+ continue
325
+ fi
326
+
233
327
  # Check CI status
234
- if ! ci_passing "${pr_number}"; then
235
- log "INFO: PR #${pr_number} (${pr_branch}): CI not passing, skipping"
328
+ ci_status=$(ci_status_for_head "${pr_number}" "${pr_head_oid}")
329
+ if [ "${ci_status}" != "passing" ]; then
330
+ log "INFO: PR #${pr_number} (${pr_branch}): CI not passing on head ${pr_head_oid} (${ci_status}), skipping"
236
331
  continue
237
332
  fi
238
333
 
@@ -249,6 +344,7 @@ while IFS= read -r pr_json; do
249
344
 
250
345
  # Rebase before merge if configured
251
346
  if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
347
+ pr_head_before_rebase="${pr_head_oid}"
252
348
  if ! rebase_pr "${pr_number}"; then
253
349
  log "WARN: PR #${pr_number}: Rebase failed, skipping"
254
350
  FAILED_PRS=$((FAILED_PRS + 1))
@@ -256,20 +352,20 @@ while IFS= read -r pr_json; do
256
352
  fi
257
353
  log "INFO: PR #${pr_number}: Rebase successful"
258
354
 
259
- # Poll CI until all checks complete after rebase (up to 5 minutes)
260
- local ci_max_wait=300
261
- local ci_waited=0
262
- local ci_poll=15
263
- while [ "${ci_waited}" -lt "${ci_max_wait}" ]; do
264
- sleep "${ci_poll}"
265
- ci_waited=$((ci_waited + ci_poll))
266
- if ci_passing "${pr_number}"; then
267
- break
268
- fi
269
- log "INFO: PR #${pr_number}: Waiting for CI after rebase (${ci_waited}s/${ci_max_wait}s)..."
270
- done
271
- if ! ci_passing "${pr_number}"; then
272
- log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${ci_waited}s), skipping"
355
+ pr_head_after_rebase=$(get_pr_head_oid "${pr_number}")
356
+ if [ -z "${pr_head_after_rebase}" ]; then
357
+ log "INFO: PR #${pr_number}: Unable to determine PR head after rebase, skipping"
358
+ continue
359
+ fi
360
+ if [ "${pr_head_after_rebase}" != "${pr_head_before_rebase}" ]; then
361
+ log "INFO: PR #${pr_number}: Head changed after rebase ${pr_head_before_rebase} -> ${pr_head_after_rebase}; waiting for fresh CI"
362
+ else
363
+ log "INFO: PR #${pr_number}: Head unchanged after rebase (${pr_head_after_rebase}); confirming CI"
364
+ fi
365
+
366
+ # Poll CI until all checks attached to the post-rebase head are complete and passing.
367
+ if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
368
+ 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"
273
369
  continue
274
370
  fi
275
371
  fi
@@ -284,12 +380,17 @@ while IFS= read -r pr_json; do
284
380
  # Rebase remaining PRs after each successful merge
285
381
  log "INFO: Rebasing remaining open PRs after merging #${pr_number}..."
286
382
  REMAINING_JSON=$(gh pr list --state open \
287
- --json number,headRefName \
383
+ --json number,headRefName,labels \
288
384
  2>/dev/null || echo "[]")
289
385
  while IFS= read -r remaining_pr; do
290
386
  remaining_number=$(echo "${remaining_pr}" | jq -r '.number')
291
387
  remaining_branch=$(echo "${remaining_pr}" | jq -r '.headRefName')
388
+ remaining_labels=$(echo "${remaining_pr}" | jq -r '[.labels[]?.name] | join(",")')
292
389
  if [ "${remaining_number}" != "${pr_number}" ]; then
390
+ if csv_has_label "${remaining_labels:-}" "${READY_TO_MERGE_LABEL}"; then
391
+ log "INFO: Skipping post-merge rebase for PR #${remaining_number} (${remaining_branch}) because it is labeled ${READY_TO_MERGE_LABEL}"
392
+ continue
393
+ fi
293
394
  log "INFO: Rebasing remaining PR #${remaining_number} (${remaining_branch})"
294
395
  gh pr update-branch --rebase "${remaining_number}" 2>/dev/null || \
295
396
  log "WARN: PR #${remaining_number}: Rebase failed (continuing)"
@@ -61,6 +61,8 @@ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PRO
61
61
  PRD_DIR="${NW_PRD_DIR:-docs/PRDs}"
62
62
  PLAN_TASK="${NW_PLAN_TASK:-}"
63
63
 
64
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
65
+
64
66
  rotate_log
65
67
  log_separator
66
68
  log "RUN-START: planner invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
@@ -63,6 +63,7 @@ PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PRO
63
63
  # NOTE: Lock file path must match resolverLockPath() in src/utils/status-data.ts
64
64
  LOCK_FILE="/tmp/night-watch-pr-resolver-${PROJECT_RUNTIME_KEY}.lock"
65
65
  SCRIPT_TYPE="pr-resolver"
66
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
66
67
 
67
68
  emit_result() {
68
69
  local status="${1:?status required}"
@@ -359,6 +360,11 @@ while IFS= read -r pr_line; do
359
360
  continue
360
361
  fi
361
362
 
363
+ if csv_has_label "${labels:-}" "${READY_LABEL}"; then
364
+ log "INFO: Skipping PR #${pr_number} (${pr_branch}) because it is labeled ${READY_LABEL}"
365
+ continue
366
+ fi
367
+
362
368
  # Apply branch pattern filter
363
369
  if ! matches_branch_patterns "${pr_branch}"; then
364
370
  log "DEBUG: Skipping PR #${pr_number} — branch '${pr_branch}' does not match patterns" "patterns=${BRANCH_PATTERNS_RAW}"
@@ -63,11 +63,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63
63
  # shellcheck source=night-watch-helpers.sh
64
64
  source "${SCRIPT_DIR}/night-watch-helpers.sh"
65
65
 
66
- # Ensure provider CLI is on PATH (nvm, fnm, volta, common bin dirs)
67
- if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
68
- echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
69
- exit 127
70
- fi
71
66
  PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
72
67
  PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
73
68
  GLOBAL_LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}.lock"
@@ -79,6 +74,7 @@ else
79
74
  fi
80
75
 
81
76
  SCRIPT_TYPE="reviewer"
77
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
82
78
  READY_FOR_REVIEW_LABEL="${NW_READY_FOR_REVIEW_LABEL:-ready-for-review}"
83
79
  READY_FOR_REVIEW_MARKER_NAME="night-watch-ready-for-review"
84
80
  READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
@@ -93,6 +89,13 @@ emit_result() {
93
89
  fi
94
90
  }
95
91
 
92
+ require_provider_on_path() {
93
+ if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
94
+ echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
95
+ exit 127
96
+ fi
97
+ }
98
+
96
99
  extract_review_score_from_text() {
97
100
  local review_text="${1:-}"
98
101
  printf '%s' "${review_text}" \
@@ -853,6 +856,8 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
853
856
  exit 0
854
857
  fi
855
858
 
859
+ require_provider_on_path
860
+
856
861
  log "PARALLEL: Launching ${#PR_NUMBER_ARRAY[@]} reviewer worker(s)"
857
862
 
858
863
  declare -a WORKER_PIDS=()
@@ -1022,6 +1027,8 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
1022
1027
  exit 0
1023
1028
  fi
1024
1029
 
1030
+ require_provider_on_path
1031
+
1025
1032
  if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
1026
1033
  log "FAIL: Unable to create isolated reviewer worktree ${REVIEW_WORKTREE_DIR}"
1027
1034
  exit 1
@@ -1226,6 +1233,10 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1226
1233
  LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
1227
1234
  REVIEWER_ATTEMPT_START=$(date +%s)
1228
1235
  REVIEWER_PROMPT="${REVIEWER_PROMPT_BASE}${TARGET_SCOPE_PROMPT}${PRD_CONTEXT_PROMPT}"
1236
+ if [ -n "${NW_PROJECT_FEEDBACK_PROMPT:-}" ]; then
1237
+ REVIEWER_PROMPT="${REVIEWER_PROMPT}"$'\n\n'"${NW_PROJECT_FEEDBACK_PROMPT}"
1238
+ log "INFO: Added project feedback prompt context"
1239
+ fi
1229
1240
 
1230
1241
  # Build provider command array using generic helper
1231
1242
  mapfile -d '' -t PROVIDER_CMD_PARTS < <(build_provider_cmd "${REVIEW_WORKTREE_DIR}" "${REVIEWER_PROMPT}")
@@ -26,6 +26,7 @@ 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
28
  VALIDATED_LABEL="${NW_QA_VALIDATED_LABEL:-e2e-validated}"
29
+ READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
29
30
  QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
30
31
  QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
31
32
  SCRIPT_START_TIME=$(date +%s)
@@ -45,6 +46,7 @@ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
45
46
  # NOTE: Lock file path must match qaLockPath() in src/utils/status-data.ts
46
47
  LOCK_FILE="/tmp/night-watch-qa-${PROJECT_RUNTIME_KEY}.lock"
47
48
  SCRIPT_TYPE="qa"
49
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
48
50
 
49
51
  emit_result() {
50
52
  local status="${1:?status required}"
@@ -366,7 +368,7 @@ fi
366
368
  rotate_log
367
369
  log_separator
368
370
  log "RUN-START: qa invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
369
- log "CONFIG: max_runtime=${MAX_RUNTIME}s artifacts=${QA_ARTIFACTS} skip_label=${SKIP_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW}"
371
+ log "CONFIG: max_runtime=${MAX_RUNTIME}s artifacts=${QA_ARTIFACTS} skip_label=${SKIP_LABEL} ready_label=${READY_TO_MERGE_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW}"
370
372
 
371
373
  if ! acquire_lock "${LOCK_FILE}"; then
372
374
  emit_result "skip_locked"
@@ -431,6 +433,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
431
433
  continue
432
434
  fi
433
435
 
436
+ if csv_has_label "${pr_labels:-}" "${READY_TO_MERGE_LABEL}"; then
437
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) is labeled ${READY_TO_MERGE_LABEL}"
438
+ continue
439
+ fi
440
+
434
441
  # Skip PRs with the skip label
435
442
  if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
436
443
  log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
@@ -35,6 +35,7 @@ source "${SCRIPT_DIR}/night-watch-helpers.sh"
35
35
  ensure_provider_on_path "${PROVIDER_CMD}" || true
36
36
  PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
37
37
  LOCK_FILE="/tmp/night-watch-slicer-${PROJECT_RUNTIME_KEY}.lock"
38
+ SCRIPT_TYPE="slicer"
38
39
  PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
39
40
 
40
41
  emit_result() {
@@ -53,6 +54,8 @@ if ! validate_provider "${PROVIDER_CMD}"; then
53
54
  exit 1
54
55
  fi
55
56
 
57
+ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
58
+
56
59
  rotate_log
57
60
  log_separator
58
61
  log "RUN-START: slicer invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
@@ -55,6 +55,27 @@
55
55
  "audit": 10
56
56
  }
57
57
  },
58
+ "webhookTriggers": {
59
+ "enabled": false,
60
+ "secretEnv": "NIGHT_WATCH_WEBHOOK_SECRET",
61
+ "allowedJobIds": [
62
+ "executor",
63
+ "reviewer",
64
+ "pr-resolver",
65
+ "slicer",
66
+ "qa",
67
+ "audit",
68
+ "analytics",
69
+ "merger"
70
+ ],
71
+ "requireTimestamp": false,
72
+ "maxSkewSeconds": 300,
73
+ "github": {
74
+ "enabled": false,
75
+ "events": [],
76
+ "rules": []
77
+ }
78
+ },
58
79
  "jobProviders": {},
59
80
  "autoMerge": false,
60
81
  "autoMergeMethod": "squash",
@@ -32,6 +32,8 @@ The PRD directory is: `{{PRD_DIR}}`
32
32
 
33
33
  3. **Write a Complete PRD** — Follow the exact template structure below. Every section must be filled with concrete information. No placeholder text.
34
34
 
35
+ The PRD MUST include these sections: Context (with Problem, Files Analyzed, Current Behavior, Integration Points), Solution (with Approach, Key Decisions), Execution Phases (with Files, Implementation steps, Tests), and Acceptance Criteria.
36
+
35
37
  4. **Write the PRD File** — Use the Write tool to create the PRD file at `{{OUTPUT_FILE_PATH}}`.
36
38
 
37
39
  ---
@@ -146,6 +148,7 @@ sequenceDiagram
146
148
  ## 4. Execution Phases
147
149
 
148
150
  Critical rules:
151
+
149
152
  1. Each phase = ONE user-testable vertical slice
150
153
  2. Max 5 files per phase (split if larger)
151
154
  3. Each phase MUST include concrete tests