@jonit-dev/night-watch-cli 1.7.35 → 1.7.36
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 +4 -0
- package/dist/scripts/night-watch-audit-cron.sh +168 -0
- package/dist/scripts/night-watch-cron.sh +484 -0
- package/dist/scripts/night-watch-helpers.sh +515 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +528 -0
- package/dist/scripts/night-watch-qa-cron.sh +281 -0
- package/dist/scripts/night-watch-slicer-cron.sh +90 -0
- package/dist/scripts/test-helpers.bats +77 -0
- package/dist/web/assets/index-BiJf9LFT.js +458 -0
- package/dist/web/assets/index-OpSgvsYu.css +1 -0
- package/dist/web/avatars/carlos.webp +0 -0
- package/dist/web/avatars/dev.webp +0 -0
- package/dist/web/avatars/maya.webp +0 -0
- package/dist/web/avatars/priya.webp +0 -0
- package/dist/web/index.html +82 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,10 @@ import "reflect-metadata";
|
|
|
14
14
|
import "reflect-metadata";
|
|
15
15
|
import "reflect-metadata";
|
|
16
16
|
import "reflect-metadata";
|
|
17
|
+
import "reflect-metadata";
|
|
18
|
+
import "reflect-metadata";
|
|
19
|
+
import "reflect-metadata";
|
|
20
|
+
import "reflect-metadata";
|
|
17
21
|
import * as fs from "fs";
|
|
18
22
|
import * as path from "path";
|
|
19
23
|
import { fileURLToPath } from "url";
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
AUDIT_PROMPT_TEMPLATE="${SCRIPT_DIR}/../templates/night-watch-audit.md"
|
|
36
|
+
|
|
37
|
+
emit_result() {
|
|
38
|
+
local status="${1:?status required}"
|
|
39
|
+
local details="${2:-}"
|
|
40
|
+
if [ -n "${details}" ]; then
|
|
41
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
42
|
+
else
|
|
43
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Validate provider
|
|
48
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
49
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
50
|
+
emit_result "failure" "reason=unknown_provider"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
rotate_log
|
|
55
|
+
|
|
56
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
57
|
+
emit_result "skip_locked"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Dry-run mode: print diagnostics and exit
|
|
62
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
63
|
+
echo "=== Dry Run: Code Auditor ==="
|
|
64
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
65
|
+
echo "Max Runtime: ${MAX_RUNTIME}s"
|
|
66
|
+
echo "Report File: ${REPORT_FILE}"
|
|
67
|
+
echo "Prompt Template: ${AUDIT_PROMPT_TEMPLATE}"
|
|
68
|
+
emit_result "skip_dry_run"
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ ! -f "${AUDIT_PROMPT_TEMPLATE}" ]; then
|
|
73
|
+
log "FAIL: Missing bundled audit prompt template at ${AUDIT_PROMPT_TEMPLATE}"
|
|
74
|
+
emit_result "failure_missing_prompt"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
AUDIT_PROMPT="$(cat "${AUDIT_PROMPT_TEMPLATE}")"
|
|
79
|
+
|
|
80
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
81
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
82
|
+
else
|
|
83
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
AUDIT_WORKTREE_BASENAME="${PROJECT_NAME}-nw-audit-runner"
|
|
87
|
+
AUDIT_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${AUDIT_WORKTREE_BASENAME}"
|
|
88
|
+
|
|
89
|
+
cleanup_worktrees "${PROJECT_DIR}" "${AUDIT_WORKTREE_BASENAME}"
|
|
90
|
+
|
|
91
|
+
if ! prepare_detached_worktree "${PROJECT_DIR}" "${AUDIT_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
92
|
+
log "FAIL: Unable to create isolated audit worktree ${AUDIT_WORKTREE_DIR}"
|
|
93
|
+
emit_result "failure" "reason=worktree_setup_failed"
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Ensure the logs dir exists inside the worktree so the provider can write the report
|
|
98
|
+
mkdir -p "${AUDIT_WORKTREE_DIR}/logs"
|
|
99
|
+
|
|
100
|
+
log "START: Running code audit for ${PROJECT_NAME} (provider: ${PROVIDER_CMD})"
|
|
101
|
+
|
|
102
|
+
EXIT_CODE=0
|
|
103
|
+
|
|
104
|
+
case "${PROVIDER_CMD}" in
|
|
105
|
+
claude)
|
|
106
|
+
if (
|
|
107
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
108
|
+
claude -p "${AUDIT_PROMPT}" \
|
|
109
|
+
--dangerously-skip-permissions \
|
|
110
|
+
>> "${LOG_FILE}" 2>&1
|
|
111
|
+
); then
|
|
112
|
+
EXIT_CODE=0
|
|
113
|
+
else
|
|
114
|
+
EXIT_CODE=$?
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
codex)
|
|
118
|
+
if (
|
|
119
|
+
cd "${AUDIT_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
120
|
+
codex --quiet \
|
|
121
|
+
--yolo \
|
|
122
|
+
--prompt "${AUDIT_PROMPT}" \
|
|
123
|
+
>> "${LOG_FILE}" 2>&1
|
|
124
|
+
); then
|
|
125
|
+
EXIT_CODE=0
|
|
126
|
+
else
|
|
127
|
+
EXIT_CODE=$?
|
|
128
|
+
fi
|
|
129
|
+
;;
|
|
130
|
+
*)
|
|
131
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
132
|
+
emit_result "failure" "reason=unknown_provider"
|
|
133
|
+
exit 1
|
|
134
|
+
;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
# Copy report back to project dir (if it was written in the worktree)
|
|
138
|
+
WORKTREE_REPORT="${AUDIT_WORKTREE_DIR}/logs/audit-report.md"
|
|
139
|
+
if [ -f "${WORKTREE_REPORT}" ]; then
|
|
140
|
+
cp "${WORKTREE_REPORT}" "${REPORT_FILE}"
|
|
141
|
+
log "INFO: Audit report copied to ${REPORT_FILE}"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
cleanup_worktrees "${PROJECT_DIR}" "${AUDIT_WORKTREE_BASENAME}"
|
|
145
|
+
|
|
146
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
147
|
+
if [ ! -f "${REPORT_FILE}" ]; then
|
|
148
|
+
log "FAIL: Audit provider exited 0 but no report was generated at ${REPORT_FILE}"
|
|
149
|
+
emit_result "failure_no_report"
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if grep -q "NO_ISSUES_FOUND" "${REPORT_FILE}" 2>/dev/null; then
|
|
154
|
+
log "DONE: Audit complete — no actionable issues found"
|
|
155
|
+
emit_result "skip_clean"
|
|
156
|
+
else
|
|
157
|
+
log "DONE: Audit complete — report written to ${REPORT_FILE}"
|
|
158
|
+
emit_result "success_audit"
|
|
159
|
+
fi
|
|
160
|
+
elif [ "${EXIT_CODE}" -eq 124 ]; then
|
|
161
|
+
log "TIMEOUT: Audit killed after ${MAX_RUNTIME}s"
|
|
162
|
+
emit_result "timeout"
|
|
163
|
+
else
|
|
164
|
+
log "FAIL: Audit exited with code ${EXIT_CODE}"
|
|
165
|
+
emit_result "failure" "provider_exit=${EXIT_CODE}"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
exit "${EXIT_CODE}"
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-cron.sh /path/to/project
|
|
6
|
+
# Finds the next eligible PRD and passes it to the configured AI provider for implementation.
|
|
7
|
+
#
|
|
8
|
+
# NOTE: This script expects environment variables to be set by the caller.
|
|
9
|
+
# The Node.js CLI will inject config values via environment variables.
|
|
10
|
+
# Required env vars (with defaults shown):
|
|
11
|
+
# NW_MAX_RUNTIME=7200 - Maximum runtime in seconds (2 hours)
|
|
12
|
+
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
13
|
+
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
14
|
+
|
|
15
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
16
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
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
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
24
|
+
LOG_FILE="${LOG_DIR}/executor.log"
|
|
25
|
+
MAX_RUNTIME="${NW_MAX_RUNTIME:-7200}" # 2 hours
|
|
26
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
27
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
28
|
+
BRANCH_PREFIX="${NW_BRANCH_PREFIX:-night-watch}"
|
|
29
|
+
|
|
30
|
+
# Ensure NVM / Node / Claude are on PATH
|
|
31
|
+
export NVM_DIR="${HOME}/.nvm"
|
|
32
|
+
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
|
|
33
|
+
|
|
34
|
+
# NOTE: Environment variables should be set by the caller (Node.js CLI).
|
|
35
|
+
# The .env.night-watch sourcing has been removed - config is now injected via env vars.
|
|
36
|
+
|
|
37
|
+
mkdir -p "${LOG_DIR}"
|
|
38
|
+
|
|
39
|
+
# Load shared helpers
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
# shellcheck source=night-watch-helpers.sh
|
|
42
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
43
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
44
|
+
# NOTE: Lock file path must match executorLockPath() in src/utils/status-data.ts
|
|
45
|
+
LOCK_FILE="/tmp/night-watch-${PROJECT_RUNTIME_KEY}.lock"
|
|
46
|
+
|
|
47
|
+
emit_result() {
|
|
48
|
+
local status="${1:?status required}"
|
|
49
|
+
local details="${2:-}"
|
|
50
|
+
if [ "${RATE_LIMIT_FALLBACK_TRIGGERED:-0}" = "1" ]; then
|
|
51
|
+
if [ -n "${details}" ]; then
|
|
52
|
+
details="${details}|rate_limit_fallback=1"
|
|
53
|
+
else
|
|
54
|
+
details="rate_limit_fallback=1"
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
if [ -n "${details}" ]; then
|
|
58
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
59
|
+
else
|
|
60
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Validate provider
|
|
65
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
66
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
rotate_log
|
|
71
|
+
|
|
72
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
73
|
+
emit_result "skip_locked"
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
78
|
+
|
|
79
|
+
ISSUE_NUMBER="" # board mode: GitHub issue number
|
|
80
|
+
ISSUE_BODY="" # board mode: issue body (PRD content)
|
|
81
|
+
ISSUE_TITLE_RAW="" # board mode: issue title
|
|
82
|
+
NW_CLI="" # board mode: resolved night-watch CLI binary
|
|
83
|
+
|
|
84
|
+
restore_issue_to_ready() {
|
|
85
|
+
local reason="${1:-Execution failed before implementation started.}"
|
|
86
|
+
if [ -n "${ISSUE_NUMBER}" ] && [ -n "${NW_CLI}" ]; then
|
|
87
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
88
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" --body "${reason}" 2>>"${LOG_FILE}" || true
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
93
|
+
# Board mode: discover next task from GitHub Projects board
|
|
94
|
+
NW_CLI=$(resolve_night_watch_cli 2>/dev/null || true)
|
|
95
|
+
if [ -z "${NW_CLI}" ]; then
|
|
96
|
+
log "ERROR: Cannot resolve night-watch CLI for board mode"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
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"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ -z "${ISSUE_NUMBER}" ]; then
|
|
131
|
+
log "ERROR: Board mode: no issue number resolved"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
# Slugify title for branch naming
|
|
135
|
+
ELIGIBLE_PRD="${ISSUE_NUMBER}-$(printf '%s' "${ISSUE_TITLE_RAW}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-\|-$//g')"
|
|
136
|
+
log "BOARD: Processing issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}"
|
|
137
|
+
trap "rm -f '${LOCK_FILE}'" EXIT
|
|
138
|
+
else
|
|
139
|
+
# Filesystem mode: scan PRD directory
|
|
140
|
+
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
|
|
141
|
+
if [ -z "${ELIGIBLE_PRD}" ]; then
|
|
142
|
+
log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
|
|
143
|
+
emit_result "skip_no_eligible_prd"
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
146
|
+
# Claim the PRD to prevent other runs from selecting it
|
|
147
|
+
claim_prd "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
148
|
+
# Update EXIT trap to also release claim
|
|
149
|
+
trap "rm -f '${LOCK_FILE}'; release_claim '${PRD_DIR}' '${ELIGIBLE_PRD}'" EXIT
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
PRD_NAME="${ELIGIBLE_PRD%.md}"
|
|
153
|
+
BRANCH_NAME="${BRANCH_PREFIX}/${PRD_NAME}"
|
|
154
|
+
WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-${PRD_NAME}"
|
|
155
|
+
BOOKKEEP_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-bookkeeping"
|
|
156
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
157
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
158
|
+
else
|
|
159
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
160
|
+
fi
|
|
161
|
+
if [[ "${PRD_DIR_REL}" = /* ]]; then
|
|
162
|
+
BOOKKEEP_PRD_DIR="${PRD_DIR_REL}"
|
|
163
|
+
else
|
|
164
|
+
BOOKKEEP_PRD_DIR="${BOOKKEEP_WORKTREE_DIR}/${PRD_DIR_REL}"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
count_prs_for_branch() {
|
|
168
|
+
local pr_state="${1:?pr_state required}"
|
|
169
|
+
local branch_name="${2:?branch_name required}"
|
|
170
|
+
local count
|
|
171
|
+
count=$(
|
|
172
|
+
{ gh pr list --state "${pr_state}" --json headRefName --jq '.[].headRefName' 2>/dev/null || true; } \
|
|
173
|
+
| { grep -xF "${branch_name}" || true; } \
|
|
174
|
+
| wc -l \
|
|
175
|
+
| tr -d '[:space:]'
|
|
176
|
+
)
|
|
177
|
+
echo "${count:-0}"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
finalize_prd_done() {
|
|
181
|
+
local reason="${1:?reason required}"
|
|
182
|
+
|
|
183
|
+
release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
184
|
+
# NOTE: PRDs are moved to done/ immediately when a PR is opened (or already merged)
|
|
185
|
+
# rather than waiting for reviewer/merge loops.
|
|
186
|
+
if prepare_detached_worktree "${PROJECT_DIR}" "${BOOKKEEP_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
187
|
+
if mark_prd_done "${BOOKKEEP_PRD_DIR}" "${ELIGIBLE_PRD}"; then
|
|
188
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" success --exit-code 0 2>/dev/null || true
|
|
189
|
+
if [[ "${PRD_DIR_REL}" = /* ]]; then
|
|
190
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" add -A "${PRD_DIR_REL}" || true
|
|
191
|
+
else
|
|
192
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" add -A "${PRD_DIR_REL}/" || true
|
|
193
|
+
fi
|
|
194
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (${reason})
|
|
195
|
+
|
|
196
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
|
|
197
|
+
git -C "${BOOKKEEP_WORKTREE_DIR}" push origin "HEAD:${DEFAULT_BRANCH}" || true
|
|
198
|
+
log "DONE: ${ELIGIBLE_PRD} ${reason}, PRD moved to done/"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
log "WARN: Failed to move ${ELIGIBLE_PRD} to done/ in bookkeeping worktree"
|
|
202
|
+
return 1
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
log "WARN: Unable to prepare bookkeeping worktree for ${ELIGIBLE_PRD}"
|
|
206
|
+
return 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME} (worktree: ${WORKTREE_DIR})"
|
|
210
|
+
|
|
211
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
212
|
+
PROMPT="Implement the following PRD (GitHub issue #${ISSUE_NUMBER}: ${ISSUE_TITLE_RAW}):
|
|
213
|
+
|
|
214
|
+
${ISSUE_BODY}
|
|
215
|
+
|
|
216
|
+
## Setup
|
|
217
|
+
- You are already inside an isolated worktree at: ${WORKTREE_DIR}
|
|
218
|
+
- Current branch is already checked out: ${BRANCH_NAME}
|
|
219
|
+
- Do NOT run git checkout/switch in ${PROJECT_DIR}
|
|
220
|
+
- Do NOT create or remove worktrees; the cron script manages that
|
|
221
|
+
- Install dependencies if needed and implement in the current worktree only
|
|
222
|
+
|
|
223
|
+
## Implementation — PRD Executor Workflow
|
|
224
|
+
Read .claude/commands/prd-executor.md and follow its FULL execution pipeline:
|
|
225
|
+
1. Parse the PRD into phases and extract dependencies
|
|
226
|
+
2. Build a dependency graph to identify parallelism
|
|
227
|
+
3. Create a task list with one task per phase
|
|
228
|
+
4. Execute phases in parallel waves using agent swarms — launch ALL independent phases concurrently
|
|
229
|
+
5. Run the project's verify/test command between waves to catch issues early
|
|
230
|
+
6. After all phases complete, run final verification and fix any issues
|
|
231
|
+
Follow all CLAUDE.md conventions (if present).
|
|
232
|
+
|
|
233
|
+
## Finalize
|
|
234
|
+
- Commit all changes, push, and open a PR:
|
|
235
|
+
git push -u origin ${BRANCH_NAME}
|
|
236
|
+
gh pr create --title \"feat: <short title>\" --body \"Closes #${ISSUE_NUMBER}
|
|
237
|
+
|
|
238
|
+
<summary>\"
|
|
239
|
+
- Do NOT process any other issues — only issue #${ISSUE_NUMBER}"
|
|
240
|
+
else
|
|
241
|
+
PROMPT_PRD_PATH="${PRD_DIR_REL}/${ELIGIBLE_PRD}"
|
|
242
|
+
PROMPT="Implement the PRD at ${PROMPT_PRD_PATH}
|
|
243
|
+
|
|
244
|
+
## Setup
|
|
245
|
+
- You are already inside an isolated worktree at: ${WORKTREE_DIR}
|
|
246
|
+
- Current branch is already checked out: ${BRANCH_NAME}
|
|
247
|
+
- Do NOT run git checkout/switch in ${PROJECT_DIR}
|
|
248
|
+
- Do NOT create or remove worktrees; the cron script manages that
|
|
249
|
+
- Install dependencies if needed and implement in the current worktree only
|
|
250
|
+
|
|
251
|
+
## Implementation — PRD Executor Workflow
|
|
252
|
+
Read .claude/commands/prd-executor.md and follow its FULL execution pipeline:
|
|
253
|
+
1. Parse the PRD into phases and extract dependencies
|
|
254
|
+
2. Build a dependency graph to identify parallelism
|
|
255
|
+
3. Create a task list with one task per phase
|
|
256
|
+
4. Execute phases in parallel waves using agent swarms — launch ALL independent phases concurrently
|
|
257
|
+
5. Run the project's verify/test command between waves to catch issues early
|
|
258
|
+
6. After all phases complete, run final verification and fix any issues
|
|
259
|
+
Follow all CLAUDE.md conventions (if present).
|
|
260
|
+
|
|
261
|
+
## Finalize
|
|
262
|
+
- Commit all changes, push, and open a PR:
|
|
263
|
+
git push -u origin ${BRANCH_NAME}
|
|
264
|
+
gh pr create --title \"feat: <short title>\" --body \"<summary referencing PRD>\"
|
|
265
|
+
- Do NOT move the PRD to done/ — the cron script handles that
|
|
266
|
+
- Do NOT process any other PRDs — only ${ELIGIBLE_PRD}"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Dry-run mode: print diagnostics and exit
|
|
270
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
271
|
+
log "DRY-RUN: Would process ${ELIGIBLE_PRD}"
|
|
272
|
+
log "DRY-RUN: Provider: ${PROVIDER_CMD}"
|
|
273
|
+
log "DRY-RUN: Runtime: ${MAX_RUNTIME}s"
|
|
274
|
+
echo "=== Dry Run: PRD Executor ==="
|
|
275
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
276
|
+
echo "Eligible PRD: ${ELIGIBLE_PRD}"
|
|
277
|
+
echo "Branch: ${BRANCH_NAME}"
|
|
278
|
+
echo "Worktree: ${WORKTREE_DIR}"
|
|
279
|
+
echo "Bookkeeping: ${BOOKKEEP_WORKTREE_DIR}"
|
|
280
|
+
echo "Timeout: ${MAX_RUNTIME}s"
|
|
281
|
+
exit 0
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# If this PRD already has a merged PR for its branch, finalize it immediately.
|
|
285
|
+
MERGED_PR_COUNT=$(count_prs_for_branch merged "${BRANCH_NAME}")
|
|
286
|
+
if [ "${MERGED_PR_COUNT}" -gt 0 ]; then
|
|
287
|
+
log "INFO: Found merged PR for ${BRANCH_NAME}; skipping provider run"
|
|
288
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
289
|
+
# Board mode: move issue to Done
|
|
290
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Done" 2>>"${LOG_FILE}" || true
|
|
291
|
+
emit_result "success_already_merged" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
292
|
+
exit 0
|
|
293
|
+
elif finalize_prd_done "already merged on ${BRANCH_NAME}"; then
|
|
294
|
+
emit_result "success_already_merged" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
295
|
+
exit 0
|
|
296
|
+
fi
|
|
297
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
298
|
+
emit_result "failure_finalize" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
if ! prepare_branch_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "${BRANCH_NAME}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
303
|
+
log "FAIL: Unable to create isolated worktree ${WORKTREE_DIR} for ${BRANCH_NAME}"
|
|
304
|
+
restore_issue_to_ready "Failed to prepare worktree for branch ${BRANCH_NAME}. Moved back to Ready for retry."
|
|
305
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
306
|
+
exit 1
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Sandbox: prevent the agent from modifying crontab during execution
|
|
310
|
+
export NW_EXECUTION_CONTEXT=agent
|
|
311
|
+
|
|
312
|
+
MAX_RETRIES="${NW_MAX_RETRIES:-3}"
|
|
313
|
+
if ! [[ "${MAX_RETRIES}" =~ ^[0-9]+$ ]] || [ "${MAX_RETRIES}" -lt 1 ]; then
|
|
314
|
+
MAX_RETRIES=1
|
|
315
|
+
fi
|
|
316
|
+
BACKOFF_BASE=300 # 5 minutes in seconds
|
|
317
|
+
EXIT_CODE=0
|
|
318
|
+
ATTEMPT=0
|
|
319
|
+
RATE_LIMIT_FALLBACK_TRIGGERED=0
|
|
320
|
+
|
|
321
|
+
while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
322
|
+
EXIT_CODE=0
|
|
323
|
+
# Capture log position before this attempt so check_rate_limited only
|
|
324
|
+
# scans lines written by the current invocation (not leftover 429s from
|
|
325
|
+
# previous runs that would cause false-positive rate-limit retries).
|
|
326
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
327
|
+
|
|
328
|
+
case "${PROVIDER_CMD}" in
|
|
329
|
+
claude)
|
|
330
|
+
if (
|
|
331
|
+
cd "${WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
332
|
+
claude -p "${PROMPT}" \
|
|
333
|
+
--dangerously-skip-permissions \
|
|
334
|
+
>> "${LOG_FILE}" 2>&1
|
|
335
|
+
); then
|
|
336
|
+
EXIT_CODE=0
|
|
337
|
+
else
|
|
338
|
+
EXIT_CODE=$?
|
|
339
|
+
fi
|
|
340
|
+
;;
|
|
341
|
+
codex)
|
|
342
|
+
if (
|
|
343
|
+
cd "${WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
344
|
+
codex --quiet \
|
|
345
|
+
--yolo \
|
|
346
|
+
--prompt "${PROMPT}" \
|
|
347
|
+
>> "${LOG_FILE}" 2>&1
|
|
348
|
+
); then
|
|
349
|
+
EXIT_CODE=0
|
|
350
|
+
else
|
|
351
|
+
EXIT_CODE=$?
|
|
352
|
+
fi
|
|
353
|
+
;;
|
|
354
|
+
*)
|
|
355
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
356
|
+
exit 1
|
|
357
|
+
;;
|
|
358
|
+
esac
|
|
359
|
+
|
|
360
|
+
# Success or timeout — don't retry
|
|
361
|
+
if [ ${EXIT_CODE} -eq 0 ] || [ ${EXIT_CODE} -eq 124 ]; then
|
|
362
|
+
break
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Check if this was a rate limit (429) error (only in lines from this attempt)
|
|
366
|
+
if check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
|
|
367
|
+
# If fallback is enabled, skip proxy retries and switch to native Claude immediately
|
|
368
|
+
if [ "${NW_FALLBACK_ON_RATE_LIMIT:-}" = "true" ] && [ "${PROVIDER_CMD}" = "claude" ]; then
|
|
369
|
+
log "RATE-LIMITED: Proxy quota exhausted — triggering native Claude fallback"
|
|
370
|
+
RATE_LIMIT_FALLBACK_TRIGGERED=1
|
|
371
|
+
break
|
|
372
|
+
fi
|
|
373
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
374
|
+
if [ "${ATTEMPT}" -ge "${MAX_RETRIES}" ]; then
|
|
375
|
+
log "RATE-LIMITED: All ${MAX_RETRIES} attempts exhausted for ${ELIGIBLE_PRD}"
|
|
376
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" rate_limited --exit-code "${EXIT_CODE}" --attempt "${ATTEMPT}" 2>/dev/null || true
|
|
377
|
+
break
|
|
378
|
+
fi
|
|
379
|
+
BACKOFF=$(( BACKOFF_BASE * (1 << (ATTEMPT - 1)) ))
|
|
380
|
+
BACKOFF_MIN=$(( BACKOFF / 60 ))
|
|
381
|
+
log "RATE-LIMITED: Attempt ${ATTEMPT}/${MAX_RETRIES}, retrying in ${BACKOFF_MIN}m"
|
|
382
|
+
sleep "${BACKOFF}"
|
|
383
|
+
else
|
|
384
|
+
# Non-retryable failure
|
|
385
|
+
break
|
|
386
|
+
fi
|
|
387
|
+
done
|
|
388
|
+
|
|
389
|
+
# ── Native Claude fallback ────────────────────────────────────────────────────
|
|
390
|
+
# When the proxy returns 429 and fallbackOnRateLimit is enabled, re-run the
|
|
391
|
+
# same prompt with native Claude (OAuth), bypassing the proxy entirely.
|
|
392
|
+
if [ "${RATE_LIMIT_FALLBACK_TRIGGERED}" = "1" ]; then
|
|
393
|
+
FALLBACK_MODEL="${NW_CLAUDE_MODEL_ID:-claude-sonnet-4-6}"
|
|
394
|
+
log "RATE-LIMIT-FALLBACK: Running native Claude (${FALLBACK_MODEL})"
|
|
395
|
+
|
|
396
|
+
# Send immediate Telegram warning (fire-and-forget)
|
|
397
|
+
send_rate_limit_fallback_warning "${FALLBACK_MODEL}" "$(basename "${PROJECT_DIR}")"
|
|
398
|
+
|
|
399
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
cd "${WORKTREE_DIR}" && \
|
|
403
|
+
unset ANTHROPIC_BASE_URL ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN \
|
|
404
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL ANTHROPIC_DEFAULT_OPUS_MODEL && \
|
|
405
|
+
timeout "${MAX_RUNTIME}" \
|
|
406
|
+
claude -p "${PROMPT}" \
|
|
407
|
+
--dangerously-skip-permissions \
|
|
408
|
+
--model "${FALLBACK_MODEL}" \
|
|
409
|
+
>> "${LOG_FILE}" 2>&1
|
|
410
|
+
); then
|
|
411
|
+
EXIT_CODE=0
|
|
412
|
+
else
|
|
413
|
+
EXIT_CODE=$?
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
log "RATE-LIMIT-FALLBACK: Native Claude exited with code ${EXIT_CODE}"
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
420
|
+
OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
|
|
421
|
+
if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
|
|
422
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
423
|
+
# Board mode: move to Review and comment with PR URL
|
|
424
|
+
PR_URL=$(gh pr list --state open --json headRefName,url \
|
|
425
|
+
--jq ".[] | select(.headRefName == \"${BRANCH_NAME}\") | .url" 2>/dev/null || true)
|
|
426
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Review" 2>>"${LOG_FILE}" || true
|
|
427
|
+
if [ -n "${PR_URL}" ]; then
|
|
428
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" --body "PR opened: ${PR_URL}" 2>>"${LOG_FILE}" || true
|
|
429
|
+
fi
|
|
430
|
+
emit_result "success_open_pr" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
431
|
+
elif finalize_prd_done "implemented, PR opened on ${BRANCH_NAME}"; then
|
|
432
|
+
emit_result "success_open_pr" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
433
|
+
else
|
|
434
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
435
|
+
emit_result "failure_finalize" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
436
|
+
EXIT_CODE=1
|
|
437
|
+
fi
|
|
438
|
+
else
|
|
439
|
+
MERGED_PR_COUNT=$(count_prs_for_branch merged "${BRANCH_NAME}")
|
|
440
|
+
if [ "${MERGED_PR_COUNT}" -gt 0 ]; then
|
|
441
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
442
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Done" 2>>"${LOG_FILE}" || true
|
|
443
|
+
emit_result "success_already_merged" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
444
|
+
elif finalize_prd_done "already merged on ${BRANCH_NAME}"; then
|
|
445
|
+
emit_result "success_already_merged" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
446
|
+
else
|
|
447
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
448
|
+
emit_result "failure_finalize" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
449
|
+
EXIT_CODE=1
|
|
450
|
+
fi
|
|
451
|
+
else
|
|
452
|
+
log "WARN: ${PROVIDER_CMD} exited 0 but no open/merged PR found on ${BRANCH_NAME} — recording cooldown to avoid repeated stuck runs"
|
|
453
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
454
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
455
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
456
|
+
--body "Execution completed but no PR was found. Moved back to Ready for retry." 2>>"${LOG_FILE}" || true
|
|
457
|
+
fi
|
|
458
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
459
|
+
emit_result "failure_no_pr_after_success" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
460
|
+
EXIT_CODE=1
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
464
|
+
log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
465
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
466
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
467
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
468
|
+
--body "Execution timed out after ${MAX_RUNTIME}s. Moved back to Ready for retry." 2>>"${LOG_FILE}" || true
|
|
469
|
+
fi
|
|
470
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" timeout --exit-code 124 2>/dev/null || true
|
|
471
|
+
emit_result "timeout" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
472
|
+
else
|
|
473
|
+
log "FAIL: Night watch exited with code ${EXIT_CODE} while processing ${ELIGIBLE_PRD}"
|
|
474
|
+
if [ -n "${ISSUE_NUMBER}" ]; then
|
|
475
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
476
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" \
|
|
477
|
+
--body "Execution failed with exit code ${EXIT_CODE}. Moved back to Ready for retry." 2>>"${LOG_FILE}" || true
|
|
478
|
+
fi
|
|
479
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code "${EXIT_CODE}" 2>/dev/null || true
|
|
480
|
+
emit_result "failure" "prd=${ELIGIBLE_PRD}|branch=${BRANCH_NAME}"
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
484
|
+
exit "${EXIT_CODE}"
|