@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.
Files changed (42) hide show
  1. package/dist/cli.js +3 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/doctor.d.ts +1 -1
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +101 -58
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/history.d.ts +7 -0
  8. package/dist/commands/history.d.ts.map +1 -0
  9. package/dist/commands/history.js +56 -0
  10. package/dist/commands/history.js.map +1 -0
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +11 -73
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/install.d.ts +11 -0
  15. package/dist/commands/install.d.ts.map +1 -1
  16. package/dist/commands/install.js +47 -11
  17. package/dist/commands/install.js.map +1 -1
  18. package/dist/commands/review.d.ts.map +1 -1
  19. package/dist/commands/review.js +2 -0
  20. package/dist/commands/review.js.map +1 -1
  21. package/dist/commands/run.d.ts.map +1 -1
  22. package/dist/commands/run.js +11 -0
  23. package/dist/commands/run.js.map +1 -1
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +36 -1
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.d.ts +4 -0
  28. package/dist/constants.d.ts.map +1 -1
  29. package/dist/constants.js +7 -0
  30. package/dist/constants.js.map +1 -1
  31. package/dist/types.d.ts +4 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utils/crontab.d.ts.map +1 -1
  34. package/dist/utils/crontab.js +4 -0
  35. package/dist/utils/crontab.js.map +1 -1
  36. package/dist/utils/execution-history.d.ts +48 -0
  37. package/dist/utils/execution-history.d.ts.map +1 -0
  38. package/dist/utils/execution-history.js +183 -0
  39. package/dist/utils/execution-history.js.map +1 -0
  40. package/package.json +1 -1
  41. package/scripts/night-watch-cron.sh +65 -27
  42. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonit-dev/night-watch-cli",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Autonomous PRD execution using AI Provider CLIs + cron",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- case "${PROVIDER_CMD}" in
125
- claude)
126
- if timeout "${MAX_RUNTIME}" \
127
- claude -p "${PROMPT}" \
128
- --dangerously-skip-permissions \
129
- >> "${LOG_FILE}" 2>&1; then
130
- EXIT_CODE=0
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
- codex)
136
- if timeout "${MAX_RUNTIME}" \
137
- codex --quiet \
138
- --yolo \
139
- --prompt "${PROMPT}" \
140
- >> "${LOG_FILE}" 2>&1; then
141
- EXIT_CODE=0
142
- else
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
+ }