deepflow 0.1.52 → 0.1.53

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,1248 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Required for spawning child `claude -p` processes.
5
+ # Without this, nested Claude Code instances conflict with the parent.
6
+ unset CLAUDECODE
7
+
8
+ # NOTE: Child `claude -p` processes will need `--dangerously-skip-permissions`
9
+ # to run without interactive approval prompts.
10
+
11
+ # Context window monitoring is handled by run_claude_monitored(), which parses
12
+ # stream-json output for token usage and restarts when hitting the threshold.
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Globals
16
+ # ---------------------------------------------------------------------------
17
+
18
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
19
+ PARALLEL=0 # 0 = unlimited
20
+ HYPOTHESES=2
21
+ MAX_CYCLES=0 # 0 = unlimited
22
+ CONTINUE=false
23
+ FRESH=false
24
+ INTERRUPTED=false
25
+
26
+ # Context window monitoring threshold (percentage). When token usage reaches
27
+ # this fraction of the context window, the claude -p process is killed and
28
+ # restarted with --resume to get a fresh context window.
29
+ CONTEXT_THRESHOLD_PCT=50
30
+
31
+ # Associative array for per-spec status tracking
32
+ # Values: "converged", "halted", "in-progress"
33
+ declare -A SPEC_STATUS
34
+ # Associative array for per-spec winner slugs
35
+ declare -A SPEC_WINNER
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Logging
39
+ # ---------------------------------------------------------------------------
40
+
41
+ auto_log() {
42
+ local msg="$1"
43
+ mkdir -p "${PROJECT_ROOT}/.deepflow"
44
+ echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $msg" >> "${PROJECT_ROOT}/.deepflow/auto-decisions.log"
45
+ }
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Usage
49
+ # ---------------------------------------------------------------------------
50
+
51
+ usage() {
52
+ cat <<'USAGE'
53
+ Usage: deepflow-auto.sh [OPTIONS]
54
+
55
+ Core orchestration loop for autonomous deepflow execution.
56
+ Discovers specs/doing-*.md files and runs hypothesis-select-reject cycles.
57
+
58
+ Options:
59
+ --parallel=N Cap concurrent processes (default: unlimited, 0 = unlimited)
60
+ --hypotheses=N Number of hypotheses per spec (default: 2)
61
+ --max-cycles=N Cap hypothesis-select-reject cycles (default: unlimited, 0 = unlimited)
62
+ --continue Resume from checkpoint (placeholder)
63
+ --fresh Ignore checkpoint, start fresh (placeholder)
64
+ --help Show this help message
65
+ USAGE
66
+ }
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Flag parsing
70
+ # ---------------------------------------------------------------------------
71
+
72
+ parse_flags() {
73
+ while [[ $# -gt 0 ]]; do
74
+ case "$1" in
75
+ --parallel=*)
76
+ PARALLEL="${1#*=}"
77
+ ;;
78
+ --hypotheses=*)
79
+ HYPOTHESES="${1#*=}"
80
+ ;;
81
+ --max-cycles=*)
82
+ MAX_CYCLES="${1#*=}"
83
+ ;;
84
+ --continue)
85
+ CONTINUE=true
86
+ ;;
87
+ --fresh)
88
+ FRESH=true
89
+ ;;
90
+ --help)
91
+ usage
92
+ exit 0
93
+ ;;
94
+ *)
95
+ echo "Error: unknown flag '$1'" >&2
96
+ usage >&2
97
+ exit 1
98
+ ;;
99
+ esac
100
+ shift
101
+ done
102
+ }
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Spec discovery
106
+ # ---------------------------------------------------------------------------
107
+
108
+ discover_specs() {
109
+ local specs_dir="${PROJECT_ROOT}/specs"
110
+ local -a found=()
111
+
112
+ if [[ ! -d "$specs_dir" ]]; then
113
+ echo "Error: specs/ directory not found at ${specs_dir}" >&2
114
+ exit 1
115
+ fi
116
+
117
+ # Collect doing-*.md files (read-only — we never modify these)
118
+ for f in "${specs_dir}"/doing-*.md; do
119
+ [[ -e "$f" ]] || continue
120
+ found+=("$f")
121
+ done
122
+
123
+ if [[ ${#found[@]} -eq 0 ]]; then
124
+ echo "Error: no specs/doing-*.md files found in ${specs_dir}" >&2
125
+ exit 1
126
+ fi
127
+
128
+ # Return the list via stdout
129
+ printf '%s\n' "${found[@]}"
130
+ }
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Context-monitored claude -p wrapper
134
+ # ---------------------------------------------------------------------------
135
+
136
+ # run_claude_monitored <working_dir> <prompt_text> [session_id]
137
+ #
138
+ # Runs `claude -p --output-format stream-json` and monitors token usage in
139
+ # real time. If usage reaches CONTEXT_THRESHOLD_PCT% of the context window the
140
+ # process is killed and automatically restarted with `--resume <session_id>` so
141
+ # it gets a fresh context window.
142
+ #
143
+ # The final result text is written to stdout. A side-effect context.json is
144
+ # written to <working_dir>/.deepflow/context.json for statusline consumption.
145
+ run_claude_monitored() {
146
+ local working_dir="$1"
147
+ local prompt_text="$2"
148
+ local session_id="${3:-}"
149
+
150
+ local result_tmp
151
+ result_tmp="$(mktemp)"
152
+
153
+ # Outer loop: may restart with --resume when threshold is hit
154
+ while true; do
155
+ # Build command arguments
156
+ local -a cmd_args=(claude -p --output-format stream-json --dangerously-skip-permissions)
157
+ if [[ -n "$session_id" ]]; then
158
+ cmd_args+=(--resume --session-id "$session_id")
159
+ fi
160
+
161
+ # Accumulated token count and context window size across events
162
+ local total_tokens=0
163
+ local context_window=0
164
+ local current_session_id=""
165
+ local threshold_hit=false
166
+ local claude_pid=""
167
+
168
+ # Launch claude -p in background, capture its PID so we can kill it
169
+ if [[ -n "$session_id" ]]; then
170
+ # When resuming, no prompt is piped
171
+ "${cmd_args[@]}" < /dev/null 2>/dev/null &
172
+ claude_pid=$!
173
+ else
174
+ echo "$prompt_text" | "${cmd_args[@]}" 2>/dev/null &
175
+ claude_pid=$!
176
+ fi
177
+
178
+ # Read stdout from the backgrounded process via /dev/fd — we need a pipe.
179
+ # Re-architect: use a FIFO so we can read line-by-line while also holding
180
+ # the PID for killing.
181
+ # Kill the background process we just started (we'll redo with a FIFO).
182
+ kill "$claude_pid" 2>/dev/null || true
183
+ wait "$claude_pid" 2>/dev/null || true
184
+
185
+ local fifo_path
186
+ fifo_path="$(mktemp -u)"
187
+ mkfifo "$fifo_path"
188
+
189
+ if [[ -n "$session_id" ]]; then
190
+ "${cmd_args[@]}" < /dev/null > "$fifo_path" 2>/dev/null &
191
+ claude_pid=$!
192
+ else
193
+ echo "$prompt_text" | "${cmd_args[@]}" > "$fifo_path" 2>/dev/null &
194
+ claude_pid=$!
195
+ fi
196
+
197
+ # Read the FIFO line-by-line
198
+ while IFS= read -r line; do
199
+ [[ -z "$line" ]] && continue
200
+
201
+ local parsed
202
+ parsed="$(node -e "
203
+ try {
204
+ const e = JSON.parse(process.argv[1]);
205
+ if (e.session_id) {
206
+ console.log('SESSION_ID:' + e.session_id);
207
+ }
208
+ if (e.type === 'assistant' && e.message && e.message.usage) {
209
+ const u = e.message.usage;
210
+ const tokens = (u.input_tokens||0) + (u.cache_creation_input_tokens||0) + (u.cache_read_input_tokens||0) + (u.output_tokens||0);
211
+ console.log('TOKENS:' + tokens);
212
+ } else if (e.type === 'result') {
213
+ const mu = e.modelUsage || {};
214
+ const model = Object.keys(mu)[0];
215
+ const cw = model ? mu[model].contextWindow : 0;
216
+ console.log('CONTEXT_WINDOW:' + cw);
217
+ console.log('RESULT_START');
218
+ console.log(e.result || '');
219
+ console.log('RESULT_END');
220
+ }
221
+ } catch(err) {}
222
+ " "$line" 2>/dev/null)" || true
223
+
224
+ # Parse the node output
225
+ local IFS_save="$IFS"
226
+ IFS=$'\n'
227
+ for pline in $parsed; do
228
+ case "$pline" in
229
+ SESSION_ID:*)
230
+ current_session_id="${pline#SESSION_ID:}"
231
+ ;;
232
+ TOKENS:*)
233
+ total_tokens="${pline#TOKENS:}"
234
+ ;;
235
+ CONTEXT_WINDOW:*)
236
+ context_window="${pline#CONTEXT_WINDOW:}"
237
+ ;;
238
+ RESULT_START)
239
+ # Next lines until RESULT_END are the result text
240
+ local capturing_result=true
241
+ ;;
242
+ RESULT_END)
243
+ capturing_result=false
244
+ ;;
245
+ *)
246
+ if [[ "${capturing_result:-false}" == "true" ]]; then
247
+ echo "$pline" >> "$result_tmp"
248
+ fi
249
+ ;;
250
+ esac
251
+ done
252
+ IFS="$IFS_save"
253
+
254
+ # Check threshold
255
+ if [[ "$context_window" -gt 0 && "$total_tokens" -gt 0 ]]; then
256
+ local pct=$(( total_tokens * 100 / context_window ))
257
+
258
+ # Write context.json for statusline
259
+ mkdir -p "${working_dir}/.deepflow"
260
+ printf '{"percentage": %d, "timestamp": "%s"}\n' "$pct" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "${working_dir}/.deepflow/context.json"
261
+
262
+ if [[ "$pct" -ge "$CONTEXT_THRESHOLD_PCT" ]]; then
263
+ auto_log "Context threshold hit: ${pct}% >= ${CONTEXT_THRESHOLD_PCT}% (tokens=${total_tokens}, window=${context_window}). Restarting with --resume."
264
+ threshold_hit=true
265
+ kill "$claude_pid" 2>/dev/null || true
266
+ wait "$claude_pid" 2>/dev/null || true
267
+ break
268
+ fi
269
+ fi
270
+ done < "$fifo_path"
271
+
272
+ # Clean up FIFO
273
+ rm -f "$fifo_path"
274
+
275
+ # Wait for claude process to finish (if it hasn't been killed)
276
+ wait "$claude_pid" 2>/dev/null || true
277
+
278
+ if [[ "$threshold_hit" == "true" && -n "$current_session_id" ]]; then
279
+ # Restart with --resume and the captured session_id
280
+ session_id="$current_session_id"
281
+ total_tokens=0
282
+ context_window=0
283
+ auto_log "Restarting claude -p with --resume session_id=${session_id}"
284
+ continue
285
+ fi
286
+
287
+ # Normal exit — break out of the restart loop
288
+ break
289
+ done
290
+
291
+ # Write final context.json
292
+ if [[ "$context_window" -gt 0 && "$total_tokens" -gt 0 ]]; then
293
+ local final_pct=$(( total_tokens * 100 / context_window ))
294
+ mkdir -p "${working_dir}/.deepflow"
295
+ printf '{"percentage": %d, "timestamp": "%s"}\n' "$final_pct" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "${working_dir}/.deepflow/context.json"
296
+ fi
297
+
298
+ # Output the result text
299
+ if [[ -f "$result_tmp" ]]; then
300
+ cat "$result_tmp"
301
+ rm -f "$result_tmp"
302
+ fi
303
+ }
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Stub functions — to be implemented by T3–T7
307
+ # ---------------------------------------------------------------------------
308
+
309
+ generate_hypotheses() {
310
+ local spec_file="$1"
311
+ local spec_name="$2"
312
+ local cycle="$3"
313
+ local hypotheses_count="$4"
314
+
315
+ auto_log "generate_hypotheses called for ${spec_file} (spec_name=${spec_name}, cycle=${cycle}, count=${hypotheses_count})"
316
+
317
+ # 1. Read spec content (read-only)
318
+ local spec_content
319
+ spec_content="$(cat "$spec_file")"
320
+
321
+ # 2. Gather failed experiment context
322
+ local failed_context=""
323
+ local experiments_dir="${PROJECT_ROOT}/.deepflow/experiments"
324
+ if [[ -d "$experiments_dir" ]]; then
325
+ for failed_file in "${experiments_dir}/${spec_name}--"*"--failed.md"; do
326
+ [[ -e "$failed_file" ]] || continue
327
+ local hypothesis_section=""
328
+ local conclusion_section=""
329
+ # Extract hypothesis section (between ## Hypothesis and next ##)
330
+ hypothesis_section="$(sed -n '/^## Hypothesis/,/^## /{ /^## Hypothesis/p; /^## [^H]/!{ /^## Hypothesis/!p; }; }' "$failed_file")"
331
+ # Extract conclusion section (between ## Conclusion and next ## or EOF)
332
+ conclusion_section="$(sed -n '/^## Conclusion/,/^## /{ /^## Conclusion/p; /^## [^C]/!{ /^## Conclusion/!p; }; }' "$failed_file")"
333
+ if [[ -n "$hypothesis_section" || -n "$conclusion_section" ]]; then
334
+ failed_context="${failed_context}
335
+ --- Failed experiment: $(basename "$failed_file") ---
336
+ ${hypothesis_section}
337
+ ${conclusion_section}
338
+ "
339
+ fi
340
+ done
341
+ fi
342
+
343
+ # 3. Build the prompt for claude -p
344
+ local failed_prompt=""
345
+ if [[ -n "$failed_context" ]]; then
346
+ failed_prompt="
347
+ The following hypotheses have already been tried and FAILED. Do NOT repeat them or suggest similar approaches:
348
+
349
+ ${failed_context}
350
+ "
351
+ fi
352
+
353
+ local prompt
354
+ prompt="You are helping with an autonomous development workflow. Given the following spec, generate exactly ${hypotheses_count} approach hypotheses for implementing it.
355
+
356
+ --- SPEC CONTENT ---
357
+ ${spec_content}
358
+ --- END SPEC ---
359
+ ${failed_prompt}
360
+ Generate exactly ${hypotheses_count} hypotheses as a JSON array. Each object must have:
361
+ - \"slug\": a URL-safe lowercase hyphenated short name (e.g. \"stream-based-parser\")
362
+ - \"hypothesis\": a one-sentence description of the approach
363
+ - \"method\": a one-sentence description of how to validate this approach
364
+
365
+ Output ONLY the JSON array. No markdown fences, no explanation, no extra text. Just the raw JSON array."
366
+
367
+ # 4. Spawn claude -p and capture output
368
+ mkdir -p "${PROJECT_ROOT}/.deepflow/hypotheses"
369
+
370
+ local raw_output
371
+ raw_output="$(run_claude_monitored "${PROJECT_ROOT}" "$prompt")" || {
372
+ auto_log "ERROR: claude -p failed for generate_hypotheses (spec=${spec_name}, cycle=${cycle})"
373
+ echo "Error: claude -p failed for hypothesis generation" >&2
374
+ return 1
375
+ }
376
+
377
+ # 5. Extract JSON array from output (strip any accidental wrapping)
378
+ local json_output
379
+ # Try to extract JSON array if surrounded by other text
380
+ json_output="$(echo "$raw_output" | sed -n '/^\[/,/^\]/p')"
381
+ if [[ -z "$json_output" ]]; then
382
+ # Fallback: maybe it's all on one line
383
+ json_output="$(echo "$raw_output" | grep -o '\[.*\]')" || true
384
+ fi
385
+ if [[ -z "$json_output" ]]; then
386
+ auto_log "ERROR: could not parse JSON from claude output for spec=${spec_name}, cycle=${cycle}"
387
+ echo "Error: failed to parse hypothesis JSON from claude output" >&2
388
+ echo "Raw output was: ${raw_output}" >&2
389
+ return 1
390
+ fi
391
+
392
+ # 6. Write hypotheses to file
393
+ local hypotheses_file="${PROJECT_ROOT}/.deepflow/hypotheses/${spec_name}-cycle-${cycle}.json"
394
+ echo "$json_output" > "$hypotheses_file"
395
+
396
+ # 7. Log each hypothesis
397
+ local count
398
+ count="$(echo "$json_output" | grep -o '"slug"' | wc -l | tr -d ' ')"
399
+ auto_log "Generated ${count} hypotheses for ${spec_name} cycle ${cycle} -> ${hypotheses_file}"
400
+
401
+ # Log individual hypotheses
402
+ # Parse slugs from JSON for logging
403
+ echo "$json_output" | grep -o '"slug"[[:space:]]*:[[:space:]]*"[^"]*"' | while read -r line; do
404
+ local slug
405
+ slug="$(echo "$line" | sed 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
406
+ auto_log " Hypothesis: ${slug} (spec=${spec_name}, cycle=${cycle})"
407
+ done
408
+
409
+ if [[ "$count" -lt "$hypotheses_count" ]]; then
410
+ auto_log "WARNING: requested ${hypotheses_count} hypotheses but got ${count} for ${spec_name} cycle ${cycle}"
411
+ echo "Warning: got ${count} hypotheses instead of requested ${hypotheses_count}" >&2
412
+ fi
413
+
414
+ echo "Generated ${count} hypotheses -> ${hypotheses_file}"
415
+ return 0
416
+ }
417
+
418
+ run_single_spike() {
419
+ local spec_name="$1"
420
+ local slug="$2"
421
+ local hypothesis="$3"
422
+ local method="$4"
423
+
424
+ local worktree_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
425
+ local branch_name="df/${spec_name}-${slug}"
426
+
427
+ auto_log "Starting spike for ${spec_name}/${slug}"
428
+
429
+ # Create worktree and branch
430
+ if [[ -d "$worktree_path" ]]; then
431
+ auto_log "Worktree already exists at ${worktree_path}, reusing"
432
+ else
433
+ git worktree add -b "$branch_name" "$worktree_path" HEAD 2>/dev/null || {
434
+ # Branch may already exist from a previous run
435
+ git worktree add "$worktree_path" "$branch_name" 2>/dev/null || {
436
+ auto_log "ERROR: failed to create worktree for ${slug}"
437
+ return 1
438
+ }
439
+ }
440
+ fi
441
+
442
+ # Build spike prompt
443
+ local spike_prompt
444
+ spike_prompt="You are running a spike experiment to validate a hypothesis for spec '${spec_name}'.
445
+
446
+ --- HYPOTHESIS ---
447
+ Slug: ${slug}
448
+ Hypothesis: ${hypothesis}
449
+ Method: ${method}
450
+ --- END HYPOTHESIS ---
451
+
452
+ Your tasks:
453
+ 1. Validate this hypothesis by implementing the minimum necessary to prove or disprove it.
454
+ 2. Write an experiment file at: .deepflow/experiments/${spec_name}--${slug}--active.md
455
+ The experiment file should contain:
456
+ - ## Hypothesis: restate the hypothesis
457
+ - ## Method: what you did to validate
458
+ - ## Results: what you observed
459
+ - ## Conclusion: PASSED or FAILED with reasoning
460
+ 3. Write a result YAML file at: .deepflow/results/spike-${slug}.yaml
461
+ The YAML must contain:
462
+ - slug: ${slug}
463
+ - spec: ${spec_name}
464
+ - status: passed OR failed
465
+ - summary: one-line summary of the result
466
+ 4. Stage and commit all changes with message: spike(${spec_name}): validate ${slug}
467
+
468
+ Important:
469
+ - Create the .deepflow/experiments and .deepflow/results directories if they don't exist.
470
+ - Be concise and focused — this is a spike, not a full implementation.
471
+ - If the hypothesis is not viable, mark it as failed and explain why."
472
+
473
+ # Run claude -p in the worktree with context monitoring
474
+ (
475
+ cd "$worktree_path"
476
+ run_claude_monitored "$worktree_path" "$spike_prompt" > /dev/null
477
+ )
478
+ local exit_code=$?
479
+
480
+ if [[ $exit_code -ne 0 ]]; then
481
+ auto_log "ERROR: claude -p exited with code ${exit_code} for spike ${slug}"
482
+ else
483
+ auto_log "Spike ${slug} claude -p completed successfully"
484
+ fi
485
+
486
+ return $exit_code
487
+ }
488
+
489
+ run_spikes() {
490
+ local spec_file="$1"
491
+ local spec_name="$2"
492
+ local cycle="$3"
493
+
494
+ local hypotheses_file="${PROJECT_ROOT}/.deepflow/hypotheses/${spec_name}-cycle-${cycle}.json"
495
+
496
+ if [[ ! -f "$hypotheses_file" ]]; then
497
+ auto_log "ERROR: hypotheses file not found: ${hypotheses_file}"
498
+ echo "Error: hypotheses file not found: ${hypotheses_file}" >&2
499
+ return 1
500
+ fi
501
+
502
+ auto_log "run_spikes starting for ${spec_name} cycle ${cycle}"
503
+
504
+ # Parse hypotheses from JSON — extract slug, hypothesis, method for each entry
505
+ local -a slugs=()
506
+ local -a hypotheses_arr=()
507
+ local -a methods_arr=()
508
+
509
+ # Use a while loop to parse the JSON entries
510
+ while IFS= read -r slug; do
511
+ slugs+=("$slug")
512
+ done < <(grep -o '"slug"[[:space:]]*:[[:space:]]*"[^"]*"' "$hypotheses_file" | sed 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
513
+
514
+ while IFS= read -r hyp; do
515
+ hypotheses_arr+=("$hyp")
516
+ done < <(grep -o '"hypothesis"[[:space:]]*:[[:space:]]*"[^"]*"' "$hypotheses_file" | sed 's/.*"hypothesis"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
517
+
518
+ while IFS= read -r meth; do
519
+ methods_arr+=("$meth")
520
+ done < <(grep -o '"method"[[:space:]]*:[[:space:]]*"[^"]*"' "$hypotheses_file" | sed 's/.*"method"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
521
+
522
+ local count=${#slugs[@]}
523
+ if [[ "$count" -eq 0 ]]; then
524
+ auto_log "ERROR: no hypotheses parsed from ${hypotheses_file}"
525
+ echo "Error: no hypotheses found in ${hypotheses_file}" >&2
526
+ return 1
527
+ fi
528
+
529
+ auto_log "Parsed ${count} hypotheses for spiking"
530
+
531
+ # Create required directories
532
+ mkdir -p "${PROJECT_ROOT}/.deepflow/experiments"
533
+ mkdir -p "${PROJECT_ROOT}/.deepflow/results"
534
+
535
+ # Spawn spikes with semaphore pattern for --parallel=N
536
+ local -a pids=()
537
+ local i
538
+ for ((i = 0; i < count; i++)); do
539
+ local slug="${slugs[$i]}"
540
+ local hypothesis="${hypotheses_arr[$i]}"
541
+ local method="${methods_arr[$i]}"
542
+
543
+ # Semaphore: if PARALLEL > 0 and we have hit the limit, wait for one to finish
544
+ if [[ $PARALLEL -gt 0 ]] && [[ ${#pids[@]} -ge $PARALLEL ]]; then
545
+ wait -n 2>/dev/null || true
546
+ # Remove finished PIDs
547
+ local -a new_pids=()
548
+ local pid
549
+ for pid in "${pids[@]}"; do
550
+ if kill -0 "$pid" 2>/dev/null; then
551
+ new_pids+=("$pid")
552
+ fi
553
+ done
554
+ pids=("${new_pids[@]}")
555
+ fi
556
+
557
+ auto_log "Spawning spike for ${slug} (hypothesis ${i}/${count})"
558
+ echo "Spawning spike: ${slug}"
559
+
560
+ run_single_spike "$spec_name" "$slug" "$hypothesis" "$method" &
561
+ pids+=($!)
562
+ done
563
+
564
+ # Wait for all remaining spikes to complete
565
+ auto_log "Waiting for all ${#pids[@]} spike(s) to complete..."
566
+ wait
567
+ auto_log "All spikes completed for ${spec_name} cycle ${cycle}"
568
+
569
+ # Collect results and process
570
+ local -a passed_slugs=()
571
+ for ((i = 0; i < count; i++)); do
572
+ local slug="${slugs[$i]}"
573
+ local worktree_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
574
+ local result_file="${worktree_path}/.deepflow/results/spike-${slug}.yaml"
575
+ local experiment_active="${worktree_path}/.deepflow/experiments/${spec_name}--${slug}--active.md"
576
+
577
+ if [[ -f "$result_file" ]]; then
578
+ # Read status from result YAML
579
+ local status
580
+ status="$(grep -m1 '^status:' "$result_file" | sed 's/^status:[[:space:]]*//' | tr -d '[:space:]')" || status="unknown"
581
+
582
+ if [[ "$status" == "passed" ]]; then
583
+ auto_log "PASSED spike: ${slug} (spec=${spec_name}, cycle=${cycle})"
584
+ echo "Spike PASSED: ${slug}"
585
+ passed_slugs+=("$slug")
586
+ else
587
+ auto_log "FAILED spike: ${slug} status=${status} (spec=${spec_name}, cycle=${cycle})"
588
+ echo "Spike FAILED: ${slug}"
589
+ # Rename active experiment to failed
590
+ if [[ -f "$experiment_active" ]]; then
591
+ local experiment_failed="${worktree_path}/.deepflow/experiments/${spec_name}--${slug}--failed.md"
592
+ mv "$experiment_active" "$experiment_failed"
593
+ # Also copy the failed experiment to the main project for future reference
594
+ mkdir -p "${PROJECT_ROOT}/.deepflow/experiments"
595
+ cp "$experiment_failed" "${PROJECT_ROOT}/.deepflow/experiments/"
596
+ fi
597
+ fi
598
+ else
599
+ auto_log "MISSING result for spike: ${slug} — treating as failed (spec=${spec_name}, cycle=${cycle})"
600
+ echo "Spike MISSING RESULT: ${slug} (treating as failed)"
601
+ # Rename active experiment to failed if it exists
602
+ if [[ -f "$experiment_active" ]]; then
603
+ local experiment_failed="${worktree_path}/.deepflow/experiments/${spec_name}--${slug}--failed.md"
604
+ mv "$experiment_active" "$experiment_failed"
605
+ mkdir -p "${PROJECT_ROOT}/.deepflow/experiments"
606
+ cp "$experiment_failed" "${PROJECT_ROOT}/.deepflow/experiments/"
607
+ fi
608
+ fi
609
+ done
610
+
611
+ # Write passed hypotheses to a file for the implementation phase
612
+ local passed_file="${PROJECT_ROOT}/.deepflow/hypotheses/${spec_name}-cycle-${cycle}-passed.json"
613
+ if [[ ${#passed_slugs[@]} -gt 0 ]]; then
614
+ # Build a filtered JSON array of passed hypotheses
615
+ local passed_json="["
616
+ local first=true
617
+ for slug in "${passed_slugs[@]}"; do
618
+ if [[ "$first" == "true" ]]; then
619
+ first=false
620
+ else
621
+ passed_json="${passed_json},"
622
+ fi
623
+ # Extract the full object for this slug from the original hypotheses file
624
+ local hyp_text method_text
625
+ hyp_text="$(grep -A1 "\"slug\"[[:space:]]*:[[:space:]]*\"${slug}\"" "$hypotheses_file" | grep '"hypothesis"' | sed 's/.*"hypothesis"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || hyp_text=""
626
+ method_text="$(grep -A2 "\"slug\"[[:space:]]*:[[:space:]]*\"${slug}\"" "$hypotheses_file" | grep '"method"' | sed 's/.*"method"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || method_text=""
627
+ passed_json="${passed_json}{\"slug\":\"${slug}\",\"hypothesis\":\"${hyp_text}\",\"method\":\"${method_text}\"}"
628
+ done
629
+ passed_json="${passed_json}]"
630
+ echo "$passed_json" > "$passed_file"
631
+ auto_log "Wrote ${#passed_slugs[@]} passed hypotheses to ${passed_file}"
632
+ echo "${#passed_slugs[@]} spike(s) passed -> ${passed_file}"
633
+ else
634
+ echo "[]" > "$passed_file"
635
+ auto_log "No spikes passed for ${spec_name} cycle ${cycle}"
636
+ echo "No spikes passed for ${spec_name} cycle ${cycle}"
637
+ fi
638
+
639
+ return 0
640
+ }
641
+
642
+ run_implementations() {
643
+ local spec_file="$1"
644
+ local spec_name="$2"
645
+ local cycle="$3"
646
+
647
+ local passed_file="${PROJECT_ROOT}/.deepflow/hypotheses/${spec_name}-cycle-${cycle}-passed.json"
648
+
649
+ if [[ ! -f "$passed_file" ]]; then
650
+ auto_log "No passed hypotheses file found: ${passed_file} — skipping implementations"
651
+ echo "No passed hypotheses to implement for ${spec_name} cycle ${cycle}"
652
+ return 0
653
+ fi
654
+
655
+ # Parse passed slugs
656
+ local -a slugs=()
657
+ while IFS= read -r slug; do
658
+ slugs+=("$slug")
659
+ done < <(grep -o '"slug"[[:space:]]*:[[:space:]]*"[^"]*"' "$passed_file" | sed 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
660
+
661
+ local count=${#slugs[@]}
662
+ if [[ "$count" -eq 0 ]]; then
663
+ auto_log "No passed hypotheses found in ${passed_file} — skipping implementations"
664
+ echo "No passed hypotheses to implement for ${spec_name} cycle ${cycle}"
665
+ return 0
666
+ fi
667
+
668
+ auto_log "run_implementations starting for ${spec_name} cycle ${cycle} with ${count} passed spike(s)"
669
+
670
+ # Read spec content once for all implementation prompts
671
+ local spec_content
672
+ spec_content="$(cat "$spec_file")"
673
+
674
+ # Spawn implementation agents in parallel
675
+ local -a pids=()
676
+ local -a impl_slugs=()
677
+ local i
678
+ for ((i = 0; i < count; i++)); do
679
+ local slug="${slugs[$i]}"
680
+ local worktree_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
681
+ local experiment_file=".deepflow/experiments/${spec_name}--${slug}--passed.md"
682
+
683
+ if [[ ! -d "$worktree_path" ]]; then
684
+ auto_log "ERROR: worktree not found for implementation: ${worktree_path}"
685
+ echo "Skipping implementation for ${slug}: worktree not found" >&2
686
+ continue
687
+ fi
688
+
689
+ # Semaphore: if PARALLEL > 0 and we have hit the limit, wait for one to finish
690
+ if [[ $PARALLEL -gt 0 ]] && [[ ${#pids[@]} -ge $PARALLEL ]]; then
691
+ wait -n 2>/dev/null || true
692
+ # Remove finished PIDs
693
+ local -a new_pids=()
694
+ local pid
695
+ for pid in "${pids[@]}"; do
696
+ if kill -0 "$pid" 2>/dev/null; then
697
+ new_pids+=("$pid")
698
+ fi
699
+ done
700
+ pids=("${new_pids[@]}")
701
+ fi
702
+
703
+ # Build implementation prompt
704
+ local impl_prompt
705
+ impl_prompt="You are implementing tasks for spec '${spec_name}' in an autonomous development workflow.
706
+ The spike experiment for approach '${slug}' has passed validation. Now implement the full solution.
707
+
708
+ --- SPEC CONTENT ---
709
+ ${spec_content}
710
+ --- END SPEC ---
711
+
712
+ The validated experiment file is at: ${experiment_file}
713
+ Review it to understand the approach that was validated during the spike.
714
+
715
+ Your tasks:
716
+ 1. Read the spec carefully and generate a list of implementation tasks from it.
717
+ 2. Implement each task with atomic commits. Each commit message must follow the format:
718
+ feat(${spec_name}): {task description}
719
+ 3. For each completed task, write a result YAML file at:
720
+ .deepflow/results/{task-slug}.yaml
721
+ Each YAML must contain:
722
+ - task: short task name
723
+ - spec: ${spec_name}
724
+ - status: passed OR failed
725
+ - summary: one-line summary of what was implemented
726
+ 4. Create the .deepflow/results directory if it does not exist.
727
+
728
+ Important:
729
+ - Build on top of the spike commits already in this worktree.
730
+ - Be thorough — this is the full implementation, not a spike.
731
+ - Stage and commit each task separately for clean atomic commits."
732
+
733
+ auto_log "Spawning implementation for ${slug} (spec=${spec_name}, cycle=${cycle})"
734
+ echo "Spawning implementation: ${slug}"
735
+
736
+ (
737
+ cd "$worktree_path"
738
+ run_claude_monitored "$worktree_path" "$impl_prompt" > /dev/null
739
+ ) &
740
+ pids+=($!)
741
+ impl_slugs+=("$slug")
742
+ done
743
+
744
+ # Wait for all implementations to complete
745
+ auto_log "Waiting for all ${#pids[@]} implementation(s) to complete..."
746
+ wait
747
+ auto_log "All implementations completed for ${spec_name} cycle ${cycle}"
748
+
749
+ # Collect results
750
+ for slug in "${impl_slugs[@]}"; do
751
+ local worktree_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
752
+ local results_dir="${worktree_path}/.deepflow/results"
753
+
754
+ if [[ -d "$results_dir" ]]; then
755
+ local result_count=0
756
+ local pass_count=0
757
+ local fail_count=0
758
+
759
+ for result_file in "${results_dir}"/*.yaml; do
760
+ [[ -e "$result_file" ]] || continue
761
+ result_count=$((result_count + 1))
762
+
763
+ local status
764
+ status="$(grep -m1 '^status:' "$result_file" | sed 's/^status:[[:space:]]*//' | tr -d '[:space:]')" || status="unknown"
765
+
766
+ local task_name
767
+ task_name="$(basename "$result_file" .yaml)"
768
+
769
+ if [[ "$status" == "passed" ]]; then
770
+ pass_count=$((pass_count + 1))
771
+ auto_log "PASSED implementation task: ${task_name} (slug=${slug}, spec=${spec_name})"
772
+ else
773
+ fail_count=$((fail_count + 1))
774
+ auto_log "FAILED implementation task: ${task_name} status=${status} (slug=${slug}, spec=${spec_name})"
775
+ fi
776
+ done
777
+
778
+ auto_log "Implementation results for ${slug}: ${result_count} tasks, ${pass_count} passed, ${fail_count} failed"
779
+ echo "Implementation ${slug}: ${result_count} tasks (${pass_count} passed, ${fail_count} failed)"
780
+ else
781
+ auto_log "No result files found for implementation ${slug} (spec=${spec_name})"
782
+ echo "Implementation ${slug}: no result files found"
783
+ fi
784
+ done
785
+
786
+ return 0
787
+ }
788
+
789
+ run_selection() {
790
+ local spec_file="$1"
791
+ local spec_name="$2"
792
+ local cycle="$3"
793
+
794
+ auto_log "run_selection called for ${spec_name} cycle ${cycle}"
795
+
796
+ # -----------------------------------------------------------------------
797
+ # 1. Gather artifacts from all implementation worktrees for this spec+cycle
798
+ # -----------------------------------------------------------------------
799
+ local -a approach_slugs=()
800
+ local artifacts_block=""
801
+
802
+ # Parse slugs from the hypotheses file for this cycle
803
+ local hypotheses_file="${PROJECT_ROOT}/.deepflow/hypotheses/${spec_name}-cycle-${cycle}.json"
804
+ if [[ ! -f "$hypotheses_file" ]]; then
805
+ auto_log "ERROR: hypotheses file not found for selection: ${hypotheses_file}"
806
+ echo "Error: no hypotheses file for selection" >&2
807
+ return 1
808
+ fi
809
+
810
+ while IFS= read -r slug; do
811
+ approach_slugs+=("$slug")
812
+ done < <(grep -o '"slug"[[:space:]]*:[[:space:]]*"[^"]*"' "$hypotheses_file" | sed 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
813
+
814
+ if [[ ${#approach_slugs[@]} -eq 0 ]]; then
815
+ auto_log "ERROR: no approaches found for selection (spec=${spec_name}, cycle=${cycle})"
816
+ echo "Error: no approaches to select from" >&2
817
+ return 1
818
+ fi
819
+
820
+ local approach_index=0
821
+ for slug in "${approach_slugs[@]}"; do
822
+ approach_index=$((approach_index + 1))
823
+ local worktree_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
824
+
825
+ artifacts_block="${artifacts_block}
826
+ === APPROACH ${approach_index}: ${slug} ===
827
+ "
828
+
829
+ # Collect result YAMLs from worktree
830
+ local results_dir="${worktree_path}/.deepflow/results"
831
+ if [[ -d "$results_dir" ]]; then
832
+ for yaml_file in "${results_dir}"/*.yaml; do
833
+ [[ -e "$yaml_file" ]] || continue
834
+ artifacts_block="${artifacts_block}
835
+ --- Result: $(basename "$yaml_file") ---
836
+ $(cat "$yaml_file")
837
+ "
838
+ done
839
+ else
840
+ artifacts_block="${artifacts_block}
841
+ [No result YAML files found]
842
+ "
843
+ fi
844
+
845
+ # Collect experiment files (passed experiments from main project dir)
846
+ local experiment_file="${PROJECT_ROOT}/.deepflow/experiments/${spec_name}--${slug}--passed.md"
847
+ if [[ -f "$experiment_file" ]]; then
848
+ artifacts_block="${artifacts_block}
849
+ --- Experiment: $(basename "$experiment_file") ---
850
+ $(cat "$experiment_file")
851
+ "
852
+ fi
853
+
854
+ artifacts_block="${artifacts_block}
855
+ === END APPROACH ${approach_index} ===
856
+ "
857
+ done
858
+
859
+ # -----------------------------------------------------------------------
860
+ # 2. Build selection prompt
861
+ # -----------------------------------------------------------------------
862
+ local selection_prompt
863
+ selection_prompt="You are an adversarial quality judge in an autonomous development workflow.
864
+ Your job is to compare implementation approaches for spec '${spec_name}' and select the best one — or reject all if quality is insufficient.
865
+
866
+ IMPORTANT:
867
+ - This selection phase ALWAYS runs, even with only 1 approach. With a single approach you act as a quality gate.
868
+ - You CAN and SHOULD reject all approaches if the quality is insufficient. Do not rubber-stamp poor work.
869
+ - Base your judgment ONLY on the artifacts provided below. Do NOT read code files.
870
+
871
+ There are ${#approach_slugs[@]} approach(es) to evaluate:
872
+
873
+ ${artifacts_block}
874
+
875
+ Respond with ONLY a JSON object (no markdown fences, no explanation). The JSON must have this exact structure:
876
+
877
+ {
878
+ \"winner\": \"slug-of-winner-or-empty-string-if-rejecting-all\",
879
+ \"rankings\": [
880
+ {\"slug\": \"approach-slug\", \"rank\": 1, \"rationale\": \"why this rank\"},
881
+ {\"slug\": \"approach-slug\", \"rank\": 2, \"rationale\": \"why this rank\"}
882
+ ],
883
+ \"reject_all\": false,
884
+ \"rejection_rationale\": \"\"
885
+ }
886
+
887
+ Rules for the JSON:
888
+ - rankings must include ALL approaches, ranked from best (1) to worst
889
+ - If reject_all is true, winner must be an empty string and rejection_rationale must explain why
890
+ - If reject_all is false, winner must be the slug of the rank-1 approach
891
+ - Output ONLY the JSON object. No other text."
892
+
893
+ # -----------------------------------------------------------------------
894
+ # 3. Spawn fresh claude -p (NOT in any worktree)
895
+ # -----------------------------------------------------------------------
896
+ auto_log "Spawning fresh claude -p for selection (spec=${spec_name}, cycle=${cycle})"
897
+
898
+ local raw_output
899
+ raw_output="$(run_claude_monitored "${PROJECT_ROOT}" "$selection_prompt")" || {
900
+ auto_log "ERROR: claude -p failed for selection (spec=${spec_name}, cycle=${cycle})"
901
+ echo "Error: claude -p failed for selection" >&2
902
+ return 1
903
+ }
904
+
905
+ # Parse JSON from output
906
+ local json_output
907
+ json_output="$(echo "$raw_output" | sed -n '/^{/,/^}/p')"
908
+ if [[ -z "$json_output" ]]; then
909
+ json_output="$(echo "$raw_output" | grep -o '{.*}')" || true
910
+ fi
911
+ if [[ -z "$json_output" ]]; then
912
+ auto_log "ERROR: could not parse JSON from selection output (spec=${spec_name}, cycle=${cycle})"
913
+ echo "Error: failed to parse selection JSON" >&2
914
+ echo "Raw output: ${raw_output}" >&2
915
+ return 1
916
+ fi
917
+
918
+ # Extract fields from JSON
919
+ local reject_all winner rejection_rationale
920
+ reject_all="$(echo "$json_output" | grep -o '"reject_all"[[:space:]]*:[[:space:]]*[a-z]*' | sed 's/.*:[[:space:]]*//')" || reject_all="false"
921
+ winner="$(echo "$json_output" | grep -o '"winner"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"winner"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || winner=""
922
+ rejection_rationale="$(echo "$json_output" | grep -o '"rejection_rationale"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"rejection_rationale"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || rejection_rationale=""
923
+
924
+ # -----------------------------------------------------------------------
925
+ # 4. Process verdict
926
+ # -----------------------------------------------------------------------
927
+ if [[ "$reject_all" == "true" ]]; then
928
+ auto_log "REJECTED ALL approaches for ${spec_name} cycle ${cycle}: ${rejection_rationale}"
929
+ echo "Selection REJECTED ALL approaches for ${spec_name}: ${rejection_rationale}"
930
+
931
+ # Find the best-ranked slug to keep
932
+ local best_slug=""
933
+ best_slug="$(echo "$json_output" | grep -o '"slug"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || true
934
+
935
+ # Clean up worktrees — keep only the best-ranked one
936
+ for slug in "${approach_slugs[@]}"; do
937
+ if [[ "$slug" == "$best_slug" ]]; then
938
+ auto_log "Keeping best-ranked rejected worktree: ${slug}"
939
+ continue
940
+ fi
941
+ local wt_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
942
+ if [[ -d "$wt_path" ]]; then
943
+ git worktree remove --force "$wt_path" 2>/dev/null || true
944
+ git branch -D "df/${spec_name}-${slug}" 2>/dev/null || true
945
+ auto_log "Cleaned up rejected worktree: ${slug}"
946
+ fi
947
+ done
948
+
949
+ return 1
950
+ fi
951
+
952
+ # Winner selected
953
+ if [[ -z "$winner" ]]; then
954
+ auto_log "ERROR: no winner slug in selection output (spec=${spec_name}, cycle=${cycle})"
955
+ echo "Error: selection returned no winner" >&2
956
+ return 1
957
+ fi
958
+
959
+ auto_log "SELECTED winner '${winner}' for ${spec_name} cycle ${cycle}"
960
+ echo "Selection WINNER: ${winner} for ${spec_name}"
961
+
962
+ # Store winner
963
+ mkdir -p "${PROJECT_ROOT}/.deepflow/selection"
964
+ cat > "${PROJECT_ROOT}/.deepflow/selection/${spec_name}-winner.json" <<WINNER_EOF
965
+ {
966
+ "spec": "${spec_name}",
967
+ "cycle": ${cycle},
968
+ "winner": "${winner}",
969
+ "selection_output": $(echo "$json_output" | head -50)
970
+ }
971
+ WINNER_EOF
972
+ auto_log "Wrote winner file: .deepflow/selection/${spec_name}-winner.json"
973
+
974
+ # Clean up non-winner worktrees
975
+ for slug in "${approach_slugs[@]}"; do
976
+ if [[ "$slug" == "$winner" ]]; then
977
+ continue
978
+ fi
979
+ local wt_path="${PROJECT_ROOT}/.deepflow/worktrees/${spec_name}-${slug}"
980
+ if [[ -d "$wt_path" ]]; then
981
+ git worktree remove --force "$wt_path" 2>/dev/null || true
982
+ git branch -D "df/${spec_name}-${slug}" 2>/dev/null || true
983
+ auto_log "Cleaned up non-winner worktree: ${slug}"
984
+ fi
985
+ done
986
+
987
+ return 0
988
+ }
989
+
990
+ generate_report() {
991
+ auto_log "generate_report called (INTERRUPTED=${INTERRUPTED})"
992
+
993
+ local report_file="${PROJECT_ROOT}/.deepflow/auto-report.md"
994
+ mkdir -p "${PROJECT_ROOT}/.deepflow"
995
+
996
+ # -----------------------------------------------------------------
997
+ # Determine overall status and build per-spec status table
998
+ # -----------------------------------------------------------------
999
+ local overall_status="converged"
1000
+ local -a all_spec_names=()
1001
+
1002
+ # Discover all spec names from doing-*.md files
1003
+ local specs_dir="${PROJECT_ROOT}/specs"
1004
+ if [[ -d "$specs_dir" ]]; then
1005
+ for f in "${specs_dir}"/doing-*.md; do
1006
+ [[ -e "$f" ]] || continue
1007
+ local sname
1008
+ sname="$(basename "$f" .md)"
1009
+ all_spec_names+=("$sname")
1010
+
1011
+ # If status was not set by the main loop, determine it now
1012
+ if [[ -z "${SPEC_STATUS[$sname]+x}" ]]; then
1013
+ # Check if winner file exists
1014
+ if [[ -f "${PROJECT_ROOT}/.deepflow/selection/${sname}-winner.json" ]]; then
1015
+ SPEC_STATUS[$sname]="converged"
1016
+ # Extract winner slug
1017
+ local w_slug
1018
+ w_slug="$(grep -o '"winner"[[:space:]]*:[[:space:]]*"[^"]*"' "${PROJECT_ROOT}/.deepflow/selection/${sname}-winner.json" | sed 's/.*"winner"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || w_slug=""
1019
+ SPEC_WINNER[$sname]="$w_slug"
1020
+ elif [[ "$INTERRUPTED" == "true" ]]; then
1021
+ SPEC_STATUS[$sname]="in-progress"
1022
+ else
1023
+ SPEC_STATUS[$sname]="halted"
1024
+ fi
1025
+ fi
1026
+ done
1027
+ fi
1028
+
1029
+ # If interrupted, override any unfinished specs
1030
+ if [[ "$INTERRUPTED" == "true" ]]; then
1031
+ for sname in "${all_spec_names[@]}"; do
1032
+ if [[ "${SPEC_STATUS[$sname]}" != "converged" ]]; then
1033
+ SPEC_STATUS[$sname]="in-progress"
1034
+ fi
1035
+ done
1036
+ overall_status="in-progress"
1037
+ else
1038
+ # Determine overall status from per-spec statuses
1039
+ for sname in "${all_spec_names[@]}"; do
1040
+ local s="${SPEC_STATUS[$sname]}"
1041
+ if [[ "$s" == "halted" ]]; then
1042
+ overall_status="halted"
1043
+ elif [[ "$s" == "in-progress" ]]; then
1044
+ overall_status="in-progress"
1045
+ fi
1046
+ done
1047
+ fi
1048
+
1049
+ # -----------------------------------------------------------------
1050
+ # Build report
1051
+ # -----------------------------------------------------------------
1052
+ {
1053
+ # Section 1: Resultado
1054
+ echo "## Resultado"
1055
+ echo ""
1056
+ echo "Status: ${overall_status}"
1057
+ echo ""
1058
+
1059
+ # Winner info (if converged)
1060
+ if [[ "$overall_status" == "converged" ]]; then
1061
+ for sname in "${all_spec_names[@]}"; do
1062
+ local w="${SPEC_WINNER[$sname]:-}"
1063
+ if [[ -n "$w" ]]; then
1064
+ local summary=""
1065
+ local winner_file="${PROJECT_ROOT}/.deepflow/selection/${sname}-winner.json"
1066
+ if [[ -f "$winner_file" ]]; then
1067
+ summary="$(grep -o '"winner"[[:space:]]*:[[:space:]]*"[^"]*"' "$winner_file" | sed 's/.*"\([^"]*\)".*/\1/')" || summary=""
1068
+ fi
1069
+ echo "Winner: ${w} (spec: ${sname})"
1070
+ fi
1071
+ done
1072
+ echo ""
1073
+ fi
1074
+
1075
+ # Per-spec status table
1076
+ echo "| Spec | Status | Winner |"
1077
+ echo "|------|--------|--------|"
1078
+ for sname in "${all_spec_names[@]}"; do
1079
+ local s="${SPEC_STATUS[$sname]:-unknown}"
1080
+ local w="${SPEC_WINNER[$sname]:--}"
1081
+ echo "| ${sname} | ${s} | ${w} |"
1082
+ done
1083
+ echo ""
1084
+
1085
+ # Section 2: Mudancas
1086
+ echo "## Mudancas"
1087
+ echo ""
1088
+
1089
+ local has_changes=false
1090
+ for sname in "${all_spec_names[@]}"; do
1091
+ local w="${SPEC_WINNER[$sname]:-}"
1092
+ if [[ -n "$w" ]]; then
1093
+ has_changes=true
1094
+ local branch_name="df/${sname}-${w}"
1095
+ echo "### ${sname} (winner: ${w})"
1096
+ echo ""
1097
+ echo '```'
1098
+ git diff --stat "main...${branch_name}" 2>/dev/null || echo "(branch ${branch_name} not found)"
1099
+ echo '```'
1100
+ echo ""
1101
+ fi
1102
+ done
1103
+ if [[ "$has_changes" == "false" ]]; then
1104
+ echo "No changes selected"
1105
+ echo ""
1106
+ fi
1107
+
1108
+ # Section 3: Decisoes
1109
+ echo "## Decisoes"
1110
+ echo ""
1111
+
1112
+ local decisions_log="${PROJECT_ROOT}/.deepflow/auto-decisions.log"
1113
+ if [[ -f "$decisions_log" ]]; then
1114
+ cat "$decisions_log"
1115
+ else
1116
+ echo "No decisions logged"
1117
+ fi
1118
+ echo ""
1119
+ } > "$report_file"
1120
+
1121
+ auto_log "Report written to ${report_file}"
1122
+ echo "Report written to ${report_file}"
1123
+ return 0
1124
+ }
1125
+
1126
+ # ---------------------------------------------------------------------------
1127
+ # Main loop
1128
+ # ---------------------------------------------------------------------------
1129
+
1130
+ run_spec_cycle() {
1131
+ local spec_file="$1"
1132
+ local cycle=0
1133
+
1134
+ auto_log "Starting cycles for spec: ${spec_file} (max_cycles=${MAX_CYCLES}, hypotheses=${HYPOTHESES})"
1135
+
1136
+ while true; do
1137
+ # Check cycle cap (0 = unlimited)
1138
+ if [[ "$MAX_CYCLES" -gt 0 && "$cycle" -ge "$MAX_CYCLES" ]]; then
1139
+ auto_log "Reached max_cycles=${MAX_CYCLES} for ${spec_file}. Halting."
1140
+ echo "Reached max cycles (${MAX_CYCLES}) for $(basename "$spec_file")"
1141
+ break
1142
+ fi
1143
+
1144
+ local spec_name
1145
+ spec_name="$(basename "$spec_file" .md)"
1146
+
1147
+ auto_log "Cycle ${cycle} for ${spec_file}"
1148
+ echo "--- Cycle ${cycle} for $(basename "$spec_file") ---"
1149
+
1150
+ generate_hypotheses "$spec_file" "$spec_name" "$cycle" "$HYPOTHESES"
1151
+ run_spikes "$spec_file" "$spec_name" "$cycle"
1152
+ run_implementations "$spec_file" "$spec_name" "$cycle"
1153
+
1154
+ if run_selection "$spec_file" "$spec_name" "$cycle"; then
1155
+ auto_log "Selection accepted for ${spec_file} at cycle ${cycle}"
1156
+ echo "Selection accepted for $(basename "$spec_file") at cycle ${cycle}"
1157
+ # Track convergence
1158
+ SPEC_STATUS[$spec_name]="converged"
1159
+ local w_slug
1160
+ w_slug="$(grep -o '"winner"[[:space:]]*:[[:space:]]*"[^"]*"' "${PROJECT_ROOT}/.deepflow/selection/${spec_name}-winner.json" | sed 's/.*"winner"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || w_slug=""
1161
+ SPEC_WINNER[$spec_name]="$w_slug"
1162
+ break
1163
+ fi
1164
+
1165
+ auto_log "Selection rejected for ${spec_file} at cycle ${cycle}. Continuing."
1166
+ cycle=$((cycle + 1))
1167
+ done
1168
+
1169
+ # If we exited the loop without converging, mark as halted
1170
+ if [[ "${SPEC_STATUS[$spec_name]:-}" != "converged" ]]; then
1171
+ SPEC_STATUS[$spec_name]="halted"
1172
+ fi
1173
+ }
1174
+
1175
+ main() {
1176
+ parse_flags "$@"
1177
+
1178
+ auto_log "deepflow-auto started (parallel=${PARALLEL}, hypotheses=${HYPOTHESES}, max_cycles=${MAX_CYCLES}, continue=${CONTINUE}, fresh=${FRESH})"
1179
+
1180
+ echo "deepflow-auto: discovering specs..."
1181
+
1182
+ local specs
1183
+ specs="$(discover_specs)"
1184
+
1185
+ local spec_count
1186
+ spec_count="$(echo "$specs" | wc -l | tr -d ' ')"
1187
+ echo "Found ${spec_count} spec(s):"
1188
+ echo "$specs" | while read -r s; do echo " - $(basename "$s")"; done
1189
+
1190
+ auto_log "Discovered ${spec_count} spec(s)"
1191
+
1192
+ # Process each spec
1193
+ echo "$specs" | while read -r spec_file; do
1194
+ local spec_name
1195
+ spec_name="$(basename "$spec_file" .md)"
1196
+
1197
+ # Validate spec before processing
1198
+ local lint_script=""
1199
+ if [[ -f "${PROJECT_ROOT}/bin/df-spec-lint.js" ]]; then
1200
+ lint_script="${PROJECT_ROOT}/bin/df-spec-lint.js"
1201
+ elif [[ -f "${PROJECT_ROOT}/hooks/df-spec-lint.js" ]]; then
1202
+ lint_script="${PROJECT_ROOT}/hooks/df-spec-lint.js"
1203
+ fi
1204
+
1205
+ if [[ -n "$lint_script" ]]; then
1206
+ if command -v node &>/dev/null; then
1207
+ if node "$lint_script" "$spec_file" --mode=auto 2>/dev/null; then
1208
+ auto_log "PASS: Spec $spec_name passed validation"
1209
+ else
1210
+ auto_log "SKIP: Spec $spec_name failed validation"
1211
+ echo "⚠ Skipping $spec_name: spec validation failed"
1212
+ continue
1213
+ fi
1214
+ else
1215
+ auto_log "WARN: node not available, skipping spec validation for $spec_name"
1216
+ fi
1217
+ else
1218
+ auto_log "WARN: df-spec-lint.js not found, skipping spec validation for $spec_name"
1219
+ fi
1220
+
1221
+ run_spec_cycle "$spec_file"
1222
+ done
1223
+
1224
+ generate_report
1225
+
1226
+ auto_log "deepflow-auto finished"
1227
+ echo "deepflow-auto: done."
1228
+ }
1229
+
1230
+ # ---------------------------------------------------------------------------
1231
+ # Signal handling
1232
+ # ---------------------------------------------------------------------------
1233
+
1234
+ handle_signal() {
1235
+ echo ""
1236
+ echo "deepflow-auto: interrupted, generating report..."
1237
+ INTERRUPTED=true
1238
+ generate_report
1239
+ auto_log "deepflow-auto interrupted by signal, exiting"
1240
+ exit 130
1241
+ }
1242
+
1243
+ trap handle_signal SIGINT SIGTERM
1244
+
1245
+ # Safety assertions: this script must never modify spec files or push to remotes.
1246
+ # These constraints are enforced by design — no write/push commands exist in this script.
1247
+
1248
+ main "$@"