@jonit-dev/night-watch-cli 1.8.10-beta.5 → 1.8.10-beta.7
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 +29 -1
- package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
- package/dist/commands/dashboard/tab-config.js +1 -0
- package/dist/commands/dashboard/tab-config.js.map +1 -1
- package/dist/commands/shared/env-builder.d.ts.map +1 -1
- package/dist/commands/shared/env-builder.js +1 -0
- package/dist/commands/shared/env-builder.js.map +1 -1
- package/dist/scripts/night-watch-cron.sh +338 -50
- package/dist/scripts/night-watch-helpers.sh +117 -0
- package/dist/scripts/night-watch-merger-cron.sh +7 -1
- package/dist/scripts/night-watch-pr-resolver-cron.sh +7 -3
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +5 -0
- package/dist/scripts/night-watch-qa-cron.sh +5 -0
- package/dist/web/assets/index-BmRrWWHn.js +406 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -140,6 +140,12 @@ ISSUE_NUMBER="" # board mode: GitHub issue number
|
|
|
140
140
|
ISSUE_BODY="" # board mode: issue body (PRD content)
|
|
141
141
|
ISSUE_TITLE_RAW="" # board mode: issue title
|
|
142
142
|
NW_CLI="" # board mode: resolved night-watch CLI binary
|
|
143
|
+
EXECUTOR_PR_JSON=""
|
|
144
|
+
EXECUTOR_PR_NUMBER=""
|
|
145
|
+
EXECUTOR_PR_URL=""
|
|
146
|
+
EXECUTOR_PR_DRAFT=""
|
|
147
|
+
RESUME_FROM_EXISTING_PR=0
|
|
148
|
+
RESUME_BRANCH_NAME=""
|
|
143
149
|
|
|
144
150
|
restore_issue_to_ready() {
|
|
145
151
|
local reason="${1:-Execution failed before implementation started.}"
|
|
@@ -149,6 +155,26 @@ restore_issue_to_ready() {
|
|
|
149
155
|
fi
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
if [ -z "${NW_TARGET_ISSUE:-}" ]; then
|
|
159
|
+
EXECUTOR_PR_JSON=$(find_executor_resume_pr "${BRANCH_PREFIX}" || true)
|
|
160
|
+
if [ -n "${EXECUTOR_PR_JSON}" ]; then
|
|
161
|
+
RESUME_FROM_EXISTING_PR=1
|
|
162
|
+
RESUME_BRANCH_NAME=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.headRefName // empty' 2>/dev/null || true)
|
|
163
|
+
EXECUTOR_PR_NUMBER=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
164
|
+
EXECUTOR_PR_URL=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.url // empty' 2>/dev/null || true)
|
|
165
|
+
EXECUTOR_PR_DRAFT=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.isDraft // false' 2>/dev/null || true)
|
|
166
|
+
if [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
167
|
+
log "RESUME: Prioritizing resumable PR #${EXECUTOR_PR_NUMBER:-unknown} on ${RESUME_BRANCH_NAME}"
|
|
168
|
+
else
|
|
169
|
+
RESUME_FROM_EXISTING_PR=0
|
|
170
|
+
EXECUTOR_PR_JSON=""
|
|
171
|
+
EXECUTOR_PR_NUMBER=""
|
|
172
|
+
EXECUTOR_PR_URL=""
|
|
173
|
+
EXECUTOR_PR_DRAFT=""
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
|
|
152
178
|
if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
153
179
|
# Board mode: discover next task from GitHub Projects board
|
|
154
180
|
NW_CLI=$(resolve_night_watch_cli 2>/dev/null || true)
|
|
@@ -169,35 +195,51 @@ if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
|
169
195
|
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
170
196
|
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
171
197
|
else
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
if [ "${RESUME_FROM_EXISTING_PR}" = "1" ] && [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
199
|
+
ELIGIBLE_PRD="${RESUME_BRANCH_NAME#*/}"
|
|
200
|
+
ISSUE_NUMBER=$(printf '%s' "${ELIGIBLE_PRD}" | grep -oE '^[0-9]+' || true)
|
|
201
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
202
|
+
ISSUE_JSON=$(gh issue view "${ISSUE_NUMBER}" --json number,title,body 2>/dev/null || true)
|
|
203
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
204
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
205
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
206
|
+
log "WARN: Failed to move resumed issue #${ISSUE_NUMBER} to In Progress"
|
|
181
207
|
else
|
|
182
|
-
|
|
208
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
183
209
|
fi
|
|
184
210
|
else
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
BOARD_DISCOVERY_STATUS=0
|
|
212
|
+
if ISSUE_JSON=$(find_eligible_board_issue "${PROJECT_DIR}" "${MAX_RUNTIME}"); then
|
|
213
|
+
BOARD_DISCOVERY_STATUS=0
|
|
214
|
+
else
|
|
215
|
+
BOARD_DISCOVERY_STATUS=$?
|
|
216
|
+
fi
|
|
217
|
+
if [ -z "${ISSUE_JSON}" ]; then
|
|
218
|
+
if [ "${BOARD_DISCOVERY_STATUS}" -eq 2 ]; then
|
|
219
|
+
log "INFO: Ready board issues were found, but all are in cooldown; skipping this run"
|
|
220
|
+
else
|
|
221
|
+
log "INFO: No Ready board issues found; skipping this run"
|
|
222
|
+
fi
|
|
223
|
+
else
|
|
224
|
+
ISSUE_NUMBER=$(printf '%s' "${ISSUE_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
225
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
226
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
227
|
+
if [ -z "${ISSUE_NUMBER}" ]; then
|
|
228
|
+
log "ERROR: Board mode: failed to parse issue number from JSON"
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|
|
231
|
+
# Move issue to In Progress (claim it on the board)
|
|
232
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
233
|
+
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
191
234
|
fi
|
|
192
|
-
# Move issue to In Progress (claim it on the board)
|
|
193
|
-
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
194
|
-
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
195
235
|
fi
|
|
196
236
|
fi
|
|
197
237
|
|
|
198
238
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
199
|
-
# Slugify title for branch naming
|
|
200
|
-
|
|
239
|
+
# Slugify title for branch naming unless we're resuming an existing PR branch.
|
|
240
|
+
if [ "${RESUME_FROM_EXISTING_PR}" != "1" ] || [ -z "${ELIGIBLE_PRD:-}" ]; then
|
|
241
|
+
ELIGIBLE_PRD="${ISSUE_NUMBER}-$(printf '%s' "${ISSUE_TITLE_RAW}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
|
|
242
|
+
fi
|
|
201
243
|
log "BOARD: Processing issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}"
|
|
202
244
|
fi
|
|
203
245
|
fi
|
|
@@ -208,8 +250,25 @@ if [ -z "${ISSUE_NUMBER}" ]; then
|
|
|
208
250
|
emit_result "skip_no_eligible_prd"
|
|
209
251
|
exit 0
|
|
210
252
|
fi
|
|
253
|
+
if [ "${RESUME_FROM_EXISTING_PR}" = "1" ] && [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
254
|
+
RESUME_PRD_NAME="${RESUME_BRANCH_NAME#*/}"
|
|
255
|
+
if [ -f "${PRD_DIR}/${RESUME_PRD_NAME}.md" ]; then
|
|
256
|
+
ELIGIBLE_PRD="${RESUME_PRD_NAME}.md"
|
|
257
|
+
log "RESUME: Using resumable filesystem PRD ${ELIGIBLE_PRD}"
|
|
258
|
+
else
|
|
259
|
+
log "WARN: Resumable PR branch ${RESUME_BRANCH_NAME} has no matching PRD file in ${PRD_DIR}; falling back to normal selection"
|
|
260
|
+
RESUME_FROM_EXISTING_PR=0
|
|
261
|
+
EXECUTOR_PR_JSON=""
|
|
262
|
+
EXECUTOR_PR_NUMBER=""
|
|
263
|
+
EXECUTOR_PR_URL=""
|
|
264
|
+
EXECUTOR_PR_DRAFT=""
|
|
265
|
+
RESUME_BRANCH_NAME=""
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
211
268
|
# Filesystem mode: scan PRD directory
|
|
212
|
-
|
|
269
|
+
if [ -z "${ELIGIBLE_PRD:-}" ]; then
|
|
270
|
+
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
|
|
271
|
+
fi
|
|
213
272
|
if [ -z "${ELIGIBLE_PRD}" ]; then
|
|
214
273
|
log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
|
|
215
274
|
emit_result "skip_no_eligible_prd"
|
|
@@ -221,7 +280,11 @@ if [ -z "${ISSUE_NUMBER}" ]; then
|
|
|
221
280
|
fi
|
|
222
281
|
|
|
223
282
|
PRD_NAME="${ELIGIBLE_PRD%.md}"
|
|
224
|
-
|
|
283
|
+
if [ -n "${RESUME_BRANCH_NAME}" ]; then
|
|
284
|
+
BRANCH_NAME="${RESUME_BRANCH_NAME}"
|
|
285
|
+
else
|
|
286
|
+
BRANCH_NAME="${BRANCH_PREFIX}/${PRD_NAME}"
|
|
287
|
+
fi
|
|
225
288
|
WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-${PRD_NAME}"
|
|
226
289
|
BOOKKEEP_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-bookkeeping"
|
|
227
290
|
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
@@ -248,6 +311,198 @@ count_prs_for_branch() {
|
|
|
248
311
|
echo "${count:-0}"
|
|
249
312
|
}
|
|
250
313
|
|
|
314
|
+
extract_prd_title() {
|
|
315
|
+
local prd_path=""
|
|
316
|
+
local title=""
|
|
317
|
+
|
|
318
|
+
if [ -n "${ISSUE_TITLE_RAW}" ]; then
|
|
319
|
+
printf '%s' "${ISSUE_TITLE_RAW}"
|
|
320
|
+
return 0
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if [ -f "${PRD_DIR}/${ELIGIBLE_PRD}" ]; then
|
|
324
|
+
prd_path="${PRD_DIR}/${ELIGIBLE_PRD}"
|
|
325
|
+
elif [ -f "${PROJECT_DIR}/${PRD_DIR_REL}/${ELIGIBLE_PRD}" ]; then
|
|
326
|
+
prd_path="${PROJECT_DIR}/${PRD_DIR_REL}/${ELIGIBLE_PRD}"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [ -n "${prd_path}" ]; then
|
|
330
|
+
title=$(awk '/^[[:space:]]*#[[:space:]]+/ { sub(/^[[:space:]]*#[[:space:]]+/, "", $0); print; exit }' "${prd_path}" 2>/dev/null || true)
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ -n "${title}" ]; then
|
|
334
|
+
printf '%s' "${title}"
|
|
335
|
+
else
|
|
336
|
+
printf '%s' "${PRD_NAME}"
|
|
337
|
+
fi
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
build_executor_pr_title() {
|
|
341
|
+
local raw_title=""
|
|
342
|
+
|
|
343
|
+
raw_title=$(extract_prd_title)
|
|
344
|
+
raw_title=$(printf '%s' "${raw_title}" | tr '\r\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//')
|
|
345
|
+
if [ -z "${raw_title}" ]; then
|
|
346
|
+
raw_title="${PRD_NAME}"
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
if printf '%s' "${raw_title}" | grep -Eqi '^feat:'; then
|
|
350
|
+
printf '%s' "${raw_title}"
|
|
351
|
+
else
|
|
352
|
+
printf 'feat: %s' "${raw_title}"
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
build_executor_pr_body() {
|
|
357
|
+
local status_blurb=""
|
|
358
|
+
|
|
359
|
+
status_blurb="Status labels:
|
|
360
|
+
- ${NW_EXECUTOR_PARTIAL_LABEL}: implementation is in progress and intentionally incomplete
|
|
361
|
+
- ${NW_EXECUTOR_RESUMABLE_LABEL}: resume this PR before starting new work
|
|
362
|
+
- ${NW_EXECUTOR_READY_REVIEW_LABEL}: implementation is complete and ready for review"
|
|
363
|
+
|
|
364
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
365
|
+
printf 'Closes #%s\n\nNight Watch manages this draft PR automatically so progress is preserved across retries and timeouts.\n\n%s\n' \
|
|
366
|
+
"${ISSUE_NUMBER}" \
|
|
367
|
+
"${status_blurb}"
|
|
368
|
+
else
|
|
369
|
+
printf 'Source PRD: `%s/%s`\n\nNight Watch manages this draft PR automatically so progress is preserved across retries and timeouts.\n\n%s\n' \
|
|
370
|
+
"${PRD_DIR_REL}" \
|
|
371
|
+
"${ELIGIBLE_PRD}" \
|
|
372
|
+
"${status_blurb}"
|
|
373
|
+
fi
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
refresh_executor_pr_metadata() {
|
|
377
|
+
EXECUTOR_PR_JSON=$(find_open_pr_for_branch "${BRANCH_NAME}" || true)
|
|
378
|
+
EXECUTOR_PR_NUMBER=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
379
|
+
EXECUTOR_PR_URL=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.url // empty' 2>/dev/null || true)
|
|
380
|
+
EXECUTOR_PR_DRAFT=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.isDraft // false' 2>/dev/null || true)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
sync_executor_pr_status() {
|
|
384
|
+
local add_labels="${1:-}"
|
|
385
|
+
local remove_labels="${2:-}"
|
|
386
|
+
local mark_ready="${3:-0}"
|
|
387
|
+
|
|
388
|
+
if [ -z "${EXECUTOR_PR_NUMBER}" ]; then
|
|
389
|
+
return 1
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
ensure_executor_status_labels
|
|
393
|
+
|
|
394
|
+
if [ -n "${add_labels}" ]; then
|
|
395
|
+
gh pr edit "${EXECUTOR_PR_NUMBER}" --add-label "${add_labels}" >> "${LOG_FILE}" 2>&1 || true
|
|
396
|
+
fi
|
|
397
|
+
if [ -n "${remove_labels}" ]; then
|
|
398
|
+
gh pr edit "${EXECUTOR_PR_NUMBER}" --remove-label "${remove_labels}" >> "${LOG_FILE}" 2>&1 || true
|
|
399
|
+
fi
|
|
400
|
+
if [ "${mark_ready}" = "1" ]; then
|
|
401
|
+
gh pr ready "${EXECUTOR_PR_NUMBER}" >> "${LOG_FILE}" 2>&1 || true
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
refresh_executor_pr_metadata
|
|
405
|
+
return 0
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
mark_executor_pr_incomplete() {
|
|
409
|
+
sync_executor_pr_status \
|
|
410
|
+
"${NW_EXECUTOR_PARTIAL_LABEL},${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
411
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
412
|
+
"0"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
mark_executor_pr_ready_for_review() {
|
|
416
|
+
sync_executor_pr_status \
|
|
417
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
418
|
+
"${NW_EXECUTOR_PARTIAL_LABEL},${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
419
|
+
"1"
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
push_executor_branch() {
|
|
423
|
+
local push_mode="${1:-update}"
|
|
424
|
+
|
|
425
|
+
if [ "${push_mode}" = "initial" ]; then
|
|
426
|
+
if git_push_for_project "${WORKTREE_DIR}" -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1; then
|
|
427
|
+
return 0
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
if git_push_for_project "${WORKTREE_DIR}" origin "${BRANCH_NAME}" --force-with-lease >> "${LOG_FILE}" 2>&1; then
|
|
432
|
+
return 0
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
git_push_for_project "${WORKTREE_DIR}" -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
executor_branch_has_commits_ahead_of_base() {
|
|
439
|
+
local base_ref=""
|
|
440
|
+
local ahead_count="0"
|
|
441
|
+
|
|
442
|
+
if git -C "${WORKTREE_DIR}" rev-parse --verify "origin/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
|
|
443
|
+
base_ref="origin/${DEFAULT_BRANCH}"
|
|
444
|
+
elif git -C "${WORKTREE_DIR}" rev-parse --verify "${DEFAULT_BRANCH}" >/dev/null 2>&1; then
|
|
445
|
+
base_ref="${DEFAULT_BRANCH}"
|
|
446
|
+
else
|
|
447
|
+
return 0
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
ahead_count=$(git -C "${WORKTREE_DIR}" rev-list --count "${base_ref}..HEAD" 2>/dev/null || echo "0")
|
|
451
|
+
[ "${ahead_count}" -gt 0 ]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
ensure_executor_pr() {
|
|
455
|
+
local pr_title=""
|
|
456
|
+
local pr_body=""
|
|
457
|
+
local create_output=""
|
|
458
|
+
|
|
459
|
+
refresh_executor_pr_metadata
|
|
460
|
+
if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
|
|
461
|
+
log "PR: Reusing existing open PR #${EXECUTOR_PR_NUMBER} for ${BRANCH_NAME}"
|
|
462
|
+
mark_executor_pr_incomplete
|
|
463
|
+
return 0
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
pr_title=$(build_executor_pr_title)
|
|
467
|
+
pr_body=$(build_executor_pr_body)
|
|
468
|
+
|
|
469
|
+
log "PR: Creating draft PR for ${BRANCH_NAME}"
|
|
470
|
+
if ! push_executor_branch "initial"; then
|
|
471
|
+
log "WARN: Initial push for ${BRANCH_NAME} failed before PR creation"
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
if ! executor_branch_has_commits_ahead_of_base; then
|
|
475
|
+
log "PR: Deferring draft PR creation for ${BRANCH_NAME} until it has commits beyond ${DEFAULT_BRANCH}"
|
|
476
|
+
return 0
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
ensure_executor_status_labels
|
|
480
|
+
if ! create_output=$(
|
|
481
|
+
gh pr create \
|
|
482
|
+
--draft \
|
|
483
|
+
--base "${DEFAULT_BRANCH}" \
|
|
484
|
+
--head "${BRANCH_NAME}" \
|
|
485
|
+
--title "${pr_title}" \
|
|
486
|
+
--body "${pr_body}" 2>> "${LOG_FILE}"
|
|
487
|
+
); then
|
|
488
|
+
log "FAIL: gh pr create failed for ${BRANCH_NAME}"
|
|
489
|
+
return 1
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
refresh_executor_pr_metadata
|
|
493
|
+
if [ -z "${EXECUTOR_PR_URL}" ]; then
|
|
494
|
+
EXECUTOR_PR_URL=$(printf '%s' "${create_output}" | grep -Eo 'https://[^[:space:]]+/pull/[0-9]+' | tail -n 1 || true)
|
|
495
|
+
fi
|
|
496
|
+
if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
|
|
497
|
+
mark_executor_pr_incomplete
|
|
498
|
+
log "PR: Draft PR ready at ${EXECUTOR_PR_URL:-unknown} for ${BRANCH_NAME}"
|
|
499
|
+
else
|
|
500
|
+
log "WARN: gh pr create succeeded for ${BRANCH_NAME}, but PR metadata lookup did not resolve a number yet"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
return 0
|
|
504
|
+
}
|
|
505
|
+
|
|
251
506
|
checkpoint_timeout_progress() {
|
|
252
507
|
local worktree_dir="${1:?worktree_dir required}"
|
|
253
508
|
local branch_name="${2:?branch_name required}"
|
|
@@ -358,7 +613,7 @@ finalize_prd_done() {
|
|
|
358
613
|
git -C "${BOOKKEEP_WORKTREE_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (${reason})
|
|
359
614
|
|
|
360
615
|
Co-Authored-By: Night Watch [${EFFECTIVE_PROVIDER_LABEL}] <noreply@anthropic.com>" || true
|
|
361
|
-
|
|
616
|
+
git_push_for_project "${BOOKKEEP_WORKTREE_DIR}" origin "HEAD:${DEFAULT_BRANCH}" || true
|
|
362
617
|
log "DONE: ${ELIGIBLE_PRD} ${reason}, PRD moved to done/"
|
|
363
618
|
return 0
|
|
364
619
|
fi
|
|
@@ -394,6 +649,7 @@ if [ -z "${EXECUTOR_PROMPT_PATH}" ]; then
|
|
|
394
649
|
exit 1
|
|
395
650
|
fi
|
|
396
651
|
EXECUTOR_PROMPT_REF=$(instruction_ref_for_prompt "${PROJECT_DIR}" "${EXECUTOR_PROMPT_PATH}")
|
|
652
|
+
PROGRESS_PUSH_CMD=$(project_git_push_command "${BRANCH_NAME}")
|
|
397
653
|
|
|
398
654
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
399
655
|
PROMPT="Implement the following PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}):
|
|
@@ -416,13 +672,13 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
|
416
672
|
5. Run the project's verify/test command between waves to catch issues early
|
|
417
673
|
Follow all CLAUDE.md conventions (if present).
|
|
418
674
|
|
|
419
|
-
##
|
|
420
|
-
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
-
|
|
675
|
+
## PR Lifecycle
|
|
676
|
+
- The controller owns PR lifecycle and labels for this branch
|
|
677
|
+
- If a draft PR already exists, do NOT create another one and do NOT edit its labels
|
|
678
|
+
- If no PR exists yet, do NOT create one manually; keep pushing to ${BRANCH_NAME} and the controller will open/update it automatically
|
|
679
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
680
|
+
${PROGRESS_PUSH_CMD}
|
|
681
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
426
682
|
- STOP immediately after the final push — do NOT do any additional work, visual checks, or exploration.
|
|
427
683
|
- Do NOT process any other issues — only issue #${ISSUE_NUMBER}"
|
|
428
684
|
else
|
|
@@ -445,11 +701,13 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
|
|
|
445
701
|
5. Run the project's verify/test command between waves to catch issues early
|
|
446
702
|
Follow all CLAUDE.md conventions (if present).
|
|
447
703
|
|
|
448
|
-
##
|
|
449
|
-
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
- After
|
|
704
|
+
## PR Lifecycle
|
|
705
|
+
- The controller owns PR lifecycle and labels for this branch
|
|
706
|
+
- If a draft PR already exists, do NOT create another one and do NOT edit its labels
|
|
707
|
+
- If no PR exists yet, do NOT create one manually; keep pushing to ${BRANCH_NAME} and the controller will open/update it automatically
|
|
708
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
709
|
+
${PROGRESS_PUSH_CMD}
|
|
710
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
453
711
|
- STOP immediately after the final push — do NOT do any additional work, visual checks, or exploration.
|
|
454
712
|
- Do NOT move the PRD to done/ — the cron script handles that
|
|
455
713
|
- Do NOT process any other PRDs — only ${ELIGIBLE_PRD}"
|
|
@@ -505,6 +763,19 @@ if ! assert_isolated_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "executor"; the
|
|
|
505
763
|
exit 1
|
|
506
764
|
fi
|
|
507
765
|
|
|
766
|
+
if ! ensure_executor_pr; then
|
|
767
|
+
log "FAIL: Unable to create or reuse executor PR for ${BRANCH_NAME}"
|
|
768
|
+
restore_issue_to_ready "Failed to create or reuse the draft PR for branch ${BRANCH_NAME}. Moved back to Ready for retry."
|
|
769
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
770
|
+
emit_result "failure" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}|reason=pr_setup_failed|detail=$(latest_failure_detail "${LOG_FILE}")"
|
|
771
|
+
exit 1
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
if [ -n "${ISSUE_NUMBER}" ] && [ -n "${NW_CLI}" ] && [ -n "${EXECUTOR_PR_URL}" ] && [ "${RESUME_FROM_EXISTING_PR}" != "1" ]; then
|
|
775
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
776
|
+
--body "Draft PR opened at executor start: ${EXECUTOR_PR_URL} (labels: \`${NW_EXECUTOR_PARTIAL_LABEL}\`, \`${NW_EXECUTOR_RESUMABLE_LABEL}\`)." 2>>"${LOG_FILE}" || true
|
|
777
|
+
fi
|
|
778
|
+
|
|
508
779
|
# Sandbox: prevent the agent from modifying crontab during execution
|
|
509
780
|
export NW_EXECUTION_CONTEXT=agent
|
|
510
781
|
|
|
@@ -583,7 +854,8 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
583
854
|
fi
|
|
584
855
|
log "CONTEXT-EXHAUSTED: Session ${ATTEMPT_NUM} hit context limit — checkpointing and resuming (${ATTEMPT}/${MAX_RETRIES})"
|
|
585
856
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
586
|
-
|
|
857
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
858
|
+
ensure_executor_pr >> "${LOG_FILE}" 2>&1 || true
|
|
587
859
|
# Switch prompt to "continue" mode for the next attempt (fresh context)
|
|
588
860
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
589
861
|
PROMPT="Continue implementing PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}).
|
|
@@ -606,13 +878,13 @@ The previous session ran out of context window. Progress has been committed on b
|
|
|
606
878
|
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
|
|
607
879
|
Follow all CLAUDE.md conventions (if present).
|
|
608
880
|
|
|
609
|
-
##
|
|
610
|
-
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
-
|
|
881
|
+
## PR Lifecycle
|
|
882
|
+
- The controller owns PR lifecycle and labels for this branch
|
|
883
|
+
- If a draft PR already exists, do NOT create another one and do NOT edit its labels
|
|
884
|
+
- If no PR exists yet, do NOT create one manually; keep pushing to ${BRANCH_NAME} and the controller will open/update it automatically
|
|
885
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
886
|
+
${PROGRESS_PUSH_CMD}
|
|
887
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
616
888
|
- STOP immediately after the final push — do NOT do any additional work, visual checks, or exploration.
|
|
617
889
|
- Do NOT process any other issues — only issue #${ISSUE_NUMBER}"
|
|
618
890
|
else
|
|
@@ -636,11 +908,13 @@ The previous session ran out of context window. Progress has been committed on b
|
|
|
636
908
|
Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
|
|
637
909
|
Follow all CLAUDE.md conventions (if present).
|
|
638
910
|
|
|
639
|
-
##
|
|
640
|
-
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
- After
|
|
911
|
+
## PR Lifecycle
|
|
912
|
+
- The controller owns PR lifecycle and labels for this branch
|
|
913
|
+
- If a draft PR already exists, do NOT create another one and do NOT edit its labels
|
|
914
|
+
- If no PR exists yet, do NOT create one manually; keep pushing to ${BRANCH_NAME} and the controller will open/update it automatically
|
|
915
|
+
- After each completed phase/wave milestone, commit and push progress to the existing branch:
|
|
916
|
+
${PROGRESS_PUSH_CMD}
|
|
917
|
+
- When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
|
|
644
918
|
- STOP immediately after the final push — do NOT do any additional work, visual checks, or exploration.
|
|
645
919
|
- Do NOT move the PRD to done/ — the cron script handles that
|
|
646
920
|
- Do NOT process any other PRDs — only ${ELIGIBLE_PRD}"
|
|
@@ -800,9 +1074,20 @@ if [ "${EXIT_CODE}" -eq 0 ] && grep -qiF 'Exceeded USD budget' "${LOG_FILE}" 2>/
|
|
|
800
1074
|
EXIT_CODE=1
|
|
801
1075
|
fi
|
|
802
1076
|
|
|
1077
|
+
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
1078
|
+
if ! ensure_executor_pr; then
|
|
1079
|
+
log "FAIL: Unable to create or reuse executor PR for ${BRANCH_NAME} after provider completion"
|
|
1080
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
1081
|
+
emit_result "failure" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}|reason=pr_setup_failed|detail=$(latest_failure_detail "${LOG_FILE}")"
|
|
1082
|
+
EXIT_CODE=1
|
|
1083
|
+
fi
|
|
1084
|
+
fi
|
|
1085
|
+
|
|
803
1086
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
804
1087
|
OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
|
|
805
1088
|
if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
|
|
1089
|
+
refresh_executor_pr_metadata
|
|
1090
|
+
mark_executor_pr_ready_for_review || true
|
|
806
1091
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
807
1092
|
# Board mode: comment with PR URL, then close issue and move to Done
|
|
808
1093
|
PR_URL=$(gh pr list --state open --json headRefName,url \
|
|
@@ -858,6 +1143,8 @@ if [ ${EXIT_CODE} -eq 0 ]; then
|
|
|
858
1143
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
859
1144
|
log "TIMEOUT: Session limit hit after ${SESSION_MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
860
1145
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
1146
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
1147
|
+
ensure_executor_pr >> "${LOG_FILE}" 2>&1 || true
|
|
861
1148
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
862
1149
|
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
863
1150
|
if [ "${SESSION_MAX_RUNTIME}" != "${MAX_RUNTIME}" ]; then
|
|
@@ -888,7 +1175,8 @@ elif check_context_exhausted "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
|
|
|
888
1175
|
# All resume attempts for context exhaustion were used up
|
|
889
1176
|
log "FAIL: Context window exhausted after ${MAX_RETRIES} resume attempts for ${ELIGIBLE_PRD}"
|
|
890
1177
|
checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
|
|
891
|
-
|
|
1178
|
+
push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
|
|
1179
|
+
ensure_executor_pr >> "${LOG_FILE}" 2>&1 || true
|
|
892
1180
|
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
893
1181
|
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
894
1182
|
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
@@ -87,6 +87,123 @@ validate_provider() {
|
|
|
87
87
|
return 1
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
night_watch_push_uses_no_verify() {
|
|
91
|
+
[ "${NW_GIT_PUSH_NO_VERIFY:-0}" = "1" ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
git_push_for_project() {
|
|
95
|
+
local repo_dir="${1:?repo_dir required}"
|
|
96
|
+
shift
|
|
97
|
+
|
|
98
|
+
if night_watch_push_uses_no_verify; then
|
|
99
|
+
git -C "${repo_dir}" push --no-verify "$@"
|
|
100
|
+
else
|
|
101
|
+
git -C "${repo_dir}" push "$@"
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
project_git_push_command() {
|
|
106
|
+
local branch_name="${1:?branch_name required}"
|
|
107
|
+
local mode="${2:-normal}"
|
|
108
|
+
local no_verify=""
|
|
109
|
+
|
|
110
|
+
if night_watch_push_uses_no_verify; then
|
|
111
|
+
no_verify=" --no-verify"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
case "${mode}" in
|
|
115
|
+
force-with-lease)
|
|
116
|
+
printf 'git push%s --force-with-lease origin %s' "${no_verify}" "${branch_name}"
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
printf 'git push%s origin %s' "${no_verify}" "${branch_name}"
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ── Executor PR status labels ────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
NW_EXECUTOR_PARTIAL_LABEL="${NW_EXECUTOR_PARTIAL_LABEL:-nw:partial}"
|
|
127
|
+
NW_EXECUTOR_RESUMABLE_LABEL="${NW_EXECUTOR_RESUMABLE_LABEL:-nw:resumable}"
|
|
128
|
+
NW_EXECUTOR_READY_REVIEW_LABEL="${NW_EXECUTOR_READY_REVIEW_LABEL:-nw:ready-review}"
|
|
129
|
+
|
|
130
|
+
csv_has_label() {
|
|
131
|
+
local csv="${1:-}"
|
|
132
|
+
local label="${2:?label required}"
|
|
133
|
+
|
|
134
|
+
printf '%s\n' "${csv}" \
|
|
135
|
+
| tr ',' '\n' \
|
|
136
|
+
| sed '/^[[:space:]]*$/d' \
|
|
137
|
+
| grep -Fxq "${label}"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ensure_github_label() {
|
|
141
|
+
local label_name="${1:?label_name required}"
|
|
142
|
+
local description="${2:-}"
|
|
143
|
+
local color="${3:-1d76db}"
|
|
144
|
+
|
|
145
|
+
gh label create "${label_name}" \
|
|
146
|
+
--description "${description}" \
|
|
147
|
+
--color "${color}" \
|
|
148
|
+
--force 2>/dev/null || true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ensure_executor_status_labels() {
|
|
152
|
+
ensure_github_label \
|
|
153
|
+
"${NW_EXECUTOR_PARTIAL_LABEL}" \
|
|
154
|
+
"Executor started this PR and work is intentionally incomplete" \
|
|
155
|
+
"fbca04"
|
|
156
|
+
ensure_github_label \
|
|
157
|
+
"${NW_EXECUTOR_RESUMABLE_LABEL}" \
|
|
158
|
+
"Executor should resume this unfinished PR before starting new work" \
|
|
159
|
+
"d93f0b"
|
|
160
|
+
ensure_github_label \
|
|
161
|
+
"${NW_EXECUTOR_READY_REVIEW_LABEL}" \
|
|
162
|
+
"Executor finished implementation and the PR is ready for automated/human review" \
|
|
163
|
+
"0e8a16"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
find_open_pr_for_branch() {
|
|
167
|
+
local branch_name="${1:?branch_name required}"
|
|
168
|
+
local pr_list=""
|
|
169
|
+
|
|
170
|
+
pr_list=$(gh pr list --state open --limit 200 \
|
|
171
|
+
--json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
|
|
172
|
+
|
|
173
|
+
printf '%s' "${pr_list}" \
|
|
174
|
+
| jq -c --arg branch_name "${branch_name}" '
|
|
175
|
+
.[]
|
|
176
|
+
| select((.headRefName // "") == $branch_name)
|
|
177
|
+
' 2>/dev/null \
|
|
178
|
+
| head -n 1 || true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
find_executor_resume_pr() {
|
|
182
|
+
local branch_prefix="${1:-night-watch}"
|
|
183
|
+
local pr_list=""
|
|
184
|
+
|
|
185
|
+
pr_list=$(gh pr list --state open --limit 200 \
|
|
186
|
+
--json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
|
|
187
|
+
|
|
188
|
+
printf '%s' "${pr_list}" \
|
|
189
|
+
| jq -c \
|
|
190
|
+
--arg primary_prefix "${branch_prefix}/" \
|
|
191
|
+
--arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" '
|
|
192
|
+
[
|
|
193
|
+
.[]
|
|
194
|
+
| select(
|
|
195
|
+
(.headRefName // "" | startswith($primary_prefix))
|
|
196
|
+
or
|
|
197
|
+
(.headRefName // "" | startswith("feat/"))
|
|
198
|
+
)
|
|
199
|
+
| .labelNames = ((.labels // []) | map(.name))
|
|
200
|
+
| select((.labelNames | index($resumable_label)) != null)
|
|
201
|
+
]
|
|
202
|
+
| sort_by(.createdAt // "")
|
|
203
|
+
| .[0] // empty
|
|
204
|
+
' 2>/dev/null || true
|
|
205
|
+
}
|
|
206
|
+
|
|
90
207
|
# ── Generic Provider Command Builder ──────────────────────────────────────────
|
|
91
208
|
|
|
92
209
|
# Build a provider command from NW_PROVIDER_* environment variables.
|
|
@@ -192,7 +192,7 @@ append_exit_trap "kill ${WATCHDOG_PID} 2>/dev/null || true"
|
|
|
192
192
|
# Discover open PRs sorted by creation date (oldest first = FIFO)
|
|
193
193
|
log "INFO: Scanning open PRs..."
|
|
194
194
|
PR_LIST_JSON=$(gh pr list --state open \
|
|
195
|
-
--json number,headRefName,createdAt,isDraft \
|
|
195
|
+
--json number,headRefName,createdAt,isDraft,labels \
|
|
196
196
|
--jq 'sort_by(.createdAt)' \
|
|
197
197
|
2>/dev/null || echo "[]")
|
|
198
198
|
|
|
@@ -211,6 +211,7 @@ while IFS= read -r pr_json; do
|
|
|
211
211
|
pr_number=$(echo "${pr_json}" | jq -r '.number')
|
|
212
212
|
pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
|
|
213
213
|
is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
|
|
214
|
+
pr_labels=$(echo "${pr_json}" | jq -r '[.labels[]?.name] | join(",")')
|
|
214
215
|
|
|
215
216
|
# Skip drafts
|
|
216
217
|
if [ "${is_draft}" = "true" ]; then
|
|
@@ -218,6 +219,11 @@ while IFS= read -r pr_json; do
|
|
|
218
219
|
continue
|
|
219
220
|
fi
|
|
220
221
|
|
|
222
|
+
if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
|
|
223
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Skipping partial executor PR"
|
|
224
|
+
continue
|
|
225
|
+
fi
|
|
226
|
+
|
|
221
227
|
# Check branch pattern filter
|
|
222
228
|
if ! matches_branch_patterns "${pr_branch}"; then
|
|
223
229
|
log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
|