@jonit-dev/night-watch-cli 1.8.14-beta.0 → 1.8.14-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 (70) hide show
  1. package/dist/cli.js +2230 -577
  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/merge.d.ts.map +1 -1
  24. package/dist/commands/merge.js +4 -0
  25. package/dist/commands/merge.js.map +1 -1
  26. package/dist/commands/plan.js +1 -1
  27. package/dist/commands/plan.js.map +1 -1
  28. package/dist/commands/prd.d.ts.map +1 -1
  29. package/dist/commands/prd.js +6 -4
  30. package/dist/commands/prd.js.map +1 -1
  31. package/dist/commands/queue.d.ts.map +1 -1
  32. package/dist/commands/queue.js +15 -7
  33. package/dist/commands/queue.js.map +1 -1
  34. package/dist/commands/run.d.ts.map +1 -1
  35. package/dist/commands/run.js +2 -1
  36. package/dist/commands/run.js.map +1 -1
  37. package/dist/commands/serve.d.ts +1 -0
  38. package/dist/commands/serve.d.ts.map +1 -1
  39. package/dist/commands/serve.js +10 -4
  40. package/dist/commands/serve.js.map +1 -1
  41. package/dist/commands/status.d.ts.map +1 -1
  42. package/dist/commands/status.js +24 -0
  43. package/dist/commands/status.js.map +1 -1
  44. package/dist/commands/uninstall.d.ts.map +1 -1
  45. package/dist/commands/uninstall.js +2 -0
  46. package/dist/commands/uninstall.js.map +1 -1
  47. package/dist/commands/ux.d.ts +14 -0
  48. package/dist/commands/ux.d.ts.map +1 -0
  49. package/dist/commands/ux.js +169 -0
  50. package/dist/commands/ux.js.map +1 -0
  51. package/dist/scripts/night-watch-audit-cron.sh +3 -3
  52. package/dist/scripts/night-watch-cron.sh +8 -7
  53. package/dist/scripts/night-watch-helpers.sh +23 -0
  54. package/dist/scripts/night-watch-manager-cron.sh +61 -0
  55. package/dist/scripts/night-watch-merger-cron.sh +254 -19
  56. package/dist/scripts/night-watch-plan-cron.sh +3 -3
  57. package/dist/scripts/night-watch-pr-resolver-cron.sh +8 -11
  58. package/dist/scripts/night-watch-pr-reviewer-cron.sh +51 -21
  59. package/dist/scripts/night-watch-qa-cron.sh +3 -3
  60. package/dist/scripts/night-watch-slicer-cron.sh +3 -3
  61. package/dist/templates/night-watch.config.json +14 -0
  62. package/dist/web/assets/index-6Yf-Q6Di.js +442 -0
  63. package/dist/web/assets/index-BbiKFOgi.css +1 -0
  64. package/dist/web/assets/index-BheLL2O2.css +1 -0
  65. package/dist/web/assets/index-BsTuwxzi.js +447 -0
  66. package/dist/web/assets/index-DatF4suf.css +1 -0
  67. package/dist/web/assets/index-Q3IYCcdZ.js +447 -0
  68. package/dist/web/assets/index-uBao8iYf.js +447 -0
  69. package/dist/web/index.html +2 -2
  70. package/package.json +1 -1
@@ -8,12 +8,14 @@ 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)
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_POLICY=fallback-local - CI gate: ci-only|fallback-local|ignore
18
+ # NW_MERGER_LOCAL_CHECK_COMMAND=... - Command run in temp PR worktree for local fallback
17
19
  # NW_MERGER_CI_MAX_WAIT=300 - Max seconds to wait for checks after rebase
18
20
  # NW_MERGER_CI_POLL_INTERVAL=15 - Seconds between check polls after rebase
19
21
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode
@@ -22,7 +24,7 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
22
24
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
23
25
  LOG_DIR="${PROJECT_DIR}/logs"
24
26
  LOG_FILE="${LOG_DIR}/merger.log"
25
- MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-1800}"
27
+ MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-0}"
26
28
  MAX_LOG_SIZE="524288" # 512 KB
27
29
  MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
28
30
  MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
@@ -30,6 +32,8 @@ REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
30
32
  MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
31
33
  CI_MAX_WAIT="${NW_MERGER_CI_MAX_WAIT:-300}"
32
34
  CI_POLL_INTERVAL="${NW_MERGER_CI_POLL_INTERVAL:-15}"
