@jonit-dev/night-watch-cli 1.8.10-beta.5 β†’ 1.8.10-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2991,7 +2991,7 @@ function sortByPriority(issues) {
2991
2991
  return aOrder - bOrder;
2992
2992
  });
2993
2993
  }
2994
- var PRIORITY_LABELS, PRIORITY_LABEL_INFO, CATEGORY_LABELS, CATEGORY_LABEL_INFO, HORIZON_LABELS, HORIZON_LABEL_INFO, PRIORITY_COLORS, NIGHT_WATCH_LABELS;
2994
+ var PRIORITY_LABELS, PRIORITY_LABEL_INFO, CATEGORY_LABELS, CATEGORY_LABEL_INFO, HORIZON_LABELS, HORIZON_LABEL_INFO, EXECUTOR_PARTIAL_LABEL, EXECUTOR_RESUMABLE_LABEL, EXECUTOR_READY_REVIEW_LABEL, PRIORITY_COLORS, NIGHT_WATCH_LABELS;
2995
2995
  var init_labels = __esm({
2996
2996
  "../core/dist/board/labels.js"() {
2997
2997
  "use strict";
@@ -3065,6 +3065,9 @@ var init_labels = __esm({
3065
3065
  "medium-term": { name: "medium-term", description: "6 weeks - 4 months delivery window" },
3066
3066
  "long-term": { name: "long-term", description: "4-12 months delivery window" }
3067
3067
  };
3068
+ EXECUTOR_PARTIAL_LABEL = "nw:partial";
3069
+ EXECUTOR_RESUMABLE_LABEL = "nw:resumable";
3070
+ EXECUTOR_READY_REVIEW_LABEL = "nw:ready-review";
3068
3071
  PRIORITY_COLORS = {
3069
3072
  P0: "b60205",
3070
3073
  P1: "d93f0b",
@@ -3099,6 +3102,21 @@ var init_labels = __esm({
3099
3102
  name: "e2e-validated",
3100
3103
  description: "PR acceptance requirements validated by e2e/integration tests",
3101
3104
  color: "0e8a16"
3105
+ },
3106
+ {
3107
+ name: EXECUTOR_PARTIAL_LABEL,
3108
+ description: "Executor started this PR and work is intentionally incomplete",
3109
+ color: "fbca04"
3110
+ },
3111
+ {
3112
+ name: EXECUTOR_RESUMABLE_LABEL,
3113
+ description: "Executor should resume this unfinished PR before starting new work",
3114
+ color: "d93f0b"
3115
+ },
3116
+ {
3117
+ name: EXECUTOR_READY_REVIEW_LABEL,
3118
+ description: "Executor finished implementation and the PR is ready for automated/human review",
3119
+ color: "0e8a16"
3102
3120
  }
3103
3121
  ];
3104
3122
  }
@@ -8155,6 +8173,9 @@ __export(dist_exports, {
8155
8173
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
8156
8174
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
8157
8175
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
8176
+ EXECUTOR_PARTIAL_LABEL: () => EXECUTOR_PARTIAL_LABEL,
8177
+ EXECUTOR_READY_REVIEW_LABEL: () => EXECUTOR_READY_REVIEW_LABEL,
8178
+ EXECUTOR_RESUMABLE_LABEL: () => EXECUTOR_RESUMABLE_LABEL,
8158
8179
  GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
8159
8180
  GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
8160
8181
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
@@ -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,177 @@ 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 opened this draft PR at executor start 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 opened this draft PR at executor start 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 -C "${WORKTREE_DIR}" push -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1; then
427
+ return 0
428
+ fi
429
+ fi
430
+
431
+ if git -C "${WORKTREE_DIR}" push origin "${BRANCH_NAME}" --force-with-lease >> "${LOG_FILE}" 2>&1; then
432
+ return 0
433
+ fi
434
+
435
+ git -C "${WORKTREE_DIR}" push -u origin "${BRANCH_NAME}" >> "${LOG_FILE}" 2>&1
436
+ }
437
+
438
+ ensure_executor_pr() {
439
+ local pr_title=""
440
+ local pr_body=""
441
+ local create_output=""
442
+
443
+ refresh_executor_pr_metadata
444
+ if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
445
+ log "PR: Reusing existing open PR #${EXECUTOR_PR_NUMBER} for ${BRANCH_NAME}"
446
+ mark_executor_pr_incomplete
447
+ return 0
448
+ fi
449
+
450
+ pr_title=$(build_executor_pr_title)
451
+ pr_body=$(build_executor_pr_body)
452
+
453
+ log "PR: Creating draft PR for ${BRANCH_NAME} before provider execution"
454
+ if ! push_executor_branch "initial"; then
455
+ log "WARN: Initial push for ${BRANCH_NAME} failed before PR creation"
456
+ fi
457
+
458
+ ensure_executor_status_labels
459
+ if ! create_output=$(
460
+ gh pr create \
461
+ --draft \
462
+ --base "${DEFAULT_BRANCH}" \
463
+ --head "${BRANCH_NAME}" \
464
+ --title "${pr_title}" \
465
+ --body "${pr_body}" 2>> "${LOG_FILE}"
466
+ ); then
467
+ log "FAIL: gh pr create failed for ${BRANCH_NAME}"
468
+ return 1
469
+ fi
470
+
471
+ refresh_executor_pr_metadata
472
+ if [ -z "${EXECUTOR_PR_URL}" ]; then
473
+ EXECUTOR_PR_URL=$(printf '%s' "${create_output}" | grep -Eo 'https://[^[:space:]]+/pull/[0-9]+' | tail -n 1 || true)
474
+ fi
475
+ if [ -n "${EXECUTOR_PR_NUMBER}" ]; then
476
+ mark_executor_pr_incomplete
477
+ log "PR: Draft PR ready at ${EXECUTOR_PR_URL:-unknown} for ${BRANCH_NAME}"
478
+ else
479
+ log "WARN: gh pr create succeeded for ${BRANCH_NAME}, but PR metadata lookup did not resolve a number yet"
480
+ fi
481
+
482
+ return 0
483
+ }
484
+
251
485
  checkpoint_timeout_progress() {
252
486
  local worktree_dir="${1:?worktree_dir required}"
253
487
  local branch_name="${2:?branch_name required}"
@@ -416,13 +650,12 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
416
650
  5. Run the project's verify/test command between waves to catch issues early
417
651
  Follow all CLAUDE.md conventions (if present).
418
652
 
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.
653
+ ## PR Lifecycle
654
+ - The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
655
+ - Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
656
+ - After each completed phase/wave milestone, commit and push progress to the existing branch:
657
+ git push origin ${BRANCH_NAME}
658
+ - When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
426
659
  - STOP immediately after the final push β€” do NOT do any additional work, visual checks, or exploration.
427
660
  - Do NOT process any other issues β€” only issue #${ISSUE_NUMBER}"
428
661
  else
@@ -445,11 +678,12 @@ Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline:
445
678
  5. Run the project's verify/test command between waves to catch issues early
446
679
  Follow all CLAUDE.md conventions (if present).
447
680
 
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.
681
+ ## PR Lifecycle
682
+ - The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
683
+ - Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
684
+ - After each completed phase/wave milestone, commit and push progress to the existing branch:
685
+ git push origin ${BRANCH_NAME}
686
+ - When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
453
687
  - STOP immediately after the final push β€” do NOT do any additional work, visual checks, or exploration.
454
688
  - Do NOT move the PRD to done/ β€” the cron script handles that
455
689
  - Do NOT process any other PRDs β€” only ${ELIGIBLE_PRD}"
@@ -505,6 +739,19 @@ if ! assert_isolated_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "executor"; the
505
739
  exit 1
506
740
  fi
507
741
 
742
+ if ! ensure_executor_pr; then
743
+ log "FAIL: Unable to create or reuse executor PR for ${BRANCH_NAME}"
744
+ restore_issue_to_ready "Failed to create or reuse the draft PR for branch ${BRANCH_NAME}. Moved back to Ready for retry."
745
+ night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
746
+ emit_result "failure" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}|reason=pr_setup_failed|detail=$(latest_failure_detail "${LOG_FILE}")"
747
+ exit 1
748
+ fi
749
+
750
+ if [ -n "${ISSUE_NUMBER}" ] && [ -n "${NW_CLI}" ] && [ -n "${EXECUTOR_PR_URL}" ] && [ "${RESUME_FROM_EXISTING_PR}" != "1" ]; then
751
+ "${NW_CLI}" board comment "${ISSUE_NUMBER}" \
752
+ --body "Draft PR opened at executor start: ${EXECUTOR_PR_URL} (labels: \`${NW_EXECUTOR_PARTIAL_LABEL}\`, \`${NW_EXECUTOR_RESUMABLE_LABEL}\`)." 2>>"${LOG_FILE}" || true
753
+ fi
754
+
508
755
  # Sandbox: prevent the agent from modifying crontab during execution
509
756
  export NW_EXECUTION_CONTEXT=agent
510
757
 
@@ -583,7 +830,7 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
583
830
  fi
584
831
  log "CONTEXT-EXHAUSTED: Session ${ATTEMPT_NUM} hit context limit β€” checkpointing and resuming (${ATTEMPT}/${MAX_RETRIES})"
585
832
  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
833
+ push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
587
834
  # Switch prompt to "continue" mode for the next attempt (fresh context)
588
835
  if [ -n "${ISSUE_NUMBER}" ]; then
589
836
  PROMPT="Continue implementing PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}).
@@ -606,13 +853,12 @@ The previous session ran out of context window. Progress has been committed on b
606
853
  Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
607
854
  Follow all CLAUDE.md conventions (if present).
608
855
 
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.
856
+ ## PR Lifecycle
857
+ - The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
858
+ - Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
859
+ - After each completed phase/wave milestone, commit and push progress to the existing branch:
860
+ git push origin ${BRANCH_NAME}
861
+ - When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
616
862
  - STOP immediately after the final push β€” do NOT do any additional work, visual checks, or exploration.
617
863
  - Do NOT process any other issues β€” only issue #${ISSUE_NUMBER}"
618
864
  else
@@ -636,11 +882,12 @@ The previous session ran out of context window. Progress has been committed on b
636
882
  Read ${EXECUTOR_PROMPT_REF} and follow the FULL execution pipeline for remaining phases only.
637
883
  Follow all CLAUDE.md conventions (if present).
638
884
 
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.
885
+ ## PR Lifecycle
886
+ - The controller already created the draft PR for this branch${EXECUTOR_PR_URL:+: ${EXECUTOR_PR_URL}}
887
+ - Do NOT create another PR and do NOT edit PR status labels; the controller owns PR lifecycle and labels
888
+ - After each completed phase/wave milestone, commit and push progress to the existing branch:
889
+ git push origin ${BRANCH_NAME}
890
+ - When all implementation is complete, run final verification (lint, typecheck, tests). If anything fails, fix it, commit, and push again.
644
891
  - STOP immediately after the final push β€” do NOT do any additional work, visual checks, or exploration.
645
892
  - Do NOT move the PRD to done/ β€” the cron script handles that
646
893
  - Do NOT process any other PRDs β€” only ${ELIGIBLE_PRD}"
@@ -803,6 +1050,8 @@ fi
803
1050
  if [ ${EXIT_CODE} -eq 0 ]; then
804
1051
  OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
805
1052
  if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
1053
+ refresh_executor_pr_metadata
1054
+ mark_executor_pr_ready_for_review || true
806
1055
  if [ -n "${ISSUE_NUMBER}" ]; then
807
1056
  # Board mode: comment with PR URL, then close issue and move to Done
808
1057
  PR_URL=$(gh pr list --state open --json headRefName,url \
@@ -858,6 +1107,7 @@ if [ ${EXIT_CODE} -eq 0 ]; then
858
1107
  elif [ ${EXIT_CODE} -eq 124 ]; then
859
1108
  log "TIMEOUT: Session limit hit after ${SESSION_MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
860
1109
  checkpoint_timeout_progress "${WORKTREE_DIR}" "${BRANCH_NAME}" "${ELIGIBLE_PRD}"
1110
+ push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
861
1111
  if [ -n "${ISSUE_NUMBER}" ]; then
862
1112
  "${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
863
1113
  if [ "${SESSION_MAX_RUNTIME}" != "${MAX_RUNTIME}" ]; then
@@ -888,7 +1138,7 @@ elif check_context_exhausted "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
888
1138
  # All resume attempts for context exhaustion were used up
889
1139
  log "FAIL: Context window exhausted after ${MAX_RETRIES} resume attempts for ${ELIGIBLE_PRD}"
890
1140
  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
1141
+ push_executor_branch "update" >> "${LOG_FILE}" 2>&1 || true
892
1142
  if [ -n "${ISSUE_NUMBER}" ]; then
893
1143
  "${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
894
1144
  "${NW_CLI}" board comment "${ISSUE_NUMBER}" \
@@ -87,6 +87,89 @@ validate_provider() {
87
87
  return 1
88
88
  }
89
89
 
90
+ # ── Executor PR status labels ────────────────────────────────────────────────
91
+
92
+ NW_EXECUTOR_PARTIAL_LABEL="${NW_EXECUTOR_PARTIAL_LABEL:-nw:partial}"
93
+ NW_EXECUTOR_RESUMABLE_LABEL="${NW_EXECUTOR_RESUMABLE_LABEL:-nw:resumable}"
94
+ NW_EXECUTOR_READY_REVIEW_LABEL="${NW_EXECUTOR_READY_REVIEW_LABEL:-nw:ready-review}"
95
+
96
+ csv_has_label() {
97
+ local csv="${1:-}"
98
+ local label="${2:?label required}"
99
+
100
+ printf '%s\n' "${csv}" \
101
+ | tr ',' '\n' \
102
+ | sed '/^[[:space:]]*$/d' \
103
+ | grep -Fxq "${label}"
104
+ }
105
+
106
+ ensure_github_label() {
107
+ local label_name="${1:?label_name required}"
108
+ local description="${2:-}"
109
+ local color="${3:-1d76db}"
110
+
111
+ gh label create "${label_name}" \
112
+ --description "${description}" \
113
+ --color "${color}" \
114
+ --force 2>/dev/null || true
115
+ }
116
+
117
+ ensure_executor_status_labels() {
118
+ ensure_github_label \
119
+ "${NW_EXECUTOR_PARTIAL_LABEL}" \
120
+ "Executor started this PR and work is intentionally incomplete" \
121
+ "fbca04"
122
+ ensure_github_label \
123
+ "${NW_EXECUTOR_RESUMABLE_LABEL}" \
124
+ "Executor should resume this unfinished PR before starting new work" \
125
+ "d93f0b"
126
+ ensure_github_label \
127
+ "${NW_EXECUTOR_READY_REVIEW_LABEL}" \
128
+ "Executor finished implementation and the PR is ready for automated/human review" \
129
+ "0e8a16"
130
+ }
131
+
132
+ find_open_pr_for_branch() {
133
+ local branch_name="${1:?branch_name required}"
134
+ local pr_list=""
135
+
136
+ pr_list=$(gh pr list --state open --limit 200 \
137
+ --json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
138
+
139
+ printf '%s' "${pr_list}" \
140
+ | jq -c --arg branch_name "${branch_name}" '
141
+ .[]
142
+ | select((.headRefName // "") == $branch_name)
143
+ ' 2>/dev/null \
144
+ | head -n 1 || true
145
+ }
146
+
147
+ find_executor_resume_pr() {
148
+ local branch_prefix="${1:-night-watch}"
149
+ local pr_list=""
150
+
151
+ pr_list=$(gh pr list --state open --limit 200 \
152
+ --json number,headRefName,url,title,isDraft,labels,createdAt 2>/dev/null || echo "[]")
153
+
154
+ printf '%s' "${pr_list}" \
155
+ | jq -c \
156
+ --arg primary_prefix "${branch_prefix}/" \
157
+ --arg resumable_label "${NW_EXECUTOR_RESUMABLE_LABEL}" '
158
+ [
159
+ .[]
160
+ | select(
161
+ (.headRefName // "" | startswith($primary_prefix))
162
+ or
163
+ (.headRefName // "" | startswith("feat/"))
164
+ )
165
+ | .labelNames = ((.labels // []) | map(.name))
166
+ | select((.labelNames | index($resumable_label)) != null)
167
+ ]
168
+ | sort_by(.createdAt // "")
169
+ | .[0] // empty
170
+ ' 2>/dev/null || true
171
+ }
172
+
90
173
  # ── Generic Provider Command Builder ──────────────────────────────────────────
91
174
 
92
175
  # 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"
@@ -631,6 +631,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
631
631
  continue
632
632
  fi
633
633
 
634
+ if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
635
+ log "INFO: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}; waiting for executor to finish"
636
+ continue
637
+ fi
638
+
634
639
  # Merge-conflict signal: this PR needs action even if CI and score look fine.
635
640
  MERGE_STATE=$(gh pr view "${pr_number}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "")
636
641
  if [ "${MERGE_STATE}" = "DIRTY" ] || [ "${MERGE_STATE}" = "CONFLICTING" ]; then
@@ -426,6 +426,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
426
426
  continue
427
427
  fi
428
428
 
429
+ if csv_has_label "${pr_labels:-}" "${NW_EXECUTOR_PARTIAL_LABEL}"; then
430
+ log "SKIP-QA: PR #${pr_number} (${pr_branch}) is labeled ${NW_EXECUTOR_PARTIAL_LABEL}"
431
+ continue
432
+ fi
433
+
429
434
  # Skip PRs with the skip label
430
435
  if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
431
436
  log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonit-dev/night-watch-cli",
3
- "version": "1.8.10-beta.5",
3
+ "version": "1.8.10-beta.6",
4
4
  "description": "AI agent that implements your specs, opens PRs, and reviews code overnight. Queue GitHub issues or PRDs, wake up to pull requests.",
5
5
  "type": "module",
6
6
  "bin": {