@imdeadpool/guardex 5.0.2 → 5.0.4

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.
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ INTERVAL_SECONDS="${MUSAFETY_REVIEW_BOT_INTERVAL_SECONDS:-30}"
5
+ AGENT_NAME="${MUSAFETY_REVIEW_BOT_AGENT_NAME:-guardex-review-bot}"
6
+ TASK_PREFIX="${MUSAFETY_REVIEW_BOT_TASK_PREFIX:-review-merge}"
7
+ STATE_FILE="${MUSAFETY_REVIEW_BOT_STATE_FILE:-}"
8
+ BASE_BRANCH="${MUSAFETY_REVIEW_BOT_BASE_BRANCH:-}"
9
+ ONLY_PR="${MUSAFETY_REVIEW_BOT_ONLY_PR:-}"
10
+ RETRY_FAILED_RAW="${MUSAFETY_REVIEW_BOT_RETRY_FAILED:-false}"
11
+ INCLUDE_DRAFT_RAW="${MUSAFETY_REVIEW_BOT_INCLUDE_DRAFT:-false}"
12
+
13
+ usage() {
14
+ cat <<'USAGE'
15
+ Usage: bash scripts/review-bot-watch.sh [options]
16
+
17
+ Continuously monitor GitHub pull requests targeting a base branch and dispatch
18
+ one Codex-agent task per newly opened/updated PR.
19
+
20
+ Options:
21
+ --base <branch> Base branch to watch (default: current branch)
22
+ --interval <seconds> Poll interval (default: 30)
23
+ --agent <name> Agent name for codex-agent (default: guardex-review-bot)
24
+ --task-prefix <prefix> Task prefix for codex-agent branches (default: review-merge)
25
+ --state-file <path> State file path (default: .omx/state/review-bot-watch-<base>.tsv)
26
+ --only-pr <number> Watch only one PR number
27
+ --include-draft Include draft PRs
28
+ --retry-failed Retry PRs that previously failed even when SHA is unchanged
29
+ --once Run one poll cycle and exit
30
+ -h, --help Show this help
31
+
32
+ Environment overrides:
33
+ MUSAFETY_REVIEW_BOT_PROMPT_APPEND Additional instructions appended to each Codex prompt
34
+ USAGE
35
+ }
36
+
37
+ normalize_bool() {
38
+ local raw="${1:-}"
39
+ local fallback="${2:-0}"
40
+ case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
41
+ 1|true|yes|on) printf '1' ;;
42
+ 0|false|no|off) printf '0' ;;
43
+ '') printf '%s' "$fallback" ;;
44
+ *) printf '%s' "$fallback" ;;
45
+ esac
46
+ }
47
+
48
+ ONCE=0
49
+
50
+ while [[ $# -gt 0 ]]; do
51
+ case "$1" in
52
+ --base)
53
+ BASE_BRANCH="${2:-}"
54
+ shift 2
55
+ ;;
56
+ --interval)
57
+ INTERVAL_SECONDS="${2:-}"
58
+ shift 2
59
+ ;;
60
+ --agent)
61
+ AGENT_NAME="${2:-}"
62
+ shift 2
63
+ ;;
64
+ --task-prefix)
65
+ TASK_PREFIX="${2:-}"
66
+ shift 2
67
+ ;;
68
+ --state-file)
69
+ STATE_FILE="${2:-}"
70
+ shift 2
71
+ ;;
72
+ --only-pr)
73
+ ONLY_PR="${2:-}"
74
+ shift 2
75
+ ;;
76
+ --retry-failed)
77
+ RETRY_FAILED_RAW="true"
78
+ shift
79
+ ;;
80
+ --include-draft)
81
+ INCLUDE_DRAFT_RAW="true"
82
+ shift
83
+ ;;
84
+ --once)
85
+ ONCE=1
86
+ shift
87
+ ;;
88
+ -h|--help)
89
+ usage
90
+ exit 0
91
+ ;;
92
+ *)
93
+ echo "[review-bot-watch] Unknown option: $1" >&2
94
+ usage >&2
95
+ exit 1
96
+ ;;
97
+ esac
98
+ done
99
+
100
+ RETRY_FAILED="$(normalize_bool "$RETRY_FAILED_RAW" "0")"
101
+ INCLUDE_DRAFT="$(normalize_bool "$INCLUDE_DRAFT_RAW" "0")"
102
+
103
+ if [[ ! "$INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || [[ "$INTERVAL_SECONDS" -lt 5 ]]; then
104
+ echo "[review-bot-watch] --interval must be an integer >= 5 seconds." >&2
105
+ exit 1
106
+ fi
107
+
108
+ if [[ -n "$ONLY_PR" ]] && [[ ! "$ONLY_PR" =~ ^[0-9]+$ ]]; then
109
+ echo "[review-bot-watch] --only-pr must be a numeric PR id." >&2
110
+ exit 1
111
+ fi
112
+
113
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
114
+ echo "[review-bot-watch] Not inside a git repository." >&2
115
+ exit 1
116
+ fi
117
+ repo_root="$(git rev-parse --show-toplevel)"
118
+
119
+ if [[ -z "$BASE_BRANCH" ]]; then
120
+ BASE_BRANCH="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
121
+ fi
122
+ if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then
123
+ BASE_BRANCH="main"
124
+ fi
125
+
126
+ if ! command -v gh >/dev/null 2>&1; then
127
+ echo "[review-bot-watch] Missing GitHub CLI (gh)." >&2
128
+ echo "[review-bot-watch] Install gh and run: gh auth login" >&2
129
+ exit 127
130
+ fi
131
+
132
+ if ! command -v codex >/dev/null 2>&1; then
133
+ echo "[review-bot-watch] Missing Codex CLI command: codex" >&2
134
+ exit 127
135
+ fi
136
+
137
+ if [[ ! -x "$repo_root/scripts/codex-agent.sh" ]]; then
138
+ echo "[review-bot-watch] Missing scripts/codex-agent.sh. Run: gx setup" >&2
139
+ exit 1
140
+ fi
141
+
142
+ if ! gh auth status >/dev/null 2>&1; then
143
+ echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2
144
+ exit 1
145
+ fi
146
+
147
+ sanitize_slug() {
148
+ local raw="$1"
149
+ local fallback="$2"
150
+ local slug
151
+ slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
152
+ if [[ -z "$slug" ]]; then
153
+ slug="$fallback"
154
+ fi
155
+ printf '%s' "$slug"
156
+ }
157
+
158
+ base_slug="$(sanitize_slug "$BASE_BRANCH" "base")"
159
+ if [[ -z "$STATE_FILE" ]]; then
160
+ STATE_FILE="$repo_root/.omx/state/review-bot-watch-${base_slug}.tsv"
161
+ fi
162
+ mkdir -p "$(dirname "$STATE_FILE")"
163
+
164
+ declare -A LAST_SHA
165
+
166
+ declare -A LAST_STATUS
167
+
168
+ load_state() {
169
+ if [[ ! -f "$STATE_FILE" ]]; then
170
+ return 0
171
+ fi
172
+ while IFS=$'\t' read -r pr sha status updated_at; do
173
+ if [[ -z "${pr:-}" ]] || [[ "${pr:0:1}" == "#" ]]; then
174
+ continue
175
+ fi
176
+ LAST_SHA["$pr"]="$sha"
177
+ LAST_STATUS["$pr"]="$status"
178
+ done < "$STATE_FILE"
179
+ }
180
+
181
+ save_state() {
182
+ {
183
+ echo "# pr\thead_sha\tstatus\tupdated_at"
184
+ for pr in "${!LAST_SHA[@]}"; do
185
+ printf '%s\t%s\t%s\t%s\n' "${pr}" "${LAST_SHA[$pr]}" "${LAST_STATUS[$pr]:-unknown}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
186
+ done | sort -n
187
+ } > "$STATE_FILE"
188
+ }
189
+
190
+ build_prompt() {
191
+ local pr="$1"
192
+ local head_branch="$2"
193
+ local head_sha="$3"
194
+ local pr_title="$4"
195
+ local pr_url="$5"
196
+
197
+ cat <<PROMPT
198
+ You are the continuous PR review+merge Codex agent.
199
+
200
+ Target PR: #${pr}
201
+ URL: ${pr_url}
202
+ Title: ${pr_title}
203
+ Base branch: ${BASE_BRANCH}
204
+ Head branch: ${head_branch}
205
+ Head SHA: ${head_sha}
206
+
207
+ Strict task:
208
+ 1) Review ONLY this PR's changes using gh CLI context (gh pr view ${pr}, gh pr diff ${pr}).
209
+ 2) If fixes are needed, implement them in your sandbox branch, run verification (at minimum npm test when available), and push your sandbox branch.
210
+ 3) When the PR is ready and checks are green, merge this PR into ${BASE_BRANCH} with:
211
+ gh pr merge ${pr} --squash --delete-branch
212
+ 4) If merge is blocked, explain the blocker and exit non-zero.
213
+ 5) Do not touch unrelated PRs.
214
+ PROMPT
215
+
216
+ if [[ -n "${MUSAFETY_REVIEW_BOT_PROMPT_APPEND:-}" ]]; then
217
+ printf '\n%s\n' "${MUSAFETY_REVIEW_BOT_PROMPT_APPEND}"
218
+ fi
219
+ }
220
+
221
+ list_open_prs() {
222
+ gh pr list \
223
+ --state open \
224
+ --base "$BASE_BRANCH" \
225
+ --json number,headRefName,headRefOid,isDraft,title,url \
226
+ --jq '.[] | "\(.number)\t\(.headRefName)\t\(.headRefOid)\t\(.isDraft)\t\(.title | gsub("\\t"; " "))\t\(.url)"'
227
+ }
228
+
229
+ should_process_pr() {
230
+ local pr="$1"
231
+ local sha="$2"
232
+
233
+ local prev_sha="${LAST_SHA[$pr]:-}"
234
+ local prev_status="${LAST_STATUS[$pr]:-}"
235
+
236
+ if [[ -z "$prev_sha" ]]; then
237
+ return 0
238
+ fi
239
+
240
+ if [[ "$prev_sha" != "$sha" ]]; then
241
+ return 0
242
+ fi
243
+
244
+ if [[ "$prev_status" == "failed" && "$RETRY_FAILED" == "1" ]]; then
245
+ return 0
246
+ fi
247
+
248
+ return 1
249
+ }
250
+
251
+ process_one_pr() {
252
+ local pr="$1"
253
+ local head_branch="$2"
254
+ local sha="$3"
255
+ local title="$4"
256
+ local url="$5"
257
+
258
+ local prompt
259
+ prompt="$(build_prompt "$pr" "$head_branch" "$sha" "$title" "$url")"
260
+
261
+ local task_name="${TASK_PREFIX}-pr-${pr}"
262
+
263
+ echo "[review-bot-watch] Dispatching Codex agent for PR #${pr} (${head_branch})"
264
+ set +e
265
+ bash "$repo_root/scripts/codex-agent.sh" \
266
+ --task "$task_name" \
267
+ --agent "$AGENT_NAME" \
268
+ --base "$BASE_BRANCH" \
269
+ -- exec "$prompt"
270
+ local exit_code="$?"
271
+ set -e
272
+
273
+ LAST_SHA["$pr"]="$sha"
274
+ if [[ "$exit_code" -eq 0 ]]; then
275
+ LAST_STATUS["$pr"]="success"
276
+ echo "[review-bot-watch] PR #${pr}: success"
277
+ else
278
+ LAST_STATUS["$pr"]="failed"
279
+ echo "[review-bot-watch] PR #${pr}: failed (exit=${exit_code})" >&2
280
+ fi
281
+
282
+ save_state
283
+ }
284
+
285
+ load_state
286
+
287
+ echo "[review-bot-watch] Starting monitor"
288
+ echo "[review-bot-watch] Base branch : ${BASE_BRANCH}"
289
+ echo "[review-bot-watch] Interval : ${INTERVAL_SECONDS}s"
290
+ echo "[review-bot-watch] State file : ${STATE_FILE}"
291
+ if [[ -n "$ONLY_PR" ]]; then
292
+ echo "[review-bot-watch] Only PR : #${ONLY_PR}"
293
+ fi
294
+
295
+ trap 'echo "[review-bot-watch] Stopped."; exit 0' INT TERM
296
+
297
+ while true; do
298
+ found=0
299
+ while IFS=$'\t' read -r pr head_branch sha is_draft title url; do
300
+ if [[ -z "${pr:-}" ]]; then
301
+ continue
302
+ fi
303
+
304
+ found=1
305
+
306
+ if [[ -n "$ONLY_PR" && "$pr" != "$ONLY_PR" ]]; then
307
+ continue
308
+ fi
309
+
310
+ if [[ "$INCLUDE_DRAFT" != "1" && "$is_draft" == "true" ]]; then
311
+ continue
312
+ fi
313
+
314
+ if ! should_process_pr "$pr" "$sha"; then
315
+ continue
316
+ fi
317
+
318
+ process_one_pr "$pr" "$head_branch" "$sha" "$title" "$url"
319
+ done < <(list_open_prs || true)
320
+
321
+ if [[ "$found" -eq 0 ]]; then
322
+ echo "[review-bot-watch] No open PRs for base '${BASE_BRANCH}'."
323
+ fi
324
+
325
+ if [[ "$ONCE" -eq 1 ]]; then
326
+ break
327
+ fi
328
+
329
+ sleep "$INTERVAL_SECONDS"
330
+ done