@jonit-dev/night-watch-cli 1.7.25 → 1.7.29
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/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/audit.d.ts +19 -0
- package/dist/src/commands/audit.d.ts.map +1 -0
- package/dist/src/commands/audit.js +98 -0
- package/dist/src/commands/audit.js.map +1 -0
- package/dist/src/commands/dashboard.js +1 -1
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +1 -6
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/install.d.ts +4 -0
- package/dist/src/commands/install.d.ts.map +1 -1
- package/dist/src/commands/install.js +25 -20
- package/dist/src/commands/install.js.map +1 -1
- package/dist/src/commands/logs.js +3 -3
- package/dist/src/commands/logs.js.map +1 -1
- package/dist/src/commands/prs.js +2 -2
- package/dist/src/commands/prs.js.map +1 -1
- package/dist/src/commands/review.d.ts.map +1 -1
- package/dist/src/commands/review.js +13 -5
- package/dist/src/commands/review.js.map +1 -1
- package/dist/src/commands/uninstall.d.ts.map +1 -1
- package/dist/src/commands/uninstall.js +3 -22
- package/dist/src/commands/uninstall.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +30 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +10 -3
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +15 -2
- package/dist/src/constants.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +50 -3
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/slack/client.d.ts +9 -1
- package/dist/src/slack/client.d.ts.map +1 -1
- package/dist/src/slack/client.js +18 -4
- package/dist/src/slack/client.js.map +1 -1
- package/dist/src/slack/deliberation.d.ts +22 -1
- package/dist/src/slack/deliberation.d.ts.map +1 -1
- package/dist/src/slack/deliberation.js +663 -51
- package/dist/src/slack/deliberation.js.map +1 -1
- package/dist/src/slack/interaction-listener.d.ts +33 -9
- package/dist/src/slack/interaction-listener.d.ts.map +1 -1
- package/dist/src/slack/interaction-listener.js +393 -197
- package/dist/src/slack/interaction-listener.js.map +1 -1
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/index.js +2 -0
- package/dist/src/storage/repositories/index.js.map +1 -1
- package/dist/src/storage/repositories/interfaces.d.ts +1 -0
- package/dist/src/storage/repositories/interfaces.d.ts.map +1 -1
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts +6 -0
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.js +37 -1
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.js.map +1 -1
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/avatar-generator.d.ts.map +1 -1
- package/dist/src/utils/avatar-generator.js +7 -2
- package/dist/src/utils/avatar-generator.js.map +1 -1
- package/dist/src/utils/notify.d.ts.map +1 -1
- package/dist/src/utils/notify.js +5 -1
- package/dist/src/utils/notify.js.map +1 -1
- package/dist/src/utils/status-data.d.ts +2 -2
- package/dist/src/utils/status-data.d.ts.map +1 -1
- package/dist/src/utils/status-data.js +78 -123
- package/dist/src/utils/status-data.js.map +1 -1
- package/package.json +3 -1
- package/scripts/night-watch-audit-cron.sh +149 -0
- package/scripts/night-watch-cron.sh +33 -14
- package/scripts/night-watch-helpers.sh +10 -2
- package/scripts/night-watch-pr-reviewer-cron.sh +224 -18
- package/web/dist/assets/index-BiJf9LFT.js +458 -0
- package/web/dist/assets/index-OpSgvsYu.css +1 -0
- package/web/dist/avatars/carlos.webp +0 -0
- package/web/dist/avatars/dev.webp +0 -0
- package/web/dist/avatars/maya.webp +0 -0
- package/web/dist/avatars/priya.webp +0 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CndIPm_F.js +0 -473
- package/web/dist/assets/index-w6Q6gxCS.css +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch Audit Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-audit-cron.sh /path/to/project
|
|
6
|
+
#
|
|
7
|
+
# NOTE: This script expects environment variables to be set by the caller.
|
|
8
|
+
# The Node.js CLI will inject config values via environment variables.
|
|
9
|
+
# Required env vars (with defaults shown):
|
|
10
|
+
# NW_AUDIT_MAX_RUNTIME=1800 - Maximum runtime in seconds (30 minutes)
|
|
11
|
+
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
|
+
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
13
|
+
|
|
14
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
15
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
16
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
17
|
+
LOG_FILE="${LOG_DIR}/audit.log"
|
|
18
|
+
REPORT_FILE="${PROJECT_DIR}/logs/audit-report.md"
|
|
19
|
+
MAX_RUNTIME="${NW_AUDIT_MAX_RUNTIME:-1800}" # 30 minutes
|
|
20
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
21
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
22
|
+
|
|
23
|
+
# Ensure NVM / Node / Claude are on PATH
|
|
24
|
+
export NVM_DIR="${HOME}/.nvm"
|
|
25
|
+
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
|
|
26
|
+
|
|
27
|
+
mkdir -p "${LOG_DIR}"
|
|
28
|
+
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
# shellcheck source=night-watch-helpers.sh
|
|
31
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
32
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
33
|
+
# NOTE: Lock file path must match auditLockPath() in src/utils/status-data.ts
|
|
34
|
+
LOCK_FILE="/tmp/night-watch-audit-${PROJECT_RUNTIME_KEY}.lock"
|
|
35
|
+
|
|
36
|
+
emit_result() {
|
|
37
|
+
local status="${1:?status required}"
|
|
38
|
+
local details="${2:-}"
|
|
39
|
+
if [ -n "${details}" ]; then
|
|
40
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
41
|
+
else
|
|
42
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Validate provider
|
|
47
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
48
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
rotate_log
|
|
53
|
+
|
|
54
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
55
|
+
emit_result "skip_locked"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Dry-run mode: print diagnostics and exit
|
|
60
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
61
|
+
echo "=== Dry Run: Code Auditor ==="
|
|
62
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
63
|
+
echo "Max Runtime: ${MAX_RUNTIME}s"
|
|
64
|
+
echo "Report File: ${REPORT_FILE}"
|
|
65
|
+
emit_result "skip_dry_run"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
70
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
71
|
+
else
|
|
72
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
AUDIT_WORKTREE_BASENAME="${PROJECT_NAME}-nw-audit-runner"
|
|
76
|
+
AUDIT_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${AUDIT_WORKTREE_BASENAME}"
|
|
77
|
+
|
|
78
|
+
cleanup_worktrees "${PROJECT_DIR}" "${AUDIT_WORKTREE_BASENAME}"
|
|
79
|
+
|
|
80
|
+
if ! prepare_detached_worktree "${PROJECT_DIR}" "${AUDIT_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
81
|
+
log "FAIL: Unable to create isolated audit worktree ${AUDIT_WORKTREE_DIR}"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Ensure the logs dir exists inside the worktree so the provider can write the report
|
|
86
|
+
mkdir -p "${AUDIT_WORKTREE_DIR}/logs"
|
|
87
|
+
|
|
88
|
+
log "START: Running code audit for ${PROJECT_NAME} (provider: ${PROVIDER_CMD})"
|
|
89
|
+
|
|
90
|
+
EXIT_CODE=0
|
|
91
|
+
|
|
92
|
+
case "${PROVIDER_CMD}" in
|
|
93
|
+
claude)
|
|
94
|
+
if (
|
|
95
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
96
|
+
claude -p "/night-watch-audit" \
|
|
97
|
+
--dangerously-skip-permissions \
|
|
98
|
+
>> "${LOG_FILE}" 2>&1
|
|
99
|
+
); then
|
|
100
|
+
EXIT_CODE=0
|
|
101
|
+
else
|
|
102
|
+
EXIT_CODE=$?
|
|
103
|
+
fi
|
|
104
|
+
;;
|
|
105
|
+
codex)
|
|
106
|
+
if (
|
|
107
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
108
|
+
codex --quiet \
|
|
109
|
+
--yolo \
|
|
110
|
+
--prompt "$(cat "${AUDIT_WORKTREE_DIR}/.claude/commands/night-watch-audit.md")" \
|
|
111
|
+
>> "${LOG_FILE}" 2>&1
|
|
112
|
+
); then
|
|
113
|
+
EXIT_CODE=0
|
|
114
|
+
else
|
|
115
|
+
EXIT_CODE=$?
|
|
116
|
+
fi
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
120
|
+
exit 1
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
# Copy report back to project dir (if it was written in the worktree)
|
|
125
|
+
WORKTREE_REPORT="${AUDIT_WORKTREE_DIR}/logs/audit-report.md"
|
|
126
|
+
if [ -f "${WORKTREE_REPORT}" ]; then
|
|
127
|
+
cp "${WORKTREE_REPORT}" "${REPORT_FILE}"
|
|
128
|
+
log "INFO: Audit report copied to ${REPORT_FILE}"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
cleanup_worktrees "${PROJECT_DIR}" "${AUDIT_WORKTREE_BASENAME}"
|
|
132
|
+
|
|
133
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
134
|
+
if [ -f "${REPORT_FILE}" ] && grep -q "NO_ISSUES_FOUND" "${REPORT_FILE}" 2>/dev/null; then
|
|
135
|
+
log "DONE: Audit complete — no actionable issues found"
|
|
136
|
+
emit_result "skip_clean"
|
|
137
|
+
else
|
|
138
|
+
log "DONE: Audit complete — report written to ${REPORT_FILE}"
|
|
139
|
+
emit_result "success_audit"
|
|
140
|
+
fi
|
|
141
|
+
elif [ "${EXIT_CODE}" -eq 124 ]; then
|
|
142
|
+
log "TIMEOUT: Audit killed after ${MAX_RUNTIME}s"
|
|
143
|
+
emit_result "timeout"
|
|
144
|
+
else
|
|
145
|
+
log "FAIL: Audit exited with code ${EXIT_CODE}"
|
|
146
|
+
emit_result "failure"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
exit "${EXIT_CODE}"
|
|
@@ -21,7 +21,7 @@ else
|
|
|
21
21
|
PRD_DIR="${PROJECT_DIR}/${PRD_DIR_REL}"
|
|
22
22
|
fi
|
|
23
23
|
LOG_DIR="${PROJECT_DIR}/logs"
|
|
24
|
-
LOG_FILE="${LOG_DIR}/
|
|
24
|
+
LOG_FILE="${LOG_DIR}/executor.log"
|
|
25
25
|
MAX_RUNTIME="${NW_MAX_RUNTIME:-7200}" # 2 hours
|
|
26
26
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
27
27
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
@@ -96,25 +96,44 @@ if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
|
96
96
|
log "ERROR: Cannot resolve night-watch CLI for board mode"
|
|
97
97
|
exit 1
|
|
98
98
|
fi
|
|
99
|
-
|
|
100
|
-
if [ -
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
|
|
100
|
+
if [ -n "${NW_TARGET_ISSUE:-}" ]; then
|
|
101
|
+
# Targeted issue pickup: use specified issue directly (already "In Progress" from Slack trigger)
|
|
102
|
+
ISSUE_NUMBER="${NW_TARGET_ISSUE}"
|
|
103
|
+
log "BOARD: Using targeted issue #${ISSUE_NUMBER} (from NW_TARGET_ISSUE)"
|
|
104
|
+
ISSUE_JSON=$(gh issue view "${ISSUE_NUMBER}" --json number,title,body 2>/dev/null || true)
|
|
105
|
+
if [ -z "${ISSUE_JSON}" ]; then
|
|
106
|
+
log "ERROR: Cannot fetch issue #${ISSUE_NUMBER} via gh"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
110
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
111
|
+
else
|
|
112
|
+
ISSUE_JSON=$(find_eligible_board_issue)
|
|
113
|
+
if [ -z "${ISSUE_JSON}" ]; then
|
|
114
|
+
log "SKIP: No eligible issues in Ready column (board mode)"
|
|
115
|
+
emit_result "skip_no_eligible_prd"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
ISSUE_NUMBER=$(printf '%s' "${ISSUE_JSON}" | jq -r '.number // empty' 2>/dev/null || true)
|
|
119
|
+
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
120
|
+
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
121
|
+
if [ -z "${ISSUE_NUMBER}" ]; then
|
|
122
|
+
log "ERROR: Board mode: failed to parse issue number from JSON"
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
# Move issue to In Progress (claim it on the board)
|
|
126
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
127
|
+
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
104
128
|
fi
|
|
105
|
-
|
|
106
|
-
ISSUE_TITLE_RAW=$(printf '%s' "${ISSUE_JSON}" | jq -r '.title // empty' 2>/dev/null || true)
|
|
107
|
-
ISSUE_BODY=$(printf '%s' "${ISSUE_JSON}" | jq -r '.body // empty' 2>/dev/null || true)
|
|
129
|
+
|
|
108
130
|
if [ -z "${ISSUE_NUMBER}" ]; then
|
|
109
|
-
log "ERROR: Board mode:
|
|
131
|
+
log "ERROR: Board mode: no issue number resolved"
|
|
110
132
|
exit 1
|
|
111
133
|
fi
|
|
112
134
|
# Slugify title for branch naming
|
|
113
135
|
ELIGIBLE_PRD="${ISSUE_NUMBER}-$(printf '%s' "${ISSUE_TITLE_RAW}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
|
|
114
|
-
log "BOARD:
|
|
115
|
-
# Move issue to In Progress (claim it on the board)
|
|
116
|
-
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "In Progress" 2>>"${LOG_FILE}" || \
|
|
117
|
-
log "WARN: Failed to move issue #${ISSUE_NUMBER} to In Progress"
|
|
136
|
+
log "BOARD: Processing issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}"
|
|
118
137
|
trap "rm -f '${LOCK_FILE}'" EXIT
|
|
119
138
|
else
|
|
120
139
|
# Filesystem mode: scan PRD directory
|
|
@@ -305,17 +305,25 @@ find_eligible_prd() {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
# ── Clean up worktrees ───────────────────────────────────────────────────────
|
|
308
|
-
# Removes
|
|
308
|
+
# Removes night-watch worktrees for this project.
|
|
309
|
+
# Optional second argument narrows cleanup to worktrees containing that token.
|
|
310
|
+
# This prevents parallel reviewer workers from deleting each other's worktrees.
|
|
309
311
|
|
|
310
312
|
cleanup_worktrees() {
|
|
311
313
|
local project_dir="${1:?project_dir required}"
|
|
314
|
+
local scope="${2:-}"
|
|
312
315
|
local project_name
|
|
313
316
|
project_name=$(basename "${project_dir}")
|
|
314
317
|
|
|
318
|
+
local match_token="${project_name}-nw"
|
|
319
|
+
if [ -n "${scope}" ]; then
|
|
320
|
+
match_token="${scope}"
|
|
321
|
+
fi
|
|
322
|
+
|
|
315
323
|
git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
|
|
316
324
|
| grep '^worktree ' \
|
|
317
325
|
| awk '{print $2}' \
|
|
318
|
-
| grep "${
|
|
326
|
+
| grep -F "${match_token}" \
|
|
319
327
|
| while read -r wt; do
|
|
320
328
|
log "CLEANUP: Removing leftover worktree ${wt}"
|
|
321
329
|
git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
|
|
@@ -16,7 +16,7 @@ set -euo pipefail
|
|
|
16
16
|
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
17
17
|
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
18
18
|
LOG_DIR="${PROJECT_DIR}/logs"
|
|
19
|
-
LOG_FILE="${LOG_DIR}/
|
|
19
|
+
LOG_FILE="${LOG_DIR}/reviewer.log"
|
|
20
20
|
MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
|
|
21
21
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
22
22
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
@@ -25,6 +25,8 @@ BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
|
25
25
|
AUTO_MERGE="${NW_AUTO_MERGE:-0}"
|
|
26
26
|
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
27
27
|
TARGET_PR="${NW_TARGET_PR:-}"
|
|
28
|
+
PARALLEL_ENABLED="${NW_REVIEWER_PARALLEL:-1}"
|
|
29
|
+
WORKER_MODE="${NW_REVIEWER_WORKER_MODE:-0}"
|
|
28
30
|
|
|
29
31
|
# Ensure NVM / Node / Claude are on PATH
|
|
30
32
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -39,8 +41,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
39
41
|
# shellcheck source=night-watch-helpers.sh
|
|
40
42
|
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
41
43
|
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
GLOBAL_LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}.lock"
|
|
45
|
+
if [ "${WORKER_MODE}" = "1" ] && [ -n "${TARGET_PR}" ]; then
|
|
46
|
+
LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}-pr-${TARGET_PR}.lock"
|
|
47
|
+
else
|
|
48
|
+
# NOTE: Lock file path must match reviewerLockPath() in src/utils/status-data.ts
|
|
49
|
+
LOCK_FILE="${GLOBAL_LOCK_FILE}"
|
|
50
|
+
fi
|
|
44
51
|
|
|
45
52
|
emit_result() {
|
|
46
53
|
local status="${1:?status required}"
|
|
@@ -52,6 +59,38 @@ emit_result() {
|
|
|
52
59
|
fi
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
emit_final_status() {
|
|
63
|
+
local exit_code="${1:?exit code required}"
|
|
64
|
+
local prs_csv="${2:-}"
|
|
65
|
+
local auto_merged="${3:-}"
|
|
66
|
+
local auto_merge_failed="${4:-}"
|
|
67
|
+
|
|
68
|
+
if [ "${exit_code}" -eq 0 ]; then
|
|
69
|
+
log "DONE: PR reviewer completed successfully"
|
|
70
|
+
emit_result "success_reviewed" "prs=${prs_csv}|auto_merged=${auto_merged}|auto_merge_failed=${auto_merge_failed}"
|
|
71
|
+
elif [ "${exit_code}" -eq 124 ]; then
|
|
72
|
+
log "TIMEOUT: PR reviewer killed after ${MAX_RUNTIME}s"
|
|
73
|
+
emit_result "timeout" "prs=${prs_csv}"
|
|
74
|
+
else
|
|
75
|
+
log "FAIL: PR reviewer exited with code ${exit_code}"
|
|
76
|
+
emit_result "failure" "prs=${prs_csv}"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
append_csv() {
|
|
81
|
+
local current="${1:-}"
|
|
82
|
+
local incoming="${2:-}"
|
|
83
|
+
if [ -z "${incoming}" ]; then
|
|
84
|
+
printf "%s" "${current}"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
if [ -z "${current}" ]; then
|
|
88
|
+
printf "%s" "${incoming}"
|
|
89
|
+
else
|
|
90
|
+
printf "%s,%s" "${current}" "${incoming}"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
# Validate provider
|
|
56
95
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
57
96
|
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
@@ -159,6 +198,62 @@ done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number,
|
|
|
159
198
|
|
|
160
199
|
if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
161
200
|
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE} (or no score yet)"
|
|
201
|
+
|
|
202
|
+
# ── Auto-merge eligible PRs ───────────────────────────────
|
|
203
|
+
if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
|
|
204
|
+
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
205
|
+
AUTO_MERGED_COUNT=0
|
|
206
|
+
|
|
207
|
+
log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
|
|
208
|
+
|
|
209
|
+
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
210
|
+
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
211
|
+
printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
|
|
212
|
+
|
|
213
|
+
# Check CI status
|
|
214
|
+
FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
|
|
215
|
+
[ "${FAILED_CHECKS}" -gt 0 ] && continue
|
|
216
|
+
|
|
217
|
+
# Check review score
|
|
218
|
+
PR_COMMENTS=$(
|
|
219
|
+
{
|
|
220
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
221
|
+
if [ -n "${REPO}" ]; then
|
|
222
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
223
|
+
fi
|
|
224
|
+
} | sort -u
|
|
225
|
+
)
|
|
226
|
+
PR_SCORE=$(echo "${PR_COMMENTS}" \
|
|
227
|
+
| grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
|
|
228
|
+
| tail -1 \
|
|
229
|
+
| grep -oP '\d+(?=/100)' || echo "")
|
|
230
|
+
|
|
231
|
+
# Skip PRs without a score or with score below threshold
|
|
232
|
+
[ -z "${PR_SCORE}" ] && continue
|
|
233
|
+
[ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
|
|
234
|
+
|
|
235
|
+
# PR is merge-ready
|
|
236
|
+
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
|
|
237
|
+
|
|
238
|
+
# Dry-run mode: show what would be merged
|
|
239
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
240
|
+
log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
|
|
241
|
+
continue
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
245
|
+
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
246
|
+
AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
|
|
247
|
+
else
|
|
248
|
+
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
249
|
+
fi
|
|
250
|
+
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
251
|
+
|
|
252
|
+
if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
|
|
253
|
+
log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
162
257
|
emit_result "skip_all_passing"
|
|
163
258
|
exit 0
|
|
164
259
|
fi
|
|
@@ -172,11 +267,121 @@ if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
|
172
267
|
else
|
|
173
268
|
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
174
269
|
fi
|
|
175
|
-
REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-review-runner"
|
|
176
270
|
|
|
177
271
|
log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
|
|
178
272
|
|
|
179
|
-
|
|
273
|
+
# Convert "#12 #34" into ["12", "34"] for worker fan-out.
|
|
274
|
+
PR_NUMBER_ARRAY=()
|
|
275
|
+
for pr_token in ${PRS_NEEDING_WORK}; do
|
|
276
|
+
PR_NUMBER_ARRAY+=("${pr_token#\#}")
|
|
277
|
+
done
|
|
278
|
+
|
|
279
|
+
if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED}" = "1" ] && [ "${#PR_NUMBER_ARRAY[@]}" -gt 1 ]; then
|
|
280
|
+
# Dry-run mode: print diagnostics and exit
|
|
281
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
282
|
+
echo "=== Dry Run: PR Reviewer ==="
|
|
283
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
284
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
285
|
+
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
286
|
+
echo "Auto-merge: ${AUTO_MERGE}"
|
|
287
|
+
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
288
|
+
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
289
|
+
fi
|
|
290
|
+
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
291
|
+
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
292
|
+
echo "Parallel Workers: ${#PR_NUMBER_ARRAY[@]}"
|
|
293
|
+
echo "Timeout: ${MAX_RUNTIME}s"
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
log "PARALLEL: Launching ${#PR_NUMBER_ARRAY[@]} reviewer worker(s)"
|
|
298
|
+
|
|
299
|
+
declare -a WORKER_PIDS=()
|
|
300
|
+
declare -a WORKER_PRS=()
|
|
301
|
+
declare -a WORKER_OUTPUTS=()
|
|
302
|
+
|
|
303
|
+
for pr_number in "${PR_NUMBER_ARRAY[@]}"; do
|
|
304
|
+
worker_output=$(mktemp "/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}-pr-${pr_number}.XXXXXX")
|
|
305
|
+
WORKER_OUTPUTS+=("${worker_output}")
|
|
306
|
+
WORKER_PRS+=("${pr_number}")
|
|
307
|
+
|
|
308
|
+
(
|
|
309
|
+
NW_TARGET_PR="${pr_number}" \
|
|
310
|
+
NW_REVIEWER_WORKER_MODE="1" \
|
|
311
|
+
NW_REVIEWER_PARALLEL="0" \
|
|
312
|
+
bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}" > "${worker_output}" 2>&1
|
|
313
|
+
) &
|
|
314
|
+
|
|
315
|
+
worker_pid=$!
|
|
316
|
+
WORKER_PIDS+=("${worker_pid}")
|
|
317
|
+
log "PARALLEL: Worker PID ${worker_pid} started for PR #${pr_number}"
|
|
318
|
+
done
|
|
319
|
+
|
|
320
|
+
EXIT_CODE=0
|
|
321
|
+
AUTO_MERGED_PRS=""
|
|
322
|
+
AUTO_MERGE_FAILED_PRS=""
|
|
323
|
+
|
|
324
|
+
for idx in "${!WORKER_PIDS[@]}"; do
|
|
325
|
+
worker_pid="${WORKER_PIDS[$idx]}"
|
|
326
|
+
worker_pr="${WORKER_PRS[$idx]}"
|
|
327
|
+
worker_output="${WORKER_OUTPUTS[$idx]}"
|
|
328
|
+
|
|
329
|
+
worker_exit_code=0
|
|
330
|
+
if wait "${worker_pid}"; then
|
|
331
|
+
worker_exit_code=0
|
|
332
|
+
else
|
|
333
|
+
worker_exit_code=$?
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
if [ -f "${worker_output}" ] && [ -s "${worker_output}" ]; then
|
|
337
|
+
cat "${worker_output}" >> "${LOG_FILE}"
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
worker_result=$(grep -o 'NIGHT_WATCH_RESULT:.*' "${worker_output}" 2>/dev/null | tail -1 || true)
|
|
341
|
+
worker_status=$(printf '%s' "${worker_result}" | sed -n 's/^NIGHT_WATCH_RESULT:\([^|]*\).*$/\1/p')
|
|
342
|
+
worker_auto_merged=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merged=)[^|]+' || true)
|
|
343
|
+
worker_auto_merge_failed=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merge_failed=)[^|]+' || true)
|
|
344
|
+
|
|
345
|
+
AUTO_MERGED_PRS=$(append_csv "${AUTO_MERGED_PRS}" "${worker_auto_merged}")
|
|
346
|
+
AUTO_MERGE_FAILED_PRS=$(append_csv "${AUTO_MERGE_FAILED_PRS}" "${worker_auto_merge_failed}")
|
|
347
|
+
|
|
348
|
+
rm -f "${worker_output}"
|
|
349
|
+
|
|
350
|
+
if [ "${worker_status}" = "failure" ] || { [ -n "${worker_status}" ] && [ "${worker_status}" != "success_reviewed" ] && [ "${worker_status}" != "timeout" ] && [ "${worker_status#skip_}" = "${worker_status}" ]; }; then
|
|
351
|
+
if [ "${EXIT_CODE}" -eq 0 ] || [ "${EXIT_CODE}" -eq 124 ]; then
|
|
352
|
+
EXIT_CODE=1
|
|
353
|
+
fi
|
|
354
|
+
log "PARALLEL: Worker for PR #${worker_pr} reported status '${worker_status:-unknown}'"
|
|
355
|
+
elif [ "${worker_status}" = "timeout" ]; then
|
|
356
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
357
|
+
EXIT_CODE=124
|
|
358
|
+
fi
|
|
359
|
+
log "PARALLEL: Worker for PR #${worker_pr} timed out"
|
|
360
|
+
elif [ "${worker_exit_code}" -ne 0 ]; then
|
|
361
|
+
if [ "${worker_exit_code}" -eq 124 ]; then
|
|
362
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
363
|
+
EXIT_CODE=124
|
|
364
|
+
fi
|
|
365
|
+
elif [ "${EXIT_CODE}" -eq 0 ] || [ "${EXIT_CODE}" -eq 124 ]; then
|
|
366
|
+
EXIT_CODE="${worker_exit_code}"
|
|
367
|
+
fi
|
|
368
|
+
log "PARALLEL: Worker for PR #${worker_pr} exited with code ${worker_exit_code}"
|
|
369
|
+
else
|
|
370
|
+
log "PARALLEL: Worker for PR #${worker_pr} completed"
|
|
371
|
+
fi
|
|
372
|
+
done
|
|
373
|
+
|
|
374
|
+
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
|
|
375
|
+
exit 0
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
REVIEW_WORKTREE_BASENAME="${PROJECT_NAME}-nw-review-runner"
|
|
379
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
380
|
+
REVIEW_WORKTREE_BASENAME="${REVIEW_WORKTREE_BASENAME}-pr-${TARGET_PR}"
|
|
381
|
+
fi
|
|
382
|
+
REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${REVIEW_WORKTREE_BASENAME}"
|
|
383
|
+
|
|
384
|
+
cleanup_worktrees "${PROJECT_DIR}" "${REVIEW_WORKTREE_BASENAME}"
|
|
180
385
|
|
|
181
386
|
# Dry-run mode: print diagnostics and exit
|
|
182
387
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
@@ -191,6 +396,10 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
191
396
|
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
192
397
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
193
398
|
echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
|
|
399
|
+
echo "Target PR: ${TARGET_PR:-all}"
|
|
400
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
401
|
+
echo "Worker Mode: ${WORKER_MODE}"
|
|
402
|
+
fi
|
|
194
403
|
echo "Timeout: ${MAX_RUNTIME}s"
|
|
195
404
|
exit 0
|
|
196
405
|
fi
|
|
@@ -201,12 +410,17 @@ if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFA
|
|
|
201
410
|
fi
|
|
202
411
|
|
|
203
412
|
EXIT_CODE=0
|
|
413
|
+
TARGET_SCOPE_PROMPT=""
|
|
414
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
415
|
+
TARGET_SCOPE_PROMPT=$'\n\n## Target Scope\n- Only process PR #'"${TARGET_PR}"$'.\n- Ignore all other PRs.\n- If this PR no longer needs work, stop immediately.\n'
|
|
416
|
+
fi
|
|
204
417
|
|
|
205
418
|
case "${PROVIDER_CMD}" in
|
|
206
419
|
claude)
|
|
420
|
+
CLAUDE_PROMPT="/night-watch-pr-reviewer${TARGET_SCOPE_PROMPT}"
|
|
207
421
|
if (
|
|
208
422
|
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
209
|
-
claude -p "
|
|
423
|
+
claude -p "${CLAUDE_PROMPT}" \
|
|
210
424
|
--dangerously-skip-permissions \
|
|
211
425
|
>> "${LOG_FILE}" 2>&1
|
|
212
426
|
); then
|
|
@@ -216,11 +430,12 @@ case "${PROVIDER_CMD}" in
|
|
|
216
430
|
fi
|
|
217
431
|
;;
|
|
218
432
|
codex)
|
|
433
|
+
CODEX_PROMPT="$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")${TARGET_SCOPE_PROMPT}"
|
|
219
434
|
if (
|
|
220
435
|
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
221
436
|
codex --quiet \
|
|
222
437
|
--yolo \
|
|
223
|
-
--prompt "$
|
|
438
|
+
--prompt "${CODEX_PROMPT}" \
|
|
224
439
|
>> "${LOG_FILE}" 2>&1
|
|
225
440
|
); then
|
|
226
441
|
EXIT_CODE=0
|
|
@@ -234,7 +449,7 @@ case "${PROVIDER_CMD}" in
|
|
|
234
449
|
;;
|
|
235
450
|
esac
|
|
236
451
|
|
|
237
|
-
cleanup_worktrees "${PROJECT_DIR}"
|
|
452
|
+
cleanup_worktrees "${PROJECT_DIR}" "${REVIEW_WORKTREE_BASENAME}"
|
|
238
453
|
|
|
239
454
|
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
240
455
|
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
@@ -310,13 +525,4 @@ if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
|
|
|
310
525
|
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
311
526
|
fi
|
|
312
527
|
|
|
313
|
-
|
|
314
|
-
log "DONE: PR reviewer completed successfully"
|
|
315
|
-
emit_result "success_reviewed" "prs=${PRS_NEEDING_WORK_CSV}|auto_merged=${AUTO_MERGED_PRS}|auto_merge_failed=${AUTO_MERGE_FAILED_PRS}"
|
|
316
|
-
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
317
|
-
log "TIMEOUT: PR reviewer killed after ${MAX_RUNTIME}s"
|
|
318
|
-
emit_result "timeout" "prs=${PRS_NEEDING_WORK_CSV}"
|
|
319
|
-
else
|
|
320
|
-
log "FAIL: PR reviewer exited with code ${EXIT_CODE}"
|
|
321
|
-
emit_result "failure" "prs=${PRS_NEEDING_WORK_CSV}"
|
|
322
|
-
fi
|
|
528
|
+
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
|