@jonit-dev/night-watch-cli 1.5.1 → 1.5.3

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.
Files changed (50) hide show
  1. package/dist/cli.js +3 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/cancel.d.ts +46 -0
  4. package/dist/commands/cancel.d.ts.map +1 -0
  5. package/dist/commands/cancel.js +239 -0
  6. package/dist/commands/cancel.js.map +1 -0
  7. package/dist/commands/dashboard/tab-config.js +1 -1
  8. package/dist/commands/dashboard/tab-config.js.map +1 -1
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +1 -0
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/prds.d.ts +13 -0
  13. package/dist/commands/prds.d.ts.map +1 -0
  14. package/dist/commands/prds.js +192 -0
  15. package/dist/commands/prds.js.map +1 -0
  16. package/dist/commands/prs.d.ts +13 -0
  17. package/dist/commands/prs.d.ts.map +1 -0
  18. package/dist/commands/prs.js +101 -0
  19. package/dist/commands/prs.js.map +1 -0
  20. package/dist/commands/retry.d.ts +9 -0
  21. package/dist/commands/retry.d.ts.map +1 -0
  22. package/dist/commands/retry.js +72 -0
  23. package/dist/commands/retry.js.map +1 -0
  24. package/dist/commands/review.d.ts.map +1 -1
  25. package/dist/commands/review.js +4 -0
  26. package/dist/commands/review.js.map +1 -1
  27. package/dist/commands/update.d.ts +21 -0
  28. package/dist/commands/update.d.ts.map +1 -0
  29. package/dist/commands/update.js +87 -0
  30. package/dist/commands/update.js.map +1 -0
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +29 -1
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/utils/notify.d.ts.map +1 -1
  37. package/dist/utils/notify.js +9 -0
  38. package/dist/utils/notify.js.map +1 -1
  39. package/dist/utils/status-data.d.ts +13 -0
  40. package/dist/utils/status-data.d.ts.map +1 -1
  41. package/dist/utils/status-data.js +24 -3
  42. package/dist/utils/status-data.js.map +1 -1
  43. package/package.json +1 -1
  44. package/scripts/night-watch-cron.sh +86 -37
  45. package/scripts/night-watch-helpers.sh +135 -1
  46. package/scripts/night-watch-pr-reviewer-cron.sh +64 -15
  47. package/templates/night-watch-pr-reviewer.md +7 -11
  48. package/templates/night-watch.md +9 -20
  49. package/web/dist/assets/{index-DWgdrh9z.js → index-CMFnDLhY.js} +13 -13
  50. package/web/dist/index.html +1 -1
@@ -15,17 +15,17 @@ set -euo pipefail
15
15
  PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
16
16
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
17
17
  PRD_DIR_REL="${NW_PRD_DIR:-docs/PRDs/night-watch}"
