@jonit-dev/night-watch-cli 1.7.48 → 1.7.50
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 +960 -377
- package/dist/commands/cron.d.ts +8 -0
- package/dist/commands/cron.d.ts.map +1 -0
- package/dist/commands/cron.js +214 -0
- package/dist/commands/cron.js.map +1 -0
- package/dist/commands/init.js +16 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +3 -27
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/review.d.ts +20 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +97 -18
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -18
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/shared/env-builder.d.ts +25 -0
- package/dist/commands/shared/env-builder.d.ts.map +1 -0
- package/dist/commands/shared/env-builder.js +48 -0
- package/dist/commands/shared/env-builder.js.map +1 -0
- package/dist/commands/slice.d.ts.map +1 -1
- package/dist/commands/slice.js +3 -23
- package/dist/commands/slice.js.map +1 -1
- package/dist/scripts/night-watch-audit-cron.sh +55 -32
- package/dist/scripts/night-watch-cron.sh +108 -2
- package/dist/scripts/night-watch-helpers.sh +36 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +193 -9
- package/dist/scripts/night-watch-qa-cron.sh +12 -2
- package/dist/templates/night-watch-pr-reviewer.md +147 -135
- package/dist/templates/night-watch-slicer.md +1 -1
- package/dist/templates/night-watch.md +1 -1
- package/package.json +1 -1
|
@@ -106,42 +106,65 @@ fi
|
|
|
106
106
|
# Ensure the logs dir exists inside the worktree so the provider can write the report
|
|
107
107
|
mkdir -p "${AUDIT_WORKTREE_DIR}/logs"
|
|
108
108
|
|
|
109
|
+
AUDIT_MAX_RETRIES="${NW_AUDIT_MAX_RETRIES:-3}"
|
|
110
|
+
AUDIT_RETRY_DELAY="${NW_AUDIT_RETRY_DELAY:-120}"
|
|
111
|
+
|
|
109
112
|
log "START: Running code audit for ${PROJECT_NAME} (provider: ${PROVIDER_CMD})"
|
|
110
113
|
|
|
111
114
|
EXIT_CODE=0
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
116
|
+
for AUDIT_ATTEMPT in $(seq 1 "${AUDIT_MAX_RETRIES}"); do
|
|
117
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
118
|
+
log "AUDIT: Attempt ${AUDIT_ATTEMPT}/${AUDIT_MAX_RETRIES}"
|
|
119
|
+
|
|
120
|
+
case "${PROVIDER_CMD}" in
|
|
121
|
+
claude)
|
|
122
|
+
if (
|
|
123
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
124
|
+
claude -p "${AUDIT_PROMPT}" \
|
|
125
|
+
--dangerously-skip-permissions \
|
|
126
|
+
>> "${LOG_FILE}" 2>&1
|
|
127
|
+
); then
|
|
128
|
+
EXIT_CODE=0
|
|
129
|
+
else
|
|
130
|
+
EXIT_CODE=$?
|
|
131
|
+
fi
|
|
132
|
+
;;
|
|
133
|
+
codex)
|
|
134
|
+
if (
|
|
135
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
136
|
+
codex --quiet \
|
|
137
|
+
--yolo \
|
|
138
|
+
--prompt "${AUDIT_PROMPT}" \
|
|
139
|
+
>> "${LOG_FILE}" 2>&1
|
|
140
|
+
); then
|
|
141
|
+
EXIT_CODE=0
|
|
142
|
+
else
|
|
143
|
+
EXIT_CODE=$?
|
|
144
|
+
fi
|
|
145
|
+
;;
|
|
146
|
+
*)
|
|
147
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
148
|
+
emit_result "failure" "reason=unknown_provider"
|
|
149
|
+
exit 1
|
|
150
|
+
;;
|
|
151
|
+
esac
|
|
152
|
+
|
|
153
|
+
# Success or timeout — don't retry
|
|
154
|
+
if [ "${EXIT_CODE}" -eq 0 ] || [ "${EXIT_CODE}" -eq 124 ]; then
|
|
155
|
+
break
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Rate-limit retry with backoff
|
|
159
|
+
if check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}" && [ "${AUDIT_ATTEMPT}" -lt "${AUDIT_MAX_RETRIES}" ]; then
|
|
160
|
+
log "RATE-LIMITED: 429 detected (attempt ${AUDIT_ATTEMPT}/${AUDIT_MAX_RETRIES}), retrying in ${AUDIT_RETRY_DELAY}s..."
|
|
161
|
+
sleep "${AUDIT_RETRY_DELAY}"
|
|
162
|
+
continue
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Non-retryable failure
|
|
166
|
+
break
|
|
167
|
+
done
|
|
145
168
|
|
|
146
169
|
# Copy report back to project dir (if it was written in the worktree)
|
|
147
170
|
WORKTREE_REPORT="${AUDIT_WORKTREE_DIR}/logs/audit-report.md"
|
|
@@ -184,6 +184,95 @@ count_prs_for_branch() {
|
|
|
184
184
|
echo "${count:-0}"
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
checkpoint_timeout_progress() {
|
|
188
|
+
local worktree_dir="${1:?worktree_dir required}"
|
|
189
|
+
local branch_name="${2:?branch_name required}"
|
|
190
|
+
local prd_file="${3:?prd_file required}"
|
|
191
|
+
|
|
192
|
+
if [ ! -d "${worktree_dir}" ]; then
|
|
193
|
+
return 0
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
if ! git -C "${worktree_dir}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [ -z "$(git -C "${worktree_dir}" status --porcelain 2>/dev/null)" ]; then
|
|
201
|
+
log "TIMEOUT: No local changes to checkpoint for ${prd_file}"
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
log "TIMEOUT: Checkpointing local progress for ${prd_file} on ${branch_name}"
|
|
206
|
+
if git -C "${worktree_dir}" add -A >/dev/null 2>&1; then
|
|
207
|
+
if ! git -C "${worktree_dir}" diff --cached --quiet >/dev/null 2>&1; then
|
|
208
|
+
git -C "${worktree_dir}" commit --no-verify \
|
|
209
|
+
-m "chore: checkpoint timed-out progress for ${prd_file}" \
|
|
210
|
+
>> "${LOG_FILE}" 2>&1 || true
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
extract_timeout_phase_titles() {
|
|
216
|
+
local issue_body="${1:-}"
|
|
217
|
+
if [ -z "${issue_body}" ]; then
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
printf '%s\n' "${issue_body}" \
|
|
222
|
+
| tr -d '\r' \
|
|
223
|
+
| awk '
|
|
224
|
+
BEGIN { count = 0 }
|
|
225
|
+
/^[[:space:]]*#{2,4}[[:space:]]*Phase[[:space:]]+[0-9]+[[:space:]]*:/ {
|
|
226
|
+
line = $0
|
|
227
|
+
sub(/^[[:space:]]*#{2,4}[[:space:]]*/, "", line)
|
|
228
|
+
gsub(/[[:space:]]+$/, "", line)
|
|
229
|
+
if (count < 3) {
|
|
230
|
+
count++
|
|
231
|
+
print line
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
build_timeout_followup_comment() {
|
|
238
|
+
local max_runtime="${1:?max_runtime required}"
|
|
239
|
+
local prd_label="${2:?prd_label required}"
|
|
240
|
+
local branch_name="${3:?branch_name required}"
|
|
241
|
+
local issue_body="${4:-}"
|
|
242
|
+
local phase_titles=""
|
|
243
|
+
local comment=""
|
|
244
|
+
|
|
245
|
+
phase_titles=$(extract_timeout_phase_titles "${issue_body}" || true)
|
|
246
|
+
|
|
247
|
+
comment="Timeout follow-up:
|
|
248
|
+
|
|
249
|
+
Execution hit the ${max_runtime}s runtime limit while processing ${prd_label}.
|
|
250
|
+
Progress was checkpointed on branch ${branch_name}, so the next run will resume from the latest checkpoint.
|
|
251
|
+
|
|
252
|
+
Suggested slices for the next runs:"
|
|
253
|
+
|
|
254
|
+
if [ -n "${phase_titles}" ]; then
|
|
255
|
+
local idx=1
|
|
256
|
+
while IFS= read -r phase_title; do
|
|
257
|
+
[ -z "${phase_title}" ] && continue
|
|
258
|
+
comment="${comment}
|
|
259
|
+
${idx}. ${phase_title}"
|
|
260
|
+
idx=$((idx + 1))
|
|
261
|
+
done <<< "${phase_titles}"
|
|
262
|
+
else
|
|
263
|
+
comment="${comment}
|
|
264
|
+
1. Phase 1: Setup and interfaces
|
|
265
|
+
2. Phase 2: Core implementation and tests
|
|
266
|
+
3. Phase 3: Integration and verification"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
comment="${comment}
|
|
270
|
+
|
|
271
|
+
Recommendation: avoid huge PRDs. Slice large work into smaller PRDs/phases so each run can finish within the runtime window."
|
|
272
|
+
|
|
273
|
+
printf '%s' "${comment}"
|
|
274
|
+
}
|
|
275
|
+
|
|
187
276
|
finalize_prd_done() {
|
|
188
277
|
local reason="${1:?reason required}"
|
|
189
278
|
|
|
@@ -215,6 +304,16 @@ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
|
|
|
215
304
|
|
|
216
305
|
log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME} (worktree: ${WORKTREE_DIR})"
|
|
217
306
|
|
|
307
|
+
EXECUTOR_PROMPT_PATH=$(resolve_instruction_path "${PROJECT_DIR}" "prd-executor.md" || true)
|
|
308
|
+
if [ -z "${EXECUTOR_PROMPT_PATH}" ]; then
|
|
309
|
+
log "FAIL: Missing PRD executor instructions. Checked instructions/, .claude/commands/, and bundled templates/"
|
|
310
|
+
restore_issue_to_ready "Failed to locate PRD executor instructions. Checked instructions/, .claude/commands/, and bundled templates/."
|
|
311
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
312
|
+
emit_result "failure" "reason=missing_executor_prompt"
|
|
313
|
+
exit 1
|
|
314
|
+
fi
|
|
315
|
+
EXECUTOR_PROMPT_REF=$(instruction_ref_for_prompt "${PROJECT_DIR}" "${EXECUTOR_PROMPT_PATH}")
|
|
316
|
+
|
|
218
317
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
219
318
|
PROMPT="Implement the following PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}):
|
|
220
319
|
|
|
@@ -228,7 +327,7 @@ ${ISSUE_BODY}
|
|
|
228
327
|
- Install dependencies if needed and implement in the current worktree only
|
|
229
328
|
|
|
230
329
|
## Implementation — PRD Executor Workflow
|
|
231
|
-
Read
|
|
330
|
+
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
232
331
|
1. Parse the PRD into phases and extract dependencies
|
|
233
332
|
2. Build a dependency graph to identify parallelism
|
|
234
333
|
3. Create a task list with one task per phase
|
|
@@ -256,7 +355,7 @@ else
|
|
|
256
355
|
- Install dependencies if needed and implement in the current worktree only
|
|
257
356
|
|
|
258
357
|
## Implementation — PRD Executor Workflow
|
|
259
|
-
Read
|
|
358
|
+
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
260
359
|
1. Parse the PRD into phases and extract dependencies
|
|
261
360
|
2. Build a dependency graph to identify parallelism
|
|
262
361
|
3. Create a task list with one task per phase
|
|
@@ -472,10 +571,17 @@ if [ ${EXIT_CODE} -eq 0 ]; then
|
|
|
472
571
|
fi
|
|
473
572
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
474
573
|
log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
574
|
+
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
475
575
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
476
576
|
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
477
577
|
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
478
578
|
--body "Execution timed out after ${MAX_RUNTIME}s. Moved back to Ready for retry." 2>>"${LOG_FILE}" || true
|
|
579
|
+
TIMEOUT_FOLLOWUP_COMMENT=$(build_timeout_followup_comment \
|
|
580
|
+
"${MAX_RUNTIME}" \
|
|
581
|
+
"${ELIGIBLE_PRD}" \
|
|
582
|
+
"${BRANCH_NAME}" \
|
|
583
|
+
"${ISSUE_BODY}")
|
|
584
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" --body "${TIMEOUT_FOLLOWUP_COMMENT}" 2>>"${LOG_FILE}" || true
|
|
479
585
|
fi
|
|
480
586
|
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" timeout --exit-code 124 2>/dev/null || true
|
|
481
587
|
emit_result "timeout" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
@@ -51,6 +51,42 @@ resolve_night_watch_cli() {
|
|
|
51
51
|
return 1
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
resolve_instruction_path() {
|
|
55
|
+
local project_dir="${1:?project_dir required}"
|
|
56
|
+
local instruction_file="${2:?instruction_file required}"
|
|
57
|
+
local script_dir=""
|
|
58
|
+
local candidate=""
|
|
59
|
+
|
|
60
|
+
if [ -n "${SCRIPT_DIR:-}" ]; then
|
|
61
|
+
script_dir="${SCRIPT_DIR}"
|
|
62
|
+
else
|
|
63
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
for candidate in \
|
|
67
|
+
"${project_dir}/instructions/${instruction_file}" \
|
|
68
|
+
"${project_dir}/.claude/commands/${instruction_file}" \
|
|
69
|
+
"${script_dir}/../templates/${instruction_file}"; do
|
|
70
|
+
if [ -f "${candidate}" ]; then
|
|
71
|
+
printf "%s" "${candidate}"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
instruction_ref_for_prompt() {
|
|
80
|
+
local root_dir="${1:?root_dir required}"
|
|
81
|
+
local instruction_path="${2:?instruction_path required}"
|
|
82
|
+
|
|
83
|
+
if [[ "${instruction_path}" == "${root_dir}/"* ]]; then
|
|
84
|
+
printf "%s" "${instruction_path#${root_dir}/}"
|
|
85
|
+
else
|
|
86
|
+
printf "%s" "${instruction_path}"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
night_watch_history() {
|
|
55
91
|
local cli_bin
|
|
56
92
|
cli_bin=$(resolve_night_watch_cli) || return 127
|
|
@@ -143,6 +143,135 @@ get_pr_score() {
|
|
|
143
143
|
| grep -oP '\d+(?=/100)' || echo ""
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
# Count failed CI checks for a PR.
|
|
147
|
+
# Uses JSON fields when available (more reliable across check name/status formats),
|
|
148
|
+
# then falls back to text parsing for older/mocked gh outputs.
|
|
149
|
+
get_pr_failed_ci_checks() {
|
|
150
|
+
local pr_number="${1:?PR number required}"
|
|
151
|
+
local failed_count=""
|
|
152
|
+
|
|
153
|
+
failed_count="$(
|
|
154
|
+
gh pr checks "${pr_number}" --json bucket,state,conclusion --jq '
|
|
155
|
+
[ .[]
|
|
156
|
+
| (.bucket // "" | ascii_downcase) as $bucket
|
|
157
|
+
| (.state // "" | ascii_downcase) as $state
|
|
158
|
+
| (.conclusion // "" | ascii_downcase) as $conclusion
|
|
159
|
+
| select(
|
|
160
|
+
$bucket == "fail" or
|
|
161
|
+
$bucket == "cancel" or
|
|
162
|
+
$state == "failure" or
|
|
163
|
+
$state == "error" or
|
|
164
|
+
$state == "cancelled" or
|
|
165
|
+
$conclusion == "failure" or
|
|
166
|
+
$conclusion == "error" or
|
|
167
|
+
$conclusion == "cancelled" or
|
|
168
|
+
$conclusion == "timed_out" or
|
|
169
|
+
$conclusion == "action_required" or
|
|
170
|
+
$conclusion == "startup_failure" or
|
|
171
|
+
$conclusion == "stale"
|
|
172
|
+
)
|
|
173
|
+
] | length
|
|
174
|
+
' 2>/dev/null || true
|
|
175
|
+
)"
|
|
176
|
+
|
|
177
|
+
if [[ "${failed_count}" =~ ^[0-9]+$ ]]; then
|
|
178
|
+
echo "${failed_count}"
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
failed_count=$(
|
|
183
|
+
gh pr checks "${pr_number}" 2>/dev/null \
|
|
184
|
+
| grep -Eci 'fail|error|cancel|timed[_ -]?out|action_required|startup_failure|stale' || true
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if [[ "${failed_count}" =~ ^[0-9]+$ ]]; then
|
|
188
|
+
echo "${failed_count}"
|
|
189
|
+
else
|
|
190
|
+
echo "0"
|
|
191
|
+
fi
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Return a semicolon-separated summary of failing CI checks for a PR.
|
|
195
|
+
# Format: "<check name> [state=<state>, conclusion=<conclusion>]"
|
|
196
|
+
get_pr_failed_ci_summary() {
|
|
197
|
+
local pr_number="${1:?PR number required}"
|
|
198
|
+
local failed_summary=""
|
|
199
|
+
|
|
200
|
+
failed_summary="$(
|
|
201
|
+
gh pr checks "${pr_number}" --json name,bucket,state,conclusion --jq '
|
|
202
|
+
[ .[]
|
|
203
|
+
| (.bucket // "" | ascii_downcase) as $bucket
|
|
204
|
+
| (.state // "" | ascii_downcase) as $state
|
|
205
|
+
| (.conclusion // "" | ascii_downcase) as $conclusion
|
|
206
|
+
| select(
|
|
207
|
+
$bucket == "fail" or
|
|
208
|
+
$bucket == "cancel" or
|
|
209
|
+
$state == "failure" or
|
|
210
|
+
$state == "error" or
|
|
211
|
+
$state == "cancelled" or
|
|
212
|
+
$conclusion == "failure" or
|
|
213
|
+
$conclusion == "error" or
|
|
214
|
+
$conclusion == "cancelled" or
|
|
215
|
+
$conclusion == "timed_out" or
|
|
216
|
+
$conclusion == "action_required" or
|
|
217
|
+
$conclusion == "startup_failure" or
|
|
218
|
+
$conclusion == "stale"
|
|
219
|
+
)
|
|
220
|
+
| "\(.name // "unknown") [state=\(.state // "unknown"), conclusion=\(.conclusion // "unknown")]"
|
|
221
|
+
] | join("; ")
|
|
222
|
+
' 2>/dev/null || true
|
|
223
|
+
)"
|
|
224
|
+
|
|
225
|
+
if [ -n "${failed_summary}" ]; then
|
|
226
|
+
echo "${failed_summary}"
|
|
227
|
+
return 0
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# Fallback for older/mocked outputs where JSON fields aren't available.
|
|
231
|
+
failed_summary=$(
|
|
232
|
+
gh pr checks "${pr_number}" 2>/dev/null \
|
|
233
|
+
| grep -Ei 'fail|error|cancel|timed[_ -]?out|action_required|startup_failure|stale' \
|
|
234
|
+
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]][[:space:]]*/ /g' \
|
|
235
|
+
| paste -sd '; ' - || true
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
echo "${failed_summary}"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Clean up reviewer-managed worktrees.
|
|
242
|
+
# - Always removes the caller's runner worktree when runner_scope is provided.
|
|
243
|
+
# - Only non-worker/controller processes perform broad cleanup to avoid
|
|
244
|
+
# parallel workers deleting each other's active worktrees.
|
|
245
|
+
cleanup_reviewer_worktrees() {
|
|
246
|
+
local runner_scope="${1:-}"
|
|
247
|
+
|
|
248
|
+
if [ -n "${runner_scope}" ]; then
|
|
249
|
+
cleanup_worktrees "${PROJECT_DIR}" "${runner_scope}"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
if [ "${WORKER_MODE}" = "1" ]; then
|
|
253
|
+
return 0
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Remove per-PR reviewer worktrees created by prompts from older runs.
|
|
257
|
+
cleanup_worktrees "${PROJECT_DIR}" "${PROJECT_NAME}-nw-review-"
|
|
258
|
+
|
|
259
|
+
# Remove legacy reviewer worktree naming used in some older prompt variants.
|
|
260
|
+
local escaped_project_name
|
|
261
|
+
escaped_project_name=$(printf '%s\n' "${PROJECT_NAME}" | sed 's/[][(){}.^$*+?|\\/]/\\&/g')
|
|
262
|
+
git -C "${PROJECT_DIR}" worktree list --porcelain 2>/dev/null \
|
|
263
|
+
| grep '^worktree ' \
|
|
264
|
+
| awk '{print $2}' \
|
|
265
|
+
| while read -r wt; do
|
|
266
|
+
local wt_basename
|
|
267
|
+
wt_basename=$(basename "${wt}")
|
|
268
|
+
if printf '%s\n' "${wt_basename}" | grep -Eq "^${escaped_project_name}-pr-?[0-9]+$"; then
|
|
269
|
+
log "CLEANUP: Removing legacy reviewer worktree ${wt}"
|
|
270
|
+
git -C "${PROJECT_DIR}" worktree remove --force "${wt}" 2>/dev/null || true
|
|
271
|
+
fi
|
|
272
|
+
done || true
|
|
273
|
+
}
|
|
274
|
+
|
|
146
275
|
# Validate provider
|
|
147
276
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
148
277
|
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
@@ -221,9 +350,14 @@ while IFS=$'\t' read -r pr_number pr_branch; do
|
|
|
221
350
|
continue
|
|
222
351
|
fi
|
|
223
352
|
|
|
224
|
-
FAILED_CHECKS=$(
|
|
353
|
+
FAILED_CHECKS=$(get_pr_failed_ci_checks "${pr_number}")
|
|
225
354
|
if [ "${FAILED_CHECKS}" -gt 0 ]; then
|
|
226
|
-
|
|
355
|
+
FAILED_SUMMARY=$(get_pr_failed_ci_summary "${pr_number}")
|
|
356
|
+
if [ -n "${FAILED_SUMMARY}" ]; then
|
|
357
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has ${FAILED_CHECKS} failed CI check(s): ${FAILED_SUMMARY}"
|
|
358
|
+
else
|
|
359
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has ${FAILED_CHECKS} failed CI check(s)"
|
|
360
|
+
fi
|
|
227
361
|
NEEDS_WORK=1
|
|
228
362
|
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
229
363
|
continue
|
|
@@ -325,6 +459,10 @@ fi
|
|
|
325
459
|
|
|
326
460
|
log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
|
|
327
461
|
|
|
462
|
+
# Remove stale reviewer worktrees from previous interrupted runs.
|
|
463
|
+
# Worker processes skip broad cleanup to avoid parallel interference.
|
|
464
|
+
cleanup_reviewer_worktrees
|
|
465
|
+
|
|
328
466
|
# Convert "#12 #34" into ["12", "34"] for worker fan-out.
|
|
329
467
|
PR_NUMBER_ARRAY=()
|
|
330
468
|
for pr_token in ${PRS_NEEDING_WORK}; do
|
|
@@ -355,7 +493,14 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
355
493
|
declare -a WORKER_PRS=()
|
|
356
494
|
declare -a WORKER_OUTPUTS=()
|
|
357
495
|
|
|
496
|
+
WORKER_IDX=0
|
|
497
|
+
WORKER_STAGGER_DELAY="${NW_REVIEWER_WORKER_STAGGER:-60}"
|
|
358
498
|
for pr_number in "${PR_NUMBER_ARRAY[@]}"; do
|
|
499
|
+
if [ "${WORKER_IDX}" -gt 0 ]; then
|
|
500
|
+
log "PARALLEL: Staggering worker launch by ${WORKER_STAGGER_DELAY}s (worker $((WORKER_IDX + 1))/${#PR_NUMBER_ARRAY[@]})"
|
|
501
|
+
sleep "${WORKER_STAGGER_DELAY}"
|
|
502
|
+
fi
|
|
503
|
+
|
|
359
504
|
worker_output=$(mktemp "/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}-pr-${pr_number}.XXXXXX")
|
|
360
505
|
WORKER_OUTPUTS+=("${worker_output}")
|
|
361
506
|
WORKER_PRS+=("${pr_number}")
|
|
@@ -370,6 +515,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
370
515
|
worker_pid=$!
|
|
371
516
|
WORKER_PIDS+=("${worker_pid}")
|
|
372
517
|
log "PARALLEL: Worker PID ${worker_pid} started for PR #${pr_number}"
|
|
518
|
+
WORKER_IDX=$((WORKER_IDX + 1))
|
|
373
519
|
done
|
|
374
520
|
|
|
375
521
|
EXIT_CODE=0
|
|
@@ -439,6 +585,10 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
439
585
|
fi
|
|
440
586
|
done
|
|
441
587
|
|
|
588
|
+
# Parent/controller process cleans up any per-PR reviewer worktrees that
|
|
589
|
+
# worker runs may have left behind.
|
|
590
|
+
cleanup_reviewer_worktrees
|
|
591
|
+
|
|
442
592
|
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}"
|
|
443
593
|
exit 0
|
|
444
594
|
fi
|
|
@@ -449,7 +599,7 @@ if [ -n "${TARGET_PR}" ]; then
|
|
|
449
599
|
fi
|
|
450
600
|
REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${REVIEW_WORKTREE_BASENAME}"
|
|
451
601
|
|
|
452
|
-
|
|
602
|
+
cleanup_reviewer_worktrees "${REVIEW_WORKTREE_BASENAME}"
|
|
453
603
|
|
|
454
604
|
# Dry-run mode: print diagnostics and exit
|
|
455
605
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
@@ -479,12 +629,38 @@ if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFA
|
|
|
479
629
|
exit 1
|
|
480
630
|
fi
|
|
481
631
|
|
|
632
|
+
REVIEWER_PROMPT_PATH=$(resolve_instruction_path "${REVIEW_WORKTREE_DIR}" "night-watch-pr-reviewer.md" || true)
|
|
633
|
+
if [ -z "${REVIEWER_PROMPT_PATH}" ]; then
|
|
634
|
+
log "FAIL: Missing reviewer prompt file. Checked instructions/, .claude/commands/, and bundled templates/"
|
|
635
|
+
emit_result "failure" "reason=missing_reviewer_prompt"
|
|
636
|
+
exit 1
|
|
637
|
+
fi
|
|
638
|
+
REVIEWER_PROMPT_BASE=$(cat "${REVIEWER_PROMPT_PATH}")
|
|
639
|
+
REVIEWER_PROMPT_REF=$(instruction_ref_for_prompt "${REVIEW_WORKTREE_DIR}" "${REVIEWER_PROMPT_PATH}")
|
|
640
|
+
log "INFO: Using reviewer prompt from ${REVIEWER_PROMPT_REF}"
|
|
641
|
+
|
|
482
642
|
EXIT_CODE=0
|
|
483
643
|
ATTEMPTS_MADE=1
|
|
484
644
|
FINAL_SCORE=""
|
|
485
645
|
TARGET_SCOPE_PROMPT=""
|
|
486
646
|
if [ -n "${TARGET_PR}" ]; then
|
|
487
647
|
TARGET_SCOPE_PROMPT=$'\n\n## Target Scope\n- Only process PR #'"${TARGET_PR}"$'.\n- Ignore all other PRs.\n- If this PR no longer needs work, stop immediately.\n'
|
|
648
|
+
|
|
649
|
+
TARGET_MERGE_STATE=$(gh pr view "${TARGET_PR}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "UNKNOWN")
|
|
650
|
+
TARGET_FAILED_CHECKS=$(get_pr_failed_ci_summary "${TARGET_PR}")
|
|
651
|
+
TARGET_SCORE=$(get_pr_score "${TARGET_PR}")
|
|
652
|
+
|
|
653
|
+
TARGET_SCOPE_PROMPT+=$'\n## Preflight Data (from CLI)\n- mergeStateStatus: '"${TARGET_MERGE_STATE}"$'\n'
|
|
654
|
+
if [ -n "${TARGET_FAILED_CHECKS}" ]; then
|
|
655
|
+
TARGET_SCOPE_PROMPT+=$'- failing checks: '"${TARGET_FAILED_CHECKS}"$'\n'
|
|
656
|
+
else
|
|
657
|
+
TARGET_SCOPE_PROMPT+=$'- failing checks: none detected\n'
|
|
658
|
+
fi
|
|
659
|
+
if [ -n "${TARGET_SCORE}" ]; then
|
|
660
|
+
TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
|
|
661
|
+
else
|
|
662
|
+
TARGET_SCOPE_PROMPT+=$'- latest review score: not found\n'
|
|
663
|
+
fi
|
|
488
664
|
fi
|
|
489
665
|
|
|
490
666
|
# ── Retry Loop for Targeted PR Review ──────────────────────────────────────────
|
|
@@ -518,13 +694,14 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
518
694
|
fi
|
|
519
695
|
|
|
520
696
|
log "RETRY: Starting attempt ${ATTEMPT}/${TOTAL_ATTEMPTS} (timeout: ${ATTEMPT_TIMEOUT}s)"
|
|
697
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
698
|
+
REVIEWER_PROMPT="${REVIEWER_PROMPT_BASE}${TARGET_SCOPE_PROMPT}"
|
|
521
699
|
|
|
522
700
|
case "${PROVIDER_CMD}" in
|
|
523
701
|
claude)
|
|
524
|
-
CLAUDE_PROMPT="/night-watch-pr-reviewer${TARGET_SCOPE_PROMPT}"
|
|
525
702
|
if (
|
|
526
703
|
cd "${REVIEW_WORKTREE_DIR}" && timeout "${ATTEMPT_TIMEOUT}" \
|
|
527
|
-
claude -p "${
|
|
704
|
+
claude -p "${REVIEWER_PROMPT}" \
|
|
528
705
|
--dangerously-skip-permissions \
|
|
529
706
|
>> "${LOG_FILE}" 2>&1
|
|
530
707
|
); then
|
|
@@ -534,12 +711,11 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
534
711
|
fi
|
|
535
712
|
;;
|
|
536
713
|
codex)
|
|
537
|
-
CODEX_PROMPT="$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")${TARGET_SCOPE_PROMPT}"
|
|
538
714
|
if (
|
|
539
715
|
cd "${REVIEW_WORKTREE_DIR}" && timeout "${ATTEMPT_TIMEOUT}" \
|
|
540
716
|
codex --quiet \
|
|
541
717
|
--yolo \
|
|
542
|
-
--prompt "${
|
|
718
|
+
--prompt "${REVIEWER_PROMPT}" \
|
|
543
719
|
>> "${LOG_FILE}" 2>&1
|
|
544
720
|
); then
|
|
545
721
|
EXIT_CODE=0
|
|
@@ -553,8 +729,16 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
553
729
|
;;
|
|
554
730
|
esac
|
|
555
731
|
|
|
556
|
-
# If provider failed (non-zero exit),
|
|
732
|
+
# If provider failed (non-zero exit), check for rate limit before giving up
|
|
557
733
|
if [ "${EXIT_CODE}" -ne 0 ]; then
|
|
734
|
+
if [ "${EXIT_CODE}" -ne 124 ] && \
|
|
735
|
+
check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}" && \
|
|
736
|
+
[ -n "${TARGET_PR}" ] && \
|
|
737
|
+
[ "${ATTEMPT}" -lt "${TOTAL_ATTEMPTS}" ]; then
|
|
738
|
+
log "RATE-LIMITED: 429 detected for PR #${TARGET_PR} (attempt ${ATTEMPT}/${TOTAL_ATTEMPTS}), retrying in 120s..."
|
|
739
|
+
sleep 120
|
|
740
|
+
continue
|
|
741
|
+
fi
|
|
558
742
|
log "RETRY: Provider exited with code ${EXIT_CODE}, not retrying"
|
|
559
743
|
break
|
|
560
744
|
fi
|
|
@@ -585,7 +769,7 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
585
769
|
fi
|
|
586
770
|
done
|
|
587
771
|
|
|
588
|
-
|
|
772
|
+
cleanup_reviewer_worktrees "${REVIEW_WORKTREE_BASENAME}"
|
|
589
773
|
|
|
590
774
|
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
591
775
|
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
@@ -217,11 +217,21 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
217
217
|
continue
|
|
218
218
|
fi
|
|
219
219
|
|
|
220
|
+
QA_PROMPT_PATH=$(resolve_instruction_path "${QA_WORKTREE_DIR}" "night-watch-qa.md" || true)
|
|
221
|
+
if [ -z "${QA_PROMPT_PATH}" ]; then
|
|
222
|
+
log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked instructions/, .claude/commands/, and bundled templates/"
|
|
223
|
+
EXIT_CODE=1
|
|
224
|
+
break
|
|
225
|
+
fi
|
|
226
|
+
QA_PROMPT=$(cat "${QA_PROMPT_PATH}")
|
|
227
|
+
QA_PROMPT_REF=$(instruction_ref_for_prompt "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}")
|
|
228
|
+
log "QA: PR #${pr_num} — using prompt from ${QA_PROMPT_REF}"
|
|
229
|
+
|
|
220
230
|
case "${PROVIDER_CMD}" in
|
|
221
231
|
claude)
|
|
222
232
|
if (
|
|
223
233
|
cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
224
|
-
claude -p "
|
|
234
|
+
claude -p "${QA_PROMPT}" \
|
|
225
235
|
--dangerously-skip-permissions \
|
|
226
236
|
>> "${LOG_FILE}" 2>&1
|
|
227
237
|
); then
|
|
@@ -241,7 +251,7 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
241
251
|
cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
242
252
|
codex --quiet \
|
|
243
253
|
--yolo \
|
|
244
|
-
--prompt "$
|
|
254
|
+
--prompt "${QA_PROMPT}" \
|
|
245
255
|
>> "${LOG_FILE}" 2>&1
|
|
246
256
|
); then
|
|
247
257
|
log "QA: PR #${pr_num} — provider completed successfully"
|