@jonit-dev/night-watch-cli 1.8.12-beta.0 → 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 (154) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +3281 -651
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/agent.d.ts +12 -0
  5. package/dist/commands/agent.d.ts.map +1 -0
  6. package/dist/commands/agent.js +307 -0
  7. package/dist/commands/agent.js.map +1 -0
  8. package/dist/commands/analytics.d.ts +14 -0
  9. package/dist/commands/analytics.d.ts.map +1 -1
  10. package/dist/commands/analytics.js +129 -0
  11. package/dist/commands/analytics.js.map +1 -0
  12. package/dist/commands/audit.d.ts +19 -0
  13. package/dist/commands/audit.d.ts.map +1 -1
  14. package/dist/commands/audit.js +189 -0
  15. package/dist/commands/audit.js.map +1 -0
  16. package/dist/commands/board.d.ts +9 -0
  17. package/dist/commands/board.js +702 -0
  18. package/dist/commands/board.js.map +1 -0
  19. package/dist/commands/cancel.d.ts +46 -0
  20. package/dist/commands/cancel.js +239 -0
  21. package/dist/commands/cancel.js.map +1 -0
  22. package/dist/commands/cron.d.ts +8 -0
  23. package/dist/commands/cron.js +134 -0
  24. package/dist/commands/cron.js.map +1 -0
  25. package/dist/commands/dashboard/tab-actions.d.ts +10 -0
  26. package/dist/commands/dashboard/tab-actions.js +247 -0
  27. package/dist/commands/dashboard/tab-actions.js.map +1 -0
  28. package/dist/commands/dashboard/tab-config.d.ts +21 -0
  29. package/dist/commands/dashboard/tab-config.js +874 -0
  30. package/dist/commands/dashboard/tab-config.js.map +1 -0
  31. package/dist/commands/dashboard/tab-logs.d.ts +10 -0
  32. package/dist/commands/dashboard/tab-logs.js +202 -0
  33. package/dist/commands/dashboard/tab-logs.js.map +1 -0
  34. package/dist/commands/dashboard/tab-schedules.d.ts +21 -0
  35. package/dist/commands/dashboard/tab-schedules.js +320 -0
  36. package/dist/commands/dashboard/tab-schedules.js.map +1 -0
  37. package/dist/commands/dashboard/tab-status.d.ts +32 -0
  38. package/dist/commands/dashboard/tab-status.js +424 -0
  39. package/dist/commands/dashboard/tab-status.js.map +1 -0
  40. package/dist/commands/dashboard/types.d.ts +42 -0
  41. package/dist/commands/dashboard/types.js +5 -0
  42. package/dist/commands/dashboard/types.js.map +1 -0
  43. package/dist/commands/dashboard.d.ts +11 -0
  44. package/dist/commands/dashboard.js +242 -0
  45. package/dist/commands/dashboard.js.map +1 -0
  46. package/dist/commands/doctor.d.ts +16 -0
  47. package/dist/commands/doctor.js +195 -0
  48. package/dist/commands/doctor.js.map +1 -0
  49. package/dist/commands/history.d.ts +7 -0
  50. package/dist/commands/history.js +49 -0
  51. package/dist/commands/history.js.map +1 -0
  52. package/dist/commands/init.d.ts +45 -0
  53. package/dist/commands/init.d.ts.map +1 -1
  54. package/dist/commands/init.js +13 -0
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/install.d.ts +65 -0
  57. package/dist/commands/install.js +405 -0
  58. package/dist/commands/install.js.map +1 -0
  59. package/dist/commands/logs.d.ts +15 -0
  60. package/dist/commands/logs.js +155 -0
  61. package/dist/commands/logs.js.map +1 -0
  62. package/dist/commands/merge.d.ts +26 -0
  63. package/dist/commands/merge.d.ts.map +1 -1
  64. package/dist/commands/merge.js +186 -0
  65. package/dist/commands/merge.js.map +1 -0
  66. package/dist/commands/notify.d.ts +7 -0
  67. package/dist/commands/notify.js +43 -0
  68. package/dist/commands/notify.js.map +1 -0
  69. package/dist/commands/plan.d.ts +19 -0
  70. package/dist/commands/plan.d.ts.map +1 -1
  71. package/dist/commands/plan.js +133 -0
  72. package/dist/commands/plan.js.map +1 -0
  73. package/dist/commands/prd-state.d.ts +12 -0
  74. package/dist/commands/prd-state.js +47 -0
  75. package/dist/commands/prd-state.js.map +1 -0
  76. package/dist/commands/prd.d.ts +18 -0
  77. package/dist/commands/prd.js +363 -0
  78. package/dist/commands/prd.js.map +1 -0
  79. package/dist/commands/prds.d.ts +13 -0
  80. package/dist/commands/prds.js +194 -0
  81. package/dist/commands/prds.js.map +1 -0
  82. package/dist/commands/prs.d.ts +14 -0
  83. package/dist/commands/prs.js +104 -0
  84. package/dist/commands/prs.js.map +1 -0
  85. package/dist/commands/qa.d.ts +34 -0
  86. package/dist/commands/qa.d.ts.map +1 -1
  87. package/dist/commands/qa.js +238 -0
  88. package/dist/commands/qa.js.map +1 -0
  89. package/dist/commands/queue.d.ts +8 -0
  90. package/dist/commands/queue.d.ts.map +1 -1
  91. package/dist/commands/queue.js +398 -0
  92. package/dist/commands/queue.js.map +1 -0
  93. package/dist/commands/resolve.d.ts +26 -0
  94. package/dist/commands/resolve.d.ts.map +1 -1
  95. package/dist/commands/resolve.js +212 -0
  96. package/dist/commands/resolve.js.map +1 -0
  97. package/dist/commands/retry.d.ts +9 -0
  98. package/dist/commands/retry.js +71 -0
  99. package/dist/commands/retry.js.map +1 -0
  100. package/dist/commands/review.d.ts +79 -0
  101. package/dist/commands/review.d.ts.map +1 -1
  102. package/dist/commands/review.js +47 -75
  103. package/dist/commands/review.js.map +1 -1
  104. package/dist/commands/run.d.ts +88 -0
  105. package/dist/commands/run.d.ts.map +1 -1
  106. package/dist/commands/run.js +85 -1
  107. package/dist/commands/run.js.map +1 -1
  108. package/dist/commands/serve.d.ts +19 -0
  109. package/dist/commands/serve.js +142 -0
  110. package/dist/commands/serve.js.map +1 -0
  111. package/dist/commands/shared/env-builder.d.ts +49 -0
  112. package/dist/commands/shared/env-builder.d.ts.map +1 -1
  113. package/dist/commands/shared/env-builder.js +152 -0
  114. package/dist/commands/shared/env-builder.js.map +1 -0
  115. package/dist/commands/shared/feedback.d.ts +24 -0
  116. package/dist/commands/shared/feedback.d.ts.map +1 -0
  117. package/dist/commands/shared/feedback.js +38 -0
  118. package/dist/commands/shared/feedback.js.map +1 -0
  119. package/dist/commands/slice.d.ts +35 -0
  120. package/dist/commands/slice.d.ts.map +1 -1
  121. package/dist/commands/slice.js +363 -0
  122. package/dist/commands/slice.js.map +1 -0
  123. package/dist/commands/state.d.ts +8 -0
  124. package/dist/commands/state.js +54 -0
  125. package/dist/commands/state.js.map +1 -0
  126. package/dist/commands/status.d.ts +14 -0
  127. package/dist/commands/status.js +297 -0
  128. package/dist/commands/status.js.map +1 -0
  129. package/dist/commands/summary.d.ts +14 -0
  130. package/dist/commands/summary.js +193 -0
  131. package/dist/commands/summary.js.map +1 -0
  132. package/dist/commands/uninstall.d.ts +25 -0
  133. package/dist/commands/uninstall.js +134 -0
  134. package/dist/commands/uninstall.js.map +1 -0
  135. package/dist/commands/update.d.ts +22 -0
  136. package/dist/commands/update.js +90 -0
  137. package/dist/commands/update.js.map +1 -0
  138. package/dist/scripts/night-watch-audit-cron.sh +1 -0
  139. package/dist/scripts/night-watch-cron.sh +186 -23
  140. package/dist/scripts/night-watch-helpers.sh +65 -2
  141. package/dist/scripts/night-watch-merger-cron.sh +137 -36
  142. package/dist/scripts/night-watch-plan-cron.sh +2 -0
  143. package/dist/scripts/night-watch-pr-resolver-cron.sh +6 -0
  144. package/dist/scripts/night-watch-pr-reviewer-cron.sh +22 -5
  145. package/dist/scripts/night-watch-qa-cron.sh +8 -1
  146. package/dist/scripts/night-watch-slicer-cron.sh +3 -0
  147. package/dist/templates/night-watch.config.json +21 -20
  148. package/dist/templates/slicer.md +3 -0
  149. package/dist/web/assets/index-C-xpWpS8.css +1 -0
  150. package/dist/web/assets/index-CEYe-290.js +412 -0
  151. package/dist/web/assets/index-DpvzoXEv.js +442 -0
  152. package/dist/web/assets/index-DyME41HV.css +1 -0
  153. package/dist/web/index.html +2 -2
  154. package/package.json +3 -1
