deepflow 0.1.52 → 0.1.54

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