@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.
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/cancel.d.ts +46 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +239 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/dashboard/tab-config.js +1 -1
- package/dist/commands/dashboard/tab-config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/prds.d.ts +13 -0
- package/dist/commands/prds.d.ts.map +1 -0
- package/dist/commands/prds.js +192 -0
- package/dist/commands/prds.js.map +1 -0
- package/dist/commands/prs.d.ts +13 -0
- package/dist/commands/prs.d.ts.map +1 -0
- package/dist/commands/prs.js +101 -0
- package/dist/commands/prs.js.map +1 -0
- package/dist/commands/retry.d.ts +9 -0
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/commands/retry.js +72 -0
- package/dist/commands/retry.js.map +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +4 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/update.d.ts +21 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +87 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +29 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/notify.d.ts.map +1 -1
- package/dist/utils/notify.js +9 -0
- package/dist/utils/notify.js.map +1 -1
- package/dist/utils/status-data.d.ts +13 -0
- package/dist/utils/status-data.d.ts.map +1 -1
- package/dist/utils/status-data.js +24 -3
- package/dist/utils/status-data.js.map +1 -1
- package/package.json +1 -1
- package/scripts/night-watch-cron.sh +86 -37
- package/scripts/night-watch-helpers.sh +135 -1
- package/scripts/night-watch-pr-reviewer-cron.sh +64 -15
- package/templates/night-watch-pr-reviewer.md +7 -11
- package/templates/night-watch.md +9 -20
- package/web/dist/assets/{index-DWgdrh9z.js → index-CMFnDLhY.js} +13 -13
- 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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
--
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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 "${
|
|
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="
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
--
|
|
155
|
-
|
|
156
|
-
|
|
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. **
|
|
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.
|
package/templates/night-watch.md
CHANGED
|
@@ -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. **
|
|
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
|
|
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. **
|
|
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.
|
|
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.
|