@@ -126,6 +126,7 @@ project_git_push_command() {
126
126
  NW_EXECUTOR_PARTIAL_LABEL="${NW_EXECUTOR_PARTIAL_LABEL:-nw:partial}"
127
127
  NW_EXECUTOR_RESUMABLE_LABEL="${NW_EXECUTOR_RESUMABLE_LABEL:-nw:resumable}"
128
128
  NW_EXECUTOR_READY_REVIEW_LABEL="${NW_EXECUTOR_READY_REVIEW_LABEL:-nw:ready-review}"
129
+ NW_PR_RESOLVER_READY_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
129
130
 
130
131
  csv_has_label() {
131
132
  local csv="${1:-}"
@@ -188,7 +189,8 @@ find_executor_resume_pr() {
188
189
  printf '%s' "${pr_list}" \
189
190
  | jq -c \
190
191
  --arg primary_prefix "${branch_prefix}/" \
191
- --arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" '
192
+ --arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" \
193
+ --arg ready_label "${NW_PR_RESOLVER_READY_LABEL}" '
192
194
  [
193
195
  .[]
194
196
  | select(
@@ -198,6 +200,7 @@ find_executor_resume_pr() {
198
200
  )
199
201
  | .labelNames = ((.labels // []) | map(.name))
200
202
  | select((.labelNames | index($resumable_label)) != null)
203
+ | select((.labelNames | index($ready_label)) == null)
201
204
  ]
202
205
  | sort_by(.createdAt // "")
203
206
  | .[0] // empty
@@ -1243,8 +1246,40 @@ or set primaryFallbackPreset / primaryFallbackModel in night-watch.config.json."
1243
1246
 
1244
1247
  # ── Board mode issue discovery ────────────────────────────────────────────────
1245
1248
 
1249
+ is_audit_board_issue_json() {
1250
+ local issue_json="${1:?issue_json required}"
1251
+
1252
+ printf '%s' "${issue_json}" \
1253
+ | jq -r '[.title // "", .body // ""] | join("\n")' 2>/dev/null \
1254
+ | grep -Eiq '(^|[[:space:]])Audit:|Night Watch audit detected|Report:[[:space:]]+`?logs/audit-report\.md`?|Finding:[[:space:]]+[0-9]+'
1255
+ }
1256
+
1257
+ is_audit_paused_or_disabled() {
1258
+ local cli_bin="${1:?cli_bin required}"
1259
+ local project_dir="${2:?project_dir required}"
1260
+
1261
+ if (cd "${project_dir}" && "${cli_bin}" job is-paused audit >/dev/null 2>&1); then
1262
+ return 0
1263
+ fi
1264
+
1265
+ local audit_enabled
1266
+ audit_enabled=$(
1267
+ cd "${project_dir}" \
1268
+ && "${cli_bin}" config get audit.enabled 2>/dev/null \
1269
+ | tr '[:upper:]' '[:lower:]' \
1270
+ | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
1271
+ ) || true
1272
+
1273
+ if [ "${audit_enabled}" = "false" ]; then
1274
+ return 0
1275
+ fi
1276
+
1277
+ return 1
1278
+ }
1279
+
1246
1280
  # Get the next eligible issue from the board provider.
1247
- # Iterates through Ready issues sorted by priority, skipping any in cooldown.
1281
+ # Iterates through Ready issues sorted by priority, skipping audit findings when
1282
+ # audit is disabled/paused and skipping any in cooldown.
1248
1283
  # Prints the JSON of the first eligible issue to stdout, or nothing if none found.
1249
1284
  # Returns 0 on success, 1 if no Ready issues were returned, 2 if Ready issues
1250
1285
  # were found but every candidate was skipped due to cooldown.
@@ -1292,6 +1327,16 @@ find_eligible_board_issue() {
1292
1327
  number=$(printf '%s' "${issue}" | jq -r '.number' 2>/dev/null)
1293
1328
  title=$(printf '%s' "${issue}" | jq -r '.title // empty' 2>/dev/null)
1294
1329
 
1330
+ # Skip stale audit findings while the audit job is paused/disabled. Audit
1331
+ # cron/config gates stop new scans, but Ready board issues can outlive them.
1332
+ if [ -n "${project_dir}" ] \
1333
+ && is_audit_board_issue_json "${issue}" \
1334
+ && is_audit_paused_or_disabled "${cli_bin}" "${project_dir}"; then
1335
+ log "SKIP-BOARD: Issue #${number} — audit issue skipped because audit is paused or disabled"
1336
+ i=$((i + 1))
1337
+ continue
1338
+ fi
1339
+
1295
1340
  # Derive PRD name (same slug format used by the cron script for history keys)
1296
1341
  prd_name="${number}-$(printf '%s' "${title}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
1297
1342
 
@@ -1335,6 +1380,22 @@ arm_global_queue_cleanup() {
1335
1380
  append_exit_trap "__night_watch_queue_cleanup \$?"
1336
1381
  }
1337
1382
 
1383
+ skip_if_job_paused() {
1384
+ local script_type="${1:?script_type required}"
1385
+ local project_dir="${2:?project_dir required}"
1386
+
1387
+ local cli_bin
1388
+ cli_bin=$(resolve_night_watch_cli) || return 0
1389
+
1390
+ if (cd "${project_dir}" && "${cli_bin}" job is-paused "${script_type}" >/dev/null 2>&1); then
1391
+ log "SKIP: ${script_type} is paused in night-watch.config.json"
1392
+ if command -v emit_result >/dev/null 2>&1; then
1393
+ emit_result "skip_paused"
1394
+ fi
1395
+ exit 0
1396
+ fi
1397
+ }
1398
+
1338
1399
  # Atomically claim a queue slot or enqueue for later dispatch.
1339
1400
  # Uses DB transaction (via `queue claim` CLI) for atomicity — no flock needed.
1340
1401
  # Sets NW_QUEUE_ENTRY_ID on success and arms the cleanup trap.
@@ -1342,6 +1403,8 @@ arm_global_queue_cleanup() {
1342
1403
  claim_or_enqueue() {
1343
1404
  local script_type="${1:?script_type required}"
1344
1405
  local project_dir="${2:?project_dir required}"
1406
+ skip_if_job_paused "${script_type}" "${project_dir}"
1407
+
1345
1408
  local provider_key
1346
1409
  provider_key=$(resolve_provider_key "${project_dir}" "${script_type}")
1347
1410
 
@@ -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,8 +74,10 @@ 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"
80
+ READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
84
81
 
85
82
  emit_result() {
86
83
  local status="${1:?status required}"
@@ -92,6 +89,13 @@ emit_result() {
92
89
  fi
93
90
  }
94
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
+
95
99
  extract_review_score_from_text() {
96
100
  local review_text="${1:-}"
97
101
  printf '%s' "${review_text}" \
@@ -701,6 +705,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
701
705
  continue
702
706
  fi
703
707
 
708
+ if csv_has_label "${pr_labels:-}" "${READY_TO_MERGE_LABEL}"; then
709
+ log "INFO: PR #${pr_number} (${pr_branch}) is labeled ${READY_TO_MERGE_LABEL}; skipping automated review"
710
+ continue
711
+ fi
712
+
704
713
  if printf '%s\n' "${pr_labels:-}" | tr ',' '\n' | grep -Fxq 'needs-human-review'; then
705
714
  log "INFO: PR #${pr_number} (${pr_branch}) is labeled needs-human-review; skipping automated review"
706
715
  continue
@@ -847,6 +856,8 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
847
856
  exit 0
848
857
  fi
849
858
 
859
+ require_provider_on_path
860
+
850
861
  log "PARALLEL: Launching ${#PR_NUMBER_ARRAY[@]} reviewer worker(s)"
851
862
 
852
863
  declare -a WORKER_PIDS=()
@@ -1016,6 +1027,8 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
1016
1027
  exit 0
1017
1028
  fi
1018
1029
 
1030
+ require_provider_on_path
1031
+
1019
1032
  if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
1020
1033
  log "FAIL: Unable to create isolated reviewer worktree ${REVIEW_WORKTREE_DIR}"
1021
1034
  exit 1
@@ -1220,6 +1233,10 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1220
1233
  LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
1221
1234
  REVIEWER_ATTEMPT_START=$(date +%s)
1222
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
1223
1240
 
1224
1241
  # Build provider command array using generic helper
1225
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,9 +55,28 @@
55
55
  "audit": 10
56
56
  }
57
57
  },
58
- "jobProviders": {
59
- "reviewer": "g51claude"
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
+ }
60
78
  },
79
+ "jobProviders": {},
61
80
  "autoMerge": false,
62
81
  "autoMergeMethod": "squash",
63
82
  "fallbackOnRateLimit": true,
@@ -75,23 +94,5 @@
75
94
  "enabled": true,
76
95
  "schedule": "50 3 * * 1",
77
96
  "maxRuntime": 1800
78
- },
79
- "providerPresets": {
80
- "g51claude": {
81
- "name": "GLM-5.1 Claude",
82
- "command": "claude",
83
- "promptFlag": "-p",
84
- "autoApproveFlag": "--dangerously-skip-permissions",
85
- "modelFlag": "--model",
86
- "model": "glm-5.1",
87
- "envVars": {
88
- "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
89
- "API_TIMEOUT_MS": "3000000",
90
- "ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.1",
91
- "ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-5.1",
92
- "ANTHROPIC_AUTH_TOKEN": "efaeca456dab4ae69cb9ce1fa8d99a1a.KSTifNTWPS6NgC6L",
93
- "ANTHROPIC_API_KEY": "efaeca456dab4ae69cb9ce1fa8d99a1a.KSTifNTWPS6NgC6L"
94
- }
95
- }
96
97
  }
97
98
  }
@@ -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