|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -uo pipefail |
| 3 | +# NightCTO overnight spec runner |
| 4 | +# Feeds bounded specs to Ricky one at a time, in dependency order. |
| 5 | +# Usage: ./scripts/run-overnight.sh <program> [--from <spec-id>] [--dry-run] [--no-pr] |
| 6 | +# Example: ./scripts/run-overnight.sh persona-migration |
| 7 | +# ./scripts/run-overnight.sh persona-migration --from 030 |
| 8 | +# |
| 9 | +# Each spec is implemented by: ricky local --spec-file <spec> --run |
| 10 | +# A draft PR is opened per wave (specs sharing the NNx hundreds digit). |
| 11 | + |
| 12 | +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 13 | +PROGRAM="${1:?Usage: run-overnight.sh <program> [--from <spec-id>] [--dry-run] [--no-pr]}"; shift || true |
| 14 | +SPEC_DIR="$ROOT/specs/$PROGRAM" |
| 15 | +[ -d "$SPEC_DIR" ] || { echo "No spec dir: $SPEC_DIR"; exit 1; } |
| 16 | + |
| 17 | +FROM=""; DRY_RUN=0; OPEN_PR=1 |
| 18 | +while [ $# -gt 0 ]; do |
| 19 | + case "$1" in |
| 20 | + --from) FROM="$2"; shift 2 ;; |
| 21 | + --dry-run) DRY_RUN=1; shift ;; |
| 22 | + --no-pr) OPEN_PR=0; shift ;; |
| 23 | + *) echo "Unknown arg: $1"; exit 1 ;; |
| 24 | + esac |
| 25 | +done |
| 26 | + |
| 27 | +# Review cycle config. The reviewer is a DIFFERENT persona from the implementer. |
| 28 | +# REVIEW_CMD receives the spec path as $1 and the diff range as $2; exits 0 on PASS, |
| 29 | +# non-zero with findings on stdout otherwise. FIX_CMD receives the spec path as $1 |
| 30 | +# and the reviewer findings file as $2. Both are overridable for local/cloud/BYOH. |
| 31 | +MAX_REVIEW_ITERS="${MAX_REVIEW_ITERS:-3}" |
| 32 | +# Each program carries its own _review.md / _fix.md (parameterized by TARGET_SPEC). |
| 33 | +REVIEW_CMD="${REVIEW_CMD:-ricky local --spec-file specs/${PROGRAM}/_review.md --run --input TARGET_SPEC=}" |
| 34 | +FIX_CMD="${FIX_CMD:-ricky local --spec-file specs/${PROGRAM}/_fix.md --run --input TARGET_SPEC=}" |
| 35 | + |
| 36 | +STATE_FILE="/tmp/nightcto-${PROGRAM}-state.env" |
| 37 | +RUN_LOG="/tmp/nightcto-${PROGRAM}.log" |
| 38 | +SPEC_LOG_DIR="/tmp/nightcto-${PROGRAM}-logs" |
| 39 | +BRANCH="results/${PROGRAM}" |
| 40 | +mkdir -p "$SPEC_LOG_DIR"; touch "$RUN_LOG" |
| 41 | + |
| 42 | +log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$RUN_LOG"; } |
| 43 | +save_state() { printf 'LAST_SPEC=%s\nLAST_STATUS=%s\nBRANCH=%s\nUPDATED_AT=%s\n' \ |
| 44 | + "$1" "$2" "$BRANCH" "$(date +%s)" > "$STATE_FILE"; } |
| 45 | + |
| 46 | +wave_of() { basename "$1" | sed -E 's/^([0-9])[0-9]{2}-.*/\1/'; } |
| 47 | + |
| 48 | +open_wave_pr() { |
| 49 | + local wave="$1" specs_md="$2" |
| 50 | + [ "$OPEN_PR" -eq 1 ] || return 0 |
| 51 | + git push origin "$BRANCH" 2>/dev/null || git push origin "$BRANCH" --force-with-lease 2>/dev/null || true |
| 52 | + local existing; existing=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 53 | + if [ -n "$existing" ] && [ "$existing" != "null" ]; then |
| 54 | + log "PR #$existing exists for $BRANCH — updated via push"; return 0 |
| 55 | + fi |
| 56 | + gh pr create --base main --head "$BRANCH" --draft \ |
| 57 | + --title "feat(${PROGRAM}): wave ${wave}" \ |
| 58 | + --body "## ${PROGRAM} — wave ${wave} |
| 59 | +
|
| 60 | +### Specs implemented this wave |
| 61 | +${specs_md} |
| 62 | +
|
| 63 | +### Verify |
| 64 | +\`\`\`bash |
| 65 | +tail -40 ${RUN_LOG} |
| 66 | +npm run build && npm test |
| 67 | +\`\`\` |
| 68 | +
|
| 69 | +*Auto-opened by \`scripts/run-overnight.sh ${PROGRAM}\`*" 2>&1 | tee -a "$RUN_LOG" || true |
| 70 | +} |
| 71 | + |
| 72 | +# Review → fix loop for one spec. Returns 0 only when the reviewer reaches PASS. |
| 73 | +review_cycle() { |
| 74 | + local spec="$1" id="$2" verdict="$SPEC_LOG_DIR/$id.review.log" |
| 75 | + local before; before="$(git rev-parse HEAD)" |
| 76 | + local i |
| 77 | + for ((i=1; i<=MAX_REVIEW_ITERS; i++)); do |
| 78 | + log "REVIEW $id (iter $i/$MAX_REVIEW_ITERS)" |
| 79 | + if [ "$DRY_RUN" -eq 1 ]; then log "DRY-RUN would run REVIEW_CMD for $id"; return 0; fi |
| 80 | + if eval "${REVIEW_CMD}${spec}" "$before..HEAD" 2>&1 | tee "$verdict" >> "$RUN_LOG"; then |
| 81 | + log "REVIEW PASS $id (iter $i)"; return 0 |
| 82 | + fi |
| 83 | + log "REVIEW found issues on $id — running fix pass (iter $i)" |
| 84 | + eval "${FIX_CMD}${spec}" "$verdict" 2>&1 | tee -a "$SPEC_LOG_DIR/$id.log" >> "$RUN_LOG" || true |
| 85 | + git add -A && git commit -m "fix(${PROGRAM}): ${id} review iter $i" 2>/dev/null || true |
| 86 | + done |
| 87 | + log "REVIEW FAILED $id after $MAX_REVIEW_ITERS iters — stopping; resume with --from $id" |
| 88 | + return 1 |
| 89 | +} |
| 90 | + |
| 91 | +run_spec() { |
| 92 | + local spec="$1" id; id="$(basename "$spec" .md)" |
| 93 | + local spec_log="$SPEC_LOG_DIR/$id.log" |
| 94 | + log "START $id" |
| 95 | + if [ "$DRY_RUN" -eq 1 ]; then log "DRY-RUN would run: ricky local --spec-file $spec --run"; return 0; fi |
| 96 | + if ! ricky local --spec-file "$spec" --run 2>&1 | tee "$spec_log" >> "$RUN_LOG"; then |
| 97 | + log "IMPLEMENT FAILED $id (see $spec_log) — stopping; resume with --from $id" |
| 98 | + save_state "$id" impl-failed; return 1 |
| 99 | + fi |
| 100 | + git add -A && git commit -m "feat(${PROGRAM}): ${id}" 2>/dev/null || true |
| 101 | + # implement → review → fix until the reviewer reaches PASS |
| 102 | + if ! review_cycle "$spec" "$id"; then save_state "$id" review-failed; return 1; fi |
| 103 | + log "DONE $id (implemented + reviewed PASS)"; save_state "$id" success |
| 104 | + return 0 |
| 105 | +} |
| 106 | + |
| 107 | +# ── branch setup ───────────────────────────────────────────────────────────── |
| 108 | +cd "$ROOT" |
| 109 | +if [ "$DRY_RUN" -eq 1 ]; then |
| 110 | + log "DRY-RUN — not creating/switching branches" |
| 111 | +elif git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then |
| 112 | + git checkout "$BRANCH"; log "Resuming branch $BRANCH" |
| 113 | +else |
| 114 | + git checkout -b "$BRANCH" main; log "Created branch $BRANCH from main" |
| 115 | +fi |
| 116 | + |
| 117 | +# ── walk specs in order ────────────────────────────────────────────────────── |
| 118 | +mapfile -t SPECS < <(find "$SPEC_DIR" -maxdepth 1 -name '[0-9][0-9][0-9]-*.md' | sort) |
| 119 | +[ "${#SPECS[@]}" -gt 0 ] || { log "No numbered specs in $SPEC_DIR"; exit 1; } |
| 120 | + |
| 121 | +started=0; [ -z "$FROM" ] && started=1 |
| 122 | +current_wave=""; wave_specs_md="" |
| 123 | +for spec in "${SPECS[@]}"; do |
| 124 | + id="$(basename "$spec" .md)" |
| 125 | + if [ "$started" -eq 0 ]; then |
| 126 | + [[ "$id" == ${FROM}* ]] && started=1 || { log "SKIP $id (before --from $FROM)"; continue; } |
| 127 | + fi |
| 128 | + w="$(wave_of "$spec")" |
| 129 | + if [ -n "$current_wave" ] && [ "$w" != "$current_wave" ]; then |
| 130 | + open_wave_pr "$current_wave" "$wave_specs_md"; wave_specs_md="" |
| 131 | + fi |
| 132 | + current_wave="$w"; wave_specs_md="${wave_specs_md}- \`${id}\` |
| 133 | +" |
| 134 | + run_spec "$spec" || exit 1 |
| 135 | +done |
| 136 | +[ -n "$current_wave" ] && open_wave_pr "$current_wave" "$wave_specs_md" |
| 137 | + |
| 138 | +log "ALL_DONE ${PROGRAM}" |
| 139 | +log "Branch: $BRANCH — review at https://github.com/AgentWorkforce/nightcto/pulls" |
0 commit comments