@jonit-dev/night-watch-cli 1.7.54 → 1.7.56

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.
package/dist/cli.js CHANGED
@@ -8,6 +8,10 @@ import "reflect-metadata";
8
8
  import "reflect-metadata";
9
9
  import "reflect-metadata";
10
10
  import "reflect-metadata";
11
+ import "reflect-metadata";
12
+ import "reflect-metadata";
13
+ import "reflect-metadata";
14
+ import "reflect-metadata";
11
15
  import * as fs from "fs";
12
16
  import * as path from "path";
13
17
  import { fileURLToPath } from "url";
@@ -112,6 +112,8 @@ prefer_bundled_prompt_if_legacy_command() {
112
112
  local script_dir=""
113
113
  local bundled_prompt_path=""
114
114
  local rel_path=""
115
+ local first_content_line=""
116
+ local looks_like_wrapper=1
115
117
 
116
118
  if [ -n "${SCRIPT_DIR:-}" ]; then
117
119
  script_dir="${SCRIPT_DIR}"
@@ -121,15 +123,27 @@ prefer_bundled_prompt_if_legacy_command() {
121
123
 
122
124
  bundled_prompt_path="${script_dir}/../templates/${bundled_prompt_name}"
123
125
 
124
- # Legacy .claude command prompt files often contain slash-command wrappers
125
- # rather than the full automation prompt. Prefer bundled templates in that case.
126
- if [[ "${resolved_prompt_path}" == "${project_root}/.claude/commands/"* ]] && [ -f "${bundled_prompt_path}" ]; then
127
- rel_path="${resolved_prompt_path#${project_root}/}"
128
- if grep -Eqi '^[[:space:]]*/[a-z0-9._/-]+' "${resolved_prompt_path}" 2>/dev/null; then
129
- log "WARN: Prompt ${rel_path} looks like a slash-command wrapper; using bundled template ${bundled_prompt_name}"
130
- printf "%s" "${bundled_prompt_path}"
131
- return 0
126
+ # Some instruction files only contain slash-command wrappers (e.g. "/night-watch-qa")
127
+ # rather than the full automation prompt. If detected, force bundled templates.
128
+ if [ -f "${bundled_prompt_path}" ] && [ -f "${resolved_prompt_path}" ]; then
129
+ first_content_line=$(
130
+ awk 'NF {print; exit}' "${resolved_prompt_path}" 2>/dev/null \
131
+ | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
132
+ )
133
+ if printf '%s' "${first_content_line}" | grep -Eqi '^/[a-z0-9._/-]+'; then
134
+ looks_like_wrapper=0
135
+ fi
136
+ fi
137
+
138
+ if [ "${looks_like_wrapper}" -eq 0 ]; then
139
+ if [[ "${resolved_prompt_path}" == "${project_root}/"* ]]; then
140
+ rel_path="${resolved_prompt_path#${project_root}/}"
141
+ else
142
+ rel_path="${resolved_prompt_path}"
132
143
  fi
144
+ log "WARN: Prompt ${rel_path} looks like a slash-command wrapper; using bundled template ${bundled_prompt_name}"
145
+ printf "%s" "${bundled_prompt_path}"
146
+ return 0
133
147
  fi
134
148
 
135
149
  printf "%s" "${resolved_prompt_path}"
@@ -172,6 +172,17 @@ append_csv() {
172
172
  fi
173
173
  }
174
174
 
175
+ provider_output_looks_invalid() {
176
+ local from_line="${1:-0}"
177
+ if [ ! -f "${LOG_FILE}" ]; then
178
+ return 1
179
+ fi
180
+
181
+ tail -n "+$((from_line + 1))" "${LOG_FILE}" 2>/dev/null \
182
+ | grep -Eqi \
183
+ 'Unknown skill:|session is in a broken state|working directory .* no longer exists|Path ".*" does not exist|Please restart this session|failed to start LSP server plugin|spawn .* ENOENT'
184
+ }
185
+
175
186
  truncate_for_prompt() {
176
187
  local text="${1:-}"
177
188
  local limit="${2:-7000}"
@@ -830,9 +841,10 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
830
841
  exit 0
831
842
  fi
832
843
 
833
- REVIEW_WORKTREE_BASENAME="${PROJECT_NAME}-nw-review-runner"
844
+ REVIEW_RUN_TOKEN="${PROJECT_RUNTIME_KEY}-$$"
845
+ REVIEW_WORKTREE_BASENAME="${PROJECT_NAME}-nw-review-runner-${REVIEW_RUN_TOKEN}"
834
846
  if [ -n "${TARGET_PR}" ]; then
835
- REVIEW_WORKTREE_BASENAME="${REVIEW_WORKTREE_BASENAME}-pr-${TARGET_PR}"
847
+ REVIEW_WORKTREE_BASENAME="${PROJECT_NAME}-nw-review-runner-pr-${TARGET_PR}-${REVIEW_RUN_TOKEN}"
836
848
  fi
837
849
  REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${REVIEW_WORKTREE_BASENAME}"
838
850
 
@@ -934,6 +946,51 @@ if [ -n "${TARGET_PR}" ]; then
934
946
  fi
935
947
  RUN_STARTED_AT=$(date +%s)
936
948
 
949
+ remaining_runtime_budget() {
950
+ local now_ts
951
+ local elapsed
952
+ local remaining
953
+
954
+ now_ts=$(date +%s)
955
+ elapsed=$((now_ts - RUN_STARTED_AT))
956
+ remaining=$((MAX_RUNTIME - elapsed))
957
+ printf "%s" "${remaining}"
958
+ }
959
+
960
+ sleep_with_runtime_budget() {
961
+ local requested_sleep="${1:-0}"
962
+ local remaining
963
+ local sleep_for
964
+
965
+ if ! [[ "${requested_sleep}" =~ ^[0-9]+$ ]]; then
966
+ requested_sleep=0
967
+ fi
968
+ if [ "${requested_sleep}" -le 0 ]; then
969
+ return 0
970
+ fi
971
+
972
+ if [ -z "${TARGET_PR}" ]; then
973
+ sleep "${requested_sleep}"
974
+ return 0
975
+ fi
976
+
977
+ remaining=$(remaining_runtime_budget)
978
+ if [ "${remaining}" -le 0 ]; then
979
+ return 124
980
+ fi
981
+
982
+ sleep_for="${requested_sleep}"
983
+ if [ "${sleep_for}" -gt "${remaining}" ]; then
984
+ sleep_for="${remaining}"
985
+ fi
986
+ if [ "${sleep_for}" -le 0 ]; then
987
+ return 124
988
+ fi
989
+
990
+ sleep "${sleep_for}"
991
+ return 0
992
+ }
993
+
937
994
  for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
938
995
  ATTEMPTS_MADE="${ATTEMPT}"
939
996
 
@@ -956,6 +1013,17 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
956
1013
  fi
957
1014
  fi
958
1015
 
1016
+ # Recreate worktree if it was removed unexpectedly between attempts.
1017
+ if [ ! -d "${REVIEW_WORKTREE_DIR}" ] || ! git -C "${REVIEW_WORKTREE_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1018
+ log "RETRY: Reviewer worktree missing for attempt ${ATTEMPT}; recreating ${REVIEW_WORKTREE_DIR}"
1019
+ cleanup_reviewer_worktrees "${REVIEW_WORKTREE_BASENAME}"
1020
+ if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
1021
+ EXIT_CODE=1
1022
+ log "RETRY: Unable to recreate reviewer worktree; aborting"
1023
+ break
1024
+ fi
1025
+ fi
1026
+
959
1027
  log "RETRY: Starting attempt ${ATTEMPT}/${TOTAL_ATTEMPTS} (timeout: ${ATTEMPT_TIMEOUT}s)"
960
1028
  LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
961
1029
  REVIEWER_PROMPT="${REVIEWER_PROMPT_BASE}${TARGET_SCOPE_PROMPT}${PRD_CONTEXT_PROMPT}"
@@ -994,23 +1062,56 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
994
1062
 
995
1063
  # If provider failed (non-zero exit), check for rate limit before giving up
996
1064
  if [ "${EXIT_CODE}" -ne 0 ]; then
997
- if [ "${EXIT_CODE}" -ne 124 ] && \
998
- check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}" && \
999
- [ -n "${TARGET_PR}" ] && \
1000
- [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1001
- log "RATE-LIMITED: 429 detected for PR #${TARGET_PR} (attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}), retrying in 120s..."
1002
- sleep 120
1003
- continue
1004
- fi
1065
+ if [ "${EXIT_CODE}" -ne 124 ] && \
1066
+ check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}" && \
1067
+ [ -n "${TARGET_PR}" ] && \
1068
+ [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1069
+ log "RATE-LIMITED: 429 detected for PR #${TARGET_PR} (attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}), retrying in 120s..."
1070
+ if ! sleep_with_runtime_budget 120; then
1071
+ EXIT_CODE=124
1072
+ log "RETRY: Runtime budget exhausted while waiting to retry PR #${TARGET_PR}"
1073
+ break
1074
+ fi
1075
+ continue
1076
+ fi
1005
1077
  log "RETRY: Provider exited with code ${EXIT_CODE}, not retrying"
1006
1078
  break
1007
1079
  fi
1008
1080
 
1081
+ if provider_output_looks_invalid "${LOG_LINE_BEFORE}"; then
1082
+ log "RETRY: Invalid provider output detected for attempt ${ATTEMPT} (broken session/wrapper output)"
1083
+ if [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1084
+ if ! sleep_with_runtime_budget "${REVIEWER_RETRY_DELAY}"; then
1085
+ EXIT_CODE=124
1086
+ log "RETRY: Runtime budget exhausted before retrying invalid provider output"
1087
+ break
1088
+ fi
1089
+ continue
1090
+ fi
1091
+ EXIT_CODE=1
1092
+ break
1093
+ fi
1094
+
1009
1095
  # Re-check score for the target PR (only in targeted mode)
1010
1096
  if [ -n "${TARGET_PR}" ]; then
1011
1097
  CURRENT_SCORE=$(get_pr_score "${TARGET_PR}")
1012
1098
  if [ -z "${CURRENT_SCORE}" ]; then
1013
- log "RETRY: No review score found for PR #${TARGET_PR} after attempt ${ATTEMPT}; not retrying"
1099
+ CURRENT_FAILED_CHECKS=$(get_pr_failed_ci_summary "${TARGET_PR}")
1100
+ if [ -z "${CURRENT_FAILED_CHECKS}" ]; then
1101
+ log "RETRY: No review score for PR #${TARGET_PR}, but CI shows no failing checks; treating as successful."
1102
+ break
1103
+ fi
1104
+ if [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1105
+ log "RETRY: No review score found for PR #${TARGET_PR} after attempt ${ATTEMPT}; retrying in ${REVIEWER_RETRY_DELAY}s..."
1106
+ if ! sleep_with_runtime_budget "${REVIEWER_RETRY_DELAY}"; then
1107
+ EXIT_CODE=124
1108
+ log "RETRY: Runtime budget exhausted before retrying missing score for PR #${TARGET_PR}"
1109
+ break
1110
+ fi
1111
+ continue
1112
+ fi
1113
+ log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; failing run."
1114
+ EXIT_CODE=1
1014
1115
  break
1015
1116
  fi
1016
1117
 
@@ -1018,13 +1119,17 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
1018
1119
  if [ "${CURRENT_SCORE}" -ge "${MIN_REVIEW_SCORE}" ]; then
1019
1120
  log "RETRY: PR #${TARGET_PR} now scores ${CURRENT_SCORE}/100 (>= ${MIN_REVIEW_SCORE}) after attempt ${ATTEMPT}"
1020
1121
  break
1021
- fi
1022
- if [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1023
- log "RETRY: PR #${TARGET_PR} scores ${CURRENT_SCORE:-unknown}/100 after attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}, retrying in ${REVIEWER_RETRY_DELAY}s..."
1024
- sleep "${REVIEWER_RETRY_DELAY}"
1025
- else
1026
- log "RETRY: PR #${TARGET_PR} still at ${CURRENT_SCORE:-unknown}/100 after ${TOTAL_ATTEMPTS} attempts - giving up"
1027
- gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
1122
+ fi
1123
+ if [ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
1124
+ log "RETRY: PR #${TARGET_PR} scores ${CURRENT_SCORE:-unknown}/100 after attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}, retrying in ${REVIEWER_RETRY_DELAY}s..."
1125
+ if ! sleep_with_runtime_budget "${REVIEWER_RETRY_DELAY}"; then
1126
+ EXIT_CODE=124
1127
+ log "RETRY: Runtime budget exhausted before retrying low score for PR #${TARGET_PR}"
1128
+ break
1129
+ fi
1130
+ else
1131
+ log "RETRY: PR #${TARGET_PR} still at ${CURRENT_SCORE:-unknown}/100 after ${TOTAL_ATTEMPTS} attempts - giving up"
1132
+ gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
1028
1133
  fi
1029
1134
  else
1030
1135
  # Non-targeted mode: no retry (reviewer handles all PRs in one shot)
@@ -514,14 +514,19 @@ Action: generating QA tests and evidence."
514
514
  fi
515
515
 
516
516
  log "QA: Checking out PR #${pr_num} in worktree"
517
- if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
518
- log "WARN: Failed to checkout PR #${pr_num}, skipping"
519
- FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
520
- FAILED_PR="#${pr_num}"
521
- FAILED_REASON="checkout_failed"
522
- EXIT_CODE=1
523
- cleanup_worktrees "${PROJECT_DIR}"
524
- continue
517
+ # Prefer detached checkout to avoid "branch already used by worktree" failures
518
+ # when the same branch is already checked out in another local worktree.
519
+ if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" --detach >> "${LOG_FILE}" 2>&1); then
520
+ log "WARN: Detached checkout failed for PR #${pr_num}; retrying with standard checkout"
521
+ if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
522
+ log "WARN: Failed to checkout PR #${pr_num}, skipping"
523
+ FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
524
+ FAILED_PR="#${pr_num}"
525
+ FAILED_REASON="checkout_failed"
526
+ EXIT_CODE=1
527
+ cleanup_worktrees "${PROJECT_DIR}"
528
+ continue
529
+ fi
525
530
  fi
526
531
 
527
532
  QA_PROMPT_PATH=$(resolve_instruction_path_with_fallback "${QA_WORKTREE_DIR}" "qa.md" "night-watch-qa.md" || true)