@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.
- package/LICENSE +21 -0
- package/README.md +509 -0
- package/bin/night-watch.mjs +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +376 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +15 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +135 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/logs.d.ts +15 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +104 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/review.d.ts +26 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +144 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/run.d.ts +26 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +161 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/status.d.ts +14 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +303 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/uninstall.d.ts +13 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +97 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +213 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +33 -0
- package/dist/constants.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crontab.d.ts +50 -0
- package/dist/utils/crontab.d.ts.map +1 -0
- package/dist/utils/crontab.js +116 -0
- package/dist/utils/crontab.js.map +1 -0
- package/dist/utils/shell.d.ts +13 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +44 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/ui.d.ts +55 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +121 -0
- package/dist/utils/ui.js.map +1 -0
- package/package.json +64 -0
- package/scripts/night-watch-cron.sh +148 -0
- package/scripts/night-watch-helpers.sh +155 -0
- package/scripts/night-watch-pr-reviewer-cron.sh +135 -0
- package/templates/night-watch-pr-reviewer.md +144 -0
- package/templates/night-watch.config.json +21 -0
- package/templates/night-watch.md +100 -0
- package/templates/prd-executor.md +235 -0
package/dist/utils/ui.js
ADDED
|
@@ -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
|