@jonit-dev/night-watch-cli 1.5.7 → 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 -168
- 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-hIqyS2ir.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}"
|
|
@@ -238,9 +238,6 @@ find_eligible_prd() {
|
|
|
238
238
|
local open_branches
|
|
239
239
|
open_branches=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null || echo "")
|
|
240
240
|
|
|
241
|
-
local cli_bin
|
|
242
|
-
cli_bin=$(resolve_night_watch_cli 2>/dev/null) || true
|
|
243
|
-
|
|
244
241
|
for prd_path in ${prd_files}; do
|
|
245
242
|
local prd_file
|
|
246
243
|
prd_file=$(basename "${prd_path}")
|
|
@@ -264,12 +261,6 @@ find_eligible_prd() {
|
|
|
264
261
|
continue
|
|
265
262
|
fi
|
|
266
263
|
|
|
267
|
-
# Skip if marked pending-review in prd-states.json
|
|
268
|
-
if [ -n "${project_dir}" ] && [ -n "${cli_bin}" ] && "${cli_bin}" prd-state list "${project_dir}" --status pending-review 2>/dev/null | grep -qF "${prd_name}"; then
|
|
269
|
-
log "SKIP-PRD: ${prd_file} — pending-review in prd-states.json"
|
|
270
|
-
continue
|
|
271
|
-
fi
|
|
272
|
-
|
|
273
264
|
# Check dependencies
|
|
274
265
|
local depends_on
|
|
275
266
|
depends_on=$(grep -i 'depends on' "${prd_path}" 2>/dev/null \
|
|
@@ -301,149 +292,78 @@ cleanup_worktrees() {
|
|
|
301
292
|
local project_dir="${1:?project_dir required}"
|
|
302
293
|
local project_name
|
|
303
294
|
project_name=$(basename "${project_dir}")
|
|
304
|
-
local marker="${2:-${project_name}-nw}"
|
|
305
295
|
|
|
306
296
|
git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
|
|
307
297
|
| grep '^worktree ' \
|
|
308
298
|
| awk '{print $2}' \
|
|
309
|
-
| grep "${
|
|
299
|
+
| grep "${project_name}-nw" \
|
|
310
300
|
| while read -r wt; do
|
|
311
301
|
log "CLEANUP: Removing leftover worktree ${wt}"
|
|
312
302
|
git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
|
|
313
303
|
done || true
|
|
314
304
|
}
|
|
315
305
|
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
project_runtime_key() {
|
|
306
|
+
# Pick the best available ref for creating a new detached worktree.
|
|
307
|
+
resolve_worktree_base_ref() {
|
|
319
308
|
local project_dir="${1:?project_dir required}"
|
|
320
|
-
local project_name
|
|
321
|
-
local hash
|
|
322
|
-
project_name=$(basename "${project_dir}")
|
|
323
|
-
|
|
324
|
-
if command -v sha1sum >/dev/null 2>&1; then
|
|
325
|
-
hash=$(printf "%s" "${project_dir}" | sha1sum | awk '{print $1}')
|
|
326
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
327
|
-
hash=$(printf "%s" "${project_dir}" | shasum | awk '{print $1}')
|
|
328
|
-
else
|
|
329
|
-
hash=$(printf "%s" "${project_dir}" | cksum | awk '{print $1}')
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
printf "%s-%s" "${project_name}" "${hash:0:12}"
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
resolve_runtime_base_ref() {
|
|
336
|
-
local git_dir="${1:?git_dir required}"
|
|
337
309
|
local default_branch="${2:?default_branch required}"
|
|
338
310
|
|
|
339
|
-
if git -C "${
|
|
340
|
-
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}"
|
|
341
313
|
return 0
|
|
342
314
|
fi
|
|
343
315
|
|
|
344
|
-
if git -C "${
|
|
345
|
-
printf "%s" "
|
|
316
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/heads/${default_branch}" >/dev/null; then
|
|
317
|
+
printf "%s" "${default_branch}"
|
|
346
318
|
return 0
|
|
347
319
|
fi
|
|
348
320
|
|
|
349
|
-
if git -C "${
|
|
350
|
-
printf "%s" "
|
|
321
|
+
if git -C "${project_dir}" rev-parse --verify --quiet "refs/remotes/origin/HEAD" >/dev/null; then
|
|
322
|
+
printf "%s" "origin/HEAD"
|
|
351
323
|
return 0
|
|
352
324
|
fi
|
|
353
325
|
|
|
354
326
|
return 1
|
|
355
327
|
}
|
|
356
328
|
|
|
357
|
-
|
|
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() {
|
|
358
332
|
local project_dir="${1:?project_dir required}"
|
|
359
|
-
local default_branch="${2:?default_branch required}"
|
|
360
|
-
local log_file="${3:-${LOG_FILE:-/dev/null}}"
|
|
361
|
-
local runtime_root="${NW_RUNTIME_ROOT:-${HOME}/.night-watch/runtime}"
|
|
362
|
-
local runtime_key
|
|
363
|
-
local runtime_base
|
|
364
|
-
local mirror_dir
|
|
365
|
-
local runs_dir
|
|
366
|
-
local worktree_dir
|
|
367
|
-
local clone_source=""
|
|
368
|
-
local base_ref=""
|
|
369
|
-
|
|
370
|
-
runtime_key=$(project_runtime_key "${project_dir}")
|
|
371
|
-
runtime_base="${runtime_root}/${runtime_key}"
|
|
372
|
-
mirror_dir="${runtime_base}/mirror.git"
|
|
373
|
-
runs_dir="${runtime_base}/runs"
|
|
374
|
-
worktree_dir="${runs_dir}/run-$(date +%Y%m%d-%H%M%S)-$$"
|
|
375
|
-
|
|
376
|
-
mkdir -p "${runs_dir}"
|
|
377
|
-
|
|
378
|
-
if [ ! -d "${mirror_dir}" ]; then
|
|
379
|
-
clone_source=$(git -C "${project_dir}" config --get remote.origin.url 2>/dev/null || echo "")
|
|
380
|
-
|
|
381
|
-
if [ -n "${clone_source}" ]; then
|
|
382
|
-
if ! git clone --mirror "${clone_source}" "${mirror_dir}" >> "${log_file}" 2>&1; then
|
|
383
|
-
git clone --mirror "${project_dir}" "${mirror_dir}" >> "${log_file}" 2>&1
|
|
384
|
-
fi
|
|
385
|
-
else
|
|
386
|
-
git clone --mirror "${project_dir}" "${mirror_dir}" >> "${log_file}" 2>&1
|
|
387
|
-
fi
|
|
388
|
-
fi
|
|
389
|
-
|
|
390
|
-
git -C "${mirror_dir}" remote update --prune >> "${log_file}" 2>&1 || true
|
|
391
|
-
|
|
392
|
-
base_ref=$(resolve_runtime_base_ref "${mirror_dir}" "${default_branch}") || return 1
|
|
393
|
-
git -C "${mirror_dir}" worktree add --detach "${worktree_dir}" "${base_ref}" >> "${log_file}" 2>&1
|
|
394
|
-
|
|
395
|
-
printf "%s\n%s\n" "${mirror_dir}" "${worktree_dir}"
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
cleanup_runtime_workspace() {
|
|
399
|
-
local mirror_dir="${1:?mirror_dir required}"
|
|
400
333
|
local worktree_dir="${2:?worktree_dir required}"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
prepare_branch_checkout() {
|
|
407
|
-
local repo_dir="${1:?repo_dir required}"
|
|
408
|
-
local branch_name="${2:?branch_name required}"
|
|
409
|
-
local default_branch="${3:?default_branch required}"
|
|
410
|
-
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}}"
|
|
411
337
|
local base_ref=""
|
|
412
338
|
|
|
413
|
-
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
|
|
414
341
|
|
|
415
|
-
if git -C "${
|
|
416
|
-
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
|
|
417
344
|
return $?
|
|
418
345
|
fi
|
|
419
346
|
|
|
420
|
-
if git -C "${
|
|
421
|
-
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
|
|
422
349
|
return $?
|
|
423
350
|
fi
|
|
424
351
|
|
|
425
|
-
|
|
426
|
-
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
|
|
427
353
|
}
|
|
428
354
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
local
|
|
432
|
-
local
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if git -C "${repo_dir}" rev-parse --verify --quiet "refs/remotes/origin/${default_branch}" >/dev/null; then
|
|
437
|
-
git -C "${repo_dir}" checkout -B "${default_branch}" "origin/${default_branch}" >> "${log_file}" 2>&1
|
|
438
|
-
return $?
|
|
439
|
-
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=""
|
|
440
362
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return $?
|
|
444
|
-
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
|
|
445
365
|
|
|
446
|
-
git -C "${
|
|
366
|
+
git -C "${project_dir}" worktree add --detach "${worktree_dir}" "${base_ref}" >> "${log_file}" 2>&1
|
|
447
367
|
}
|
|
448
368
|
|
|
449
369
|
# ── Mark PRD as done ─────────────────────────────────────────────────────────
|
|
@@ -465,62 +385,6 @@ mark_prd_done() {
|
|
|
465
385
|
fi
|
|
466
386
|
}
|
|
467
387
|
|
|
468
|
-
mark_prd_pending_review() {
|
|
469
|
-
local prd_dir="${1:?prd_dir required}"
|
|
470
|
-
local prd_file="${2:?prd_file required}"
|
|
471
|
-
local project_dir="${3:?project_dir required}"
|
|
472
|
-
local branch_name="${4:-}"
|
|
473
|
-
local prd_name="${prd_file%.md}"
|
|
474
|
-
|
|
475
|
-
local cli_bin
|
|
476
|
-
cli_bin=$(resolve_night_watch_cli) || {
|
|
477
|
-
log "WARN: Could not resolve night-watch CLI — skipping prd-state set"
|
|
478
|
-
return 1
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
"${cli_bin}" prd-state set "${project_dir}" "${prd_name}" --branch "${branch_name}" 2>/dev/null
|
|
482
|
-
log "PENDING-REVIEW: ${prd_file} marked as pending-review in prd-states.json"
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
# Check prd-states.json for pending-review PRDs and promote any whose PR has been merged to done/
|
|
486
|
-
promote_merged_prds() {
|
|
487
|
-
local original_prd_dir="${1:?original_prd_dir required}"
|
|
488
|
-
local project_dir="${2:?project_dir required}"
|
|
489
|
-
local done_dir="${original_prd_dir}/done"
|
|
490
|
-
|
|
491
|
-
local cli_bin
|
|
492
|
-
cli_bin=$(resolve_night_watch_cli) || return 0
|
|
493
|
-
|
|
494
|
-
local pending_prds
|
|
495
|
-
pending_prds=$("${cli_bin}" prd-state list "${project_dir}" --status pending-review 2>/dev/null || echo "")
|
|
496
|
-
[ -z "${pending_prds}" ] && return 0
|
|
497
|
-
|
|
498
|
-
local merged_branches
|
|
499
|
-
merged_branches=$(cd "${project_dir}" && gh pr list --state merged --json headRefName --jq '.[].headRefName' 2>/dev/null || true)
|
|
500
|
-
[ -z "${merged_branches}" ] && return 0
|
|
501
|
-
|
|
502
|
-
while IFS= read -r prd_name; do
|
|
503
|
-
[ -z "${prd_name}" ] && continue
|
|
504
|
-
local prd_file="${prd_name}.md"
|
|
505
|
-
local branch_name="night-watch/${prd_name}"
|
|
506
|
-
|
|
507
|
-
if echo "${merged_branches}" | grep -qF "${branch_name}"; then
|
|
508
|
-
mkdir -p "${done_dir}"
|
|
509
|
-
if [ -f "${original_prd_dir}/${prd_file}" ]; then
|
|
510
|
-
mv "${original_prd_dir}/${prd_file}" "${done_dir}/${prd_file}"
|
|
511
|
-
"${cli_bin}" prd-state clear "${project_dir}" "${prd_name}" 2>/dev/null || true
|
|
512
|
-
log "MERGED: ${prd_name} PR was merged — moved to done/ and cleared from prd-states.json"
|
|
513
|
-
else
|
|
514
|
-
# File already gone (e.g. moved manually) — just clear the state
|
|
515
|
-
"${cli_bin}" prd-state clear "${project_dir}" "${prd_name}" 2>/dev/null || true
|
|
516
|
-
log "MERGED: ${prd_name} PR was merged — cleared from prd-states.json (file not found)"
|
|
517
|
-
fi
|
|
518
|
-
fi
|
|
519
|
-
done <<< "${pending_prds}"
|
|
520
|
-
|
|
521
|
-
return 0
|
|
522
|
-
}
|
|
523
|
-
|
|
524
388
|
# ── Rate limit detection ────────────────────────────────────────────────────
|
|
525
389
|
|
|
526
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
|