@jonit-dev/night-watch-cli 1.0.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +509 -0
  3. package/bin/night-watch.mjs +2 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +35 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/init.d.ts +8 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.js +376 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/install.d.ts +15 -0
  13. package/dist/commands/install.d.ts.map +1 -0
  14. package/dist/commands/install.js +135 -0
  15. package/dist/commands/install.js.map +1 -0
  16. package/dist/commands/logs.d.ts +15 -0
  17. package/dist/commands/logs.d.ts.map +1 -0
  18. package/dist/commands/logs.js +104 -0
  19. package/dist/commands/logs.js.map +1 -0
  20. package/dist/commands/review.d.ts +26 -0
  21. package/dist/commands/review.d.ts.map +1 -0
  22. package/dist/commands/review.js +144 -0
  23. package/dist/commands/review.js.map +1 -0
  24. package/dist/commands/run.d.ts +26 -0
  25. package/dist/commands/run.d.ts.map +1 -0
  26. package/dist/commands/run.js +161 -0
  27. package/dist/commands/run.js.map +1 -0
  28. package/dist/commands/status.d.ts +14 -0
  29. package/dist/commands/status.d.ts.map +1 -0
  30. package/dist/commands/status.js +303 -0
  31. package/dist/commands/status.js.map +1 -0
  32. package/dist/commands/uninstall.d.ts +13 -0
  33. package/dist/commands/uninstall.d.ts.map +1 -0
  34. package/dist/commands/uninstall.js +97 -0
  35. package/dist/commands/uninstall.js.map +1 -0
  36. package/dist/config.d.ts +23 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +213 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/constants.d.ts +21 -0
  41. package/dist/constants.d.ts.map +1 -0
  42. package/dist/constants.js +33 -0
  43. package/dist/constants.js.map +1 -0
  44. package/dist/types.d.ts +35 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +5 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/utils/crontab.d.ts +50 -0
  49. package/dist/utils/crontab.d.ts.map +1 -0
  50. package/dist/utils/crontab.js +116 -0
  51. package/dist/utils/crontab.js.map +1 -0
  52. package/dist/utils/shell.d.ts +13 -0
  53. package/dist/utils/shell.d.ts.map +1 -0
  54. package/dist/utils/shell.js +44 -0
  55. package/dist/utils/shell.js.map +1 -0
  56. package/dist/utils/ui.d.ts +55 -0
  57. package/dist/utils/ui.d.ts.map +1 -0
  58. package/dist/utils/ui.js +121 -0
  59. package/dist/utils/ui.js.map +1 -0
  60. package/package.json +64 -0
  61. package/scripts/night-watch-cron.sh +148 -0
  62. package/scripts/night-watch-helpers.sh +155 -0
  63. package/scripts/night-watch-pr-reviewer-cron.sh +135 -0
  64. package/templates/night-watch-pr-reviewer.md +144 -0
  65. package/templates/night-watch.config.json +21 -0
  66. package/templates/night-watch.md +100 -0
  67. package/templates/prd-executor.md +235 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * UI utilities for Night Watch CLI
