@jonit-dev/night-watch-cli 1.8.14-beta.1 → 1.8.14-beta.11

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 (66) hide show
  1. package/dist/cli.js +2212 -582
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/dashboard/tab-config.js +2 -2
  4. package/dist/commands/dashboard/tab-config.js.map +1 -1
  5. package/dist/commands/dashboard/tab-schedules.d.ts +1 -1
  6. package/dist/commands/dashboard/tab-schedules.d.ts.map +1 -1
  7. package/dist/commands/dashboard/tab-schedules.js +15 -6
  8. package/dist/commands/dashboard/tab-schedules.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +5 -0
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/install.d.ts +8 -0
  13. package/dist/commands/install.d.ts.map +1 -1
  14. package/dist/commands/install.js +50 -3
  15. package/dist/commands/install.js.map +1 -1
  16. package/dist/commands/logs.d.ts.map +1 -1
  17. package/dist/commands/logs.js +24 -4
  18. package/dist/commands/logs.js.map +1 -1
  19. package/dist/commands/manager.d.ts +23 -0
  20. package/dist/commands/manager.d.ts.map +1 -0
  21. package/dist/commands/manager.js +220 -0
  22. package/dist/commands/manager.js.map +1 -0
  23. package/dist/commands/plan.js +1 -1
  24. package/dist/commands/plan.js.map +1 -1
  25. package/dist/commands/prd.d.ts.map +1 -1
  26. package/dist/commands/prd.js +6 -4
  27. package/dist/commands/prd.js.map +1 -1
  28. package/dist/commands/queue.d.ts.map +1 -1
  29. package/dist/commands/queue.js +15 -7
  30. package/dist/commands/queue.js.map +1 -1
  31. package/dist/commands/run.d.ts.map +1 -1
  32. package/dist/commands/run.js +2 -1
  33. package/dist/commands/run.js.map +1 -1
  34. package/dist/commands/serve.d.ts +1 -0
  35. package/dist/commands/serve.d.ts.map +1 -1
  36. package/dist/commands/serve.js +10 -4
  37. package/dist/commands/serve.js.map +1 -1
  38. package/dist/commands/status.d.ts.map +1 -1
  39. package/dist/commands/status.js +24 -0
  40. package/dist/commands/status.js.map +1 -1
  41. package/dist/commands/uninstall.d.ts.map +1 -1
  42. package/dist/commands/uninstall.js +2 -0
  43. package/dist/commands/uninstall.js.map +1 -1
  44. package/dist/commands/ux.d.ts +14 -0
  45. package/dist/commands/ux.d.ts.map +1 -0
  46. package/dist/commands/ux.js +169 -0
  47. package/dist/commands/ux.js.map +1 -0
  48. package/dist/scripts/night-watch-audit-cron.sh +3 -3
  49. package/dist/scripts/night-watch-cron.sh +8 -7
  50. package/dist/scripts/night-watch-helpers.sh +23 -0
  51. package/dist/scripts/night-watch-manager-cron.sh +61 -0
  52. package/dist/scripts/night-watch-merger-cron.sh +149 -23
  53. package/dist/scripts/night-watch-plan-cron.sh +3 -3
  54. package/dist/scripts/night-watch-pr-resolver-cron.sh +8 -11
  55. package/dist/scripts/night-watch-pr-reviewer-cron.sh +51 -21
  56. package/dist/scripts/night-watch-qa-cron.sh +3 -3
  57. package/dist/scripts/night-watch-slicer-cron.sh +3 -3
  58. package/dist/templates/night-watch.config.json +14 -0
  59. package/dist/web/assets/index-BbiKFOgi.css +1 -0
  60. package/dist/web/assets/index-BheLL2O2.css +1 -0
  61. package/dist/web/assets/index-BsTuwxzi.js +447 -0
  62. package/dist/web/assets/index-DatF4suf.css +1 -0
  63. package/dist/web/assets/index-Q3IYCcdZ.js +447 -0
  64. package/dist/web/assets/index-uBao8iYf.js +447 -0
  65. package/dist/web/index.html +2 -2
  66. package/package.json +1 -1
@@ -8,7 +8,7 @@ set -euo pipefail
8
8
  # (oldest PR first by creation date). Rebases remaining PRs after each merge.
9
9
  #
10
10
  # Required env vars (with defaults shown):
11
- # NW_MERGER_MAX_RUNTIME=1800 - Maximum runtime in seconds (30 min)
11
+ # NW_MERGER_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
12
12
  # NW_MERGER_MERGE_METHOD=squash - Merge method: squash|merge|rebase
