@jonit-dev/night-watch-cli 1.5.6 → 1.5.8
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 +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/cancel.d.ts.map +1 -1
- package/dist/commands/cancel.js +3 -0
- package/dist/commands/cancel.js.map +1 -1
- package/dist/commands/prds.d.ts.map +1 -1
- package/dist/commands/prds.js +6 -2
- package/dist/commands/prds.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +51 -0
- package/dist/server/index.js.map +1 -1
- package/dist/utils/status-data.d.ts.map +1 -1
- package/dist/utils/status-data.js +30 -4
- package/dist/utils/status-data.js.map +1 -1
- package/package.json +3 -1
- package/scripts/night-watch-cron.sh +64 -85
- package/scripts/night-watch-helpers.sh +32 -159
- package/scripts/night-watch-pr-reviewer-cron.sh +23 -53
- package/templates/night-watch-pr-reviewer.md +11 -6
- package/templates/night-watch.md +14 -9
- package/web/dist/assets/index-CP0r6Epl.js +350 -0
- package/web/dist/index.html +1 -15
- package/web/dist/assets/index-CmRcnnnG.js +0 -335
|
@@ -15,18 +15,18 @@ 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
|
|
18
23
|
LOG_DIR="${PROJECT_DIR}/logs"
|
|
19
24
|
LOG_FILE="${LOG_DIR}/night-watch.log"
|
|
20
|
-
|
|
25
|
+
# NOTE: Lock file path must match LOCK_FILE_PREFIX in src/constants.ts
|
|
26
|
+
LOCK_FILE="/tmp/night-watch-${PROJECT_NAME}.lock"
|
|
21
27
|
MAX_RUNTIME="${NW_MAX_RUNTIME:-7200}" # 2 hours
|
|
22
28
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
23
29
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
24
|
-
RUNTIME_MIRROR_DIR=""
|
|
25
|
-
RUNTIME_PROJECT_DIR=""
|
|
26
|
-
PRD_DIR=""
|
|
27
|
-
ORIGINAL_PRD_DIR=""
|
|
28
|
-
ELIGIBLE_PRD=""
|
|
29
|
-
CLAIMED=0
|
|
30
30
|
|
|
31
31
|
# Ensure NVM / Node / Claude are on PATH
|
|
32
32
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -41,8 +41,6 @@ mkdir -p "${LOG_DIR}"
|
|
|
41
41
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
42
|
# shellcheck source=night-watch-helpers.sh
|
|
43
43
|
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
44
|
-
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
45
|
-
LOCK_FILE="/tmp/night-watch-${PROJECT_RUNTIME_KEY}.lock"
|
|
46
44
|
|
|
47
45
|
# Validate provider
|
|
48
46
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
@@ -56,53 +54,7 @@ if ! acquire_lock "${LOCK_FILE}"; then
|
|
|
56
54
|
exit 0
|
|
57
55
|
fi
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
rm -f "${LOCK_FILE}"
|
|
61
|
-
|
|
62
|
-
if [ "${CLAIMED}" = "1" ] && [ -n "${ELIGIBLE_PRD}" ] && [ -n "${PRD_DIR}" ]; then
|
|
63
|
-
release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}" || true
|
|
64
|
-
if [ -n "${ORIGINAL_PRD_DIR}" ] && [ "${ORIGINAL_PRD_DIR}" != "${PRD_DIR}" ]; then
|
|
65
|
-
release_claim "${ORIGINAL_PRD_DIR}" "${ELIGIBLE_PRD}" || true
|
|
66
|
-
fi
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
if [ -n "${RUNTIME_MIRROR_DIR}" ] && [ -n "${RUNTIME_PROJECT_DIR}" ]; then
|
|
70
|
-
cleanup_runtime_workspace "${RUNTIME_MIRROR_DIR}" "${RUNTIME_PROJECT_DIR}" || true
|
|
71
|
-
fi
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
trap cleanup_on_exit EXIT
|
|
75
|
-
|
|
76
|
-
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
77
|
-
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
78
|
-
else
|
|
79
|
-
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
80
|
-
fi
|
|
81
|
-
|
|
82
|
-
runtime_info=()
|
|
83
|
-
if mapfile -t runtime_info < <(prepare_runtime_workspace "${PROJECT_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"); then
|
|
84
|
-
RUNTIME_MIRROR_DIR="${runtime_info[0]:-}"
|
|
85
|
-
RUNTIME_PROJECT_DIR="${runtime_info[1]:-}"
|
|
86
|
-
else
|
|
87
|
-
log "FAIL: Could not prepare runtime workspace for ${PROJECT_DIR}"
|
|
88
|
-
exit 1
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
if [ -z "${RUNTIME_MIRROR_DIR}" ] || [ -z "${RUNTIME_PROJECT_DIR}" ]; then
|
|
92
|
-
log "FAIL: Runtime workspace paths are missing"
|
|
93
|
-
exit 1
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
if [[ "${PRD_DIR_REL}" = /* ]]; then
|
|
97
|
-
PRD_DIR="${PRD_DIR_REL}"
|
|
98
|
-
ORIGINAL_PRD_DIR="${PRD_DIR_REL}"
|
|
99
|
-
else
|
|
100
|
-
PRD_DIR="${RUNTIME_PROJECT_DIR}/${PRD_DIR_REL}"
|
|
101
|
-
ORIGINAL_PRD_DIR="${PROJECT_DIR}/${PRD_DIR_REL}"
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
# Promote any pending-review PRDs whose PRs have been merged
|
|
105
|
-
promote_merged_prds "${ORIGINAL_PRD_DIR}" "${PROJECT_DIR}" || true
|
|
57
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
106
58
|
|
|
107
59
|
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
|
|
108
60
|
|
|
@@ -113,30 +65,35 @@ fi
|
|
|
113
65
|
|
|
114
66
|
# Claim the PRD to prevent other runs from selecting it
|
|
115
67
|
claim_prd "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
fi
|
|
120
|
-
CLAIMED=1
|
|
68
|
+
|
|
69
|
+
# Update EXIT trap to also release claim
|
|
70
|
+
trap "rm -f '${LOCK_FILE}'; release_claim '${PRD_DIR}' '${ELIGIBLE_PRD}'" EXIT
|
|
121
71
|
|
|
122
72
|
PRD_NAME="${ELIGIBLE_PRD%.md}"
|
|
123
73
|
BRANCH_NAME="night-watch/${PRD_NAME}"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
74
|
+
WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-${PRD_NAME}"
|
|
75
|
+
BOOKKEEP_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-bookkeeping"
|
|
76
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
77
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
78
|
+
else
|
|
79
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
80
|
+
fi
|
|
81
|
+
if [[ "${PRD_DIR_REL}" = /* ]]; then
|
|
82
|
+
BOOKKEEP_PRD_DIR="${PRD_DIR_REL}"
|
|
83
|
+
else
|
|
84
|
+
BOOKKEEP_PRD_DIR="${BOOKKEEP_WORKTREE_DIR}/${PRD_DIR_REL}"
|
|
131
85
|
fi
|
|
132
86
|
|
|
87
|
+
log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME} (worktree: ${WORKTREE_DIR})"
|
|
88
|
+
|
|
133
89
|
PROMPT="Implement the PRD at docs/PRDs/night-watch/${ELIGIBLE_PRD}
|
|
134
90
|
|
|
135
91
|
## Setup
|
|
136
|
-
- You are already inside an isolated
|
|
137
|
-
- Current branch is already
|
|
138
|
-
- Do
|
|
139
|
-
- Do
|
|
92
|
+
- You are already inside an isolated worktree at: ${WORKTREE_DIR}
|
|
93
|
+
- Current branch is already checked out: ${BRANCH_NAME}
|
|
94
|
+
- Do NOT run git checkout/switch in ${PROJECT_DIR}
|
|
95
|
+
- Do NOT create or remove worktrees; the cron script manages that
|
|
96
|
+
- Install dependencies if needed and implement in the current worktree only
|
|
140
97
|
|
|
141
98
|
## Implementation — PRD Executor Workflow
|
|
142
99
|
Read .claude/commands/prd-executor.md and follow its FULL execution pipeline:
|
|
@@ -164,11 +121,18 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
164
121
|
echo "Provider: ${PROVIDER_CMD}"
|
|
165
122
|
echo "Eligible PRD: ${ELIGIBLE_PRD}"
|
|
166
123
|
echo "Branch: ${BRANCH_NAME}"
|
|
167
|
-
echo "
|
|
124
|
+
echo "Worktree: ${WORKTREE_DIR}"
|
|
125
|
+
echo "Bookkeeping: ${BOOKKEEP_WORKTREE_DIR}"
|
|
168
126
|
echo "Timeout: ${MAX_RUNTIME}s"
|
|
169
127
|
exit 0
|
|
170
128
|
fi
|
|
171
129
|
|
|
130
|
+
if ! prepare_branch_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "${BRANCH_NAME}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
131
|
+
log "FAIL: Unable to create isolated worktree ${WORKTREE_DIR} for ${BRANCH_NAME}"
|
|
132
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
172
136
|
# Sandbox: prevent the agent from modifying crontab during execution
|
|
173
137
|
export NW_EXECUTION_CONTEXT=agent
|
|
174
138
|
|
|
@@ -186,7 +150,7 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
186
150
|
case "${PROVIDER_CMD}" in
|
|
187
151
|
claude)
|
|
188
152
|
if (
|
|
189
|
-
cd "${
|
|
153
|
+
cd "${WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
190
154
|
claude -p "${PROMPT}" \
|
|
191
155
|
--dangerously-skip-permissions \
|
|
192
156
|
>> "${LOG_FILE}" 2>&1
|
|
@@ -198,7 +162,7 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
198
162
|
;;
|
|
199
163
|
codex)
|
|
200
164
|
if (
|
|
201
|
-
cd "${
|
|
165
|
+
cd "${WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
202
166
|
codex --quiet \
|
|
203
167
|
--yolo \
|
|
204
168
|
--prompt "${PROMPT}" \
|
|
@@ -239,20 +203,33 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
239
203
|
done
|
|
240
204
|
|
|
241
205
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
242
|
-
PR_EXISTS=$(
|
|
206
|
+
PR_EXISTS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
|
|
243
207
|
if [ "${PR_EXISTS}" -gt 0 ]; then
|
|
244
208
|
release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
209
|
+
# NOTE: PRDs are moved to done/ immediately when a PR is opened rather than waiting for
|
|
210
|
+
# the PR to merge. This is intentional — once a PR exists, the agent's implementation
|
|
211
|
+
# work is complete and it should not pick up this PRD again. Human review takes it from here.
|
|
212
|
+
if prepare_detached_worktree "${PROJECT_DIR}" "${BOOKKEEP_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
213
|
+
if mark_prd_done "${BOOKKEEP_PRD_DIR}" "${ELIGIBLE_PRD}"; then
|
|
214
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" success --exit-code 0 2>/dev/null || true
|
|
215
|
+
if [[ "${PRD_DIR_REL}" = /* ]]; then
|
|
216
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" add -A "${PRD_DIR_REL}" || true
|
|
217
|
+
else
|
|
218
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" add -A "${PRD_DIR_REL}/" || true
|
|
219
|
+
fi
|
|
220
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
|
|
221
|
+
|
|
222
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
|
|
223
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" push origin "HEAD:${DEFAULT_BRANCH}" || true
|
|
224
|
+
log "DONE: ${ELIGIBLE_PRD} implemented, PR opened, PRD moved to done/"
|
|
225
|
+
else
|
|
226
|
+
log "WARN: Failed to move ${ELIGIBLE_PRD} to done/ in bookkeeping worktree"
|
|
227
|
+
fi
|
|
228
|
+
else
|
|
229
|
+
log "WARN: Unable to prepare bookkeeping worktree for ${ELIGIBLE_PRD}"
|
|
249
230
|
fi
|
|
250
|
-
|
|
251
|
-
mark_prd_pending_review "${ORIGINAL_PRD_DIR}" "${ELIGIBLE_PRD}" "${PROJECT_DIR}" "${BRANCH_NAME}"
|
|
252
|
-
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" success --exit-code 0 2>/dev/null || true
|
|
253
|
-
log "PENDING-REVIEW: ${ELIGIBLE_PRD} implemented, PR opened, waiting for merge"
|
|
254
231
|
else
|
|
255
|
-
log "WARN: ${PROVIDER_CMD} exited 0 but no PR found on ${BRANCH_NAME} — PRD NOT moved to
|
|
232
|
+
log "WARN: ${PROVIDER_CMD} exited 0 but no PR found on ${BRANCH_NAME} — PRD NOT moved to done"
|
|
256
233
|
fi
|
|
257
234
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
258
235
|
log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
@@ -261,3 +238,5 @@ else
|
|
|
261
238
|
log "FAIL: Night watch exited with code ${EXIT_CODE} while processing ${ELIGIBLE_PRD}"
|
|
262
239
|
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code "${EXIT_CODE}" 2>/dev/null || true
|
|
263
240
|
fi
|
|
241
|
+
|
|
242
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
@@ -292,149 +292,78 @@ 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}"
|
|
296
295
|
|
|
297
296
|
git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
|
|
298
297
|
| grep '^worktree ' \
|
|
299
298
|
| awk '{print $2}' \
|
|
300
|
-
| grep "${
|
|
299
|
+
| grep "${project_name}-nw" \
|
|
301
300
|
| while read -r wt; do
|
|
302
301
|
log "CLEANUP: Removing leftover worktree ${wt}"
|
|
303
302
|
git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
|
|
304
303
|
done || true
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
project_runtime_key() {
|
|
306
|
+
# Pick the best available ref for creating a new detached worktree.
|
|
307
|
+
resolve_worktree_base_ref() {
|
|
310
308
|
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
309
|
local default_branch="${2:?default_branch required}"
|
|
329
310
|
|
|
330
|
-
if git -C "${
|
|
331
|
-
printf "%s" "
|
|
311
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/remotes/origin/${default_branch}" >/dev/null; then
|
|
312
|
+
printf "%s" "origin/${default_branch}"
|
|
332
313
|
return 0
|
|
333
314
|
fi
|
|
334
315
|
|
|
335
|
-
if git -C "${
|
|
336
|
-
printf "%s" "
|
|
316
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/heads/${default_branch}" >/dev/null; then
|
|
317
|
+
printf "%s" "${default_branch}"
|
|
337
318
|
return 0
|
|
338
319
|
fi
|
|
339
320
|
|
|
340
|
-
if git -C "${
|
|
341
|
-
printf "%s" "
|
|
321
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/remotes/origin/HEAD" >/dev/null; then
|
|
322
|
+
printf "%s" "origin/HEAD"
|
|
342
323
|
return 0
|
|
343
324
|
fi
|
|
344
325
|
|
|
345
326
|
return 1
|
|
346
327
|
}
|
|
347
328
|
|
|
348
|
-
|
|
329
|
+
# Create an isolated worktree on a branch without checking out that branch
|
|
330
|
+
# in the user's current project directory.
|
|
331
|
+
prepare_branch_worktree() {
|
|
349
332
|
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
333
|
local worktree_dir="${2:?worktree_dir required}"
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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}}"
|
|
334
|
+
local branch_name="${3:?branch_name required}"
|
|
335
|
+
local default_branch="${4:?default_branch required}"
|
|
336
|
+
local log_file="${5:-${LOG_FILE:-/dev/null}}"
|
|
402
337
|
local base_ref=""
|
|
403
338
|
|
|
404
|
-
git -C "${
|
|
339
|
+
git -C "${project_dir}" fetch origin "${default_branch}" >> "${log_file}" 2>&1 || true
|
|
340
|
+
base_ref=$(resolve_worktree_base_ref "${project_dir}" "${default_branch}") || return 1
|
|
405
341
|
|
|
406
|
-
if git -C "${
|
|
407
|
-
git -C "${
|
|
342
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/heads/${branch_name}" >/dev/null; then
|
|
343
|
+
git -C "${project_dir}" worktree add "${worktree_dir}" "${branch_name}" >> "${log_file}" 2>&1
|
|
408
344
|
return $?
|
|
409
345
|
fi
|
|
410
346
|
|
|
411
|
-
if git -C "${
|
|
412
|
-
git -C "${
|
|
347
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/remotes/origin/${branch_name}" >/dev/null; then
|
|
348
|
+
git -C "${project_dir}" worktree add -b "${branch_name}" "${worktree_dir}" "origin/${branch_name}" >> "${log_file}" 2>&1
|
|
413
349
|
return $?
|
|
414
350
|
fi
|
|
415
351
|
|
|
416
|
-
|
|
417
|
-
git -C "${repo_dir}" checkout -b "${branch_name}" "${base_ref}" >> "${log_file}" 2>&1
|
|
352
|
+
git -C "${project_dir}" worktree add -b "${branch_name}" "${worktree_dir}" "${base_ref}" >> "${log_file}" 2>&1
|
|
418
353
|
}
|
|
419
354
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
local
|
|
423
|
-
local
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
355
|
+
# Create an isolated detached worktree (useful for reviewer/controller flows).
|
|
356
|
+
prepare_detached_worktree() {
|
|
357
|
+
local project_dir="${1:?project_dir required}"
|
|
358
|
+
local worktree_dir="${2:?worktree_dir required}"
|
|
359
|
+
local default_branch="${3:?default_branch required}"
|
|
360
|
+
local log_file="${4:-${LOG_FILE:-/dev/null}}"
|
|
361
|
+
local base_ref=""
|
|
431
362
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return $?
|
|
435
|
-
fi
|
|
363
|
+
git -C "${project_dir}" fetch origin "${default_branch}" >> "${log_file}" 2>&1 || true
|
|
364
|
+
base_ref=$(resolve_worktree_base_ref "${project_dir}" "${default_branch}") || return 1
|
|
436
365
|
|
|
437
|
-
git -C "${
|
|
366
|
+
git -C "${project_dir}" worktree add --detach "${worktree_dir}" "${base_ref}" >> "${log_file}" 2>&1
|
|
438
367
|
}
|
|
439
368
|
|
|
440
369
|
# ── Mark PRD as done ─────────────────────────────────────────────────────────
|
|
@@ -456,62 +385,6 @@ mark_prd_done() {
|
|
|
456
385
|
fi
|
|
457
386
|
}
|
|
458
387
|
|
|
459
|
-
mark_prd_pending_review() {
|
|
460
|
-
local prd_dir="${1:?prd_dir required}"
|
|
461
|
-
local prd_file="${2:?prd_file required}"
|
|
462
|
-
local project_dir="${3:?project_dir required}"
|
|
463
|
-
local branch_name="${4:-}"
|
|
464
|
-
local prd_name="${prd_file%.md}"
|
|
465
|
-
|
|
466
|
-
local cli_bin
|
|
467
|
-
cli_bin=$(resolve_night_watch_cli) || {
|
|
468
|
-
log "WARN: Could not resolve night-watch CLI — skipping prd-state set"
|
|
469
|
-
return 1
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
"${cli_bin}" prd-state set "${project_dir}" "${prd_name}" --branch "${branch_name}" 2>/dev/null
|
|
473
|
-
log "PENDING-REVIEW: ${prd_file} marked as pending-review in prd-states.json"
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
# Check prd-states.json for pending-review PRDs and promote any whose PR has been merged to done/
|
|
477
|
-
promote_merged_prds() {
|
|
478
|
-
local original_prd_dir="${1:?original_prd_dir required}"
|
|
479
|
-
local project_dir="${2:?project_dir required}"
|
|
480
|
-
local done_dir="${original_prd_dir}/done"
|
|
481
|
-
|
|
482
|
-
local cli_bin
|
|
483
|
-
cli_bin=$(resolve_night_watch_cli) || return 0
|
|
484
|
-
|
|
485
|
-
local pending_prds
|
|
486
|
-
pending_prds=$("${cli_bin}" prd-state list "${project_dir}" --status pending-review 2>/dev/null || echo "")
|
|
487
|
-
[ -z "${pending_prds}" ] && return 0
|
|
488
|
-
|
|
489
|
-
local merged_branches
|
|
490
|
-
merged_branches=$(cd "${project_dir}" && gh pr list --state merged --json headRefName --jq '.[].headRefName' 2>/dev/null || true)
|
|
491
|
-
[ -z "${merged_branches}" ] && return 0
|
|
492
|
-
|
|
493
|
-
while IFS= read -r prd_name; do
|
|
494
|
-
[ -z "${prd_name}" ] && continue
|
|
495
|
-
local prd_file="${prd_name}.md"
|
|
496
|
-
local branch_name="night-watch/${prd_name}"
|
|
497
|
-
|
|
498
|
-
if echo "${merged_branches}" | grep -qF "${branch_name}"; then
|
|
499
|
-
mkdir -p "${done_dir}"
|
|
500
|
-
if [ -f "${original_prd_dir}/${prd_file}" ]; then
|
|
501
|
-
mv "${original_prd_dir}/${prd_file}" "${done_dir}/${prd_file}"
|
|
502
|
-
"${cli_bin}" prd-state clear "${project_dir}" "${prd_name}" 2>/dev/null || true
|
|
503
|
-
log "MERGED: ${prd_name} PR was merged — moved to done/ and cleared from prd-states.json"
|
|
504
|
-
else
|
|
505
|
-
# File already gone (e.g. moved manually) — just clear the state
|
|
506
|
-
"${cli_bin}" prd-state clear "${project_dir}" "${prd_name}" 2>/dev/null || true
|
|
507
|
-
log "MERGED: ${prd_name} PR was merged — cleared from prd-states.json (file not found)"
|
|
508
|
-
fi
|
|
509
|
-
fi
|
|
510
|
-
done <<< "${pending_prds}"
|
|
511
|
-
|
|
512
|
-
return 0
|
|
513
|
-
}
|
|
514
|
-
|
|
515
388
|
# ── Rate limit detection ────────────────────────────────────────────────────
|
|
516
389
|
|
|
517
390
|
# Check if the last N lines of the log contain a 429 rate limit error.
|
|
@@ -15,14 +15,13 @@ 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
|
-
|
|
18
|
+
# NOTE: Lock file path must match LOCK_FILE_PREFIX in src/constants.ts (reviewer: prefix + "pr-reviewer-" + name)
|
|
19
|
+
LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_NAME}.lock"
|
|
19
20
|
MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
|
|
20
21
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
21
22
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
22
23
|
MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
|
|
23
24
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
24
|
-
RUNTIME_MIRROR_DIR=""
|
|
25
|
-
RUNTIME_PROJECT_DIR=""
|
|
26
25
|
|
|
27
26
|
# Ensure NVM / Node / Claude are on PATH
|
|
28
27
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -36,8 +35,6 @@ mkdir -p "${LOG_DIR}"
|
|
|
36
35
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
36
|
# shellcheck source=night-watch-helpers.sh
|
|
38
37
|
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"
|
|
41
38
|
|
|
42
39
|
# Validate provider
|
|
43
40
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
@@ -51,37 +48,7 @@ if ! acquire_lock "${LOCK_FILE}"; then
|
|
|
51
48
|
exit 0
|
|
52
49
|
fi
|
|
53
50
|
|
|
54
|
-
|
|
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
|
+
cd "${PROJECT_DIR}"
|
|
85
52
|
|
|
86
53
|
# Convert comma-separated branch prefixes into a regex that matches branch starts.
|
|
87
54
|
BRANCH_REGEX=""
|
|
@@ -154,41 +121,42 @@ if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
|
154
121
|
exit 0
|
|
155
122
|
fi
|
|
156
123
|
|
|
124
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
125
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
126
|
+
else
|
|
127
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
128
|
+
fi
|
|
129
|
+
REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-review-runner"
|
|
130
|
+
|
|
157
131
|
log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
|
|
158
132
|
|
|
133
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
134
|
+
|
|
159
135
|
# Dry-run mode: print diagnostics and exit
|
|
160
136
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
161
137
|
echo "=== Dry Run: PR Reviewer ==="
|
|
162
138
|
echo "Provider: ${PROVIDER_CMD}"
|
|
163
139
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
164
140
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
165
|
-
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
166
|
-
echo "Runtime Dir: ${RUNTIME_PROJECT_DIR}"
|
|
167
141
|
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
142
|
+
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
143
|
+
echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
|
|
168
144
|
echo "Timeout: ${MAX_RUNTIME}s"
|
|
169
145
|
exit 0
|
|
170
146
|
fi
|
|
171
147
|
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
REVIEW_WORKFLOW=$(cat "${SCRIPT_DIR}/../templates/night-watch-pr-reviewer.md")
|
|
148
|
+
if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
149
|
+
log "FAIL: Unable to create isolated reviewer worktree ${REVIEW_WORKTREE_DIR}"
|
|
150
|
+
exit 1
|
|
176
151
|
fi
|
|
177
152
|
|
|
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
|
-
|
|
185
153
|
EXIT_CODE=0
|
|
186
154
|
|
|
187
155
|
case "${PROVIDER_CMD}" in
|
|
188
156
|
claude)
|
|
189
157
|
if (
|
|
190
|
-
cd "${
|
|
191
|
-
claude -p "
|
|
158
|
+
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
159
|
+
claude -p "/night-watch-pr-reviewer" \
|
|
192
160
|
--dangerously-skip-permissions \
|
|
193
161
|
>> "${LOG_FILE}" 2>&1
|
|
194
162
|
); then
|
|
@@ -199,10 +167,10 @@ case "${PROVIDER_CMD}" in
|
|
|
199
167
|
;;
|
|
200
168
|
codex)
|
|
201
169
|
if (
|
|
202
|
-
cd "${
|
|
170
|
+
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
203
171
|
codex --quiet \
|
|
204
172
|
--yolo \
|
|
205
|
-
--prompt "${
|
|
173
|
+
--prompt "$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")" \
|
|
206
174
|
>> "${LOG_FILE}" 2>&1
|
|
207
175
|
); then
|
|
208
176
|
EXIT_CODE=0
|
|
@@ -216,6 +184,8 @@ case "${PROVIDER_CMD}" in
|
|
|
216
184
|
;;
|
|
217
185
|
esac
|
|
218
186
|
|
|
187
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
188
|
+
|
|
219
189
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
220
190
|
log "DONE: PR reviewer completed successfully"
|
|
221
191
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
@@ -67,14 +67,15 @@ 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. **
|
|
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**:
|
|
70
|
+
a. **Create an isolated review worktree**:
|
|
76
71
|
```
|
|
77
72
|
git fetch origin
|
|
73
|
+
git worktree add --detach ../${PROJECT_NAME}-nw-review-<branch-name> origin/${DEFAULT_BRANCH}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
b. **Check out the PR branch inside that worktree**:
|
|
77
|
+
```
|
|
78
|
+
cd ../${PROJECT_NAME}-nw-review-<branch-name>
|
|
78
79
|
git checkout <branch-name>
|
|
79
80
|
git pull origin <branch-name>
|
|
80
81
|
```
|
|
@@ -135,6 +136,10 @@ A PR needs attention if **either** the review score is below 80 **or** any CI jo
|
|
|
135
136
|
Night Watch PR Reviewer"
|
|
136
137
|
```
|
|
137
138
|
|
|
139
|
+
h. **Clean up worktree**: `git worktree remove ../${PROJECT_NAME}-nw-review-<branch-name>`
|
|
140
|
+
|
|
138
141
|
5. **Repeat** for all open PRs that need work.
|
|
139
142
|
|
|
143
|
+
6. When done, return to ${DEFAULT_BRANCH}: `git checkout ${DEFAULT_BRANCH}`
|
|
144
|
+
|
|
140
145
|
Start now. Check for open PRs that need review feedback addressed or CI failures fixed.
|