35
+ CI_POLICY="${NW_MERGER_CI_POLICY:-fallback-local}"
36
+ LOCAL_CHECK_COMMAND="${NW_MERGER_LOCAL_CHECK_COMMAND:-yarn install --frozen-lockfile && yarn verify && yarn test}"
33
37
  BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
34
38
  READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
35
39
  SCRIPT_START_TIME=$(date +%s)
@@ -55,6 +59,10 @@ case "${MERGE_METHOD}" in
55
59
  squash|merge|rebase) ;;
56
60
  *) MERGE_METHOD="squash" ;;
57
61
  esac
62
+ case "${CI_POLICY}" in
63
+ ci-only|fallback-local|ignore) ;;
64
+ *) CI_POLICY="fallback-local" ;;
65
+ esac
58
66
 
59
67
  mkdir -p "${LOG_DIR}"
60
68
 
@@ -71,6 +79,7 @@ skip_if_job_paused "${SCRIPT_TYPE}" "${PROJECT_DIR}"
71
79
  MERGED_PRS=0
72
80
  FAILED_PRS=0
73
81
  MERGED_PR_LIST=""
82
+ LAST_LOCAL_CHECK_OUTPUT=""
74
83
 
75
84
  emit_result() {
76
85
  local status="${1:?status required}"
@@ -261,6 +270,220 @@ wait_for_ci_passing_on_head() {
261
270
  [ "${LAST_CI_STATUS}" = "passing" ]
262
271
  }
263
272
 
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() {
318
+ local pr_number="${1}"
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}"
351
+ local temp_parent=""
352
+ local worktree_dir=""
353
+ local existing_worktree_dir=""
354
+ local temp_ref=""
355
+
356
+ if [ -z "${LOCAL_CHECK_COMMAND}" ]; then
357
+ log "INFO: PR #${pr_number}: Local check command is empty, treating fallback as failed"
358
+ return 1
359
+ fi
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
+
367
+ temp_parent=$(mktemp -d "${TMPDIR:-/tmp}/night-watch-merger-${pr_number}.XXXXXX")
368
+ worktree_dir="${temp_parent}/worktree"
369
+ temp_ref="refs/night-watch/merger/${pr_number}-${expected_head}"
370
+
371
+ cleanup_local_check_worktree() {
372
+ git worktree remove --force "${worktree_dir}" >/dev/null 2>&1 || true
373
+ git update-ref -d "${temp_ref}" >/dev/null 2>&1 || true
374
+ rm -rf "${temp_parent}" >/dev/null 2>&1 || true
375
+ }
376
+
377
+ log "INFO: PR #${pr_number}: Running local checks for head ${expected_head}: ${LOCAL_CHECK_COMMAND}"
378
+
379
+ if ! git cat-file -e "${expected_head}^{commit}" >/dev/null 2>&1; then
380
+ log "INFO: PR #${pr_number}: Fetching PR head ${expected_head} for local checks"
381
+ git fetch --quiet --force origin "pull/${pr_number}/head:${temp_ref}" >/dev/null 2>&1 || {
382
+ log "INFO: PR #${pr_number}: Unable to fetch PR head for local checks"
383
+ cleanup_local_check_worktree
384
+ return 1
385
+ }
386
+ fi
387
+
388
+ if ! git worktree add --detach --quiet "${worktree_dir}" "${expected_head}" >/dev/null 2>&1; then
389
+ if ! git worktree add --detach --quiet "${worktree_dir}" "${temp_ref}" >/dev/null 2>&1; then
390
+ log "INFO: PR #${pr_number}: Unable to create local check worktree"
391
+ cleanup_local_check_worktree
392
+ return 1
393
+ fi
394
+ fi
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
+
425
+ set +e
426
+ (
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}"
434
+ ) 2>&1 | tee -a "${LOG_FILE}"
435
+ reviewer_exit=${PIPESTATUS[0]}
436
+ set -e
437
+
438
+ if [ "${reviewer_exit}" -eq 0 ]; then
439
+ log "INFO: PR #${pr_number} (${pr_branch}): Targeted reviewer repair completed"
440
+ return 0
441
+ fi
442
+
443
+ log "INFO: PR #${pr_number} (${pr_branch}): Targeted reviewer repair failed (exit ${reviewer_exit})"
444
+ return 1
445
+ }
446
+
447
+ ci_gate_allows_head() {
448
+ local pr_number="${1}"
449
+ local pr_branch="${2}"
450
+ local expected_head="${3}"
451
+ local context="${4:-CI}"
452
+ local ci_status
453
+
454
+ if [ "${CI_POLICY}" = "ignore" ]; then
455
+ log "INFO: PR #${pr_number} (${pr_branch}): CI policy is ignore; skipping ${context} gate for head ${expected_head}"
456
+ return 0
457
+ fi
458
+
459
+ ci_status=$(ci_status_for_head "${pr_number}" "${expected_head}")
460
+ if [ "${ci_status}" = "passing" ]; then
461
+ return 0
462
+ fi
463
+
464
+ if [ "${CI_POLICY}" = "fallback-local" ]; then
465
+ log "INFO: PR #${pr_number} (${pr_branch}): ${context} not passing on head ${expected_head} (${ci_status}); trying local checks"
466
+ if run_local_checks_for_head "${pr_number}" "${pr_branch}" "${expected_head}"; then
467
+ return 0
468
+ fi
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"
480
+ return 1
481
+ fi
482
+
483
+ log "INFO: PR #${pr_number} (${pr_branch}): ${context} not passing on head ${expected_head} (${ci_status}), skipping"
484
+ return 1
485
+ }
486
+
264
487
  # Rebase a PR against its base branch