13
13
  # NW_MERGER_MIN_REVIEW_SCORE=80 - Minimum review score threshold
14
14
  # NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
@@ -24,7 +24,7 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
24
24
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
25
25
  LOG_DIR="${PROJECT_DIR}/logs"
26
26
  LOG_FILE="${LOG_DIR}/merger.log"
27
- MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-1800}"
27
+ MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-0}"
28
28
  MAX_LOG_SIZE="524288" # 512 KB
29
29
  MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
30
30
  MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
@@ -79,6 +79,7 @@ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
79
79
  MERGED_PRS=0
80
80
  FAILED_PRS=0
81
81
  MERGED_PR_LIST=""
82
+ LAST_LOCAL_CHECK_OUTPUT=""
82
83
 
83
84
  emit_result() {
84
85
  local status="${1:?status required}"
@@ -269,19 +270,100 @@ wait_for_ci_passing_on_head() {
269
270
  [ "${LAST_CI_STATUS}" = "passing" ]
270
271
  }
271
272
 
272
- run_local_checks_for_head() {
273
+ find_existing_worktree_for_head() {
274
+ local pr_branch="${1}"
275
+ local expected_head="${2}"
276
+ local project_real=""
277
+ local worktree_path=""
278
+ local branch=""
279
+ local head=""
280
+
281
+ project_real=$(cd "${PROJECT_DIR}" && pwd -P 2>/dev/null || echo "${PROJECT_DIR}")
282
+
283
+ while IFS= read -r line; do
284
+ if [[ "${line}" == worktree\ * ]]; then
285
+ worktree_path="${line#worktree }"
286
+ branch=""
287
+ head=""
288
+ continue
289
+ fi
290
+ if [[ "${line}" == HEAD\ * ]]; then
291
+ head="${line#HEAD }"
292
+ continue
293
+ fi
294
+ if [[ "${line}" == branch\ * ]]; then
295
+ branch="${line#branch refs/heads/}"
296
+ if [ -n "${worktree_path}" ] \
297
+ && [ "${branch}" = "${pr_branch}" ] \
298
+ && [ "${head}" = "${expected_head}" ] \
299
+ && [ -d "${worktree_path}" ]; then
300
+ local worktree_real=""
301
+ worktree_real=$(cd "${worktree_path}" && pwd -P 2>/dev/null || echo "${worktree_path}")
302
+ if [ "${worktree_real}" = "${project_real}" ]; then
303
+ continue
304
+ fi
305
+ if git -C "${worktree_path}" diff --quiet >/dev/null 2>&1 \
306
+ && git -C "${worktree_path}" diff --cached --quiet >/dev/null 2>&1; then
307
+ printf "%s" "${worktree_path}"
308
+ return 0
309
+ fi
310
+ fi
311
+ fi
312
+ done < <(git worktree list --porcelain 2>/dev/null || true)
313
+
314
+ return 1
315
+ }
316
+
317
+ run_local_check_command_in_dir() {
273
318
  local pr_number="${1}"
274
319
  local expected_head="${2}"
320
+ local check_dir="${3}"
321
+ local check_exit=1
322
+ local output_file=""
323
+
324
+ output_file=$(mktemp "${TMPDIR:-/tmp}/night-watch-merger-local-check-${pr_number}.XXXXXX")
325
+ LAST_LOCAL_CHECK_OUTPUT=""
326
+
327
+ set +e
328
+ (
329
+ cd "${check_dir}"
330
+ bash -lc "${LOCAL_CHECK_COMMAND}"
331
+ ) 2>&1 | tee "${output_file}" | tee -a "${LOG_FILE}"
332
+ check_exit=${PIPESTATUS[0]}
333
+ set -e
334
+
335
+ LAST_LOCAL_CHECK_OUTPUT=$(head -c 10000 "${output_file}" 2>/dev/null || true)
336
+ rm -f "${output_file}" 2>/dev/null || true
337
+
338
+ if [ "${check_exit}" -eq 0 ]; then
339
+ log "INFO: PR #${pr_number}: Local checks passed for head ${expected_head}"
340
+ return 0
341
+ fi
342
+
343
+ log "INFO: PR #${pr_number}: Local checks failed for head ${expected_head} (exit ${check_exit})"
344
+ return 1
345
+ }
346
+
347
+ run_local_checks_for_head() {
348
+ local pr_number="${1}"
349
+ local pr_branch="${2}"
350
+ local expected_head="${3}"
275
351
  local temp_parent=""
276
352
  local worktree_dir=""
353
+ local existing_worktree_dir=""
277
354
  local temp_ref=""
278
- local check_exit=1
279
355
 
280
356
  if [ -z "${LOCAL_CHECK_COMMAND}" ]; then
281
357
  log "INFO: PR #${pr_number}: Local check command is empty, treating fallback as failed"
282
358
  return 1
283
359
  fi
284
360
 
361
+ if existing_worktree_dir=$(find_existing_worktree_for_head "${pr_branch}" "${expected_head}"); then
362
+ log "INFO: PR #${pr_number}: Reusing existing worktree for local checks: ${existing_worktree_dir}"
363
+ run_local_check_command_in_dir "${pr_number}" "${expected_head}" "${existing_worktree_dir}"
364
+ return $?
365
+ fi
366
+
285
367
  temp_parent=$(mktemp -d "${TMPDIR:-/tmp}/night-watch-merger-${pr_number}.XXXXXX")
286
368
  worktree_dir="${temp_parent}/worktree"
287
369
  temp_ref="refs/night-watch/merger/${pr_number}-${expected_head}"
@@ -311,22 +393,54 @@ run_local_checks_for_head() {
311
393
  fi
312
394
  fi
313
395
 
396
+ run_local_check_command_in_dir "${pr_number}" "${expected_head}" "${worktree_dir}"
397
+ local check_result=$?
398
+
399
+ cleanup_local_check_worktree
400
+
401
+ return "${check_result}"
402
+ }
403
+
404
+ run_reviewer_repair_for_pr() {
405
+ local pr_number="${1}"
406
+ local pr_branch="${2}"
407
+ local expected_head="${3}"
408
+ local elapsed=0
409
+ local remaining=0
410
+ local reviewer_exit=1
411
+
412
+ elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
413
+ if is_runtime_limited "${MAX_RUNTIME}"; then
414
+ remaining=$(( MAX_RUNTIME - elapsed - 30 ))
415
+ else
416
+ remaining=0
417
+ fi
418
+ if is_runtime_limited "${MAX_RUNTIME}" && [ "${remaining}" -lt 120 ]; then
419
+ log "INFO: PR #${pr_number} (${pr_branch}): Not enough merger runtime left for targeted reviewer repair (${remaining}s), skipping repair"
420
+ return 1
421
+ fi
422
+
423
+ log "INFO: PR #${pr_number} (${pr_branch}): Running targeted reviewer repair after local check failure on head ${expected_head}"
424
+
314
425
  set +e
315
426
  (
316
- cd "${worktree_dir}"
317
- bash -lc "${LOCAL_CHECK_COMMAND}"
427
+ NW_TARGET_PR="${pr_number}" \
428
+ NW_REVIEWER_WORKER_MODE="1" \
429
+ NW_REVIEWER_PARALLEL="0" \
430
+ NW_REVIEWER_MAX_RUNTIME="${remaining}" \
431
+ NW_TARGET_LOCAL_CHECK_COMMAND="${LOCAL_CHECK_COMMAND}" \
432
+ NW_TARGET_LOCAL_CHECK_OUTPUT="${LAST_LOCAL_CHECK_OUTPUT}" \
433
+ bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}"
318
434
  ) 2>&1 | tee -a "${LOG_FILE}"
319
- check_exit=${PIPESTATUS[0]}
435
+ reviewer_exit=${PIPESTATUS[0]}
320
436
  set -e
321
437
 
322
- cleanup_local_check_worktree
323
-
324
- if [ "${check_exit}" -eq 0 ]; then
325
- log "INFO: PR #${pr_number}: Local checks passed for head ${expected_head}"
438
+ if [ "${reviewer_exit}" -eq 0 ]; then
439
+ log "INFO: PR #${pr_number} (${pr_branch}): Targeted reviewer repair completed"
326
440
  return 0
327
441
  fi
328
442
 
329
- log "INFO: PR #${pr_number}: Local checks failed for head ${expected_head} (exit ${check_exit})"
443
+ log "INFO: PR #${pr_number} (${pr_branch}): Targeted reviewer repair failed (exit ${reviewer_exit})"
330
444
  return 1
331
445
  }
332
446
 
@@ -349,10 +463,20 @@ ci_gate_allows_head() {
349
463
 
350
464
  if [ "${CI_POLICY}" = "fallback-local" ]; then
351
465
  log "INFO: PR #${pr_number} (${pr_branch}): ${context} not passing on head ${expected_head} (${ci_status}); trying local checks"
352
- if run_local_checks_for_head "${pr_number}" "${expected_head}"; then
466
+ if run_local_checks_for_head "${pr_number}" "${pr_branch}" "${expected_head}"; then
353
467
  return 0
354
468
  fi
355
- log "INFO: PR #${pr_number} (${pr_branch}): Local check fallback failed, skipping"
469
+ if run_reviewer_repair_for_pr "${pr_number}" "${pr_branch}" "${expected_head}"; then
470
+ local repaired_head=""
471
+ repaired_head=$(get_pr_head_oid "${pr_number}")
472
+ if [ -n "${repaired_head}" ]; then
473
+ log "INFO: PR #${pr_number} (${pr_branch}): Re-checking local checks after targeted repair on head ${repaired_head}"
474
+ if run_local_checks_for_head "${pr_number}" "${pr_branch}" "${repaired_head}"; then
475
+ return 0
476
+ fi
477
+ fi
478
+ fi
479
+ log "INFO: PR #${pr_number} (${pr_branch}): Local check fallback failed after repair attempt, skipping"
356
480
  return 1
357
481
  fi
358
482
 
@@ -430,13 +554,15 @@ if [ "${DRY_RUN}" = "1" ]; then
430
554
  fi
431
555
 
432
556
  # Timeout watchdog
433
- (
434
- sleep "${MAX_RUNTIME}"
435
- log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
436
- kill -TERM $$ 2>/dev/null || true
437
- ) &
438
- WATCHDOG_PID=$!
439
- append_exit_trap "cleanup_watchdog ${WATCHDOG_PID}"
557
+ if is_runtime_limited "${MAX_RUNTIME}"; then
558
+ (
559
+ sleep "${MAX_RUNTIME}"
560
+ log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
561
+ kill -TERM $$ 2>/dev/null || true
562
+ ) &
563
+ WATCHDOG_PID=$!
564
+ append_exit_trap "cleanup_watchdog ${WATCHDOG_PID}"
565
+ fi
440
566
 
441
567
  # Discover open PRs sorted by creation date (oldest first = FIFO)
442
568
  log "INFO: Scanning open PRs..."
@@ -540,7 +666,7 @@ while IFS= read -r pr_json; do
540
666
  if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
541
667
  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)"
542
668
  if [ "${CI_POLICY}" = "fallback-local" ]; then
543
- if ! run_local_checks_for_head "${pr_number}" "${pr_head_after_rebase}"; then
669
+ if ! run_local_checks_for_head "${pr_number}" "${pr_branch}" "${pr_head_after_rebase}"; then
544
670
  log "INFO: PR #${pr_number}: Fresh local check fallback failed after rebase, skipping"
545
671
  continue
546
672
  fi
@@ -593,7 +719,7 @@ while IFS= read -r pr_json; do
593
719
 
594
720
  # Enforce global timeout
595
721
  elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
596
- if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
722
+ if is_runtime_limited "${MAX_RUNTIME}" && [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
597
723
  log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
598
724
  break
599
725
  fi
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # Required env vars (with defaults shown):
10
10
  # NW_PLAN_TASK='' - Task/feature description to plan (required)
11
11
  # NW_PRD_DIR=docs/PRDs - PRD output directory (relative to project)
12
- # NW_PLAN_MAX_RUNTIME=1800 - Maximum runtime in seconds
12
+ # NW_PLAN_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
13
13
  # NW_PROVIDER_CMD=claude - AI provider CLI to use
14
14
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode
15
15
 
@@ -18,7 +18,7 @@ PROJECT_NAME=$(basename "${PROJECT_DIR}")
18
18
  LOG_DIR="${PROJECT_DIR}/logs"
19
19
  LOG_FILE="${LOG_DIR}/plan.log"
20
20
  LOCK_FILE=""
21
- MAX_RUNTIME="${NW_PLAN_MAX_RUNTIME:-1800}"
21
+ MAX_RUNTIME="${NW_PLAN_MAX_RUNTIME:-0}"
22
22
  MAX_LOG_SIZE="524288" # 512 KB
23
23
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
24
24
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
@@ -132,7 +132,7 @@ EXIT_CODE=0
132
132
  mapfile -d '' -t PROVIDER_CMD_PARTS < <(build_provider_cmd "${PROJECT_DIR}" "${CREATOR_PROMPT}")
133
133
 
134
134
  # Execute in the project directory so the provider can explore the codebase
135
- if (cd "${PROJECT_DIR}" && timeout "${MAX_RUNTIME}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
135
+ if (cd "${PROJECT_DIR}" && run_with_optional_timeout "${MAX_RUNTIME}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
136
136
  EXIT_CODE=0
137
137
  else
138
138
  EXIT_CODE=$?
@@ -7,11 +7,11 @@ set -euo pipefail
7
7
  # NOTE: This script expects environment variables to be set by the caller.
8
8
  # The Node.js CLI will inject config values via environment variables.
9
9
  # Required env vars (with defaults shown):
10
- # NW_PR_RESOLVER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
10
+ # NW_PR_RESOLVER_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
11
11
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
12
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
13
13
  # NW_PR_RESOLVER_MAX_PRS_PER_RUN=0 - Max PRs to process per run (0 = unlimited)
14
- # NW_PR_RESOLVER_PER_PR_TIMEOUT=600 - Per-PR AI timeout in seconds
14
+ # NW_PR_RESOLVER_PER_PR_TIMEOUT=0 - Per-PR AI timeout in seconds (0 = no timeout)
15
15
  # NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION=1 - Set to 1 to use AI for conflict resolution
16
16
  # NW_PR_RESOLVER_AI_REVIEW_RESOLUTION=0 - Set to 1 to also address review comments
17
17
  # NW_PR_RESOLVER_READY_LABEL=ready-to-merge - Label to add when PR is conflict-free
@@ -21,12 +21,12 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
21
21
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
22
22
  LOG_DIR="${PROJECT_DIR}/logs"
23
23
  LOG_FILE="${LOG_DIR}/pr-resolver.log"
24
- MAX_RUNTIME="${NW_PR_RESOLVER_MAX_RUNTIME:-3600}" # 1 hour
24
+ MAX_RUNTIME="${NW_PR_RESOLVER_MAX_RUNTIME:-0}" # 0 = no global timeout
25
25
  MAX_LOG_SIZE="524288" # 512 KB
26
26
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
27
27
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
28
28
  MAX_PRS_PER_RUN="${NW_PR_RESOLVER_MAX_PRS_PER_RUN:-0}"
29
- PER_PR_TIMEOUT="${NW_PR_RESOLVER_PER_PR_TIMEOUT:-600}"
29
+ PER_PR_TIMEOUT="${NW_PR_RESOLVER_PER_PR_TIMEOUT:-0}"
30
30
  AI_CONFLICT_RESOLUTION="${NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION:-1}"
31
31
  AI_REVIEW_RESOLUTION="${NW_PR_RESOLVER_AI_REVIEW_RESOLUTION:-0}"
32
32
  READY_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
@@ -38,14 +38,11 @@ if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
38
38
  MAX_PRS_PER_RUN="0"
39
39
  fi
40
40
  if ! [[ "${PER_PR_TIMEOUT}" =~ ^[0-9]+$ ]]; then
41
- PER_PR_TIMEOUT="600"
41
+ PER_PR_TIMEOUT="0"
42
42
  fi
43
43
  if [ "${MAX_PRS_PER_RUN}" -gt 100 ]; then
44
44
  MAX_PRS_PER_RUN="100"
45
45
  fi
46
- if [ "${PER_PR_TIMEOUT}" -gt 3600 ]; then
47
- PER_PR_TIMEOUT="3600"
48
- fi
49
46
 
50
47
  mkdir -p "${LOG_DIR}"
51
48
 
@@ -183,7 +180,7 @@ Work exclusively in the directory: ${worktree_dir}"
183
180
  local -a cmd_parts
184
181
  mapfile -d '' -t cmd_parts < <(build_provider_cmd "${worktree_dir}" "${ai_prompt}")
185
182
 
186
- if timeout "${PER_PR_TIMEOUT}" "${cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
183
+ if run_with_optional_timeout "${PER_PR_TIMEOUT}" "${cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
187
184
  rebase_success=1
188
185
  log "INFO: AI resolved conflicts for PR #${pr_number}" "branch=${pr_branch}"
189
186
  else
@@ -241,7 +238,7 @@ Work in the directory: ${review_workdir}"
241
238
  local -a review_cmd_parts
242
239
  mapfile -d '' -t review_cmd_parts < <(build_provider_cmd "${review_workdir}" "${review_prompt}")
243
240
 
244
- if timeout "${PER_PR_TIMEOUT}" "${review_cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
241
+ if run_with_optional_timeout "${PER_PR_TIMEOUT}" "${review_cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
245
242
  log "INFO: AI addressed review comments for PR #${pr_number}" "branch=${pr_branch}"
246
243
  else
247
244
  log "WARN: AI failed to address review comments for PR #${pr_number}" "branch=${pr_branch}"
@@ -379,7 +376,7 @@ while IFS= read -r pr_line; do
379
376
 
380
377
  # Enforce global timeout
381
378
  elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
382
- if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
379
+ if is_runtime_limited "${MAX_RUNTIME}" && [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
383
380
  log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
384
381
  break
385
382
  fi
@@ -7,7 +7,7 @@ set -euo pipefail
7
7
  # NOTE: This script expects environment variables to be set by the caller.
8
8
  # The Node.js CLI will inject config values via environment variables.
9
9
  # Required env vars (with defaults shown):
10
- # NW_REVIEWER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
10
+ # NW_REVIEWER_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
11
11
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
12
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
13
13
 
@@ -15,7 +15,7 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
15
15
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
16
16
  LOG_DIR="${PROJECT_DIR}/logs"
17
17
  LOG_FILE="${LOG_DIR}/reviewer.log"
18
- MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
18
+ MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-0}" # 0 = no provider timeout
19
19
  MAX_LOG_SIZE="524288" # 512 KB
20
20
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
21
21
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
@@ -858,6 +858,17 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
858
858
  continue
859
859
  fi
860
860
 
861
+ if [ -n "${TARGET_PR}" ] && [ "${pr_number}" = "${TARGET_PR}" ] && [ -n "${NW_TARGET_LOCAL_CHECK_COMMAND:-}" ]; then
862
+ if [ "${local_ready_for_review_label_present}" -eq 1 ]; then
863
+ log "INFO: PR #${pr_number} (${pr_branch}) failed merge-gate local checks; removing stale ${READY_FOR_REVIEW_LABEL} label"
864
+ clear_ready_for_human_review_label "${pr_number}"
865
+ fi
866
+ log "INFO: PR #${pr_number} (${pr_branch}) failed merge-gate local checks; targeted repair required"
867
+ NEEDS_WORK=1
868
+ PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
869
+ continue
870
+ fi
871
+
861
872
  if has_ready_for_human_review_marker "${all_comments}" "${current_head_sha}"; then
862
873
  SKIPPED_ALREADY_REVIEWED_CURRENT_HEAD=1
863
874
  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"
@@ -1001,21 +1012,24 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
1001
1012
  worker_pr="${WORKER_PRS[$idx]}"
1002
1013
  worker_output="${WORKER_OUTPUTS[$idx]}"
1003
1014
 
1004
- # Guard: abort the wait loop when the global budget is exhausted
1005
- PARENT_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
1006
- PARENT_REMAINING=$(( MAX_RUNTIME - PARENT_ELAPSED ))
1007
- if [ "${PARENT_REMAINING}" -le 0 ]; then
1008
- log "PARALLEL: global timeout exhausted — killing remaining workers"
1009
- for remaining_idx in $(seq "${idx}" $(( ${#WORKER_PIDS[@]} - 1 ))); do
1010
- kill "${WORKER_PIDS[$remaining_idx]}" 2>/dev/null || true
1011
- done
1012
- EXIT_CODE=124
1013
- break
1014
- fi
1015
+ watchdog_pid=""
1016
+ if is_runtime_limited "${MAX_RUNTIME}"; then
1017
+ # Guard: abort the wait loop when the global budget is exhausted
1018
+ PARENT_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
1019
+ PARENT_REMAINING=$(( MAX_RUNTIME - PARENT_ELAPSED ))
1020
+ if [ "${PARENT_REMAINING}" -le 0 ]; then
1021
+ log "PARALLEL: global timeout exhausted — killing remaining workers"
1022
+ for remaining_idx in $(seq "${idx}" $(( ${#WORKER_PIDS[@]} - 1 ))); do
1023
+ kill "${WORKER_PIDS[$remaining_idx]}" 2>/dev/null || true
1024
+ done
1025
+ EXIT_CODE=124
1026
+ break
1027
+ fi
1015
1028
 
1016
- # Watchdog: kill the worker if it outlives the remaining budget
1017
- ( sleep "${PARENT_REMAINING}" 2>/dev/null; kill "${worker_pid}" 2>/dev/null || true ) &
1018
- watchdog_pid=$!
1029
+ # Watchdog: kill the worker if it outlives the remaining budget
1030
+ ( sleep "${PARENT_REMAINING}" 2>/dev/null; kill "${worker_pid}" 2>/dev/null || true ) &
1031
+ watchdog_pid=$!
1032
+ fi
1019
1033
 
1020
1034
  worker_exit_code=0
1021
1035
  if wait "${worker_pid}" 2>/dev/null; then
@@ -1025,8 +1039,10 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
1025
1039
  fi
1026
1040
 
1027
1041
  # Cancel the watchdog — the worker finished in time
1028
- kill "${watchdog_pid}" 2>/dev/null || true
1029
- wait "${watchdog_pid}" 2>/dev/null || true
1042
+ if [ -n "${watchdog_pid}" ]; then
1043
+ kill "${watchdog_pid}" 2>/dev/null || true
1044
+ wait "${watchdog_pid}" 2>/dev/null || true
1045
+ fi
1030
1046
 
1031
1047
  if [ -f "${worker_output}" ] && [ -s "${worker_output}" ]; then
1032
1048
  cat "${worker_output}" >> "${LOG_FILE}"
@@ -1202,6 +1218,15 @@ if [ -n "${TARGET_PR}" ]; then
1202
1218
  TARGET_SCOPE_PROMPT+=$'- latest review score: not found\n'
1203
1219
  TARGET_SCOPE_PROMPT+=$'- action: review\n'
1204
1220
  fi
1221
+ if [ -n "${NW_TARGET_LOCAL_CHECK_COMMAND:-}" ]; then
1222
+ TARGET_SCOPE_PROMPT+=$'\n## Local Check Failure From Merge Gate\n'
1223
+ TARGET_SCOPE_PROMPT+=$'- command: '"${NW_TARGET_LOCAL_CHECK_COMMAND}"$'\n'
1224
+ TARGET_SCOPE_PROMPT+=$'- action: fix the PR so this local command passes before the PR can merge\n'
1225
+ if [ -n "${NW_TARGET_LOCAL_CHECK_OUTPUT:-}" ]; then
1226
+ TRUNCATED_LOCAL_CHECK_OUTPUT=$(printf '%s' "${NW_TARGET_LOCAL_CHECK_OUTPUT}" | head -c 10000)
1227
+ TARGET_SCOPE_PROMPT+=$'\n### Local Check Output\n```text\n'"${TRUNCATED_LOCAL_CHECK_OUTPUT}"$'\n```\n'
1228
+ fi
1229
+ fi
1205
1230
  fi
1206
1231
 
1207
1232
  PRD_CONTEXT_PROMPT=""
@@ -1248,6 +1273,11 @@ if [ -n "${TARGET_PR}" ]; then
1248
1273
  fi
1249
1274
 
1250
1275
  remaining_runtime_budget() {
1276
+ if ! is_runtime_limited "${MAX_RUNTIME}"; then
1277
+ printf "0"
1278
+ return 0
1279
+ fi
1280
+
1251
1281
  local now_ms
1252
1282
  local elapsed_ms
1253
1283
  local remaining_ms
@@ -1279,7 +1309,7 @@ sleep_with_runtime_budget() {
1279
1309
  return 0
1280
1310
  fi
1281
1311
 
1282
- if [ -z "${TARGET_PR}" ]; then
1312
+ if [ -z "${TARGET_PR}" ] || ! is_runtime_limited "${MAX_RUNTIME}"; then
1283
1313
  sleep "${requested_sleep}"
1284
1314
  return 0
1285
1315
  fi
@@ -1305,7 +1335,7 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1305
1335
  ATTEMPTS_MADE="${ATTEMPT}"
1306
1336
 
1307
1337
  ATTEMPT_TIMEOUT="${MAX_RUNTIME}"
1308
- if [ -n "${TARGET_PR}" ]; then
1338
+ if [ -n "${TARGET_PR}" ] && is_runtime_limited "${MAX_RUNTIME}"; then
1309
1339
  # Give each targeted attempt the full remaining runtime budget.
1310
1340
  # Retries only happen after a quick return (low score / invalid output / rate limit);
1311
1341
  # a timed-out provider run is not retried, so pre-splitting the budget would
@@ -1347,7 +1377,7 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1347
1377
  mapfile -d '' -t PROVIDER_CMD_PARTS < <(build_provider_cmd "${REVIEW_WORKTREE_DIR}" "${REVIEWER_PROMPT}")
1348
1378
 
1349
1379
  # Execute — always cd into worktree so provider tools resolve project files correctly
1350
- if (cd "${REVIEW_WORKTREE_DIR}" && timeout "${ATTEMPT_TIMEOUT}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
1380
+ if (cd "${REVIEW_WORKTREE_DIR}" && run_with_optional_timeout "${ATTEMPT_TIMEOUT}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
1351
1381
  EXIT_CODE=0
1352
1382
  else
1353
1383
  EXIT_CODE=$?
@@ -7,7 +7,7 @@ set -euo pipefail
7
7
  # NOTE: This script expects environment variables to be set by the caller.
8
8
  # The Node.js CLI will inject config values via environment variables.
9
9
  # Required env vars (with defaults shown):
10
- # NW_QA_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
10
+ # NW_QA_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
11
11
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
12
12
  # NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
13
13
  # NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
@@ -19,7 +19,7 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
19
19
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
20
20
  LOG_DIR="${PROJECT_DIR}/logs"
21
21
  LOG_FILE="${LOG_DIR}/night-watch-qa.log"
22
- MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
22
+ MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-0}" # 0 = no provider timeout
23
23
  MAX_LOG_SIZE="524288" # 512 KB
24
24
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
25
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
@@ -599,7 +599,7 @@ for pr_ref in ${PRS_NEEDING_QA}; do
599
599
  mapfile -d '' -t PROVIDER_CMD_PARTS < <(build_provider_cmd "${QA_WORKTREE_DIR}" "${QA_PROMPT}")
600
600
 
601
601
  # Execute — always cd into worktree so provider tools resolve project files correctly
602
- if (cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
602
+ if (cd "${QA_WORKTREE_DIR}" && run_with_optional_timeout "${MAX_RUNTIME}" "${PROVIDER_CMD_PARTS[@]}" 2>&1 | tee -a "${LOG_FILE}"); then
603
603
  PROVIDER_OK=1
604
604
  else
605
605
  local_exit=$?
@@ -10,7 +10,7 @@ set -euo pipefail
10
10
  # NOTE: This script expects environment variables to be set by the caller.
11
11
  # The Node.js CLI will inject config values via environment variables.
12
12
  # Required env vars (with defaults shown):
13
- # NW_SLICER_MAX_RUNTIME=600 - Maximum runtime in seconds (10 minutes)
13
+ # NW_SLICER_MAX_RUNTIME=0 - Maximum runtime in seconds (0 = no timeout)
14
14
  # NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
15
15
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
16
16
 
@@ -19,7 +19,7 @@ PROJECT_NAME=$(basename "${PROJECT_DIR}")
19
19
  LOG_DIR="${PROJECT_DIR}/logs"
20
20
  LOG_FILE="${LOG_DIR}/slicer.log"
21
21
  LOCK_FILE=""
22
- MAX_RUNTIME="${NW_SLICER_MAX_RUNTIME:-600}" # 10 minutes
22
+ MAX_RUNTIME="${NW_SLICER_MAX_RUNTIME:-0}" # 0 = no timeout
23
23
  MAX_LOG_SIZE="524288" # 512 KB
24
24
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
25
25
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
@@ -110,7 +110,7 @@ fi
110
110
  EXIT_CODE=0
111
111
  SLICER_RUN_START=$(date +%s)
112
112
  log "SLICER: Starting night-watch slice timeout=${MAX_RUNTIME}s"
113
- if timeout "${MAX_RUNTIME}" "${CLI_BIN}" slice >> "${LOG_FILE}" 2>&1; then
113
+ if run_with_optional_timeout "${MAX_RUNTIME}" "${CLI_BIN}" slice >> "${LOG_FILE}" 2>&1; then
114
114
  EXIT_CODE=0
115
115
  else
116
116
  EXIT_CODE=$?
@@ -52,6 +52,7 @@
52
52
  "reviewer": 40,
53
53
  "slicer": 30,
54
54
  "qa": 20,
55
+ "ux": 10,
55
56
  "audit": 10
56
57
  }
57
58
  },
@@ -65,6 +66,7 @@
65
66
  "slicer",
66
67
  "qa",
67
68
  "audit",
69
+ "ux",
68
70
  "analytics",
69
71
  "merger"
70
72
  ],
@@ -94,5 +96,17 @@
94
96
  "enabled": true,
95
97
  "schedule": "50 3 * * 1",
96
98
  "maxRuntime": 1800
99
+ },
100
+ "ux": {
101
+ "enabled": false,
102
+ "schedule": "0 7 * * 1",
103
+ "maxRuntime": 0,
104
+ "targetColumn": "Draft",
105
+ "baseUrl": "",
106
+ "startUrl": "",
107
+ "flows": [],
108
+ "autoInstallPlaywright": true,
109
+ "maxIssues": 10,
110
+ "reportPrompt": ""
97
111
  }
98
112
  }