Repository navigation
Expand file tree
/
Copy pathharness.sh
More file actions
executable file
·298 lines (247 loc) · 9.06 KB
/
Copy pathharness.sh
File metadata and controls
executable file
·298 lines (247 loc) · 9.06 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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/bin/bash
set -euo pipefail
###############################################################################
# ccclone build harness
#
# A deterministic loop that spawns Claude Code agent sessions to build the
# project incrementally. Each session gets a fresh context window, reads
# progress files to orient itself, implements the next feature, and exits.
#
# Usage:
# ./harness.sh # Run full build (init + coding loop)
# ./harness.sh --skip-init # Skip initializer, go straight to coding loop
# ./harness.sh --dry-run # Show what would happen without spawning agents
###############################################################################
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROMPTS_DIR="$PROJECT_DIR/prompts"
FEATURE_LIST="$PROJECT_DIR/feature_list.json"
PROGRESS_FILE="$PROJECT_DIR/claude-progress.txt"
LOG_DIR="$PROJECT_DIR/.harness-logs"
MAX_SESSIONS=50 # Safety limit: stop after this many coding sessions
MAX_STUCK_SESSIONS=3 # If no progress for this many sessions, stop
SKIP_INIT=false
DRY_RUN=false
# Parse arguments
for arg in "$@"; do
case $arg in
--skip-init) SKIP_INIT=true ;;
--dry-run) DRY_RUN=true ;;
*) echo "Unknown argument: $arg"; exit 1 ;;
esac
done
# Create log directory
mkdir -p "$LOG_DIR"
###############################################################################
# Helper functions
###############################################################################
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
echo "[$(timestamp)] $1"
}
count_remaining_features() {
if [ ! -f "$FEATURE_LIST" ]; then
echo "-1"
return
fi
jq '[.[] | select(.passes == false)] | length' "$FEATURE_LIST"
}
count_passing_features() {
if [ ! -f "$FEATURE_LIST" ]; then
echo "0"
return
fi
jq '[.[] | select(.passes == true)] | length' "$FEATURE_LIST"
}
count_total_features() {
if [ ! -f "$FEATURE_LIST" ]; then
echo "0"
return
fi
jq 'length' "$FEATURE_LIST"
}
get_last_commit_hash() {
git -C "$PROJECT_DIR" rev-parse HEAD 2>/dev/null || echo "none"
}
run_agent() {
local prompt_file="$1"
local session_label="$2"
local log_file="$LOG_DIR/${session_label}.log"
if [ "$DRY_RUN" = true ]; then
log "[DRY RUN] Would spawn agent with prompt: $prompt_file"
return 0
fi
log "Spawning agent session: $session_label"
log "Log file: $log_file"
# Spawn claude in non-interactive mode with the prompt
# --dangerously-skip-permissions: required for autonomous operation
# since there's no human to approve tool calls in -p mode
# --output-format stream-json: stream tool calls and text as they happen
# --verbose: additional debug output
# Pipe through jq to extract readable text from the stream, while
# also saving the raw JSON stream for debugging
local raw_log="$log_file.raw.jsonl"
claude -p "$(cat "$prompt_file")" \
--dangerously-skip-permissions \
--output-format stream-json \
--verbose \
--disable-slash-commands \
--strict-mcp-config \
--mcp-config '{"mcpServers":{}}' \
--no-session-persistence \
2>&1 | tee "$raw_log" | while IFS= read -r line; do
# Extract content from stream-json events for readable output
local type
type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null) || continue
case "$type" in
assistant)
# Text content from assistant
echo "$line" | jq -r '.message.content[]? | select(.type=="text") | .text // empty' 2>/dev/null
;;
content_block_delta)
# Streaming text delta
echo -n "$(echo "$line" | jq -r '.delta.text // empty' 2>/dev/null)"
;;
tool_use)
local tool_name
tool_name=$(echo "$line" | jq -r '.tool_name // .name // empty' 2>/dev/null)
echo "[TOOL] $tool_name"
;;
tool_result)
echo "[RESULT] ($(echo "$line" | jq -r '.content | length' 2>/dev/null) chars)"
;;
result)
# Final result
echo ""
echo "--- SESSION COMPLETE ---"
echo "$line" | jq -r '.result // empty' 2>/dev/null
;;
esac
done | tee "$log_file"
local exit_code=${PIPESTATUS[0]}
if [ $exit_code -ne 0 ]; then
log "WARNING: Agent session $session_label exited with code $exit_code"
fi
return $exit_code
}
###############################################################################
# Phase 1: Initializer
###############################################################################
run_initializer() {
if [ -f "$FEATURE_LIST" ]; then
log "feature_list.json already exists. Skipping initializer."
return 0
fi
log "=========================================="
log "PHASE 1: INITIALIZER"
log "=========================================="
run_agent "$PROMPTS_DIR/initializer.md" "session-000-init"
# Validate initializer output
if [ ! -f "$FEATURE_LIST" ]; then
log "ERROR: Initializer did not create feature_list.json"
exit 1
fi
if ! jq empty "$FEATURE_LIST" 2>/dev/null; then
log "ERROR: feature_list.json is not valid JSON"
exit 1
fi
local total
total=$(count_total_features)
log "Initializer created $total features"
log "Initializer complete."
}
###############################################################################
# Phase 2: Coding Loop
###############################################################################
run_coding_loop() {
log "=========================================="
log "PHASE 2: CODING LOOP"
log "=========================================="
local session=0
local stuck_count=0
local last_remaining
last_remaining=$(count_remaining_features)
while [ $session -lt $MAX_SESSIONS ]; do
session=$((session + 1))
local remaining
remaining=$(count_remaining_features)
local passing
passing=$(count_passing_features)
local total
total=$(count_total_features)
log "------------------------------------------"
log "SESSION $session / $MAX_SESSIONS"
log "Features: $passing/$total passing, $remaining remaining"
log "------------------------------------------"
# Check if done
if [ "$remaining" -eq 0 ]; then
log "ALL FEATURES COMPLETE!"
log "Total sessions used: $session"
return 0
fi
# Check for stuck state
if [ "$remaining" -eq "$last_remaining" ] && [ $session -gt 1 ]; then
stuck_count=$((stuck_count + 1))
log "WARNING: No progress made. Stuck count: $stuck_count/$MAX_STUCK_SESSIONS"
if [ $stuck_count -ge $MAX_STUCK_SESSIONS ]; then
log "ERROR: No progress for $MAX_STUCK_SESSIONS sessions. Stopping."
log "Last $MAX_STUCK_SESSIONS session logs are in $LOG_DIR/"
log "Manual intervention needed."
return 1
fi
else
stuck_count=0
fi
last_remaining=$remaining
# Record pre-session state
local pre_commit
pre_commit=$(get_last_commit_hash)
# Run the coding agent
local session_label
session_label=$(printf "session-%03d-coding" "$session")
run_agent "$PROMPTS_DIR/coding-agent.md" "$session_label" || true
# Post-session checks
local post_commit
post_commit=$(get_last_commit_hash)
if [ "$pre_commit" = "$post_commit" ]; then
log "NOTE: No new commits in session $session"
else
local new_commits
new_commits=$(git -C "$PROJECT_DIR" log --oneline "$pre_commit".."$post_commit" 2>/dev/null | wc -l | tr -d ' ')
log "Session produced $new_commits new commit(s)"
fi
log "Session $session complete."
done
log "Hit max sessions ($MAX_SESSIONS). Manual review needed."
local remaining
remaining=$(count_remaining_features)
log "Features remaining: $remaining"
return 1
}
###############################################################################
# Main
###############################################################################
main() {
log "=========================================="
log "ccclone Build Harness"
log "Project: $PROJECT_DIR"
log "=========================================="
cd "$PROJECT_DIR"
# Phase 1
if [ "$SKIP_INIT" = false ]; then
run_initializer
else
log "Skipping initializer (--skip-init)"
fi
# Phase 2
run_coding_loop
local result=$?
log "=========================================="
log "HARNESS COMPLETE"
log "Final state: $(count_passing_features)/$(count_total_features) features passing"
log "Session logs: $LOG_DIR/"
log "=========================================="
return $result
}
main "$@"