@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.
@@ -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
- BOARD_DISCOVERY_STATUS=0
173
- if ISSUE_JSON=$(find_eligible_board_issue "${PROJECT_DIR}" "${MAX_RUNTIME}"); then
174
- BOARD_DISCOVERY_STATUS=0
175
- else
176
- BOARD_DISCOVERY_STATUS=$?
177
- fi
178
- if [ -z "${ISSUE_JSON}" ]; then
179
- if [ "${BOARD_DISCOVERY_STATUS}" -eq 2 ]; then
180
- log "INFO: Ready board issues were found, but all are in cooldown; skipping this run"
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
- log "INFO: No Ready board issues found; skipping this run"
208
+ ISSUE_TITLE_RAW=$(printf '%s' "${EXECUTOR_PR_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
183
209
  fi
184
210
  else
185
- ISSUE_NUMBER=$(printf '%s' "${ISSUE_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
186
- ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
187
- ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
188
- if [ -z "${ISSUE_NUMBER}" ]; then
189
- log "ERROR: Board mode: failed to parse issue number from JSON"
190
- exit 1
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
- ELIGIBLE_PRD="${ISSUE_NUMBER}-$(printf '%s' "${ISSUE_TITLE_RAW}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
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
- ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
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
- BRANCH_NAME="${BRANCH_PREFIX}/${PRD_NAME}"
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
- git -C "${BOOKKEEP_WORKTREE_DIR}" push origin "HEAD:${DEFAULT_BRANCH}" || true
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
- ## Finalize — open the PR FIRST, then verify
420
- - Commit all changes, push, and open the PR immediately:
421
- git push -u origin ${BRANCH_NAME}
422
- gh pr create --title \"feat: <short title>\" --body \"Closes #${ISSUE_NUMBER}
423
-
424
- <summary>\"
425
- - After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
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
- ## Finalize — open the PR FIRST, then verify
449
- - Commit all changes, push, and open the PR immediately:
450
- git push -u origin ${BRANCH_NAME}
451
- gh pr create --title \"feat: <short title>\" --body \"<summary referencing PRD>\"
452
- - After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
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
- git -C "${WORKTREE_DIR}" push origin "${BRANCH_NAME}" --force-with-lease >> "${LOG_FILE}" 2>&1 || true
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
- ## Finalize — open the PR FIRST, then verify
610
- - Commit all changes, push, and open the PR immediately:
611
- git push -u origin ${BRANCH_NAME}
612
- gh pr create --title \"feat: <short title>\" --body \"Closes #${ISSUE_NUMBER}
613
-
614
- <summary>\"
615
- - After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
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
- ## Finalize — open the PR FIRST, then verify
640
- - Commit all changes, push, and open the PR immediately:
641
- git push -u origin ${BRANCH_NAME}
642
- gh pr create --title \"feat: <short title>\" --body \"<summary referencing PRD>\"
643
- - After the PR is open, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
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
- git -C "${WORKTREE_DIR}" push origin "${BRANCH_NAME}" --force-with-lease >> "${LOG_FILE}" 2>&1 || true
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"