18
- if [[ "${PRD_DIR_REL}" = /* ]]; then
19
- PRD_DIR="${PRD_DIR_REL}"
20
- else
21
- PRD_DIR="${PROJECT_DIR}/${PRD_DIR_REL}"
22
- fi
23
18
  LOG_DIR="${PROJECT_DIR}/logs"
24
19
  LOG_FILE="${LOG_DIR}/night-watch.log"
25
- LOCK_FILE="/tmp/night-watch-${PROJECT_NAME}.lock"
20
+ LOCK_FILE=""
26
21
  MAX_RUNTIME="${NW_MAX_RUNTIME:-7200}" # 2 hours
27
22
  MAX_LOG_SIZE="524288" # 512 KB
28
23
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
24
+ RUNTIME_MIRROR_DIR=""
25
+ RUNTIME_PROJECT_DIR=""
26
+ PRD_DIR=""
27
+ ELIGIBLE_PRD=""
28
+ CLAIMED=0
29
29
 
30
30
  # Ensure NVM / Node / Claude are on PATH
31
31
  export NVM_DIR="${HOME}/.nvm"
@@ -40,6 +40,8 @@ mkdir -p "${LOG_DIR}"
40
40
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
41
41
  # shellcheck source=night-watch-helpers.sh
42
42
  source "${SCRIPT_DIR}/night-watch-helpers.sh"
43
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
44
+ LOCK_FILE="/tmp/night-watch-${PROJECT_RUNTIME_KEY}.lock"
43
45
 
44
46
  # Validate provider
45
47
  if ! validate_provider "${PROVIDER_CMD}"; then
@@ -53,7 +55,45 @@ if ! acquire_lock "${LOCK_FILE}"; then
53
55
  exit 0
54
56
  fi
55
57
 
56
- cleanup_worktrees "${PROJECT_DIR}"
58
+ cleanup_on_exit() {
59
+ rm -f "${LOCK_FILE}"
60
+
61
+ if [ "${CLAIMED}" = "1" ] && [ -n "${ELIGIBLE_PRD}" ] && [ -n "${PRD_DIR}" ]; then
62
+ release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}" || true
63
+ fi
64
+
65
+ if [ -n "${RUNTIME_MIRROR_DIR}" ] && [ -n "${RUNTIME_PROJECT_DIR}" ]; then
66
+ cleanup_runtime_workspace "${RUNTIME_MIRROR_DIR}" "${RUNTIME_PROJECT_DIR}" || true
67
+ fi
68
+ }
69
+
70
+ trap cleanup_on_exit EXIT
71
+
72
+ if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
73
+ DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
74
+ else
75
+ DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
76
+ fi
77
+
78
+ runtime_info=()
79
+ if mapfile -t runtime_info < <(prepare_runtime_workspace "${PROJECT_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"); then
80
+ RUNTIME_MIRROR_DIR="${runtime_info[0]:-}"
81
+ RUNTIME_PROJECT_DIR="${runtime_info[1]:-}"
82
+ else
83
+ log "FAIL: Could not prepare runtime workspace for ${PROJECT_DIR}"
84
+ exit 1
85
+ fi
86
+
87
+ if [ -z "${RUNTIME_MIRROR_DIR}" ] || [ -z "${RUNTIME_PROJECT_DIR}" ]; then
88
+ log "FAIL: Runtime workspace paths are missing"
89
+ exit 1
90
+ fi
91
+
92
+ if [[ "${PRD_DIR_REL}" = /* ]]; then
93
+ PRD_DIR="${PRD_DIR_REL}"
94
+ else
95
+ PRD_DIR="${RUNTIME_PROJECT_DIR}/${PRD_DIR_REL}"
96
+ fi
57
97
 
58
98
  ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
59
99
 
@@ -64,29 +104,26 @@ fi
64
104
 
65
105
  # Claim the PRD to prevent other runs from selecting it
66
106
  claim_prd "${PRD_DIR}" "${ELIGIBLE_PRD}"
67
-
68
- # Update EXIT trap to also release claim
69
- trap "rm -f '${LOCK_FILE}'; release_claim '${PRD_DIR}' '${ELIGIBLE_PRD}'" EXIT
107
+ CLAIMED=1
70
108
 
71
109
  PRD_NAME="${ELIGIBLE_PRD%.md}"
72
110
  BRANCH_NAME="night-watch/${PRD_NAME}"
73
- if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
74
- DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
75
- else
76
- DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
77
- fi
78
111
 
79
- log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME}"
112
+ log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME} in runtime workspace ${RUNTIME_PROJECT_DIR}"
80
113
 
81
- cd "${PROJECT_DIR}"
114
+ if ! prepare_branch_checkout "${RUNTIME_PROJECT_DIR}" "${BRANCH_NAME}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
115
+ log "FAIL: Could not prepare branch ${BRANCH_NAME} in runtime workspace"
116
+ night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
117
+ exit 1
118
+ fi
82
119
 
83
120
  PROMPT="Implement the PRD at docs/PRDs/night-watch/${ELIGIBLE_PRD}
84
121
 
85
122
  ## Setup
86
- - Branch name MUST be exactly: ${BRANCH_NAME}
87
- - Create the branch from ${DEFAULT_BRANCH}: git checkout ${DEFAULT_BRANCH} && git pull origin ${DEFAULT_BRANCH} && git checkout -b ${BRANCH_NAME}
88
- - Use a git worktree: git worktree add ../${PROJECT_NAME}-nw-${PRD_NAME} ${BRANCH_NAME}
89
- - cd into the worktree, install dependencies
123
+ - You are already inside an isolated runtime workspace: ${RUNTIME_PROJECT_DIR}
124
+ - Current branch is already prepared: ${BRANCH_NAME}
125
+ - Do not run git checkout/switch in ${PROJECT_DIR}
126
+ - Do not create or remove worktrees; the runtime controller handles isolation and cleanup
90
127
 
91
128
  ## Implementation — PRD Executor Workflow
92
129
  Read .claude/commands/prd-executor.md and follow its FULL execution pipeline:
@@ -102,7 +139,6 @@ Follow all CLAUDE.md conventions (if present).
102
139
  - Commit all changes, push, and open a PR:
103
140
  git push -u origin ${BRANCH_NAME}
104
141
  gh pr create --title \"feat: <short title>\" --body \"<summary referencing PRD>\"
105
- - After PR is created, clean up: git worktree remove ../${PROJECT_NAME}-nw-${PRD_NAME}
106
142
  - Do NOT move the PRD to done/ — the cron script handles that
107
143
  - Do NOT process any other PRDs — only ${ELIGIBLE_PRD}"
108
144
 
@@ -115,6 +151,7 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
115
151
  echo "Provider: ${PROVIDER_CMD}"
116
152
  echo "Eligible PRD: ${ELIGIBLE_PRD}"
117
153
  echo "Branch: ${BRANCH_NAME}"
154
+ echo "Runtime Dir: ${RUNTIME_PROJECT_DIR}"
118
155
  echo "Timeout: ${MAX_RUNTIME}s"
119
156
  exit 0
120
157
  fi
@@ -135,21 +172,25 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
135
172
 
136
173
  case "${PROVIDER_CMD}" in
137
174
  claude)
138
- if timeout "${MAX_RUNTIME}" \
139
- claude -p "${PROMPT}" \
140
- --dangerously-skip-permissions \
141
- >> "${LOG_FILE}" 2>&1; then
175
+ if (
176
+ cd "${RUNTIME_PROJECT_DIR}" && timeout "${MAX_RUNTIME}" \
177
+ claude -p "${PROMPT}" \
178
+ --dangerously-skip-permissions \
179
+ >> "${LOG_FILE}" 2>&1
180
+ ); then
142
181
  EXIT_CODE=0
143
182
  else
144
183
  EXIT_CODE=$?
145
184
  fi
146
185
  ;;
147
186
  codex)
148
- if timeout "${MAX_RUNTIME}" \
149
- codex --quiet \
150
- --yolo \
151
- --prompt "${PROMPT}" \
152
- >> "${LOG_FILE}" 2>&1; then
187
+ if (
188
+ cd "${RUNTIME_PROJECT_DIR}" && timeout "${MAX_RUNTIME}" \
189
+ codex --quiet \
190
+ --yolo \
191
+ --prompt "${PROMPT}" \
192
+ >> "${LOG_FILE}" 2>&1
193
+ ); then
153
194
  EXIT_CODE=0
154
195
  else
155
196
  EXIT_CODE=$?
@@ -185,16 +226,26 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
185
226
  done
186
227
 
187
228
  if [ ${EXIT_CODE} -eq 0 ]; then
188
- PR_EXISTS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
229
+ PR_EXISTS=$(cd "${RUNTIME_PROJECT_DIR}" && gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
189
230
  if [ "${PR_EXISTS}" -gt 0 ]; then
190
231
  release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}"
232
+ CLAIMED=0
233
+ if ! checkout_default_branch "${RUNTIME_PROJECT_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
234
+ log "WARN: Could not switch runtime workspace to ${DEFAULT_BRANCH}; PRD not moved to done/"
235
+ exit 0
236
+ fi
237
+
191
238
  mark_prd_done "${PRD_DIR}" "${ELIGIBLE_PRD}"
192
239
  night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" success --exit-code 0 2>/dev/null || true
193
- git -C "${PROJECT_DIR}" add -A docs/PRDs/night-watch/
194
- git -C "${PROJECT_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
240
+ if [[ "${PRD_DIR_REL}" = /* ]]; then
241
+ log "WARN: PRD directory is absolute; skipping auto-commit of done/ move"
242
+ else
243
+ git -C "${RUNTIME_PROJECT_DIR}" add -A "${PRD_DIR_REL}/" || true
244
+ git -C "${RUNTIME_PROJECT_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
195
245
 
196
246
  Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
197
- git -C "${PROJECT_DIR}" push origin "${DEFAULT_BRANCH}" || true
247
+ git -C "${RUNTIME_PROJECT_DIR}" push origin "${DEFAULT_BRANCH}" || true
248
+ fi
198
249
  log "DONE: ${ELIGIBLE_PRD} implemented, PR opened, PRD moved to done/"
199
250
  else
200
251
  log "WARN: ${PROVIDER_CMD} exited 0 but no PR found on ${BRANCH_NAME} — PRD NOT moved to done"
@@ -202,9 +253,7 @@ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
202
253
  elif [ ${EXIT_CODE} -eq 124 ]; then
203
254
  log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
204
255
  night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" timeout --exit-code 124 2>/dev/null || true
205
- cleanup_worktrees "${PROJECT_DIR}"
206
256
  else
207
257
  log "FAIL: Night watch exited with code ${EXIT_CODE} while processing ${ELIGIBLE_PRD}"
208
258
  night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code "${EXIT_CODE}" 2>/dev/null || true
209
- cleanup_worktrees "${PROJECT_DIR}"
210
259
  fi
@@ -292,17 +292,151 @@ cleanup_worktrees() {
292
292
  local project_dir="${1:?project_dir required}"
293
293
  local project_name
294
294
  project_name=$(basename "${project_dir}")
295
+ local marker="${2:-${project_name}-nw}"
295
296
 
296
297
  git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
297
298
  | grep '^worktree ' \
298
299
  | awk '{print $2}' \
299
- | grep "${project_name}-nw" \
300
+ | grep "${marker}" \
300
301
  | while read -r wt; do
301
302
  log "CLEANUP: Removing leftover worktree ${wt}"
302
303
  git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
303
304
  done || true
304
305
  }
305
306
 
307
+ # ── Runtime workspace isolation ───────────────────────────────────────────────
308
+
309
+ project_runtime_key() {
310
+ local project_dir="${1:?project_dir required}"
311
+ local project_name
312
+ local hash
313
+ project_name=$(basename "${project_dir}")
314
+
315
+ if command -v sha1sum >/dev/null 2>&1; then
316
+ hash=$(printf "%s" "${project_dir}" | sha1sum | awk '{print $1}')
317
+ elif command -v shasum >/dev/null 2>&1; then
318
+ hash=$(printf "%s" "${project_dir}" | shasum | awk '{print $1}')
319
+ else
320
+ hash=$(printf "%s" "${project_dir}" | cksum | awk '{print $1}')
321
+ fi
322
+
323
+ printf "%s-%s" "${project_name}" "${hash:0:12}"
324
+ }
325
+
326
+ resolve_runtime_base_ref() {
327
+ local git_dir="${1:?git_dir required}"
328
+ local default_branch="${2:?default_branch required}"
329
+
330
+ if git -C "${git_dir}" rev-parse --verify --quiet "refs/remotes/origin/${default_branch}" >/dev/null; then
331
+ printf "%s" "refs/remotes/origin/${default_branch}"
332
+ return 0
333
+ fi
334
+
335
+ if git -C "${git_dir}" rev-parse --verify --quiet "refs/heads/${default_branch}" >/dev/null; then
336
+ printf "%s" "refs/heads/${default_branch}"
337
+ return 0
338
+ fi
339
+
340
+ if git -C "${git_dir}" rev-parse --verify --quiet "refs/remotes/origin/HEAD" >/dev/null; then
341
+ printf "%s" "refs/remotes/origin/HEAD"
342
+ return 0
343
+ fi
344
+
345
+ return 1
346
+ }
347
+
348
+ prepare_runtime_workspace() {
349
+ local project_dir="${1:?project_dir required}"
350
+ local default_branch="${2:?default_branch required}"
351
+ local log_file="${3:-${LOG_FILE:-/dev/null}}"
352
+ local runtime_root="${NW_RUNTIME_ROOT:-${HOME}/.night-watch/runtime}"
353
+ local runtime_key
354
+ local runtime_base
355
+ local mirror_dir
356
+ local runs_dir
357
+ local worktree_dir
358
+ local clone_source=""
359
+ local base_ref=""
360
+
361
+ runtime_key=$(project_runtime_key "${project_dir}")
362
+ runtime_base="${runtime_root}/${runtime_key}"
363
+ mirror_dir="${runtime_base}/mirror.git"
364
+ runs_dir="${runtime_base}/runs"
365
+ worktree_dir="${runs_dir}/run-$(date +%Y%m%d-%H%M%S)-$$"
366
+
367
+ mkdir -p "${runs_dir}"
368
+
369
+ if [ ! -d "${mirror_dir}" ]; then
370
+ clone_source=$(git -C "${project_dir}" config --get remote.origin.url 2>/dev/null || echo "")
371
+
372
+ if [ -n "${clone_source}" ]; then
373
+ if ! git clone --mirror "${clone_source}" "${mirror_dir}" >> "${log_file}" 2>&1; then
374
+ git clone --mirror "${project_dir}" "${mirror_dir}" >> "${log_file}" 2>&1
375
+ fi
376
+ else
377
+ git clone --mirror "${project_dir}" "${mirror_dir}" >> "${log_file}" 2>&1
378
+ fi
379
+ fi
380
+
381
+ git -C "${mirror_dir}" remote update --prune >> "${log_file}" 2>&1 || true
382
+
383
+ base_ref=$(resolve_runtime_base_ref "${mirror_dir}" "${default_branch}") || return 1
384
+ git -C "${mirror_dir}" worktree add --detach "${worktree_dir}" "${base_ref}" >> "${log_file}" 2>&1
385
+
386
+ printf "%s\n%s\n" "${mirror_dir}" "${worktree_dir}"
387
+ }
388
+
389
+ cleanup_runtime_workspace() {
390
+ local mirror_dir="${1:?mirror_dir required}"
391
+ local worktree_dir="${2:?worktree_dir required}"
392
+
393
+ git -C "${mirror_dir}" worktree remove --force "${worktree_dir}" 2>/dev/null || true
394
+ git -C "${mirror_dir}" worktree prune 2>/dev/null || true
395
+ }
396
+
397
+ prepare_branch_checkout() {
398
+ local repo_dir="${1:?repo_dir required}"
399
+ local branch_name="${2:?branch_name required}"
400
+ local default_branch="${3:?default_branch required}"
401
+ local log_file="${4:-${LOG_FILE:-/dev/null}}"
402
+ local base_ref=""
403
+
404
+ git -C "${repo_dir}" fetch origin "${default_branch}" "${branch_name}" >> "${log_file}" 2>&1 || true
405
+
406
+ if git -C "${repo_dir}" rev-parse --verify --quiet "refs/heads/${branch_name}" >/dev/null; then
407
+ git -C "${repo_dir}" checkout "${branch_name}" >> "${log_file}" 2>&1
408
+ return $?
409
+ fi
410
+
411
+ if git -C "${repo_dir}" rev-parse --verify --quiet "refs/remotes/origin/${branch_name}" >/dev/null; then
412
+ git -C "${repo_dir}" checkout -b "${branch_name}" "origin/${branch_name}" >> "${log_file}" 2>&1
413
+ return $?
414
+ fi
415
+
416
+ base_ref=$(resolve_runtime_base_ref "${repo_dir}" "${default_branch}") || return 1
417
+ git -C "${repo_dir}" checkout -b "${branch_name}" "${base_ref}" >> "${log_file}" 2>&1
418
+ }
419
+
420
+ checkout_default_branch() {
421
+ local repo_dir="${1:?repo_dir required}"
422
+ local default_branch="${2:?default_branch required}"
423
+ local log_file="${3:-${LOG_FILE:-/dev/null}}"
424
+
425
+ git -C "${repo_dir}" fetch origin "${default_branch}" >> "${log_file}" 2>&1 || true
426
+
427
+ if git -C "${repo_dir}" rev-parse --verify --quiet "refs/remotes/origin/${default_branch}" >/dev/null; then
428
+ git -C "${repo_dir}" checkout -B "${default_branch}" "origin/${default_branch}" >> "${log_file}" 2>&1
429
+ return $?
430
+ fi
431
+
432
+ if git -C "${repo_dir}" rev-parse --verify --quiet "refs/heads/${default_branch}" >/dev/null; then
433
+ git -C "${repo_dir}" checkout "${default_branch}" >> "${log_file}" 2>&1
434
+ return $?
435
+ fi
436
+
437
+ git -C "${repo_dir}" checkout -B "${default_branch}" HEAD >> "${log_file}" 2>&1
438
+ }
439
+
306
440
  # ── Mark PRD as done ─────────────────────────────────────────────────────────
307
441
 
308
442
  mark_prd_done() {
@@ -15,12 +15,14 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
15
15
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
16
16
  LOG_DIR="${PROJECT_DIR}/logs"
17
17
  LOG_FILE="${LOG_DIR}/night-watch-pr-reviewer.log"
18
- LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_NAME}.lock"
18
+ LOCK_FILE=""
19
19
  MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
20
20
  MAX_LOG_SIZE="524288" # 512 KB
21
21
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
22
22
  MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
23
23
  BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
24
+ RUNTIME_MIRROR_DIR=""
25
+ RUNTIME_PROJECT_DIR=""
24
26
 
25
27
  # Ensure NVM / Node / Claude are on PATH
26
28
  export NVM_DIR="${HOME}/.nvm"
@@ -34,6 +36,8 @@ mkdir -p "${LOG_DIR}"
34
36
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
37
  # shellcheck source=night-watch-helpers.sh
36
38
  source "${SCRIPT_DIR}/night-watch-helpers.sh"
39
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
40
+ LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}.lock"
37
41
 
38
42
  # Validate provider
39
43
  if ! validate_provider "${PROVIDER_CMD}"; then
@@ -47,7 +51,37 @@ if ! acquire_lock "${LOCK_FILE}"; then
47
51
  exit 0
48
52
  fi
49
53
 
50
- cd "${PROJECT_DIR}"
54
+ cleanup_on_exit() {
55
+ rm -f "${LOCK_FILE}"
56
+
57
+ if [ -n "${RUNTIME_MIRROR_DIR}" ] && [ -n "${RUNTIME_PROJECT_DIR}" ]; then
58
+ cleanup_runtime_workspace "${RUNTIME_MIRROR_DIR}" "${RUNTIME_PROJECT_DIR}" || true
59
+ fi
60
+ }
61
+
62
+ trap cleanup_on_exit EXIT
63
+
64
+ if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
65
+ DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
66
+ else
67
+ DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
68
+ fi
69
+
70
+ runtime_info=()
71
+ if mapfile -t runtime_info < <(prepare_runtime_workspace "${PROJECT_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"); then
72
+ RUNTIME_MIRROR_DIR="${runtime_info[0]:-}"
73
+ RUNTIME_PROJECT_DIR="${runtime_info[1]:-}"
74
+ else
75
+ log "FAIL: Could not prepare runtime workspace for reviewer"
76
+ exit 1
77
+ fi
78
+
79
+ if [ -z "${RUNTIME_MIRROR_DIR}" ] || [ -z "${RUNTIME_PROJECT_DIR}" ]; then
80
+ log "FAIL: Runtime workspace paths are missing for reviewer"
81
+ exit 1
82
+ fi
83
+
84
+ cd "${RUNTIME_PROJECT_DIR}"
51
85
 
52
86
  # Convert comma-separated branch prefixes into a regex that matches branch starts.
53
87
  BRANCH_REGEX=""
@@ -122,38 +156,55 @@ fi
122
156
 
123
157
  log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
124
158
 
125
- cleanup_worktrees "${PROJECT_DIR}"
126
-
127
159
  # Dry-run mode: print diagnostics and exit
128
160
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
129
161
  echo "=== Dry Run: PR Reviewer ==="
130
162
  echo "Provider: ${PROVIDER_CMD}"
131
163
  echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
132
164
  echo "Min Review Score: ${MIN_REVIEW_SCORE}"
165
+ echo "Default Branch: ${DEFAULT_BRANCH}"
166
+ echo "Runtime Dir: ${RUNTIME_PROJECT_DIR}"
133
167
  echo "Open PRs needing work:${PRS_NEEDING_WORK}"
134
168
  echo "Timeout: ${MAX_RUNTIME}s"
135
169
  exit 0
136
170
  fi
137
171
 
172
+ if [ -f "${RUNTIME_PROJECT_DIR}/.claude/commands/night-watch-pr-reviewer.md" ]; then
173
+ REVIEW_WORKFLOW=$(cat "${RUNTIME_PROJECT_DIR}/.claude/commands/night-watch-pr-reviewer.md")
174
+ else
175
+ REVIEW_WORKFLOW=$(cat "${SCRIPT_DIR}/../templates/night-watch-pr-reviewer.md")
176
+ fi
177
+
178
+ REVIEW_PROMPT="You are running in an isolated runtime workspace at ${RUNTIME_PROJECT_DIR}.
179
+ Do not run git checkout/switch in ${PROJECT_DIR}.
180
+ Do not create or remove worktrees; the runtime controller handles that.
181
+ Apply all fixes only inside the current runtime workspace.
182
+
183
+ ${REVIEW_WORKFLOW}"
184
+
138
185
  EXIT_CODE=0
139
186
 
140
187
  case "${PROVIDER_CMD}" in
141
188
  claude)
142
- if timeout "${MAX_RUNTIME}" \
143
- claude -p "/night-watch-pr-reviewer" \
144
- --dangerously-skip-permissions \
145
- >> "${LOG_FILE}" 2>&1; then
189
+ if (
190
+ cd "${RUNTIME_PROJECT_DIR}" && timeout "${MAX_RUNTIME}" \
191
+ claude -p "${REVIEW_PROMPT}" \
192
+ --dangerously-skip-permissions \
193
+ >> "${LOG_FILE}" 2>&1
194
+ ); then
146
195
  EXIT_CODE=0
147
196
  else
148
197
  EXIT_CODE=$?
149
198
  fi
150
199
  ;;
151
200
  codex)
152
- if timeout "${MAX_RUNTIME}" \
153
- codex --quiet \
154
- --yolo \
155
- --prompt "$(cat "${PROJECT_DIR}/.claude/commands/night-watch-pr-reviewer.md")" \
156
- >> "${LOG_FILE}" 2>&1; then
201
+ if (
202
+ cd "${RUNTIME_PROJECT_DIR}" && timeout "${MAX_RUNTIME}" \
203
+ codex --quiet \
204
+ --yolo \
205
+ --prompt "${REVIEW_PROMPT}" \
206
+ >> "${LOG_FILE}" 2>&1
207
+ ); then
157
208
  EXIT_CODE=0
158
209
  else
159
210
  EXIT_CODE=$?
@@ -165,8 +216,6 @@ case "${PROVIDER_CMD}" in
165
216
  ;;
166
217
  esac
167
218
 
168
- cleanup_worktrees "${PROJECT_DIR}"
169
-
170
219
  if [ ${EXIT_CODE} -eq 0 ]; then
171
220
  log "DONE: PR reviewer completed successfully"
172
221
  elif [ ${EXIT_CODE} -eq 124 ]; then
@@ -67,18 +67,18 @@ A PR needs attention if **either** the review score is below 80 **or** any CI jo
67
67
 
68
68
  4. **Fix the PR**:
69
69
 
70
- a. **Check out the PR branch**:
70
+ a. **Use the pre-provisioned runtime workspace**:
71
+ - You are already running in an isolated runtime workspace.
72
+ - Do **not** run `git checkout`/`git switch` in the original project directory.
73
+ - Do **not** create/remove worktrees manually; the runtime controller handles isolation and cleanup.
74
+
75
+ b. **Check out the PR branch inside the runtime workspace**:
71
76
  ```
72
77
  git fetch origin
73
78
  git checkout <branch-name>
74
79
  git pull origin <branch-name>
75
80
  ```
76
-
77
- b. **Create a worktree** for the fixes:
78
- ```
79
- git worktree add ../${PROJECT_NAME}-nw-review-<branch-name> <branch-name>
80
- ```
81
- `cd` into worktree, run package install (npm install, yarn install, or pnpm install as appropriate).
81
+ Run package install (npm install, yarn install, or pnpm install as appropriate).
82
82
 
83
83
  c. **Address CI failures** (if any):
84
84
  - Read the failed job logs carefully to understand the root cause.
@@ -135,10 +135,6 @@ A PR needs attention if **either** the review score is below 80 **or** any CI jo
135
135
  Night Watch PR Reviewer"
136
136
  ```
137
137
 
138
- h. **Clean up worktree**: `git worktree remove ../${PROJECT_NAME}-nw-review-<branch-name>`
139
-
140
138
  5. **Repeat** for all open PRs that need work.
141
139
 
142
- 6. When done, return to ${DEFAULT_BRANCH}: `git checkout ${DEFAULT_BRANCH}`
143
-
144
140
  Start now. Check for open PRs that need review feedback addressed or CI failures fixed.
@@ -20,20 +20,13 @@ You are the Night Watch agent. Your job is to autonomously pick up PRD tickets a
20
20
 
21
21
  b. **Branch naming**: The branch MUST be named exactly `night-watch/<prd-filename-without-.md>`. Do NOT use `feat/`, `feature/`, or any other prefix. Example: for `health-check-endpoints.md` the branch is `night-watch/health-check-endpoints`.
22
22
 
23
- c. **Create a feature branch** from ${DEFAULT_BRANCH}:
23
+ c. **Use the pre-provisioned runtime workspace**:
24
+ - You are already running in an isolated runtime workspace.
25
+ - The target branch `night-watch/<prd-filename-without-.md>` is already prepared.
26
+ - Do **not** run `git checkout`/`git switch` in the original project directory.
27
+ - Do **not** create/remove worktrees manually; the runtime controller handles isolation and cleanup.
24
28
 
25
- ```
26
- git checkout ${DEFAULT_BRANCH} && git pull origin ${DEFAULT_BRANCH}
27
- git checkout -b night-watch/<prd-filename-without-.md>
28
- ```
29
-
30
- d. **Create a git worktree** for isolated work:
31
-
32
- ```
33
- git worktree add ../${PROJECT_NAME}-nw-<prd-name> night-watch/<prd-name>
34
- ```
35
-
36
- Then `cd` into the worktree and run package install (npm install, yarn install, or pnpm install as appropriate).
29
+ d. Install dependencies in the current runtime workspace (npm install, yarn install, or pnpm install as appropriate).
37
30
 
38
31
  e. **Implement the PRD using the PRD Executor workflow**:
39
32
  - Read `.claude/commands/prd-executor.md` and follow its full execution pipeline.
@@ -64,11 +57,9 @@ You are the Night Watch agent. Your job is to autonomously pick up PRD tickets a
64
57
  gh pr create --title "feat: <short title>" --body "<summary with PRD reference>"
65
58
  ```
66
59
 
67
- j. **Move PRD to done** (back in main repo on ${DEFAULT_BRANCH}):
60
+ j. **Move PRD to done**:
68
61
 
69
62
  ```
70
- cd ${PROJECT_DIR}
71
- git checkout ${DEFAULT_BRANCH}
72
63
  mkdir -p docs/PRDs/night-watch/done
73
64
  mv docs/PRDs/night-watch/<file>.md docs/PRDs/night-watch/done/
74
65
  ```
@@ -91,10 +82,8 @@ You are the Night Watch agent. Your job is to autonomously pick up PRD tickets a
91
82
 
92
83
  l. **Commit** the move + summary update, push ${DEFAULT_BRANCH}.
93
84
 
94
- m. **Clean up worktree**: `git worktree remove ../${PROJECT_NAME}-nw-<prd-name>`
95
-
96
- n. **STOP after this PRD**. Do NOT continue to the next PRD. One PRD per run prevents timeouts and reduces risk. The next cron trigger will pick up the next PRD.
85
+ m. **STOP after this PRD**. Do NOT continue to the next PRD. One PRD per run prevents timeouts and reduces risk. The next cron trigger will pick up the next PRD.
97
86
 
98
- 5. **On failure**: Do NOT move the PRD to done. Log the failure in NIGHT-WATCH-SUMMARY.md with status "Failed" and the reason. Clean up worktree and **stop** -- do not attempt the next PRD.
87
+ 5. **On failure**: Do NOT move the PRD to done. Log the failure in NIGHT-WATCH-SUMMARY.md with status "Failed" and the reason. The runtime controller handles cleanup. Then **stop** -- do not attempt the next PRD.
99
88
 
100
89
  Start now. Scan for available PRDs and process the first eligible one.