-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrun.sh
More file actions
executable file
·264 lines (222 loc) · 10.2 KB
/
run.sh
File metadata and controls
executable file
·264 lines (222 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/bin/bash
# Long-running AI agent loop
# Usage: ./run.sh [max_iterations] [tui] [extra_prompt_info...]
set -e
# Prevent concurrent runs — acquire an exclusive lock or exit immediately
LOCKFILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.run.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Another run.sh is already running. Exiting."; exit 0; }
# Parse arguments
MAX_ITERATIONS=10
USE_TUI=false
EXTRA_PROMPT=""
PAST_TUI=false
for arg in "$@"; do
if [ "$PAST_TUI" = true ]; then
# Everything after 'tui' is extra prompt info
if [ -n "$EXTRA_PROMPT" ]; then
EXTRA_PROMPT="$EXTRA_PROMPT $arg"
else
EXTRA_PROMPT="$arg"
fi
elif [[ "$arg" =~ ^[0-9]+$ ]]; then
MAX_ITERATIONS="$arg"
elif [[ "$arg" == "tui" ]]; then
USE_TUI=true
PAST_TUI=true
fi
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/scripts/lib/load-config.sh"
PRD_FILE="$SCRIPT_DIR/data/prd.json"
PROGRESS_FILE="$SCRIPT_DIR/data/progress.txt"
LOGS_DIR="$SCRIPT_DIR/logs"
RUN_STATE_FILE="$SCRIPT_DIR/data/run-state.json"
# Resolve target repo path from config.json (required)
GIT_REPO="${BOT_TARGET_REPO_PATH:-}"
if [ -z "$GIT_REPO" ]; then
echo "Error: project.targetRepoPath not set in config.json"
exit 1
fi
if [[ "$GIT_REPO" != /* ]]; then
PARENT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GIT_REPO="$PARENT_ROOT/$GIT_REPO"
fi
# Function to switch back to master branch on exit
cleanup_and_return_to_master() {
if [ -n "$GIT_REPO" ] && [ -d "$GIT_REPO/.git" ]; then
echo ""
echo "Switching back to $BOT_DEFAULT_BRANCH branch in $GIT_REPO..."
cd "$GIT_REPO"
git stash --include-untracked 2>/dev/null || true
git checkout "$BOT_DEFAULT_BRANCH" 2>/dev/null || echo "Could not switch to $BOT_DEFAULT_BRANCH branch"
fi
}
# Register cleanup function to run on exit
trap cleanup_and_return_to_master EXIT
# Initialize progress file if it doesn't exist
if [ ! -f "$PROGRESS_FILE" ]; then
echo "# Progress Log" > "$PROGRESS_FILE"
echo "Started: $(date)" >> "$PROGRESS_FILE"
echo "---" >> "$PROGRESS_FILE"
fi
# Create logs directory if it doesn't exist
mkdir -p "$LOGS_DIR"
# Verify org members file exists (required for prompt injection protection)
ORG_MEMBERS_FILE="$SCRIPT_DIR/.ignore/org-members.txt"
if [ ! -f "$ORG_MEMBERS_FILE" ]; then
echo "Error: Org members file not found at $ORG_MEMBERS_FILE"
echo "This file is required for prompt injection protection."
echo "Run 'make setup' or create it manually:"
echo " mkdir -p $SCRIPT_DIR/.ignore && gh api 'orgs/$BOT_ORG/members' --paginate | jq -r '.[].login' > $ORG_MEMBERS_FILE"
exit 1
fi
echo "Starting Claude Code agent - Max iterations: $MAX_ITERATIONS"
echo "Logs will be saved to: $LOGS_DIR"
# Fetch nightly version once per run (avoids redundant WebFetch in each iteration)
NIGHTLY_VERSION=$(python3 "$SCRIPT_DIR/scripts/get-nightly-version.py" 2>/dev/null || echo "")
if [ -n "$NIGHTLY_VERSION" ]; then
echo "Nightly version: $NIGHTLY_VERSION"
else
echo "Warning: Could not fetch nightly version (agent will fetch if needed)"
fi
# Reset run state at the start of each run
echo "Resetting run state for fresh start..."
"$SCRIPT_DIR/scripts/reset-run-state.sh"
# Track both loop count (for max iterations) and work iterations (actual state changes)
loop_count=0
work_iteration=0
while [ $loop_count -lt $MAX_ITERATIONS ]; do
((++loop_count))
# Initialize runId if it's null (start of new run)
RUN_ID=$(jq -r '.runId // "null"' "$RUN_STATE_FILE" 2>/dev/null || echo "null")
if [ "$RUN_ID" = "null" ]; then
# Initialize new run with current timestamp
RUN_ID=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TMP_RUN_STATE=$(mktemp)
jq --arg runId "$RUN_ID" '.runId = $runId | .storiesCheckedThisRun = [] | .lastIterationHadStateChange = true' "$RUN_STATE_FILE" > "$TMP_RUN_STATE" && mv "$TMP_RUN_STATE" "$RUN_STATE_FILE"
fi
# Generate log file path for this iteration (needed by select-task.py)
RUN_ID_SAFE=$(echo "$RUN_ID" | sed 's/[^a-zA-Z0-9-]/-/g')
ITERATION_LOG="$LOGS_DIR/iteration-${RUN_ID_SAFE}-loop-${loop_count}.log"
# Select next task — this is the gate check; exit early if no candidates
TASK_JSON=""
if [ -n "$EXTRA_PROMPT" ]; then
TASK_JSON=$(python3 "$SCRIPT_DIR/scripts/select-task.py" \
--prd "$PRD_FILE" \
--run-state "$RUN_STATE_FILE" \
--iteration-log "$ITERATION_LOG" \
--claude-bin "$BOT_CLAUDE_BIN" \
--extra-prompt "$EXTRA_PROMPT") || true
else
TASK_JSON=$(python3 "$SCRIPT_DIR/scripts/select-task.py" \
--prd "$PRD_FILE" \
--run-state "$RUN_STATE_FILE" \
--iteration-log "$ITERATION_LOG") || true
fi
TASK_SELECTED=$(echo "$TASK_JSON" | jq -r '.selected // false' 2>/dev/null || echo "false")
if [ "$TASK_SELECTED" != "true" ]; then
REASON=$(echo "$TASK_JSON" | jq -r '.reason // "unknown"' 2>/dev/null || echo "unknown")
echo "No more tasks to process: $REASON"
break
fi
STORY_ID=$(echo "$TASK_JSON" | jq -r '.storyId')
STORY_STATUS=$(echo "$TASK_JSON" | jq -r '.status')
TIER_NAME=$(echo "$TASK_JSON" | jq -r '.tierName')
STORY_TITLE=$(echo "$TASK_JSON" | jq -r '.title')
STORY_DETAILS=$(echo "$TASK_JSON" | jq -c '.storyDetails')
echo "Selected: $STORY_ID - $STORY_TITLE (status: $STORY_STATUS, tier: $TIER_NAME)"
# Task confirmed — proceed with iteration setup
# Store the current iteration log path in run-state.json
TMP_RUN_STATE=$(mktemp)
jq --arg logPath "$ITERATION_LOG" '.currentIterationLogPath = $logPath' "$RUN_STATE_FILE" > "$TMP_RUN_STATE" && mv "$TMP_RUN_STATE" "$RUN_STATE_FILE"
# Check if last iteration had state change (default to true for first iteration)
HAD_STATE_CHANGE=$(jq -r '.lastIterationHadStateChange // true' "$RUN_STATE_FILE" 2>/dev/null || echo "true")
# Only increment work iteration counter if there was actual state change
if [ "$HAD_STATE_CHANGE" = "true" ]; then
((++work_iteration))
echo ""
echo "==============================================================="
echo " Work Iteration $work_iteration (loop $loop_count of $MAX_ITERATIONS)"
echo "==============================================================="
else
echo ""
echo "==============================================================="
echo " Checking next task (work iteration $work_iteration, loop $loop_count of $MAX_ITERATIONS)"
echo " Previous check had no state change - continuing without incrementing work iteration"
echo "==============================================================="
fi
echo "Logging to: $ITERATION_LOG"
# Run Claude Code with the agent prompt
# Use a temp file to capture output while allowing real-time streaming
TEMP_OUTPUT=$(mktemp)
# Change to the parent directory (brave-browser) so relative paths in .claude/CLAUDE.md work
BRAVE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$BRAVE_ROOT"
# Pre-generate session ID so we can print the resume command before Claude starts
SESSION_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
echo ""
echo "To monitor this session (read-only): claude --resume $SESSION_ID"
echo ""
# Claude Code: use --dangerously-skip-permissions for autonomous operation
# In print mode: use stream-json output format with verbose flag to capture detailed execution logs
# In TUI mode: omit --print to show the interactive TUI
BOT_DIRNAME=$(basename "$SCRIPT_DIR")
BOT_CONFIG=$(cat "$SCRIPT_DIR/config.json")
CLAUDE_PROMPT="You are working on story $STORY_ID (current status: $STORY_STATUS).
Follow ./$BOT_DIRNAME/docs/workflow-${STORY_STATUS}.md for the workflow.
Follow the general instructions in ./$BOT_DIRNAME/.claude/CLAUDE.md.
Story details:
$STORY_DETAILS
Bot config (from config.json — do NOT read this file):
$BOT_CONFIG"
if [ -n "$NIGHTLY_VERSION" ]; then
CLAUDE_PROMPT="$CLAUDE_PROMPT
Nightly version: $NIGHTLY_VERSION (use this for milestone names like '$NIGHTLY_VERSION - Nightly', do NOT fetch the release schedule)."
fi
if [ -n "$EXTRA_PROMPT" ]; then
CLAUDE_PROMPT="$CLAUDE_PROMPT
Additional context: $EXTRA_PROMPT"
fi
# Log the prompt to the iteration log so it can be audited later
if [ "$USE_TUI" != true ]; then
jq -n --arg storyId "$STORY_ID" --arg status "$STORY_STATUS" --arg tier "$TIER_NAME" --arg prompt "$CLAUDE_PROMPT" \
'{"type":"prompt","storyId":$storyId,"status":$status,"tier":$tier,"prompt":$prompt}' >> "$ITERATION_LOG"
fi
# Build optional model flag
CLAUDE_MODEL_FLAG=""
if [ -n "$BOT_CLAUDE_MODEL" ]; then
CLAUDE_MODEL_FLAG="--model $BOT_CLAUDE_MODEL"
fi
# Run Claude from the bot directory so it picks up .claude/CLAUDE.md as project settings
if [ "$USE_TUI" = true ]; then
# TUI mode: let Claude own the terminal directly (no piping)
(cd "$SCRIPT_DIR" && "$SCRIPT_DIR/scripts/timeout-tree.sh" 7200 $BOT_CLAUDE_BIN $CLAUDE_MODEL_FLAG --dangerously-skip-permissions --session-id "$SESSION_ID" "$CLAUDE_PROMPT") 200>&- || true
else
(cd "$SCRIPT_DIR" && "$SCRIPT_DIR/scripts/timeout-tree.sh" 7200 $BOT_CLAUDE_BIN $CLAUDE_MODEL_FLAG --dangerously-skip-permissions --print --verbose --output-format stream-json --session-id "$SESSION_ID" "$CLAUDE_PROMPT") 200>&- </dev/null 2>&1 | tee -a "$ITERATION_LOG" > "$TEMP_OUTPUT" || true
fi
echo "To continue this session: claude --resume $SESSION_ID"
# Check for completion signal (print mode only — TUI mode skips this since user is watching)
COMPLETION_CHECK=0
if [ "$USE_TUI" != true ]; then
# Extract text from stream-json, check for completion signal.
# Use tail -1 to guarantee a single integer (jq on mixed stderr/json can produce multiline output).
COMPLETION_CHECK=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text' "$TEMP_OUTPUT" 2>/dev/null | grep -c "<promise>COMPLETE</promise>" 2>/dev/null | tail -1)
COMPLETION_CHECK=$((COMPLETION_CHECK + 0))
fi
if [ "$COMPLETION_CHECK" -gt 0 ]; then
echo ""
echo "Agent completed all tasks!"
echo "Completed at work iteration $work_iteration (loop $loop_count of $MAX_ITERATIONS)"
rm -f "$TEMP_OUTPUT"
exit 0
fi
rm -f "$TEMP_OUTPUT"
echo "Loop $loop_count complete. Starting fresh context..."
sleep 2
done
echo ""
echo "Agent reached max loop iterations ($MAX_ITERATIONS) without completing all tasks."
echo "Work iterations completed: $work_iteration"
echo "Check $PROGRESS_FILE for status."
exit 1