@jonit-dev/night-watch-cli 1.8.10-beta.1 → 1.8.10-beta.12

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 (209) hide show
  1. package/dist/cli.js +2453 -1378
  2. package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
  3. package/dist/commands/queue.d.ts.map +1 -1
  4. package/dist/commands/shared/env-builder.d.ts.map +1 -1
  5. package/dist/scripts/night-watch-audit-cron.sh +6 -0
  6. package/dist/scripts/night-watch-cron.sh +352 -53
  7. package/dist/scripts/night-watch-helpers.sh +212 -12
  8. package/dist/scripts/night-watch-merger-cron.sh +8 -2
  9. package/dist/scripts/night-watch-plan-cron.sh +1 -1
  10. package/dist/scripts/night-watch-pr-resolver-cron.sh +7 -3
  11. package/dist/scripts/night-watch-pr-reviewer-cron.sh +11 -0
  12. package/dist/scripts/night-watch-qa-cron.sh +11 -0
  13. package/dist/scripts/night-watch-slicer-cron.sh +1 -1
  14. package/dist/web/assets/index-B6E6kOoR.js +406 -0
  15. package/dist/web/assets/index-DIMUXIP8.css +1 -0
  16. package/dist/web/assets/index-Ds8OqaCa.css +1 -0
  17. package/dist/web/assets/index-NR27JE3b.js +406 -0
  18. package/dist/web/index.html +2 -2
  19. package/package.json +1 -1
  20. package/dist/cli.d.ts +0 -3
  21. package/dist/cli.js.map +0 -1
  22. package/dist/commands/analytics.d.ts +0 -14
  23. package/dist/commands/analytics.js +0 -69
  24. package/dist/commands/analytics.js.map +0 -1
  25. package/dist/commands/audit.d.ts +0 -19
  26. package/dist/commands/audit.js +0 -144
  27. package/dist/commands/audit.js.map +0 -1
  28. package/dist/commands/board.d.ts +0 -9
  29. package/dist/commands/board.js +0 -702
  30. package/dist/commands/board.js.map +0 -1
  31. package/dist/commands/cancel.d.ts +0 -46
  32. package/dist/commands/cancel.js +0 -239
  33. package/dist/commands/cancel.js.map +0 -1
  34. package/dist/commands/cron.d.ts +0 -8
  35. package/dist/commands/cron.js +0 -134
  36. package/dist/commands/cron.js.map +0 -1
  37. package/dist/commands/dashboard/tab-actions.d.ts +0 -10
  38. package/dist/commands/dashboard/tab-actions.js +0 -247
  39. package/dist/commands/dashboard/tab-actions.js.map +0 -1
  40. package/dist/commands/dashboard/tab-config.d.ts +0 -21
  41. package/dist/commands/dashboard/tab-config.js +0 -873
  42. package/dist/commands/dashboard/tab-config.js.map +0 -1
  43. package/dist/commands/dashboard/tab-logs.d.ts +0 -10
  44. package/dist/commands/dashboard/tab-logs.js +0 -202
  45. package/dist/commands/dashboard/tab-logs.js.map +0 -1
  46. package/dist/commands/dashboard/tab-schedules.d.ts +0 -21
  47. package/dist/commands/dashboard/tab-schedules.js +0 -320
  48. package/dist/commands/dashboard/tab-schedules.js.map +0 -1
  49. package/dist/commands/dashboard/tab-status.d.ts +0 -32
  50. package/dist/commands/dashboard/tab-status.js +0 -424
  51. package/dist/commands/dashboard/tab-status.js.map +0 -1
  52. package/dist/commands/dashboard/types.d.ts +0 -42
  53. package/dist/commands/dashboard/types.js +0 -5
  54. package/dist/commands/dashboard/types.js.map +0 -1
  55. package/dist/commands/dashboard.d.ts +0 -11
  56. package/dist/commands/dashboard.js +0 -242
  57. package/dist/commands/dashboard.js.map +0 -1
  58. package/dist/commands/doctor.d.ts +0 -16
  59. package/dist/commands/doctor.js +0 -195
  60. package/dist/commands/doctor.js.map +0 -1
  61. package/dist/commands/history.d.ts +0 -7
  62. package/dist/commands/history.js +0 -49
  63. package/dist/commands/history.js.map +0 -1
  64. package/dist/commands/init.d.ts +0 -45
  65. package/dist/commands/init.js +0 -777
  66. package/dist/commands/init.js.map +0 -1
  67. package/dist/commands/install.d.ts +0 -65
  68. package/dist/commands/install.js +0 -405
  69. package/dist/commands/install.js.map +0 -1
  70. package/dist/commands/logs.d.ts +0 -15
  71. package/dist/commands/logs.js +0 -155
  72. package/dist/commands/logs.js.map +0 -1
  73. package/dist/commands/merge.d.ts +0 -26
  74. package/dist/commands/merge.js +0 -159
  75. package/dist/commands/merge.js.map +0 -1
  76. package/dist/commands/notify.d.ts +0 -7
  77. package/dist/commands/notify.js +0 -43
  78. package/dist/commands/notify.js.map +0 -1
  79. package/dist/commands/plan.d.ts +0 -19
  80. package/dist/commands/plan.js +0 -88
  81. package/dist/commands/plan.js.map +0 -1
  82. package/dist/commands/prd-state.d.ts +0 -12
  83. package/dist/commands/prd-state.js +0 -47
  84. package/dist/commands/prd-state.js.map +0 -1
  85. package/dist/commands/prd.d.ts +0 -18
  86. package/dist/commands/prd.js +0 -363
  87. package/dist/commands/prd.js.map +0 -1
  88. package/dist/commands/prds.d.ts +0 -13
  89. package/dist/commands/prds.js +0 -194
  90. package/dist/commands/prds.js.map +0 -1
  91. package/dist/commands/prs.d.ts +0 -14
  92. package/dist/commands/prs.js +0 -104
  93. package/dist/commands/prs.js.map +0 -1
  94. package/dist/commands/qa.d.ts +0 -34
  95. package/dist/commands/qa.js +0 -214
  96. package/dist/commands/qa.js.map +0 -1
  97. package/dist/commands/queue.d.ts +0 -8
  98. package/dist/commands/queue.js +0 -376
  99. package/dist/commands/queue.js.map +0 -1
  100. package/dist/commands/resolve.d.ts +0 -26
  101. package/dist/commands/resolve.js +0 -186
  102. package/dist/commands/resolve.js.map +0 -1
  103. package/dist/commands/retry.d.ts +0 -9
  104. package/dist/commands/retry.js +0 -71
  105. package/dist/commands/retry.js.map +0 -1
  106. package/dist/commands/review.d.ts +0 -82
  107. package/dist/commands/review.js +0 -479
  108. package/dist/commands/review.js.map +0 -1
  109. package/dist/commands/run.d.ts +0 -73
  110. package/dist/commands/run.js +0 -509
  111. package/dist/commands/run.js.map +0 -1
  112. package/dist/commands/serve.d.ts +0 -19
  113. package/dist/commands/serve.js +0 -142
  114. package/dist/commands/serve.js.map +0 -1
  115. package/dist/commands/shared/env-builder.d.ts +0 -49
  116. package/dist/commands/shared/env-builder.js +0 -150
  117. package/dist/commands/shared/env-builder.js.map +0 -1
  118. package/dist/commands/slice.d.ts +0 -35
  119. package/dist/commands/slice.js +0 -316
  120. package/dist/commands/slice.js.map +0 -1
  121. package/dist/commands/state.d.ts +0 -8
  122. package/dist/commands/state.js +0 -54
  123. package/dist/commands/state.js.map +0 -1
  124. package/dist/commands/status.d.ts +0 -14
  125. package/dist/commands/status.js +0 -297
  126. package/dist/commands/status.js.map +0 -1
  127. package/dist/commands/summary.d.ts +0 -14
  128. package/dist/commands/summary.js +0 -193
  129. package/dist/commands/summary.js.map +0 -1
  130. package/dist/commands/uninstall.d.ts +0 -25
  131. package/dist/commands/uninstall.js +0 -134
  132. package/dist/commands/uninstall.js.map +0 -1
  133. package/dist/commands/update.d.ts +0 -22
  134. package/dist/commands/update.js +0 -90
  135. package/dist/commands/update.js.map +0 -1
  136. package/dist/web/assets/index-2JY0x_Ij.js +0 -381
  137. package/dist/web/assets/index-3h8pgmqL.css +0 -1
  138. package/dist/web/assets/index-B-wbyZq7.js +0 -386
  139. package/dist/web/assets/index-B1BnOpiO.css +0 -1
  140. package/dist/web/assets/index-B3CnV08_.js +0 -365
  141. package/dist/web/assets/index-B5QjuFh9.css +0 -1
  142. package/dist/web/assets/index-B8FW2ecQ.js +0 -370
  143. package/dist/web/assets/index-BFxPiKyy.js +0 -381
  144. package/dist/web/assets/index-BGqNh_Da.js +0 -365
  145. package/dist/web/assets/index-BIONU0qz.css +0 -1
  146. package/dist/web/assets/index-B_l_3wnA.js +0 -370
  147. package/dist/web/assets/index-Ba-4YvTQ.js +0 -365
  148. package/dist/web/assets/index-Bbb4-39N.js +0 -370
  149. package/dist/web/assets/index-BdgdShEN.js +0 -365
  150. package/dist/web/assets/index-BhiC4Z-G.js +0 -381
  151. package/dist/web/assets/index-BjhCFjZi.js +0 -381
  152. package/dist/web/assets/index-BlRxmrnQ.css +0 -1
  153. package/dist/web/assets/index-BqwbXsHS.js +0 -365
  154. package/dist/web/assets/index-BsC7RT48.css +0 -1
  155. package/dist/web/assets/index-Bvh8XI8_.js +0 -370
  156. package/dist/web/assets/index-C01r2ymn.js +0 -381
  157. package/dist/web/assets/index-C51Rbsmk.js +0 -381
  158. package/dist/web/assets/index-C7lMNxRE.js +0 -370
  159. package/dist/web/assets/index-CJLObgsn.js +0 -386
  160. package/dist/web/assets/index-CLuRf7Zt.js +0 -381
  161. package/dist/web/assets/index-CM3xFd3e.css +0 -1
  162. package/dist/web/assets/index-CNkBtDK7.js +0 -370
  163. package/dist/web/assets/index-CPQbZ1BL.css +0 -1
  164. package/dist/web/assets/index-CTy5dUDU.css +0 -1
  165. package/dist/web/assets/index-CU15COKs.js +0 -370
  166. package/dist/web/assets/index-CiRJZI4z.js +0 -386
  167. package/dist/web/assets/index-Cp7RYjoy.css +0 -1
  168. package/dist/web/assets/index-CvPkZOWT.js +0 -381
  169. package/dist/web/assets/index-CvUk-33B.css +0 -1
  170. package/dist/web/assets/index-Cvmj-oF6.css +0 -1
  171. package/dist/web/assets/index-CxE5iQVO.js +0 -381
  172. package/dist/web/assets/index-D7lZQpFV.js +0 -365
  173. package/dist/web/assets/index-DAyP4GOi.css +0 -1
  174. package/dist/web/assets/index-DCG0n8Kg.js +0 -386
  175. package/dist/web/assets/index-DEEI8cyF.css +0 -1
  176. package/dist/web/assets/index-DF99BowV.js +0 -381
  177. package/dist/web/assets/index-DGWsvFj6.css +0 -1
  178. package/dist/web/assets/index-DGpU39Cp.css +0 -1
  179. package/dist/web/assets/index-DI4kFgOi.js +0 -370
  180. package/dist/web/assets/index-DIyTcPw5.css +0 -1
  181. package/dist/web/assets/index-DTsfDC7m.js +0 -381
  182. package/dist/web/assets/index-DcgNAi4A.js +0 -386
  183. package/dist/web/assets/index-DgOAgkZy.css +0 -1
  184. package/dist/web/assets/index-DnHkqbOa.js +0 -386
  185. package/dist/web/assets/index-DnR7Idcf.css +0 -1
  186. package/dist/web/assets/index-DpVirMEe.css +0 -1
  187. package/dist/web/assets/index-DsYIWZ86.css +0 -1
  188. package/dist/web/assets/index-DtrDkci5.js +0 -381
  189. package/dist/web/assets/index-DyjIth5M.js +0 -386
  190. package/dist/web/assets/index-FwIKfHPL.css +0 -1
  191. package/dist/web/assets/index-IKrZymWk.css +0 -1
  192. package/dist/web/assets/index-MA6fM0ab.js +0 -381
  193. package/dist/web/assets/index-N_QxaSEg.css +0 -1
  194. package/dist/web/assets/index-OcU-0TCQ.css +0 -1
  195. package/dist/web/assets/index-OyhrmG-L.js +0 -381
  196. package/dist/web/assets/index-SQlBKu_s.js +0 -386
  197. package/dist/web/assets/index-Sv2B60J4.js +0 -370
  198. package/dist/web/assets/index-Vgyivb5u.js +0 -365
  199. package/dist/web/assets/index-ZABWMEZR.js +0 -381
  200. package/dist/web/assets/index-ZE5lOeJp.js +0 -386
  201. package/dist/web/assets/index-aCHmkAcJ.css +0 -1
  202. package/dist/web/assets/index-bFijnpuU.js +0 -381
  203. package/dist/web/assets/index-bUPZgSoZ.css +0 -1
  204. package/dist/web/assets/index-mz1VIYsP.css +0 -1
  205. package/dist/web/assets/index-oOp_MFeE.js +0 -376
  206. package/dist/web/assets/index-rfU713Zm.js +0 -386
  207. package/dist/web/assets/index-tuNH9gmb.js +0 -448
  208. package/dist/web/assets/index-viSwHyDD.js +0 -365
  209. package/dist/web/assets/index-yKEQysks.js +0 -365