265
488
  rebase_pr() {
266
489
  local pr_number="${1}"
@@ -308,7 +531,7 @@ cd "${PROJECT_DIR}"
308
531
 
309
532
  log "========================================"
310
533
  log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
311
- 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>}"
534
+ 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 ci_policy=${CI_POLICY} local_check_command=${LOCAL_CHECK_COMMAND} ready_label=${READY_TO_MERGE_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
312
535
  log "========================================"
313
536
 
314
537
  if ! acquire_lock "${LOCK_FILE}"; then
@@ -331,13 +554,15 @@ if [ "${DRY_RUN}" = "1" ]; then
331
554
  fi
332
555
 
333
556
  # Timeout watchdog
334
- (
335
- sleep "${MAX_RUNTIME}"
336
- log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
337
- kill -TERM $$ 2>/dev/null || true
338
- ) &
339
- WATCHDOG_PID=$!
340
- 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
341
566
 
342
567
  # Discover open PRs sorted by creation date (oldest first = FIFO)
343
568
  log "INFO: Scanning open PRs..."
@@ -391,10 +616,8 @@ while IFS= read -r pr_json; do
391
616
  continue
392
617
  fi
393
618
 
394
- # Check CI status
395
- ci_status=$(ci_status_for_head "${pr_number}" "${pr_head_oid}")
396
- if [ "${ci_status}" != "passing" ]; then
397
- log "INFO: PR #${pr_number} (${pr_branch}): CI not passing on head ${pr_head_oid} (${ci_status}), skipping"
619
+ # Check CI status, optionally falling back to local checks for provider/billing failures.
620
+ if ! ci_gate_allows_head "${pr_number}" "${pr_branch}" "${pr_head_oid}" "CI"; then
398
621
  continue
399
622
  fi
400
623
 
@@ -436,10 +659,22 @@ while IFS= read -r pr_json; do
436
659
  log "INFO: PR #${pr_number}: Head unchanged after rebase (${pr_head_after_rebase}); confirming CI"
437
660
  fi
438
661
 
439
- # Poll CI until all checks attached to the post-rebase head are complete and passing.
440
- if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
441
- log "INFO: PR #${pr_number}: Fresh CI not passing on head ${pr_head_after_rebase} after rebase (${LAST_CI_STATUS}, waited ${CI_MAX_WAIT}s), skipping"
442
- continue
662
+ if [ "${CI_POLICY}" = "ignore" ]; then
663
+ log "INFO: PR #${pr_number}: CI policy is ignore; skipping fresh CI gate after rebase"
664
+ else
665
+ # Poll CI until all checks attached to the post-rebase head are complete and passing.
666
+ if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
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)"
668
+ if [ "${CI_POLICY}" = "fallback-local" ]; then
669
+ if ! run_local_checks_for_head "${pr_number}" "${pr_branch}" "${pr_head_after_rebase}"; then
670
+ log "INFO: PR #${pr_number}: Fresh local check fallback failed after rebase, skipping"
671
+ continue
672
+ fi
673
+ else
674
+ log "INFO: PR #${pr_number}: Skipping because CI policy requires GitHub CI"
675
+ continue
676
+ fi
677
+ fi
443
678
  fi
444
679
  fi
445
680
 
@@ -484,7 +719,7 @@ while IFS= read -r pr_json; do
484
719
 
485
720
  # Enforce global timeout
486
721
  elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
487
- if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
722
+ if is_runtime_limited "${MAX_RUNTIME}" && [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
488
723
  log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
489
724
  break
490
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=$?