@jonit-dev/night-watch-cli 1.7.51 → 1.7.52
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 +1098 -2189
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +6 -24
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +16 -4
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +5 -4
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/shared/env-builder.d.ts +5 -0
- package/dist/commands/shared/env-builder.d.ts.map +1 -1
- package/dist/commands/shared/env-builder.js +32 -0
- package/dist/commands/shared/env-builder.js.map +1 -1
- package/dist/commands/slice.d.ts +8 -0
- package/dist/commands/slice.d.ts.map +1 -1
- package/dist/commands/slice.js +90 -2
- package/dist/commands/slice.js.map +1 -1
- package/dist/scripts/night-watch-audit-cron.sh +16 -3
- package/dist/scripts/night-watch-cron.sh +19 -5
- package/dist/scripts/night-watch-helpers.sh +89 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +69 -2
- package/dist/scripts/night-watch-qa-cron.sh +321 -18
- package/dist/scripts/night-watch-slicer-cron.sh +14 -3
- package/dist/templates/night-watch-pr-reviewer.md +27 -0
- package/dist/templates/night-watch.config.json +31 -1
- package/dist/templates/night-watch.md +31 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
12
|
# NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
|
|
13
13
|
# NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
|
|
14
|
-
# NW_QA_ARTIFACTS=both - Artifact mode (
|
|
14
|
+
# NW_QA_ARTIFACTS=both - Artifact mode (screenshot, video, both)
|
|
15
15
|
# NW_QA_AUTO_INSTALL_PLAYWRIGHT=1 - Auto-install Playwright browsers
|
|
16
16
|
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
17
17
|
|
|
@@ -22,6 +22,7 @@ LOG_FILE="${LOG_DIR}/night-watch-qa.log"
|
|
|
22
22
|
MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
|
|
23
23
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
24
24
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
25
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
25
26
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
26
27
|
SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
|
|
27
28
|
QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
|
|
@@ -65,6 +66,147 @@ decode_base64_value() {
|
|
|
65
66
|
fi
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
append_csv() {
|
|
70
|
+
local current="${1:-}"
|
|
71
|
+
local incoming="${2:-}"
|
|
72
|
+
if [ -z "${incoming}" ]; then
|
|
73
|
+
printf "%s" "${current}"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
if [ -z "${current}" ]; then
|
|
77
|
+
printf "%s" "${incoming}"
|
|
78
|
+
else
|
|
79
|
+
printf "%s,%s" "${current}" "${incoming}"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
csv_or_none() {
|
|
84
|
+
local value="${1:-}"
|
|
85
|
+
if [ -n "${value}" ]; then
|
|
86
|
+
printf "%s" "${value}"
|
|
87
|
+
else
|
|
88
|
+
printf "none"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe_qa_artifacts() {
|
|
93
|
+
local mode="${1:-both}"
|
|
94
|
+
case "${mode}" in
|
|
95
|
+
screenshot)
|
|
96
|
+
printf "screenshots only"
|
|
97
|
+
;;
|
|
98
|
+
video)
|
|
99
|
+
printf "videos only"
|
|
100
|
+
;;
|
|
101
|
+
both)
|
|
102
|
+
printf "screenshots + videos"
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
printf "custom (%s)" "${mode}"
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
normalize_qa_screenshot_url() {
|
|
111
|
+
local raw_url="${1:-}"
|
|
112
|
+
if [ -z "${raw_url}" ]; then
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if printf '%s' "${raw_url}" | grep -Eq '^https?://'; then
|
|
117
|
+
printf '%s' "${raw_url}"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^\.\./blob/'; then
|
|
122
|
+
printf 'https://github.com/%s/%s' "${REPO}" "${raw_url#../}"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^blob/'; then
|
|
127
|
+
printf 'https://github.com/%s/%s' "${REPO}" "${raw_url}"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
printf '%s' "${raw_url}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
extract_url_host() {
|
|
135
|
+
local raw_url="${1:-}"
|
|
136
|
+
if [ -z "${raw_url}" ]; then
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
printf '%s' "${raw_url}" | sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://##; s#/.*$##'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resolve_claude_model_hint() {
|
|
143
|
+
local sonnet="${ANTHROPIC_DEFAULT_SONNET_MODEL:-}"
|
|
144
|
+
local opus="${ANTHROPIC_DEFAULT_OPUS_MODEL:-}"
|
|
145
|
+
local native_model="${NW_CLAUDE_MODEL_ID:-}"
|
|
146
|
+
|
|
147
|
+
if [ -n "${sonnet}" ] && [ -n "${opus}" ]; then
|
|
148
|
+
if [ "${sonnet}" = "${opus}" ]; then
|
|
149
|
+
printf "%s" "${sonnet}"
|
|
150
|
+
else
|
|
151
|
+
printf "sonnet=%s, opus=%s" "${sonnet}" "${opus}"
|
|
152
|
+
fi
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
if [ -n "${sonnet}" ]; then
|
|
156
|
+
printf "%s" "${sonnet}"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
if [ -n "${opus}" ]; then
|
|
160
|
+
printf "%s" "${opus}"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
if [ -n "${native_model}" ]; then
|
|
164
|
+
printf "%s" "${native_model}"
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
printf "default"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resolve_provider_model_display() {
|
|
171
|
+
local provider_cmd="${1:?provider command required}"
|
|
172
|
+
local provider_label="${2:-}"
|
|
173
|
+
local label_trimmed=""
|
|
174
|
+
local model_hint=""
|
|
175
|
+
local endpoint_host=""
|
|
176
|
+
local details=""
|
|
177
|
+
|
|
178
|
+
label_trimmed=$(printf '%s' "${provider_label}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
179
|
+
|
|
180
|
+
case "${provider_cmd}" in
|
|
181
|
+
claude)
|
|
182
|
+
model_hint=$(resolve_claude_model_hint)
|
|
183
|
+
endpoint_host=$(extract_url_host "${ANTHROPIC_BASE_URL:-}")
|
|
184
|
+
details="${model_hint}"
|
|
185
|
+
if [ -n "${endpoint_host}" ]; then
|
|
186
|
+
details="${details} via ${endpoint_host}"
|
|
187
|
+
fi
|
|
188
|
+
if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Claude" ] && [ "${label_trimmed}" != "Claude (proxy)" ]; then
|
|
189
|
+
details="${label_trimmed}; ${details}"
|
|
190
|
+
fi
|
|
191
|
+
printf "%s (%s)" "${provider_cmd}" "${details}"
|
|
192
|
+
;;
|
|
193
|
+
codex)
|
|
194
|
+
if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Codex" ]; then
|
|
195
|
+
printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
|
|
196
|
+
else
|
|
197
|
+
printf "%s" "${provider_cmd}"
|
|
198
|
+
fi
|
|
199
|
+
;;
|
|
200
|
+
*)
|
|
201
|
+
if [ -n "${label_trimmed}" ]; then
|
|
202
|
+
printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
|
|
203
|
+
else
|
|
204
|
+
printf "%s" "${provider_cmd}"
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
}
|
|
209
|
+
|
|
68
210
|
get_pr_comment_bodies_base64() {
|
|
69
211
|
local pr_number="${1:?PR number required}"
|
|
70
212
|
gh pr view "${pr_number}" --json comments --jq '.comments[]?.body | @base64' 2>/dev/null || true
|
|
@@ -90,6 +232,61 @@ get_latest_qa_comment_body() {
|
|
|
90
232
|
printf "%s" "${latest}"
|
|
91
233
|
}
|
|
92
234
|
|
|
235
|
+
get_qa_screenshot_links() {
|
|
236
|
+
local pr_number="${1:?PR number required}"
|
|
237
|
+
local qa_comment=""
|
|
238
|
+
|
|
239
|
+
qa_comment=$(get_latest_qa_comment_body "${pr_number}")
|
|
240
|
+
if [ -z "${qa_comment}" ]; then
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
printf '%s' "${qa_comment}" \
|
|
245
|
+
| { grep -Eo '!\[[^]]*\]\(([^)]*qa-artifacts/[^)]*)\)' || true; } \
|
|
246
|
+
| sed -E 's/^!\[[^]]*\]\(([^)]*)\)$/\1/' \
|
|
247
|
+
| while IFS= read -r raw_url; do
|
|
248
|
+
[ -z "${raw_url}" ] && continue
|
|
249
|
+
normalize_qa_screenshot_url "${raw_url}"
|
|
250
|
+
printf '\n'
|
|
251
|
+
done \
|
|
252
|
+
| awk 'NF && !seen[$0]++'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
classify_qa_comment_outcome() {
|
|
256
|
+
local pr_number="${1:?PR number required}"
|
|
257
|
+
local qa_comment=""
|
|
258
|
+
local status_lines=""
|
|
259
|
+
|
|
260
|
+
qa_comment=$(get_latest_qa_comment_body "${pr_number}")
|
|
261
|
+
if [ -z "${qa_comment}" ]; then
|
|
262
|
+
printf "unclassified"
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if printf '%s' "${qa_comment}" | grep -Eqi 'QA: No tests needed for this PR|No tests needed'; then
|
|
267
|
+
printf "no_tests_needed"
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
status_lines=$(printf '%s' "${qa_comment}" | grep -E '^- \*\*Status\*\*:' || true)
|
|
272
|
+
if [ -z "${status_lines}" ]; then
|
|
273
|
+
printf "unclassified"
|
|
274
|
+
return 0
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if printf '%s' "${status_lines}" | grep -Eqi 'failing|failed|error|timed out|timeout'; then
|
|
278
|
+
printf "issues_found"
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
if printf '%s' "${status_lines}" | grep -Eqi 'all passing'; then
|
|
283
|
+
printf "passing"
|
|
284
|
+
return 0
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
printf "unclassified"
|
|
288
|
+
}
|
|
289
|
+
|
|
93
290
|
pr_has_qa_generated_files() {
|
|
94
291
|
local pr_number="${1:?PR number required}"
|
|
95
292
|
gh pr view "${pr_number}" --json files --jq '.files[]?.path' 2>/dev/null \
|
|
@@ -152,8 +349,13 @@ fi
|
|
|
152
349
|
|
|
153
350
|
cd "${PROJECT_DIR}"
|
|
154
351
|
|
|
352
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
353
|
+
QA_ARTIFACTS_DESC=$(describe_qa_artifacts "${QA_ARTIFACTS}")
|
|
354
|
+
|
|
155
355
|
send_telegram_status_message "🧪 Night Watch QA: started" "Project: ${PROJECT_NAME}
|
|
156
|
-
Provider: ${
|
|
356
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
357
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
358
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW}
|
|
157
359
|
Scanning open PRs for QA candidates."
|
|
158
360
|
|
|
159
361
|
# Convert comma-separated branch prefixes into a regex that matches branch starts.
|
|
@@ -185,7 +387,9 @@ OPEN_PRS=$(
|
|
|
185
387
|
if [ "${OPEN_PRS}" -eq 0 ]; then
|
|
186
388
|
log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
|
|
187
389
|
send_telegram_status_message "🧪 Night Watch QA: no matching PRs" "Project: ${PROJECT_NAME}
|
|
188
|
-
|
|
390
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
391
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW}
|
|
392
|
+
Result: 0 open PRs matched."
|
|
189
393
|
emit_result "skip_no_open_prs"
|
|
190
394
|
exit 0
|
|
191
395
|
fi
|
|
@@ -242,7 +446,9 @@ done < <(
|
|
|
242
446
|
if [ "${QA_NEEDED}" -eq 0 ]; then
|
|
243
447
|
log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
|
|
244
448
|
send_telegram_status_message "🧪 Night Watch QA: nothing to do" "Project: ${PROJECT_NAME}
|
|
245
|
-
|
|
449
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
450
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
451
|
+
Result: All matching PRs already have QA results."
|
|
246
452
|
emit_result "skip_all_qa_done"
|
|
247
453
|
exit 0
|
|
248
454
|
fi
|
|
@@ -265,10 +471,10 @@ cleanup_worktrees "${PROJECT_DIR}"
|
|
|
265
471
|
# Dry-run mode: print diagnostics and exit
|
|
266
472
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
267
473
|
echo "=== Dry Run: QA Runner ==="
|
|
268
|
-
echo "Provider: ${
|
|
474
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
269
475
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
270
476
|
echo "Skip Label: ${SKIP_LABEL}"
|
|
271
|
-
echo "QA Artifacts: ${QA_ARTIFACTS}"
|
|
477
|
+
echo "QA Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})"
|
|
272
478
|
echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
|
|
273
479
|
echo "Open PRs needing QA:${PRS_NEEDING_QA}"
|
|
274
480
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
@@ -278,17 +484,31 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
278
484
|
fi
|
|
279
485
|
|
|
280
486
|
EXIT_CODE=0
|
|
487
|
+
PROCESSED_PRS_CSV=""
|
|
488
|
+
PASSING_PRS_CSV=""
|
|
489
|
+
ISSUES_FOUND_PRS_CSV=""
|
|
490
|
+
NO_TESTS_PRS_CSV=""
|
|
491
|
+
UNCLASSIFIED_PRS_CSV=""
|
|
492
|
+
FAILED_AUTOMATION_PRS_CSV=""
|
|
493
|
+
FAILED_PR=""
|
|
494
|
+
FAILED_REASON="unknown"
|
|
495
|
+
QA_SCREENSHOT_SUMMARY=""
|
|
281
496
|
|
|
282
497
|
# Process each PR that needs QA
|
|
283
498
|
for pr_ref in ${PRS_NEEDING_QA}; do
|
|
284
499
|
pr_num="${pr_ref#\#}"
|
|
500
|
+
PROCESSED_PRS_CSV=$(append_csv "${PROCESSED_PRS_CSV}" "#${pr_num}")
|
|
285
501
|
send_telegram_status_message "🧪 Night Watch QA: processing PR #${pr_num}" "Project: ${PROJECT_NAME}
|
|
286
|
-
Provider: ${
|
|
287
|
-
Artifacts: ${QA_ARTIFACTS}
|
|
502
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
503
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
504
|
+
Action: generating QA tests and evidence."
|
|
288
505
|
|
|
289
506
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
290
507
|
if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
291
508
|
log "FAIL: Unable to create isolated QA worktree ${QA_WORKTREE_DIR} for PR #${pr_num}"
|
|
509
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
510
|
+
FAILED_PR="#${pr_num}"
|
|
511
|
+
FAILED_REASON="worktree_setup_failed"
|
|
292
512
|
EXIT_CODE=1
|
|
293
513
|
break
|
|
294
514
|
fi
|
|
@@ -296,6 +516,9 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
296
516
|
log "QA: Checking out PR #${pr_num} in worktree"
|
|
297
517
|
if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
|
|
298
518
|
log "WARN: Failed to checkout PR #${pr_num}, skipping"
|
|
519
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
520
|
+
FAILED_PR="#${pr_num}"
|
|
521
|
+
FAILED_REASON="checkout_failed"
|
|
299
522
|
EXIT_CODE=1
|
|
300
523
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
301
524
|
continue
|
|
@@ -304,6 +527,9 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
304
527
|
QA_PROMPT_PATH=$(resolve_instruction_path_with_fallback "${QA_WORKTREE_DIR}" "qa.md" "night-watch-qa.md" || true)
|
|
305
528
|
if [ -z "${QA_PROMPT_PATH}" ]; then
|
|
306
529
|
log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked qa.md/night-watch-qa.md in instructions/, .claude/commands/, and bundled templates/"
|
|
530
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
531
|
+
FAILED_PR="#${pr_num}"
|
|
532
|
+
FAILED_REASON="missing_prompt"
|
|
307
533
|
EXIT_CODE=1
|
|
308
534
|
break
|
|
309
535
|
fi
|
|
@@ -316,6 +542,10 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
316
542
|
QA_PROMPT_REF=$(instruction_ref_for_prompt "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}")
|
|
317
543
|
log "QA: PR #${pr_num} — using prompt from ${QA_PROMPT_REF}"
|
|
318
544
|
|
|
545
|
+
# Inject provider attribution requirement into the QA prompt.
|
|
546
|
+
QA_PROVIDER_LABEL="${NW_PROVIDER_LABEL:-${PROVIDER_CMD}}"
|
|
547
|
+
QA_PROMPT="${QA_PROMPT}"$'\n\n'"## QA Attribution (Required)"$'\n'"At the very end of each QA result comment you post, add this footer on its own line:"$'\n'"> 🧪 QA run by ${QA_PROVIDER_LABEL}"
|
|
548
|
+
|
|
319
549
|
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
320
550
|
PROVIDER_OK=0
|
|
321
551
|
case "${PROVIDER_CMD}" in
|
|
@@ -331,9 +561,15 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
331
561
|
local_exit=$?
|
|
332
562
|
log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
|
|
333
563
|
if [ ${local_exit} -eq 124 ]; then
|
|
564
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
565
|
+
FAILED_PR="#${pr_num}"
|
|
566
|
+
FAILED_REASON="timeout"
|
|
334
567
|
EXIT_CODE=124
|
|
335
568
|
break
|
|
336
569
|
fi
|
|
570
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
571
|
+
FAILED_PR="#${pr_num}"
|
|
572
|
+
FAILED_REASON="provider_exit_${local_exit}"
|
|
337
573
|
EXIT_CODE=${local_exit}
|
|
338
574
|
fi
|
|
339
575
|
;;
|
|
@@ -350,9 +586,15 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
350
586
|
local_exit=$?
|
|
351
587
|
log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
|
|
352
588
|
if [ ${local_exit} -eq 124 ]; then
|
|
589
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
590
|
+
FAILED_PR="#${pr_num}"
|
|
591
|
+
FAILED_REASON="timeout"
|
|
353
592
|
EXIT_CODE=124
|
|
354
593
|
break
|
|
355
594
|
fi
|
|
595
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
596
|
+
FAILED_PR="#${pr_num}"
|
|
597
|
+
FAILED_REASON="provider_exit_${local_exit}"
|
|
356
598
|
EXIT_CODE=${local_exit}
|
|
357
599
|
fi
|
|
358
600
|
;;
|
|
@@ -365,10 +607,37 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
365
607
|
if [ "${PROVIDER_OK}" -eq 1 ]; then
|
|
366
608
|
if provider_output_looks_invalid "${LOG_LINE_BEFORE}"; then
|
|
367
609
|
log "FAIL-QA-EVIDENCE: PR #${pr_num} provider output indicates an invalid automation run"
|
|
610
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
611
|
+
FAILED_PR="#${pr_num}"
|
|
612
|
+
FAILED_REASON="invalid_provider_output"
|
|
368
613
|
EXIT_CODE=1
|
|
369
614
|
elif ! validate_qa_evidence "${pr_num}"; then
|
|
615
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
616
|
+
FAILED_PR="#${pr_num}"
|
|
617
|
+
FAILED_REASON="qa_evidence_validation_failed"
|
|
370
618
|
EXIT_CODE=1
|
|
371
619
|
else
|
|
620
|
+
QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
|
|
621
|
+
case "${QA_OUTCOME}" in
|
|
622
|
+
passing)
|
|
623
|
+
PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
|
|
624
|
+
;;
|
|
625
|
+
issues_found)
|
|
626
|
+
ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
|
|
627
|
+
;;
|
|
628
|
+
no_tests_needed)
|
|
629
|
+
NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
|
|
630
|
+
;;
|
|
631
|
+
*)
|
|
632
|
+
UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
|
|
633
|
+
;;
|
|
634
|
+
esac
|
|
635
|
+
|
|
636
|
+
PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
|
|
637
|
+
if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
|
|
638
|
+
QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
|
|
639
|
+
fi
|
|
640
|
+
|
|
372
641
|
log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
|
|
373
642
|
fi
|
|
374
643
|
fi
|
|
@@ -378,34 +647,68 @@ done
|
|
|
378
647
|
|
|
379
648
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
380
649
|
|
|
650
|
+
FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
|
|
651
|
+
PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
|
|
652
|
+
ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
|
|
653
|
+
NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
|
|
654
|
+
UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
|
|
655
|
+
FAILED_AUTOMATION_PRS_SUMMARY=$(csv_or_none "${FAILED_AUTOMATION_PRS_CSV}")
|
|
656
|
+
FAILED_PR_SUMMARY=$(csv_or_none "${FAILED_PR}")
|
|
657
|
+
|
|
381
658
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
382
659
|
log "DONE: QA runner completed successfully"
|
|
383
|
-
|
|
384
|
-
|
|
660
|
+
TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
|
|
661
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
662
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
663
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
664
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
665
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
666
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
667
|
+
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
|
|
668
|
+
if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
|
|
669
|
+
TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
|
|
670
|
+
Screenshot links:
|
|
671
|
+
${QA_SCREENSHOT_SUMMARY}"
|
|
672
|
+
fi
|
|
673
|
+
send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
|
|
385
674
|
if [ -n "${REPO}" ]; then
|
|
386
|
-
emit_result "success_qa" "prs=${
|
|
675
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
|
|
387
676
|
else
|
|
388
|
-
emit_result "success_qa" "prs=${
|
|
677
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
|
|
389
678
|
fi
|
|
390
679
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
391
680
|
log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
|
|
392
681
|
send_telegram_status_message "🧪 Night Watch QA: timeout" "Project: ${PROJECT_NAME}
|
|
682
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
393
683
|
Timeout: ${MAX_RUNTIME}s
|
|
394
|
-
|
|
684
|
+
Failed PR: ${FAILED_PR_SUMMARY}
|
|
685
|
+
Failure reason: ${FAILED_REASON}
|
|
686
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
687
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
688
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
689
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
690
|
+
Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
395
691
|
if [ -n "${REPO}" ]; then
|
|
396
|
-
emit_result "timeout" "prs=${
|
|
692
|
+
emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
|
|
397
693
|
else
|
|
398
|
-
emit_result "timeout" "prs=${
|
|
694
|
+
emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
399
695
|
fi
|
|
400
696
|
else
|
|
401
697
|
log "FAIL: QA runner exited with code ${EXIT_CODE}"
|
|
402
698
|
send_telegram_status_message "🧪 Night Watch QA: failed" "Project: ${PROJECT_NAME}
|
|
699
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
403
700
|
Exit code: ${EXIT_CODE}
|
|
404
|
-
|
|
701
|
+
Failed PR: ${FAILED_PR_SUMMARY}
|
|
702
|
+
Failure reason: ${FAILED_REASON}
|
|
703
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
704
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
705
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
706
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
707
|
+
Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
405
708
|
if [ -n "${REPO}" ]; then
|
|
406
|
-
emit_result "failure" "prs=${
|
|
709
|
+
emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
|
|
407
710
|
else
|
|
408
|
-
emit_result "failure" "prs=${
|
|
711
|
+
emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
409
712
|
fi
|
|
410
713
|
fi
|
|
411
714
|
|
|
@@ -22,6 +22,7 @@ LOCK_FILE=""
|
|
|
22
22
|
MAX_RUNTIME="${NW_SLICER_MAX_RUNTIME:-600}" # 10 minutes
|
|
23
23
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
24
24
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
25
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
25
26
|
|
|
26
27
|
# Ensure NVM / Node / Night Watch CLI are on PATH
|
|
27
28
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -34,6 +35,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
34
35
|
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
35
36
|
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
36
37
|
LOCK_FILE="/tmp/night-watch-slicer-${PROJECT_RUNTIME_KEY}.lock"
|
|
38
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
37
39
|
|
|
38
40
|
# Validate provider
|
|
39
41
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
@@ -55,14 +57,16 @@ trap cleanup_on_exit EXIT
|
|
|
55
57
|
|
|
56
58
|
log "START: Running roadmap slicer for ${PROJECT_DIR}"
|
|
57
59
|
send_telegram_status_message "📋 Night Watch Planner: started" "Project: ${PROJECT_NAME}
|
|
58
|
-
Provider: ${
|
|
59
|
-
|
|
60
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
61
|
+
Roadmap path: ${NW_ROADMAP_PATH:-ROADMAP.md}
|
|
62
|
+
Action: planning next roadmap item into a PRD."
|
|
60
63
|
|
|
61
64
|
# Dry-run mode: print diagnostics and exit
|
|
62
65
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
63
66
|
echo "=== Dry Run: Roadmap Slicer ==="
|
|
64
|
-
echo "Provider: ${
|
|
67
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
65
68
|
echo "Project Dir: ${PROJECT_DIR}"
|
|
69
|
+
echo "Roadmap Path: ${NW_ROADMAP_PATH:-ROADMAP.md}"
|
|
66
70
|
echo "Timeout: ${MAX_RUNTIME}s"
|
|
67
71
|
exit 0
|
|
68
72
|
fi
|
|
@@ -71,6 +75,9 @@ fi
|
|
|
71
75
|
CLI_BIN=""
|
|
72
76
|
if ! CLI_BIN=$(resolve_night_watch_cli); then
|
|
73
77
|
log "ERROR: Could not resolve night-watch CLI"
|
|
78
|
+
send_telegram_status_message "📋 Night Watch Planner: failed" "Project: ${PROJECT_NAME}
|
|
79
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
80
|
+
Failure reason: cli_not_found"
|
|
74
81
|
exit 1
|
|
75
82
|
fi
|
|
76
83
|
|
|
@@ -85,14 +92,18 @@ fi
|
|
|
85
92
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
86
93
|
log "DONE: Slicer completed successfully"
|
|
87
94
|
send_telegram_status_message "📋 Night Watch Planner: completed" "Project: ${PROJECT_NAME}
|
|
95
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
88
96
|
PRD planning run finished successfully."
|
|
89
97
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
90
98
|
log "TIMEOUT: Slicer killed after ${MAX_RUNTIME}s"
|
|
91
99
|
send_telegram_status_message "📋 Night Watch Planner: timeout" "Project: ${PROJECT_NAME}
|
|
100
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
92
101
|
Timeout: ${MAX_RUNTIME}s"
|
|
93
102
|
else
|
|
94
103
|
log "FAIL: Slicer exited with code ${EXIT_CODE}"
|
|
95
104
|
send_telegram_status_message "📋 Night Watch Planner: failed" "Project: ${PROJECT_NAME}
|
|
105
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
106
|
+
Failure reason: provider_exit_${EXIT_CODE}
|
|
96
107
|
Exit code: ${EXIT_CODE}"
|
|
97
108
|
fi
|
|
98
109
|
|
|
@@ -137,6 +137,16 @@ Parse the review score from the comment body. Look for patterns like:
|
|
|
137
137
|
```
|
|
138
138
|
gh pr checks <number> --json name,state,conclusion
|
|
139
139
|
```
|
|
140
|
+
- First enumerate all checks/jobs from GitHub (source of truth):
|
|
141
|
+
```
|
|
142
|
+
gh pr checks <number> --json name,state,conclusion --jq '.[] | [.name, .state, .conclusion] | @tsv'
|
|
143
|
+
```
|
|
144
|
+
- To inspect the latest workflow run's job list in detail:
|
|
145
|
+
```
|
|
146
|
+
RUN_ID=$(gh run list --branch <branch-name> --limit 1 --json databaseId --jq '.[0].databaseId')
|
|
147
|
+
gh run view "${RUN_ID}" --json jobs --jq '.jobs[] | [.name, .status, .conclusion] | @tsv'
|
|
148
|
+
gh run view "${RUN_ID}" --log-failed
|
|
149
|
+
```
|
|
140
150
|
- Read the failed job logs carefully to understand the root cause.
|
|
141
151
|
- Fix checks based on their actual names and errors (for example: `typecheck`, `lint`, `test`, `build`, `verify`, `executor`, `qa`, `audit`).
|
|
142
152
|
- Do not assume only a fixed set of CI job names.
|
|
@@ -201,3 +211,20 @@ Parse the review score from the comment body. Look for patterns like:
|
|
|
201
211
|
6. When done, return to ${DEFAULT_BRANCH}: `git checkout ${DEFAULT_BRANCH}`
|
|
202
212
|
|
|
203
213
|
Start now. Check for open PRs that need merge conflicts resolved, review feedback addressed, or CI failures fixed.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Board Mode (when board provider is enabled)
|
|
218
|
+
|
|
219
|
+
When reviewing a PR that references a board issue (`Closes #N` in the body):
|
|
220
|
+
|
|
221
|
+
1. After pushing review fixes, comment on the issue:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
night-watch board comment <N> --body "Review fixes pushed: <commit-sha>"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
2. If review score >= threshold AND CI passes, move to Done:
|
|
228
|
+
```
|
|
229
|
+
night-watch board move-issue <N> --column "Done"
|
|
230
|
+
```
|
|
@@ -3,18 +3,43 @@
|
|
|
3
3
|
"projectName": "",
|
|
4
4
|
"defaultBranch": "",
|
|
5
5
|
"provider": "claude",
|
|
6
|
+
"providerLabel": "",
|
|
6
7
|
"executorEnabled": true,
|
|
7
8
|
"reviewerEnabled": true,
|
|
9
|
+
"providerEnv": {},
|
|
8
10
|
"prdDir": "docs/PRDs/night-watch",
|
|
9
11
|
"templatesDir": ".night-watch/templates",
|
|
10
12
|
"maxRuntime": 7200,
|
|
11
13
|
"reviewerMaxRuntime": 3600,
|
|
12
14
|
"branchPrefix": "night-watch",
|
|
13
|
-
"branchPatterns": [
|
|
15
|
+
"branchPatterns": [
|
|
16
|
+
"feat/",
|
|
17
|
+
"night-watch/"
|
|
18
|
+
],
|
|
14
19
|
"minReviewScore": 80,
|
|
15
20
|
"maxLogSize": 524288,
|
|
16
21
|
"cronSchedule": "0 0-21 * * *",
|
|
17
22
|
"reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *",
|
|
23
|
+
"cronScheduleOffset": 0,
|
|
24
|
+
"maxRetries": 3,
|
|
25
|
+
"reviewerMaxRetries": 2,
|
|
26
|
+
"reviewerRetryDelay": 30,
|
|
27
|
+
"notifications": {
|
|
28
|
+
"webhooks": []
|
|
29
|
+
},
|
|
30
|
+
"prdPriority": [],
|
|
31
|
+
"roadmapScanner": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"roadmapPath": "ROADMAP.md",
|
|
34
|
+
"autoScanInterval": 300,
|
|
35
|
+
"slicerSchedule": "0 */6 * * *",
|
|
36
|
+
"slicerMaxRuntime": 600
|
|
37
|
+
},
|
|
38
|
+
"boardProvider": {
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"provider": "github"
|
|
41
|
+
},
|
|
42
|
+
"jobProviders": {},
|
|
18
43
|
"autoMerge": false,
|
|
19
44
|
"autoMergeMethod": "squash",
|
|
20
45
|
"fallbackOnRateLimit": false,
|
|
@@ -27,5 +52,10 @@
|
|
|
27
52
|
"artifacts": "both",
|
|
28
53
|
"skipLabel": "skip-qa",
|
|
29
54
|
"autoInstallPlaywright": true
|
|
55
|
+
},
|
|
56
|
+
"audit": {
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"schedule": "0 3 * * *",
|
|
59
|
+
"maxRuntime": 1800
|
|
30
60
|
}
|
|
31
61
|
}
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
You are the Night Watch agent. Your job is to autonomously pick up PRD tickets and implement them.
|
|
2
2
|
|
|
3
|
+
## Board Mode (when `NW_BOARD_ENABLED=true` or board provider is configured)
|
|
4
|
+
|
|
5
|
+
If `NW_BOARD_ENABLED` is set to `true` in the environment, use board mode instead of filesystem scanning:
|
|
6
|
+
|
|
7
|
+
1. **Get next task**: `night-watch board next-issue --column "Ready" --json`
|
|
8
|
+
- If no issues are in "Ready", STOP — nothing to do.
|
|
9
|
+
|
|
10
|
+
2. **Claim the task**: `night-watch board move-issue <number> --column "In Progress"`
|
|
11
|
+
|
|
12
|
+
3. **Read the spec**: The issue body IS the PRD. Parse it for phases and requirements.
|
|
13
|
+
|
|
14
|
+
4. **Branch naming**: `night-watch/<issue-number>-<slugified-title>` (e.g., `night-watch/42-my-feature`)
|
|
15
|
+
|
|
16
|
+
5. **Create worktree and implement** as normal (create branch, worktree, implement, test, commit).
|
|
17
|
+
|
|
18
|
+
6. **Open PR**: Include `Closes #<issue-number>` in the PR body so the issue auto-closes when merged:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
gh pr create --title "feat: <short title>" --body "Closes #<number>\n\n<summary>"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
7. **Move to Review**: `night-watch board move-issue <number> --column "Review"`
|
|
25
|
+
|
|
26
|
+
8. **Comment on issue**: `night-watch board comment <number> --body "PR opened: <url>"`
|
|
27
|
+
|
|
28
|
+
9. **Clean up** worktree and **STOP** — one task per run.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Filesystem Mode (default, when board mode is not active)
|
|
33
|
+
|
|
3
34
|
## Instructions
|
|
4
35
|
|
|
5
36
|
1. **Scan for PRDs**: Use `night-watch prd list --json` to get available PRDs. Each PRD is a ticket.
|