@@ -87,6 +87,123 @@ validate_provider() {
87
87
  return 1
88
88
  }
89
89
 
90
+ night_watch_push_uses_no_verify() {
91
+ [ "${NW_GIT_PUSH_NO_VERIFY:-0}" = "1" ]
92
+ }
93
+
94
+ git_push_for_project() {
95
+ local repo_dir="${1:?repo_dir required}"
96
+ shift
97
+
98
+ if night_watch_push_uses_no_verify; then
99
+ git -C "${repo_dir}" push --no-verify "$@"
100
+ else
101
+ git -C "${repo_dir}" push "$@"
102
+ fi
103
+ }
104
+
105
+ project_git_push_command() {
106
+ local branch_name="${1:?branch_name required}"
107
+ local mode="${2:-normal}"
108
+ local no_verify=""
109
+
110
+ if night_watch_push_uses_no_verify; then
111
+ no_verify=" --no-verify"
112
+ fi
113
+
114
+ case "${mode}" in
115
+ force-with-lease)
116
+ printf 'git push%s --force-with-lease origin %s' "${no_verify}" "${branch_name}"
117
+ ;;
118
+ *)
119
+ printf 'git push%s origin %s' "${no_verify}" "${branch_name}"
120
+ ;;
121
+ esac
122
+ }
123
+
124
+ # ── Executor PR status labels ────────────────────────────────────────────────
125
+
126
+ NW_EXECUTOR_PARTIAL_LABEL="${NW_EXECUTOR_PARTIAL_LABEL:-nw:partial}"
127
+ NW_EXECUTOR_RESUMABLE_LABEL="${NW_EXECUTOR_RESUMABLE_LABEL:-nw:resumable}"
128
+ NW_EXECUTOR_READY_REVIEW_LABEL="${NW_EXECUTOR_READY_REVIEW_LABEL:-nw:ready-review}"
129
+
130
+ csv_has_label() {
131
+ local csv="${1:-}"
132
+ local label="${2:?label required}"
133
+
134
+ printf '%s\n' "${csv}" \
135
+ | tr ',' '\n' \
136
+ | sed '/^[[:space:]]*$/d' \
137
+ | grep -Fxq "${label}"
138
+ }
139
+
140
+ ensure_github_label() {
141
+ local label_name="${1:?label_name required}"
142
+ local description="${2:-}"
143
+ local color="${3:-1d76db}"
144
+
145
+ gh label create "${label_name}" \
146
+ --description "${description}" \
147
+ --color "${color}" \
148
+ --force 2>/dev/null || true
149
+ }
150
+
151
+ ensure_executor_status_labels() {
152
+ ensure_github_label \
153
+ "${NW_EXECUTOR_PARTIAL_LABEL}" \
154
+ "Executor started this PR and work is intentionally incomplete" \
155
+ "fbca04"
156
+ ensure_github_label \
157
+ "${NW_EXECUTOR_RESUMABLE_LABEL}" \
158
+ "Executor should resume this unfinished PR before starting new work" \
159
+ "d93f0b"
160
+ ensure_github_label \
161
+ "${NW_EXECUTOR_READY_REVIEW_LABEL}" \
162
+ "Executor finished implementation and the PR is ready for automated/human review" \
163
+ "0e8a16"
164
+ }
165
+
166
+ find_open_pr_for_branch() {
167
+ local branch_name="${1:?branch_name required}"
168
+ local pr_list=""
169
+
170
+ pr_list=$(gh pr list --state open --limit 200 \
171
+ --json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
172
+
173
+ printf '%s' "${pr_list}" \
174
+ | jq -c --arg branch_name "${branch_name}" '
175
+ .[]
176
+ | select((.headRefName // "") == $branch_name)
177
+ ' 2>/dev/null \
178
+ | head -n 1 || true
179
+ }
180
+
181
+ find_executor_resume_pr() {
182
+ local branch_prefix="${1:-night-watch}"
183
+ local pr_list=""
184
+
185
+ pr_list=$(gh pr list --state open --limit 200 \
186
+ --json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
187
+
188
+ printf '%s' "${pr_list}" \
189
+ | jq -c \
190
+ --arg primary_prefix "${branch_prefix}/" \
191
+ --arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" '
192
+ [
193
+ .[]
194
+ | select(
195
+ (.headRefName // "" | startswith($primary_prefix))
196
+ or
197
+ (.headRefName // "" | startswith("feat/"))
198
+ )
199
+ | .labelNames = ((.labels // []) | map(.name))
200
+ | select((.labelNames | index($resumable_label)) != null)
201
+ ]
202
+ | sort_by(.createdAt // "")
203
+ | .[0] // empty
204
+ ' 2>/dev/null || true
205
+ }
206
+
90
207
  # ── Generic Provider Command Builder ──────────────────────────────────────────
91
208
 
92
209
  # Build a provider command from NW_PROVIDER_* environment variables.
@@ -180,6 +297,13 @@ resolve_night_watch_cli() {
180
297
 
181
298
  local bundled_bin="${script_dir}/../bin/night-watch.mjs"
182
299
  if [ -x "${bundled_bin}" ]; then
300
+ # Verify dist/ exists — prevents ERR_MODULE_NOT_FOUND crashes
301
+ local dist_dir
302
+ dist_dir="$(cd "$(dirname "${bundled_bin}")" && pwd)/../dist"
303
+ if [ ! -d "${dist_dir}" ]; then
304
+ echo "ERROR: night-watch dist/ not found at ${dist_dir}; run 'yarn build'" >&2
305
+ return 1
306
+ fi
183
307
  printf "%s" "${bundled_bin}"
184
308
  return 0
185
309
  fi
@@ -360,33 +484,71 @@ acquire_lock() {
360
484
  local lock_file="${1:?lock_file required}"
361
485
 
362
486
  if [ -f "${lock_file}" ]; then
363
- local lock_pid
364
- lock_pid=$(cat "${lock_file}" 2>/dev/null || echo "")
487
+ local lock_content lock_pid lock_ts
488
+ lock_content=$(cat "${lock_file}" 2>/dev/null || echo "")
489
+ lock_pid=$(echo "${lock_content}" | awk '{print $1}')
490
+ lock_ts=$(echo "${lock_content}" | awk '{print $2}')
491
+
365
492
  if [ -n "${lock_pid}" ] && kill -0 "${lock_pid}" 2>/dev/null; then
366
- log "SKIP: Previous run (PID ${lock_pid}) still active"
367
- return 1
493
+ # PID is alive — but guard against PID reuse via /proc start time
494
+ if _is_lock_holder_alive "${lock_pid}" "${lock_ts}"; then
495
+ log "SKIP: Previous run (PID ${lock_pid}) still active"
496
+ return 1
497
+ fi
498
+ log "WARN: PID ${lock_pid} reused by another process, lock is stale"
499
+ else
500
+ log "WARN: Stale lock file found (PID ${lock_pid}), removing"
368
501
  fi
369
- log "WARN: Stale lock file found (PID ${lock_pid}), removing"
370
502
  rm -f "${lock_file}"
371
503
  fi
372
504
 
373
505
  local quoted_lock_file=""
374
506
  printf -v quoted_lock_file '%q' "${lock_file}"
375
507
  append_exit_trap "rm -f -- ${quoted_lock_file}"
376
- echo $$ > "${lock_file}"
508
+ echo "$$ $(date +%s)" > "${lock_file}"
509
+ return 0
510
+ }
511
+
512
+ # Check if the lock holder process is genuinely the one that wrote the lock.
513
+ # Guards against PID reuse: if the process started after the lock was created,
514
+ # it's a different process that reused the same PID.
515
+ _is_lock_holder_alive() {
516
+ local pid="${1}" lock_ts="${2:-}"
517
+
518
+ # No timestamp in lock file (old format) — fall back to simple PID check
519
+ if [ -z "${lock_ts}" ]; then
520
+ return 0
521
+ fi
522
+
523
+ # On Linux, check /proc/<pid>/stat for process start time
524
+ if [ -f "/proc/${pid}/stat" ]; then
525
+ local stat_content boot_time_secs start_ticks clk_tck proc_start_secs
526
+ stat_content=$(cat "/proc/${pid}/stat" 2>/dev/null) || return 0
527
+ # Field 22 is starttime (in clock ticks since boot)
528
+ start_ticks=$(echo "${stat_content}" | awk '{print $22}')
529
+ clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100)
530
+ boot_time_secs=$(awk '/^btime/{print $2}' /proc/stat 2>/dev/null) || return 0
531
+ proc_start_secs=$(( boot_time_secs + start_ticks / clk_tck ))
532
+
533
+ # If the process started more than 2 seconds after the lock was written, PID was reused
534
+ if [ "${proc_start_secs}" -gt "$(( lock_ts + 2 ))" ]; then
535
+ return 1
536
+ fi
537
+ fi
538
+
377
539
  return 0
378
540
  }
379
541
 
380
542
  append_exit_trap() {
381
543
  local command="${1:?command required}"
382
- local existing=""
383
544
 
384
- existing=$(trap -p EXIT | sed -n "s/^trap -- '\\(.*\\)' EXIT$/\\1/p")
385
- if [ -n "${existing}" ]; then
386
- trap "${existing}; ${command}" EXIT
545
+ if [ -n "${NW_EXIT_TRAP_CHAIN:-}" ]; then
546
+ NW_EXIT_TRAP_CHAIN="${NW_EXIT_TRAP_CHAIN}; ${command}"
387
547
  else
388
- trap "${command}" EXIT
548
+ NW_EXIT_TRAP_CHAIN="${command}"
389
549
  fi
550
+
551
+ trap "${NW_EXIT_TRAP_CHAIN}" EXIT
390
552
  }
391
553
 
392
554
  # ── Detect default branch ───────────────────────────────────────────────────
@@ -639,6 +801,31 @@ cleanup_worktrees() {
639
801
  git -C "${project_dir}" worktree prune >/dev/null 2>&1 || true
640
802
  }
641
803
 
804
+ cleanup_worktree_path() {
805
+ local project_dir="${1:?project_dir required}"
806
+ local worktree_dir="${2:?worktree_dir required}"
807
+
808
+ if [ -z "${worktree_dir}" ] || [ "${worktree_dir}" = "${project_dir}" ]; then
809
+ return 0
810
+ fi
811
+
812
+ git -C "${project_dir}" worktree prune >/dev/null 2>&1 || true
813
+
814
+ if git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
815
+ | grep -qF "worktree ${worktree_dir}"; then
816
+ log "CLEANUP: Removing worktree ${worktree_dir}"
817
+ git -C "${project_dir}" worktree remove --force "${worktree_dir}" 2>/dev/null || true
818
+ fi
819
+
820
+ if [ -d "${worktree_dir}" ] && ! git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
821
+ | grep -qF "worktree ${worktree_dir}"; then
822
+ log "CLEANUP: Removing stale worktree directory ${worktree_dir}"
823
+ rm -rf "${worktree_dir}" 2>/dev/null || true
824
+ fi
825
+
826
+ git -C "${project_dir}" worktree prune >/dev/null 2>&1 || true
827
+ }
828
+
642
829
  # Pick the best available ref for creating a new detached worktree.
643
830
  resolve_worktree_base_ref() {
644
831
  local project_dir="${1:?project_dir required}"
@@ -802,6 +989,19 @@ check_rate_limited() {
802
989
  fi
803
990
  }
804
991
 
992
+ # Detect transient API network errors (e.g. "Network error" in a 400 response).
993
+ # Usage: check_network_error <log_file> [start_line]
994
+ # Returns 0 if a network error was detected, 1 otherwise.
995
+ check_network_error() {
996
+ local log_file="${1:?log_file required}"
997
+ local start_line="${2:-0}"
998
+ if [ "${start_line}" -gt 0 ] 2>/dev/null; then
999
+ tail -n "+$((start_line + 1))" "${log_file}" 2>/dev/null | grep -qi "Network error"
1000
+ else
1001
+ tail -20 "${log_file}" 2>/dev/null | grep -qi "Network error"
1002
+ fi
1003
+ }
1004
+
805
1005
  # Detect context window exhaustion from Claude API logs.
806
1006
  # Usage: check_context_exhausted <log_file> [start_line]
807
1007
  # Returns 0 if context exhausted, 1 otherwise.
@@ -1105,7 +1305,7 @@ claim_or_enqueue() {
1105
1305
  }
1106
1306
 
1107
1307
  local claim_id
1108
- if claim_id=$("${cli_bin}" queue claim "${script_type}" "${project_dir}" --provider-key "${provider_key}" 2>/dev/null); then
1308
+ if claim_id=$("${cli_bin}" queue claim "${script_type}" "${project_dir}" --provider-key "${provider_key}" --pid $$ 2>/dev/null); then
1109
1309
  NW_QUEUE_ENTRY_ID="${claim_id}"
1110
1310
  export NW_QUEUE_ENTRY_ID
1111
1311
  arm_global_queue_cleanup
@@ -187,12 +187,12 @@ fi
187
187
  kill -TERM $$ 2>/dev/null || true
188
188
  ) &
189
189
  WATCHDOG_PID=$!
190
- trap 'kill ${WATCHDOG_PID} 2>/dev/null || true; rm -f "${LOCK_FILE}"' EXIT
190
+ append_exit_trap "kill ${WATCHDOG_PID} 2>/dev/null || true"
191
191
 
192
192
  # Discover open PRs sorted by creation date (oldest first = FIFO)
193
193
  log "INFO: Scanning open PRs..."
194
194
  PR_LIST_JSON=$(gh pr list --state open \
195
- --json number,headRefName,createdAt,isDraft \
195
+ --json number,headRefName,createdAt,isDraft,labels \
196
196
  --jq 'sort_by(.createdAt)' \
197
197
  2>/dev/null || echo "[]")
198
198
 
@@ -211,6 +211,7 @@ while IFS= read -r pr_json; do
211
211
  pr_number=$(echo "${pr_json}" | jq -r '.number')
212
212
  pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
213
213
  is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
214
+ pr_labels=$(echo "${pr_json}" | jq -r '[.labels[]?.name] | join(",")')
214
215
 
215
216
  # Skip drafts
216
217
  if [ "${is_draft}" = "true" ]; then
@@ -218,6 +219,11 @@ while IFS= read -r pr_json; do
218
219
  continue
219
220
  fi
220
221
 
222
+ if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
223
+ log "INFO: PR #${pr_number} (${pr_branch}): Skipping partial executor PR"
224
+ continue
225
+ fi
226
+
221
227
  # Check branch pattern filter
222
228
  if ! matches_branch_patterns "${pr_branch}"; then
223
229
  log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
@@ -83,7 +83,7 @@ cleanup_on_exit() {
83
83
  rm -f "${LOCK_FILE}"
84
84
  }
85
85
 
86
- trap cleanup_on_exit EXIT
86
+ append_exit_trap "cleanup_on_exit"
87
87
 
88
88
  # Dry-run mode
89
89
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
@@ -167,6 +167,8 @@ process_pr() {
167
167
  log "INFO: Invoking AI to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
168
168
 
169
169
  local ai_prompt
170
+ local force_push_cmd
171
+ force_push_cmd=$(project_git_push_command "${pr_branch}" "force-with-lease")
170
172
  ai_prompt="You are working in a git repository at ${worktree_dir}. \
171
173
  Branch '${pr_branch}' has merge conflicts with '${default_branch}'. \
172
174
  Please resolve the merge conflicts by: \
@@ -174,7 +176,7 @@ Please resolve the merge conflicts by: \
174
176
  2) Resolving any conflict markers in the affected files \
175
177
  3) Staging resolved files with: git add <files> \
176
178
  4) Continuing the rebase with: git rebase --continue \
177
- 5) Finally pushing with: git push --force-with-lease origin ${pr_branch} \
179
+ 5) Finally pushing with: ${force_push_cmd} \
178
180
  Work exclusively in the directory: ${worktree_dir}"
179
181
 
180
182
  local -a cmd_parts
@@ -203,7 +205,7 @@ Work exclusively in the directory: ${worktree_dir}"
203
205
  return 1
204
206
  fi
205
207
  # Push the rebased branch (AI may have already pushed; --force-with-lease is idempotent)
206
- git -C "${worktree_dir}" push --force-with-lease origin "${pr_branch}" >> "${LOG_FILE}" 2>&1 || {
208
+ git_push_for_project "${worktree_dir}" --force-with-lease origin "${pr_branch}" >> "${LOG_FILE}" 2>&1 || {
207
209
  log "WARN: Push after rebase failed for PR #${pr_number}" "branch=${pr_branch}"
208
210
  }
209
211
  fi
@@ -224,13 +226,15 @@ Work exclusively in the directory: ${worktree_dir}"
224
226
  fi
225
227
 
226
228
  local review_prompt
229
+ local review_push_cmd
230
+ review_push_cmd=$(project_git_push_command "${pr_branch}")
227
231
  review_prompt="You are working in the git repository at ${review_workdir}. \
228
232
  PR #${pr_number} on branch '${pr_branch}' has unresolved review comments requesting changes. \
229
233
  Please: \
230
234
  1) Run 'gh pr view ${pr_number} --comments' to read the review comments \
231
235
  2) Implement the requested changes \
232
236
  3) Commit the changes with a descriptive message \
233
- 4) Push with: git push origin ${pr_branch} \
237
+ 4) Push with: ${review_push_cmd} \
234
238
  Work in the directory: ${review_workdir}"
235
239
 
236
240
  local -a review_cmd_parts
@@ -631,6 +631,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
631
631
  continue
632
632
  fi
633
633
 
634
+ if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
635
+ log "INFO: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}; waiting for executor to finish"
636
+ continue
637
+ fi
638
+
634
639
  # Merge-conflict signal: this PR needs action even if CI and score look fine.
635
640
  MERGE_STATE=$(gh pr view "${pr_number}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "")
636
641
  if [ "${MERGE_STATE}" = "DIRTY" ] || [ "${MERGE_STATE}" = "CONFLICTING" ]; then
@@ -876,6 +881,12 @@ if [ -n "${TARGET_PR}" ]; then
876
881
  fi
877
882
  REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${REVIEW_WORKTREE_BASENAME}"
878
883
 
884
+ cleanup_reviewer_runner_worktree_on_exit() {
885
+ cleanup_worktree_path "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}"
886
+ }
887
+
888
+ append_exit_trap "cleanup_reviewer_runner_worktree_on_exit"
889
+
879
890
  cleanup_reviewer_worktrees "${REVIEW_WORKTREE_BASENAME}"
880
891
 
881
892
  # Dry-run mode: print diagnostics and exit
@@ -426,6 +426,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
426
426
  continue
427
427
  fi
428
428
 
429
+ if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
430
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}"
431
+ continue
432
+ fi
433
+
429
434
  # Skip PRs with the skip label
430
435
  if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
431
436
  log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
@@ -476,6 +481,12 @@ else
476
481
  fi
477
482
  QA_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-qa-runner"
478
483
 
484
+ cleanup_qa_worktree_on_exit() {
485
+ cleanup_worktree_path "${PROJECT_DIR}" "${QA_WORKTREE_DIR}"
486
+ }
487
+
488
+ append_exit_trap "cleanup_qa_worktree_on_exit"
489
+
479
490
  log "START: Found PR(s) needing QA:${PRS_NEEDING_QA}"
480
491
 
481
492
  cleanup_worktrees "${PROJECT_DIR}"
@@ -75,7 +75,7 @@ cleanup_on_exit() {
75
75
  rm -f "${LOCK_FILE}"
76
76
  }
77
77
 
78
- trap cleanup_on_exit EXIT
78
+ append_exit_trap "cleanup_on_exit"
79
79
 
80
80
  log "START: Running roadmap slicer for ${PROJECT_DIR}"
81
81
  send_telegram_status_message "📋 Night Watch Planner: started" "Project: ${PROJECT_NAME}