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.
- package/README.md +156 -55
- package/bin/deepflow-auto.sh +1264 -0
- package/bin/install.js +21 -0
- package/hooks/df-spec-lint.js +197 -0
- package/package.json +1 -1
- package/src/commands/df/plan.md +4 -0
- package/src/commands/df/spec.md +7 -1
|
@@ -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 "$@"
|