3
+ * Provides colored output, spinners, and table formatting
4
+ */
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import Table from "cli-table3";
8
+ /**
9
+ * Print a success message with green check prefix
10
+ */
11
+ export function success(msg) {
12
+ console.log(chalk.green("✔"), msg);
13
+ }
14
+ /**
15
+ * Print an error message with red cross prefix
16
+ */
17
+ export function error(msg) {
18
+ console.log(chalk.red("✖"), msg);
19
+ }
20
+ /**
21
+ * Print a warning message with yellow warning prefix
22
+ */
23
+ export function warn(msg) {
24
+ console.log(chalk.yellow("⚠"), msg);
25
+ }
26
+ /**
27
+ * Print an info message with cyan info prefix
28
+ */
29
+ export function info(msg) {
30
+ console.log(chalk.cyan("ℹ"), msg);
31
+ }
32
+ /**
33
+ * Print a bold section header with underline
34
+ */
35
+ export function header(title) {
36
+ const line = "─".repeat(Math.max(40, title.length + 4));
37
+ console.log();
38
+ console.log(chalk.bold(title));
39
+ console.log(chalk.dim(line));
40
+ }
41
+ /**
42
+ * Print dimmed text for secondary information
43
+ */
44
+ export function dim(msg) {
45
+ console.log(chalk.dim(msg));
46
+ }
47
+ /**
48
+ * Format and print a key-value pair with consistent alignment
49
+ */
50
+ export function label(key, value) {
51
+ const paddedKey = key.padEnd(18);
52
+ console.log(` ${chalk.dim(paddedKey)}${value}`);
53
+ }
54
+ /**
55
+ * Create an ora spinner instance
56
+ */
57
+ export function createSpinner(text) {
58
+ return ora({
59
+ text,
60
+ spinner: "dots",
61
+ });
62
+ }
63
+ /**
64
+ * Create a configured cli-table3 instance with sensible defaults
65
+ */
66
+ export function createTable(options) {
67
+ const defaultOptions = {
68
+ chars: {
69
+ top: "─",
70
+ "top-mid": "┬",
71
+ "top-left": "┌",
72
+ "top-right": "┐",
73
+ bottom: "─",
74
+ "bottom-mid": "┴",
75
+ "bottom-left": "└",
76
+ "bottom-right": "┘",
77
+ left: "│",
78
+ "left-mid": "├",
79
+ mid: "─",
80
+ "mid-mid": "┼",
81
+ right: "│",
82
+ "right-mid": "┤",
83
+ middle: "│",
84
+ },
85
+ style: {
86
+ "padding-left": 1,
87
+ "padding-right": 1,
88
+ head: ["cyan"],
89
+ border: ["dim"],
90
+ },
91
+ };
92
+ return new Table({ ...defaultOptions, ...options });
93
+ }
94
+ /**
95
+ * Format status indicator: green running or dim not running
96
+ */
97
+ export function formatRunningStatus(running, pid) {
98
+ if (running) {
99
+ return chalk.green(`● Running (PID: ${pid})`);
100
+ }
101
+ if (pid) {
102
+ return chalk.dim(`○ Stale lock (PID: ${pid})`);
103
+ }
104
+ return chalk.dim("○ Not running");
105
+ }
106
+ /**
107
+ * Format installed status: green installed or yellow not installed
108
+ */
109
+ export function formatInstalledStatus(installed) {
110
+ if (installed) {
111
+ return chalk.green("✔ Installed");
112
+ }
113
+ return chalk.yellow("⚠ Not installed");
114
+ }
115
+ /**
116
+ * Print a step message with step number
117
+ */
118
+ export function step(current, total, msg) {
119
+ console.log(chalk.dim(`[${current}/${total}]`), msg);
120
+ }
121
+ //# sourceMappingURL=ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/utils/ui.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@jonit-dev/night-watch-cli",
3
+ "version": "1.0.0",
4
+ "description": "Autonomous PRD execution using AI Provider CLIs + cron",
5
+ "type": "module",
6
+ "bin": {
7
+ "night-watch": "./bin/night-watch.mjs"
8
+ },
9
+ "main": "./dist/cli.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/cli.js",
13
+ "types": "./dist/cli.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "test": "vitest run",
19
+ "dev": "tsx src/cli.ts",
20
+ "prepublishOnly": "npm run build && npm test",
21
+ "publish:npm": "npm publish --access public"
22
+ },
23
+ "files": [
24
+ "dist/",
25
+ "bin/",
26
+ "scripts/",
27
+ "templates/"
28
+ ],
29
+ "keywords": [
30
+ "claude",
31
+ "codex",
32
+ "ai",
33
+ "prd",
34
+ "automation",
35
+ "cron",
36
+ "cli",
37
+ "night-watch"
38
+ ],
39
+ "author": "Joao Pio",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/joaopio/night-watch-cli.git"
44
+ },
45
+ "homepage": "https://github.com/joaopio/night-watch-cli#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/joaopio/night-watch-cli/issues"
48
+ },
49
+ "dependencies": {
50
+ "chalk": "^5.6.2",
51
+ "cli-table3": "^0.6.5",
52
+ "commander": "^12.0.0",
53
+ "ora": "^9.3.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.11.0",
57
+ "tsx": "^4.7.0",
58
+ "typescript": "^5.3.0",
59
+ "vitest": "^1.2.0"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.0.0"
63
+ }
64
+ }
@@ -0,0 +1,148 @@
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="${PROJECT_DIR}/docs/PRDs/night-watch"
18
+ LOG_DIR="${PROJECT_DIR}/logs"
19
+ LOG_FILE="${LOG_DIR}/night-watch.log"
20
+ LOCK_FILE="/tmp/night-watch-${PROJECT_NAME}.lock"
21
+ MAX_RUNTIME="${NW_MAX_RUNTIME:-7200}" # 2 hours
22
+ MAX_LOG_SIZE="524288" # 512 KB
23
+ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
24
+
25
+ # Ensure NVM / Node / Claude are on PATH
26
+ export NVM_DIR="${HOME}/.nvm"
27
+ [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
28
+
29
+ # NOTE: Environment variables should be set by the caller (Node.js CLI).
30
+ # The .env.night-watch sourcing has been removed - config is now injected via env vars.
31
+
32
+ mkdir -p "${LOG_DIR}"
33
+
34
+ # Load shared helpers
35
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ # shellcheck source=night-watch-helpers.sh
37
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
38
+
39
+ # Validate provider
40
+ if ! validate_provider "${PROVIDER_CMD}"; then
41
+ echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
42
+ exit 1
43
+ fi
44
+
45
+ rotate_log
46
+
47
+ if ! acquire_lock "${LOCK_FILE}"; then
48
+ exit 0
49
+ fi
50
+
51
+ cleanup_worktrees "${PROJECT_DIR}"
52
+
53
+ ELIGIBLE_PRD=$(find_eligible_prd "${PRD_DIR}")
54
+
55
+ if [ -z "${ELIGIBLE_PRD}" ]; then
56
+ log "SKIP: No eligible PRDs (all done, in-progress, or blocked)"
57
+ exit 0
58
+ fi
59
+
60
+ PRD_NAME="${ELIGIBLE_PRD%.md}"
61
+ BRANCH_NAME="night-watch/${PRD_NAME}"
62
+ DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
63
+
64
+ log "START: Processing ${ELIGIBLE_PRD} on branch ${BRANCH_NAME}"
65
+
66
+ cd "${PROJECT_DIR}"
67
+
68
+ PROMPT="Implement the PRD at docs/PRDs/night-watch/${ELIGIBLE_PRD}
69
+
70
+ ## Setup
71
+ - Branch name MUST be exactly: ${BRANCH_NAME}
72
+ - Create the branch from ${DEFAULT_BRANCH}: git checkout ${DEFAULT_BRANCH} && git pull origin ${DEFAULT_BRANCH} && git checkout -b ${BRANCH_NAME}
73
+ - Use a git worktree: git worktree add ../${PROJECT_NAME}-nw-${PRD_NAME} ${BRANCH_NAME}
74
+ - cd into the worktree, install dependencies
75
+
76
+ ## Implementation — PRD Executor Workflow
77
+ Read .claude/commands/prd-executor.md and follow its FULL execution pipeline:
78
+ 1. Parse the PRD into phases and extract dependencies
79
+ 2. Build a dependency graph to identify parallelism
80
+ 3. Create a task list with one task per phase
81
+ 4. Execute phases in parallel waves using agent swarms — launch ALL independent phases concurrently
82
+ 5. Run the project's verify/test command between waves to catch issues early
83
+ 6. After all phases complete, run final verification and fix any issues
84
+ Follow all CLAUDE.md conventions (if present).
85
+
86
+ ## Finalize
87
+ - Commit all changes, push, and open a PR:
88
+ git push -u origin ${BRANCH_NAME}
89
+ gh pr create --title \"feat: <short title>\" --body \"<summary referencing PRD>\"
90
+ - After PR is created, clean up: git worktree remove ../${PROJECT_NAME}-nw-${PRD_NAME}
91
+ - Do NOT move the PRD to done/ — the cron script handles that
92
+ - Do NOT process any other PRDs — only ${ELIGIBLE_PRD}"
93
+
94
+ # Dry-run mode: print diagnostics and exit
95
+ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
96
+ log "DRY-RUN: Would process ${ELIGIBLE_PRD}"
97
+ log "DRY-RUN: Provider: ${PROVIDER_CMD}"
98
+ log "DRY-RUN: Runtime: ${MAX_RUNTIME}s"
99
+ echo "=== Dry Run: PRD Executor ==="
100
+ echo "Provider: ${PROVIDER_CMD}"
101
+ echo "Eligible PRD: ${ELIGIBLE_PRD}"
102
+ echo "Branch: ${BRANCH_NAME}"
103
+ echo "Timeout: ${MAX_RUNTIME}s"
104
+ exit 0
105
+ fi
106
+
107
+ case "${PROVIDER_CMD}" in
108
+ claude)
109
+ timeout "${MAX_RUNTIME}" \
110
+ claude -p "${PROMPT}" \
111
+ --dangerously-skip-permissions \
112
+ >> "${LOG_FILE}" 2>&1
113
+ ;;
114
+ codex)
115
+ timeout "${MAX_RUNTIME}" \
116
+ codex --quiet \
117
+ --yolo \
118
+ --prompt "${PROMPT}" \
119
+ >> "${LOG_FILE}" 2>&1
120
+ ;;
121
+ *)
122
+ log "ERROR: Unknown provider: ${PROVIDER_CMD}"
123
+ exit 1
124
+ ;;
125
+ esac
126
+
127
+ EXIT_CODE=$?
128
+
129
+ if [ ${EXIT_CODE} -eq 0 ]; then
130
+ PR_EXISTS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -cF "${BRANCH_NAME}" || echo "0")
131
+ if [ "${PR_EXISTS}" -gt 0 ]; then
132
+ mark_prd_done "${PRD_DIR}" "${ELIGIBLE_PRD}"
133
+ git -C "${PROJECT_DIR}" add -A docs/PRDs/night-watch/
134
+ git -C "${PROJECT_DIR}" commit -m "chore: mark ${ELIGIBLE_PRD} as done (PR opened on ${BRANCH_NAME})
135
+
136
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>" || true
137
+ git -C "${PROJECT_DIR}" push origin "${DEFAULT_BRANCH}" || true
138
+ log "DONE: ${ELIGIBLE_PRD} implemented, PR opened, PRD moved to done/"
139
+ else
140
+ log "WARN: ${PROVIDER_CMD} exited 0 but no PR found on ${BRANCH_NAME} — PRD NOT moved to done"
141
+ fi
142
+ elif [ ${EXIT_CODE} -eq 124 ]; then
143
+ log "TIMEOUT: Night watch killed after ${MAX_RUNTIME}s while processing ${ELIGIBLE_PRD}"
144
+ cleanup_worktrees "${PROJECT_DIR}"
145
+ else
146
+ log "FAIL: Night watch exited with code ${EXIT_CODE} while processing ${ELIGIBLE_PRD}"
147
+ cleanup_worktrees "${PROJECT_DIR}"
148
+ fi
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # Night Watch helper functions — shared by cron scripts.
3
+ # Source this file, don't execute it directly.
4
+
5
+ # ── Provider validation ───────────────────────────────────────────────────────
6
+
7
+ # Validates that the provider command is supported.
8
+ # Returns 0 if valid, 1 if unknown.
9
+ # Supported providers: claude, codex
10
+ validate_provider() {
11
+ local provider="${1:?provider required}"
12
+ case "${provider}" in
13
+ claude|codex)
14
+ return 0
15
+ ;;
16
+ *)
17
+ return 1
18
+ ;;
19
+ esac
20
+ }
21
+
22
+ # ── Logging ──────────────────────────────────────────────────────────────────
23
+
24
+ log() {
25
+ local log_file="${LOG_FILE:?LOG_FILE not set}"
26
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "${log_file}"
27
+ }
28
+
29
+ # ── Log rotation ─────────────────────────────────────────────────────────────
30
+
31
+ rotate_log() {
32
+ local log_file="${LOG_FILE:?LOG_FILE not set}"
33
+ local max_size="${MAX_LOG_SIZE:-524288}"
34
+
35
+ if [ -f "${log_file}" ] && [ "$(stat -c%s "${log_file}" 2>/dev/null || echo 0)" -gt "${max_size}" ]; then
36
+ mv "${log_file}" "${log_file}.old"
37
+ fi
38
+ }
39
+
40
+ # ── Lock management ──────────────────────────────────────────────────────────
41
+
42
+ acquire_lock() {
43
+ local lock_file="${1:?lock_file required}"
44
+
45
+ if [ -f "${lock_file}" ]; then
46
+ local lock_pid
47
+ lock_pid=$(cat "${lock_file}" 2>/dev/null || echo "")
48
+ if [ -n "${lock_pid}" ] && kill -0 "${lock_pid}" 2>/dev/null; then
49
+ log "SKIP: Previous run (PID ${lock_pid}) still active"
50
+ return 1
51
+ fi
52
+ log "WARN: Stale lock file found (PID ${lock_pid}), removing"
53
+ rm -f "${lock_file}"
54
+ fi
55
+
56
+ trap "rm -f '${lock_file}'" EXIT
57
+ echo $$ > "${lock_file}"
58
+ return 0
59
+ }
60
+
61
+ # ── Detect default branch ───────────────────────────────────────────────────
62
+
63
+ detect_default_branch() {
64
+ local project_dir="${1:?project_dir required}"
65
+ git -C "${project_dir}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \
66
+ | sed 's@^refs/remotes/origin/@@' || echo "master"
67
+ }
68
+
69
+ # ── Find next eligible PRD ───────────────────────────────────────────────────
70
+
71
+ find_eligible_prd() {
72
+ local prd_dir="${1:?prd_dir required}"
73
+ local done_dir="${prd_dir}/done"
74
+
75
+ local prd_files
76
+ prd_files=$(find "${prd_dir}" -maxdepth 1 -name '*.md' ! -name 'NIGHT-WATCH-SUMMARY.md' -type f 2>/dev/null | sort)
77
+
78
+ if [ -z "${prd_files}" ]; then
79
+ return 0
80
+ fi
81
+
82
+ local open_branches
83
+ open_branches=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null || echo "")
84
+
85
+ for prd_path in ${prd_files}; do
86
+ local prd_file
87
+ prd_file=$(basename "${prd_path}")
88
+ local prd_name="${prd_file%.md}"
89
+
90
+ # Skip if a PR already exists for this PRD
91
+ if echo "${open_branches}" | grep -qF "${prd_name}"; then
92
+ log "SKIP-PRD: ${prd_file} — open PR already exists"
93
+ continue
94
+ fi
95
+
96
+ # Check dependencies
97
+ local depends_on
98
+ depends_on=$(grep -i 'depends on' "${prd_path}" 2>/dev/null \
99
+ | head -1 \
100
+ | grep -oP '[a-z0-9_-]+\.md' || echo "")
101
+ if [ -n "${depends_on}" ]; then
102
+ local dep_met=true
103
+ for dep_file in ${depends_on}; do
104
+ if [ ! -f "${done_dir}/${dep_file}" ]; then
105
+ log "SKIP-PRD: ${prd_file} — unmet dependency: ${dep_file}"
106
+ dep_met=false
107
+ break
108
+ fi
109
+ done
110
+ if [ "${dep_met}" = false ]; then
111
+ continue
112
+ fi
113
+ fi
114
+
115
+ echo "${prd_file}"
116
+ return 0
117
+ done
118
+ }
119
+
120
+ # ── Clean up worktrees ───────────────────────────────────────────────────────
121
+ # Removes any worktrees with "-nw-" in the path (night-watch worktrees).
122
+
123
+ cleanup_worktrees() {
124
+ local project_dir="${1:?project_dir required}"
125
+ local project_name
126
+ project_name=$(basename "${project_dir}")
127
+
128
+ git -C "${project_dir}" worktree list --porcelain 2>/dev/null \
129
+ | grep '^worktree ' \
130
+ | awk '{print $2}' \
131
+ | grep "${project_name}-nw" \
132
+ | while read -r wt; do
133
+ log "CLEANUP: Removing leftover worktree ${wt}"
134
+ git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
135
+ done || true
136
+ }
137
+
138
+ # ── Mark PRD as done ─────────────────────────────────────────────────────────
139
+
140
+ mark_prd_done() {
141
+ local prd_dir="${1:?prd_dir required}"
142
+ local prd_file="${2:?prd_file required}"
143
+ local done_dir="${prd_dir}/done"
144
+
145
+ mkdir -p "${done_dir}"
146
+
147
+ if [ -f "${prd_dir}/${prd_file}" ]; then
148
+ mv "${prd_dir}/${prd_file}" "${done_dir}/${prd_file}"
149
+ log "DONE-PRD: Moved ${prd_file} to done/"
150
+ return 0
151
+ else
152
+ log "WARN: PRD file not found: ${prd_dir}/${prd_file}"
153
+ return 1
154
+ fi
155
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Night Watch PR Reviewer Cron Runner (project-agnostic)
5
+ # Usage: night-watch-pr-reviewer-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_REVIEWER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
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}/night-watch-pr-reviewer.log"
18
+ LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_NAME}.lock"
19
+ MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
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
+ # NOTE: Environment variables should be set by the caller (Node.js CLI).
28
+ # The .env.night-watch sourcing has been removed - config is now injected via env vars.
29
+
30
+ mkdir -p "${LOG_DIR}"
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ # shellcheck source=night-watch-helpers.sh
34
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
35
+
36
+ # Validate provider
37
+ if ! validate_provider "${PROVIDER_CMD}"; then
38
+ echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
39
+ exit 1
40
+ fi
41
+
42
+ rotate_log
43
+
44
+ if ! acquire_lock "${LOCK_FILE}"; then
45
+ exit 0
46
+ fi
47
+
48
+ cd "${PROJECT_DIR}"
49
+
50
+ OPEN_PRS=$(gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null | grep -E '^(feat/|night-watch/)' | wc -l)
51
+
52
+ if [ "${OPEN_PRS}" -eq 0 ]; then
53
+ log "SKIP: No open night-watch/ or feat/ PRs to review"
54
+ exit 0
55
+ fi
56
+
57
+ NEEDS_WORK=0
58
+ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)
59
+ PRS_NEEDING_WORK=""
60
+
61
+ while IFS=$'\t' read -r pr_number pr_branch; do
62
+ FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
63
+ if [ "${FAILED_CHECKS}" -gt 0 ]; then
64
+ log "INFO: PR #${pr_number} (${pr_branch}) has ${FAILED_CHECKS} failed CI check(s)"
65
+ NEEDS_WORK=1
66
+ PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
67
+ continue
68
+ fi
69
+
70
+ ALL_COMMENTS=$(
71
+ {
72
+ gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null
73
+ gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null
74
+ } | sort -u
75
+ )
76
+ LATEST_SCORE=$(echo "${ALL_COMMENTS}" \
77
+ | grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
78
+ | tail -1 \
79
+ | grep -oP '\d+(?=/100)' || echo "")
80
+ if [ -n "${LATEST_SCORE}" ] && [ "${LATEST_SCORE}" -lt 80 ]; then
81
+ log "INFO: PR #${pr_number} (${pr_branch}) has review score ${LATEST_SCORE}/100"
82
+ NEEDS_WORK=1
83
+ PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
84
+ fi
85
+ done < <(gh pr list --state open --json number,headRefName --jq '.[] | select(.headRefName | test("^(feat/|night-watch/)")) | [.number, .headRefName] | @tsv' 2>/dev/null)
86
+
87
+ if [ "${NEEDS_WORK}" -eq 0 ]; then
88
+ log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= 80 (or no score yet)"
89
+ exit 0
90
+ fi
91
+
92
+ log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
93
+
94
+ cleanup_worktrees "${PROJECT_DIR}"
95
+
96
+ # Dry-run mode: print diagnostics and exit
97
+ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
98
+ echo "=== Dry Run: PR Reviewer ==="
99
+ echo "Provider: ${PROVIDER_CMD}"
100
+ echo "Open PRs needing work:${PRS_NEEDING_WORK}"
101
+ echo "Timeout: ${MAX_RUNTIME}s"
102
+ exit 0
103
+ fi
104
+
105
+ case "${PROVIDER_CMD}" in
106
+ claude)
107
+ timeout "${MAX_RUNTIME}" \
108
+ claude -p "/night-watch-pr-reviewer" \
109
+ --dangerously-skip-permissions \
110
+ >> "${LOG_FILE}" 2>&1
111
+ ;;
112
+ codex)
113
+ timeout "${MAX_RUNTIME}" \
114
+ codex --quiet \
115
+ --yolo \
116
+ --prompt "$(cat "${PROJECT_DIR}/.claude/commands/night-watch-pr-reviewer.md")" \
117
+ >> "${LOG_FILE}" 2>&1
118
+ ;;
119
+ *)
120
+ log "ERROR: Unknown provider: ${PROVIDER_CMD}"
121
+ exit 1
122
+ ;;
123
+ esac
124
+
125
+ EXIT_CODE=$?
126
+
127
+ cleanup_worktrees "${PROJECT_DIR}"
128
+
129
+ if [ ${EXIT_CODE} -eq 0 ]; then
130
+ log "DONE: PR reviewer completed successfully"
131
+ elif [ ${EXIT_CODE} -eq 124 ]; then
132
+ log "TIMEOUT: PR reviewer killed after ${MAX_RUNTIME}s"
133
+ else
134
+ log "FAIL: PR reviewer exited with code ${EXIT_CODE}"
135
+ fi