@jonit-dev/night-watch-cli 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +101 -58
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/history.d.ts +7 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +56 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -73
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +11 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +47 -11
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +2 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +11 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +36 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/crontab.d.ts.map +1 -1
- package/dist/utils/crontab.js +4 -0
- package/dist/utils/crontab.js.map +1 -1
- package/dist/utils/execution-history.d.ts +48 -0
- package/dist/utils/execution-history.d.ts.map +1 -0
- package/dist/utils/execution-history.js +183 -0
- package/dist/utils/execution-history.js.map +1 -0
- package/package.json +1 -1
- package/scripts/night-watch-cron.sh +65 -27
- package/scripts/night-watch-helpers.sh +54 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execution-history.d.ts","sourceRoot":"","sources":["../../src/utils/execution-history.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAAC;AAElF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAS5E;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAIvC;AAwFD;;GAEG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAE/C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAU,GAClB,IAAI,CAmCN;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,gBAAgB,GAAG,IAAI,CAQzB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GACrB,OAAO,CAYT"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution history ledger for Night Watch CLI
|
|
3
|
+
* Stores PRD execution records in ~/.night-watch/history.json
|
|
4
|
+
* Decoupled from PRD file paths — keyed by project directory + PRD filename.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { GLOBAL_CONFIG_DIR, HISTORY_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, } from "../constants.js";
|
|
10
|
+
const HISTORY_LOCK_SUFFIX = ".lock";
|
|
11
|
+
const HISTORY_LOCK_TIMEOUT_MS = 5000;
|
|
12
|
+
const HISTORY_LOCK_STALE_MS = 30000;
|
|
13
|
+
const HISTORY_LOCK_POLL_MS = 25;
|
|
14
|
+
const sleepState = new Int32Array(new SharedArrayBuffer(4));
|
|
15
|
+
/**
|
|
16
|
+
* Get the path to the history file
|
|
17
|
+
*/
|
|
18
|
+
export function getHistoryPath() {
|
|
19
|
+
const base = process.env.NIGHT_WATCH_HOME || path.join(os.homedir(), GLOBAL_CONFIG_DIR);
|
|
20
|
+
return path.join(base, HISTORY_FILE_NAME);
|
|
21
|
+
}
|
|
22
|
+
function sleepMs(ms) {
|
|
23
|
+
Atomics.wait(sleepState, 0, 0, ms);
|
|
24
|
+
}
|
|
25
|
+
function acquireHistoryLock(historyPath) {
|
|
26
|
+
const lockPath = `${historyPath}${HISTORY_LOCK_SUFFIX}`;
|
|
27
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
28
|
+
const deadline = Date.now() + HISTORY_LOCK_TIMEOUT_MS;
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.openSync(lockPath, "wx");
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const err = error;
|
|
35
|
+
if (err.code !== "EEXIST") {
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const lockStats = fs.statSync(lockPath);
|
|
40
|
+
if (Date.now() - lockStats.mtimeMs > HISTORY_LOCK_STALE_MS) {
|
|
41
|
+
fs.unlinkSync(lockPath);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Lock may have disappeared between checks; retry.
|
|
47
|
+
}
|
|
48
|
+
if (Date.now() >= deadline) {
|
|
49
|
+
throw new Error(`Timed out acquiring execution history lock: ${lockPath}`);
|
|
50
|
+
}
|
|
51
|
+
sleepMs(HISTORY_LOCK_POLL_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function releaseHistoryLock(lockFd, historyPath) {
|
|
56
|
+
const lockPath = `${historyPath}${HISTORY_LOCK_SUFFIX}`;
|
|
57
|
+
try {
|
|
58
|
+
fs.closeSync(lockFd);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore close errors; lock cleanup still attempted.
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
fs.unlinkSync(lockPath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore lock cleanup errors.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function loadHistoryFromPath(historyPath) {
|
|
71
|
+
if (!fs.existsSync(historyPath)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const content = fs.readFileSync(historyPath, "utf-8");
|
|
76
|
+
const parsed = JSON.parse(content);
|
|
77
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function saveHistoryAtomic(historyPath, history) {
|
|
87
|
+
const dir = path.dirname(historyPath);
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
const tmpPath = path.join(dir, `${HISTORY_FILE_NAME}.${process.pid}.${Date.now()}.tmp`);
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(tmpPath, JSON.stringify(history, null, 2) + "\n");
|
|
92
|
+
fs.renameSync(tmpPath, historyPath);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (fs.existsSync(tmpPath)) {
|
|
96
|
+
fs.rmSync(tmpPath, { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Load execution history from disk. Returns empty object if missing or invalid.
|
|
102
|
+
*/
|
|
103
|
+
export function loadHistory() {
|
|
104
|
+
return loadHistoryFromPath(getHistoryPath());
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Save execution history to disk.
|
|
108
|
+
*/
|
|
109
|
+
export function saveHistory(history) {
|
|
110
|
+
const historyPath = getHistoryPath();
|
|
111
|
+
const lockFd = acquireHistoryLock(historyPath);
|
|
112
|
+
try {
|
|
113
|
+
saveHistoryAtomic(historyPath, history);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
releaseHistoryLock(lockFd, historyPath);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Record a PRD execution result.
|
|
121
|
+
* Appends a record and trims to MAX_HISTORY_RECORDS_PER_PRD.
|
|
122
|
+
*/
|
|
123
|
+
export function recordExecution(projectDir, prdFile, outcome, exitCode, attempt = 1) {
|
|
124
|
+
const historyPath = getHistoryPath();
|
|
125
|
+
const lockFd = acquireHistoryLock(historyPath);
|
|
126
|
+
const resolved = path.resolve(projectDir);
|
|
127
|
+
try {
|
|
128
|
+
const history = loadHistoryFromPath(historyPath);
|
|
129
|
+
if (!history[resolved]) {
|
|
130
|
+
history[resolved] = {};
|
|
131
|
+
}
|
|
132
|
+
if (!history[resolved][prdFile]) {
|
|
133
|
+
history[resolved][prdFile] = { records: [] };
|
|
134
|
+
}
|
|
135
|
+
const record = {
|
|
136
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
137
|
+
outcome,
|
|
138
|
+
exitCode,
|
|
139
|
+
attempt,
|
|
140
|
+
};
|
|
141
|
+
history[resolved][prdFile].records.push(record);
|
|
142
|
+
// Trim to max records (keep most recent)
|
|
143
|
+
const records = history[resolved][prdFile].records;
|
|
144
|
+
if (records.length > MAX_HISTORY_RECORDS_PER_PRD) {
|
|
145
|
+
history[resolved][prdFile].records = records.slice(records.length - MAX_HISTORY_RECORDS_PER_PRD);
|
|
146
|
+
}
|
|
147
|
+
saveHistoryAtomic(historyPath, history);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
releaseHistoryLock(lockFd, historyPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the most recent execution record for a PRD.
|
|
155
|
+
* Returns null if no history exists.
|
|
156
|
+
*/
|
|
157
|
+
export function getLastExecution(projectDir, prdFile) {
|
|
158
|
+
const resolved = path.resolve(projectDir);
|
|
159
|
+
const history = loadHistory();
|
|
160
|
+
const prdHistory = history[resolved]?.[prdFile];
|
|
161
|
+
if (!prdHistory || prdHistory.records.length === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return prdHistory.records[prdHistory.records.length - 1];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if a PRD is in cooldown after a recent non-success execution.
|
|
168
|
+
* Returns true if the PRD should be skipped.
|
|
169
|
+
*/
|
|
170
|
+
export function isInCooldown(projectDir, prdFile, cooldownPeriod) {
|
|
171
|
+
const last = getLastExecution(projectDir, prdFile);
|
|
172
|
+
if (!last) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// Success records don't trigger cooldown
|
|
176
|
+
if (last.outcome === "success") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const now = Math.floor(Date.now() / 1000);
|
|
180
|
+
const age = now - last.timestamp;
|
|
181
|
+
return age < cooldownPeriod;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=execution-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execution-history.js","sourceRoot":"","sources":["../../src/utils/execution-history.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -55,7 +55,7 @@ fi
|
|
|
55
55
|
|
|
56
56
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
57
57
|
|
|
58
|
-
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}")
|
|
58
|
+
ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}" "${MAX_RUNTIME}" "${PROJECT_DIR}")
|
|
59
59
|
|
|
60
60
|
if [ -z "${ELIGIBLE_PRD}" ]; then
|
|
61
61
|
log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
|
|
@@ -119,41 +119,77 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
119
119
|
exit 0
|
|
120
120
|
fi
|
|
121
121
|
|
|
122
|
+
# Sandbox: prevent the agent from modifying crontab during execution
|
|
123
|
+
export NW_EXECUTION_CONTEXT=agent
|
|
124
|
+
|
|
125
|
+
MAX_RETRIES="${NW_MAX_RETRIES:-3}"
|
|
126
|
+
if ! [[ "${MAX_RETRIES}" =~ ^[0-9]+$ ]] || [ "${MAX_RETRIES}" -lt 1 ]; then
|
|
127
|
+
MAX_RETRIES=1
|
|
128
|
+
fi
|
|
129
|
+
BACKOFF_BASE=300 # 5 minutes in seconds
|
|
122
130
|
EXIT_CODE=0
|
|
131
|
+
ATTEMPT=0
|
|
132
|
+
|
|
133
|
+
while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
134
|
+
EXIT_CODE=0
|
|
135
|
+
|
|
136
|
+
case "${PROVIDER_CMD}" in
|
|
137
|
+
claude)
|
|
138
|
+
if timeout "${MAX_RUNTIME}" \
|
|
139
|
+
claude -p "${PROMPT}" \
|
|
140
|
+
--dangerously-skip-permissions \
|
|
141
|
+
>> "${LOG_FILE}" 2>&1; then
|
|
142
|
+
EXIT_CODE=0
|
|
143
|
+
else
|
|
144
|
+
EXIT_CODE=$?
|
|
145
|
+
fi
|
|
146
|
+
;;
|
|
147
|
+
codex)
|
|
148
|
+
if timeout "${MAX_RUNTIME}" \
|
|
149
|
+
codex --quiet \
|
|
150
|
+
--yolo \
|
|
151
|
+
--prompt "${PROMPT}" \
|
|
152
|
+
>> "${LOG_FILE}" 2>&1; then
|
|
153
|
+
EXIT_CODE=0
|
|
154
|
+
else
|
|
155
|
+
EXIT_CODE=$?
|
|
156
|
+
fi
|
|
157
|
+
;;
|
|
158
|
+
*)
|
|
159
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
160
|
+
exit 1
|
|
161
|
+
;;
|
|
162
|
+
esac
|
|
163
|
+
|
|
164
|
+
# Success or timeout — don't retry
|
|
165
|
+
if [ ${EXIT_CODE} -eq 0 ] || [ ${EXIT_CODE} -eq 124 ]; then
|
|
166
|
+
break
|
|
167
|
+
fi
|
|
123
168
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
else
|
|
132
|
-
EXIT_CODE=$?
|
|
169
|
+
# Check if this was a rate limit (429) error
|
|
170
|
+
if check_rate_limited "${LOG_FILE}"; then
|
|
171
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
172
|
+
if [ "${ATTEMPT}" -ge "${MAX_RETRIES}" ]; then
|
|
173
|
+
log "RATE-LIMITED: All ${MAX_RETRIES} attempts exhausted for ${ELIGIBLE_PRD}"
|
|
174
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" rate_limited --exit-code "${EXIT_CODE}" --attempt "${ATTEMPT}" 2>/dev/null || true
|
|
175
|
+
break
|
|
133
176
|
fi
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
EXIT_CODE=$?
|
|
144
|
-
fi
|
|
145
|
-
;;
|
|
146
|
-
*)
|
|
147
|
-
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
148
|
-
exit 1
|
|
149
|
-
;;
|
|
150
|
-
esac
|
|
177
|
+
BACKOFF=$(( BACKOFF_BASE * (1 << (ATTEMPT - 1)) ))
|
|
178
|
+
BACKOFF_MIN=$(( BACKOFF / 60 ))
|
|
179
|
+
log "RATE-LIMITED: Attempt ${ATTEMPT}/${MAX_RETRIES}, retrying in ${BACKOFF_MIN}m"
|
|
180
|
+
sleep "${BACKOFF}"
|
|
181
|
+
else
|
|
182
|
+
# Non-retryable failure
|
|
183
|
+
break
|
|
184
|
+
fi
|
|
185
|
+
done
|
|
151
186
|
|
|
152
187
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
153
188
|
PR_EXISTS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
|
|
154
189
|
if [ "${PR_EXISTS}" -gt 0 ]; then
|
|
155
190
|
release_claim "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
156
191
|
mark_prd_done "${PRD_DIR}" "${ELIGIBLE_PRD}"
|
|
192
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" success --exit-code 0 2>/dev/null || true
|
|
157
193
|
git -C "${PROJECT_DIR}" add -A docs/PRDs/night-watch/
|
|
158
194
|
git -C "${PROJECT_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
|
|
159
195
|
|
|
@@ -165,8 +201,10 @@ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
|
|
|
165
201
|
fi
|
|
166
202
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
167
203
|
log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
|
|
204
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" timeout --exit-code 124 2>/dev/null || true
|
|
168
205
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
169
206
|
else
|
|
170
207
|
log "FAIL: Night watch exited with code ${EXIT_CODE} while processing ${ELIGIBLE_PRD}"
|
|
208
|
+
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code "${EXIT_CODE}" 2>/dev/null || true
|
|
171
209
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
172
210
|
fi
|
|
@@ -19,6 +19,44 @@ validate_provider() {
|
|
|
19
19
|
esac
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
# Resolve a usable night-watch CLI binary for nested script calls.
|
|
23
|
+
# Resolution order:
|
|
24
|
+
# 1) NW_CLI_BIN from parent environment (absolute path set by installer/runtime)
|
|
25
|
+
# 2) `night-watch` found in PATH
|
|
26
|
+
# 3) bundled bin path next to scripts/ in this package checkout/install
|
|
27
|
+
resolve_night_watch_cli() {
|
|
28
|
+
if [ -n "${NW_CLI_BIN:-}" ] && [ -x "${NW_CLI_BIN}" ]; then
|
|
29
|
+
printf "%s" "${NW_CLI_BIN}"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if command -v night-watch >/dev/null 2>&1; then
|
|
34
|
+
printf "%s" "night-watch"
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
local script_dir
|
|
39
|
+
if [ -n "${SCRIPT_DIR:-}" ]; then
|
|
40
|
+
script_dir="${SCRIPT_DIR}"
|
|
41
|
+
else
|
|
42
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
local bundled_bin="${script_dir}/../bin/night-watch.mjs"
|
|
46
|
+
if [ -x "${bundled_bin}" ]; then
|
|
47
|
+
printf "%s" "${bundled_bin}"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
return 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
night_watch_history() {
|
|
55
|
+
local cli_bin
|
|
56
|
+
cli_bin=$(resolve_night_watch_cli) || return 127
|
|
57
|
+
"${cli_bin}" history "$@"
|
|
58
|
+
}
|
|
59
|
+
|
|
22
60
|
# ── Logging ──────────────────────────────────────────────────────────────────
|
|
23
61
|
|
|
24
62
|
log() {
|
|
@@ -167,6 +205,7 @@ is_claimed() {
|
|
|
167
205
|
find_eligible_prd() {
|
|
168
206
|
local prd_dir="${1:?prd_dir required}"
|
|
169
207
|
local max_runtime="${2:-7200}"
|
|
208
|
+
local project_dir="${3:-}"
|
|
170
209
|
local done_dir="${prd_dir}/done"
|
|
171
210
|
|
|
172
211
|
local prd_files
|
|
@@ -210,6 +249,12 @@ find_eligible_prd() {
|
|
|
210
249
|
continue
|
|
211
250
|
fi
|
|
212
251
|
|
|
252
|
+
# Skip if in cooldown after a recent failure (checked via execution history ledger)
|
|
253
|
+
if [ -n "${project_dir}" ] && night_watch_history check "${project_dir}" "${prd_file}" --cooldown "${max_runtime}" 2>/dev/null; then
|
|
254
|
+
log "SKIP-PRD: ${prd_file} — in cooldown after recent failure"
|
|
255
|
+
continue
|
|
256
|
+
fi
|
|
257
|
+
|
|
213
258
|
# Skip if a PR already exists for this PRD
|
|
214
259
|
if echo "${open_branches}" | grep -qF "${prd_name}"; then
|
|
215
260
|
log "SKIP-PRD: ${prd_file} — open PR already exists"
|
|
@@ -276,3 +321,12 @@ mark_prd_done() {
|
|
|
276
321
|
return 1
|
|
277
322
|
fi
|
|
278
323
|
}
|
|
324
|
+
|
|
325
|
+
# ── Rate limit detection ────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
# Check if the last N lines of the log contain a 429 rate limit error.
|
|
328
|
+
# Returns 0 if rate limited, 1 otherwise.
|
|
329
|
+
check_rate_limited() {
|
|
330
|
+
local log_file="${1:?log_file required}"
|
|
331
|
+
tail -20 "${log_file}" 2>/dev/null | grep -q "429"
|
|
332
|
+
}
|