@ai-dev-methodologies/rlp-desk 0.7.4 → 0.8.0

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.
@@ -1,2678 +0,0 @@
1
- #!/bin/zsh
2
- set -uo pipefail
3
- # NOTE: We use set -u (undefined var check) and pipefail, but NOT set -e
4
- # because the main loop uses explicit error checks throughout.
5
-
6
- # =============================================================================
7
- # Ralph Desk Tmux Runner
8
- #
9
- # Implements the Leader loop from governance.md section 7 as a shell script.
10
- # Uses tmux proven patterns: write-then-notify, pane IDs (%N),
11
- # copy-mode guards, verification-based retry, heartbeat monitoring,
12
- # idle pane nudging, exponential backoff restarts, atomic file writes.
13
- #
14
- # Usage:
15
- # LOOP_NAME=<slug> ./run_ralph_desk.zsh
16
- #
17
- # Required env:
18
- # LOOP_NAME - slug identifier for the campaign
19
- #
20
- # Optional env:
21
- # ROOT - project root (default: $PWD)
22
- # MAX_ITER - max iterations (default: 20)
23
- # WORKER_MODEL - claude model for Worker (default: sonnet)
24
- # VERIFIER_MODEL - claude model for Verifier (default: opus)
25
- # POLL_INTERVAL - seconds between signal checks (default: 5)
26
- # ITER_TIMEOUT - per-iteration timeout in seconds (default: 600)
27
- # HEARTBEAT_STALE_THRESHOLD - seconds before heartbeat is stale (default: 120)
28
- # MAX_RESTARTS - max restart attempts per worker (default: 3)
29
- # IDLE_NUDGE_THRESHOLD - seconds of idle before nudge (default: 30)
30
- # MAX_NUDGES - max nudges per pane per iteration (default: 3)
31
- #
32
- # Per-role codex config:
33
- # WORKER_CODEX_MODEL - codex model for Worker (default: gpt-5.4)
34
- # WORKER_CODEX_REASONING - codex reasoning for Worker (default: high)
35
- # VERIFIER_CODEX_MODEL - codex model for Verifier (default: gpt-5.4)
36
- # VERIFIER_CODEX_REASONING - codex reasoning for Verifier (default: high)
37
- #
38
- # Consensus scope:
39
- # CONSENSUS_SCOPE - when consensus applies (default: all)
40
- # all=every verify, final-only=final ALL only
41
- #
42
- # Dependencies: tmux, claude CLI, jq
43
- # Optional: codex CLI (required when WORKER_ENGINE=codex, VERIFIER_ENGINE=codex, or VERIFY_CONSENSUS=1)
44
- # =============================================================================
45
-
46
- # --- Environment Variables ---
47
- SLUG="${LOOP_NAME:?ERROR: LOOP_NAME is required. Set it to the campaign slug.}"
48
- ROOT="${ROOT:-$PWD}"
49
- MAX_ITER="${MAX_ITER:-20}"
50
- WORKER_MODEL="${WORKER_MODEL:-haiku}"
51
- VERIFIER_MODEL="${VERIFIER_MODEL:-sonnet}"
52
- FINAL_VERIFIER_MODEL="${FINAL_VERIFIER_MODEL:-opus}"
53
- POLL_INTERVAL="${POLL_INTERVAL:-5}"
54
- ITER_TIMEOUT="${ITER_TIMEOUT:-600}"
55
- HEARTBEAT_STALE_THRESHOLD="${HEARTBEAT_STALE_THRESHOLD:-120}"
56
- MAX_RESTARTS="${MAX_RESTARTS:-3}"
57
- IDLE_NUDGE_THRESHOLD="${IDLE_NUDGE_THRESHOLD:-30}"
58
- MAX_NUDGES="${MAX_NUDGES:-3}"
59
- WITH_SELF_VERIFICATION="${WITH_SELF_VERIFICATION:-0}"
60
-
61
- # --- Engine Selection (auto-detect from model format: name=claude, name:reasoning=codex) ---
62
- # If model contains ":", it's codex format — auto-set engine and split model/reasoning
63
- _auto_detect_engine() {
64
- local model_var="$1" engine_var="$2" codex_model_var="$3" codex_reasoning_var="$4"
65
- local model_val="${(P)model_var}"
66
- if [[ "$model_val" == *:* ]]; then
67
- local model_part="${model_val%%:*}"
68
- local reasoning_part="${model_val##*:}"
69
- [[ "$model_part" == "spark" ]] && model_part="gpt-5.3-codex-spark"
70
- eval "$engine_var=codex"
71
- eval "$model_var=$model_part"
72
- [[ -n "$codex_model_var" ]] && eval "$codex_model_var=$model_part"
73
- [[ -n "$codex_reasoning_var" ]] && eval "$codex_reasoning_var=$reasoning_part"
74
- fi
75
- }
76
-
77
- WORKER_ENGINE="${WORKER_ENGINE:-claude}"
78
- VERIFIER_ENGINE="${VERIFIER_ENGINE:-claude}"
79
- FINAL_VERIFIER_ENGINE="${FINAL_VERIFIER_ENGINE:-claude}"
80
-
81
- # Auto-detect engine from model format for env var path (CLI path uses parse_model_flag)
82
- _auto_detect_engine WORKER_MODEL WORKER_ENGINE WORKER_CODEX_MODEL WORKER_CODEX_REASONING
83
- _auto_detect_engine VERIFIER_MODEL VERIFIER_ENGINE VERIFIER_CODEX_MODEL VERIFIER_CODEX_REASONING
84
- _auto_detect_engine FINAL_VERIFIER_MODEL FINAL_VERIFIER_ENGINE "" ""
85
- WORKER_CODEX_MODEL="${WORKER_CODEX_MODEL:-gpt-5.4}"
86
- WORKER_CODEX_REASONING="${WORKER_CODEX_REASONING:-high}" # low|medium|high
87
- VERIFIER_CODEX_MODEL="${VERIFIER_CODEX_MODEL:-gpt-5.4}"
88
- VERIFIER_CODEX_REASONING="${VERIFIER_CODEX_REASONING:-high}" # low|medium|high
89
- CODEX_BIN="" # resolved by check_dependencies when engine=codex
90
-
91
- # --- Verify Mode ---
92
- VERIFY_MODE="${VERIFY_MODE:-per-us}" # per-us|batch
93
- # Consensus: off|all|final-only (replaces VERIFY_CONSENSUS + FINAL_CONSENSUS + CONSENSUS_SCOPE)
94
- CONSENSUS_MODE="${CONSENSUS_MODE:-off}" # off|all|final-only
95
- CONSENSUS_MODEL="${CONSENSUS_MODEL:-gpt-5.4:medium}" # per-US cross-verifier (lighter)
96
- FINAL_CONSENSUS_MODEL="${FINAL_CONSENSUS_MODEL:-gpt-5.4:high}" # final cross-verifier (stricter)
97
- # Legacy compat: map old flags to CONSENSUS_MODE
98
- if [[ "${VERIFY_CONSENSUS:-0}" = "1" ]]; then
99
- CONSENSUS_MODE="${CONSENSUS_SCOPE:-all}"
100
- elif [[ "${FINAL_CONSENSUS:-0}" = "1" ]]; then
101
- CONSENSUS_MODE="final-only"
102
- fi
103
- CONSENSUS_SCOPE="${CONSENSUS_SCOPE:-${CONSENSUS_MODE}}"
104
- CB_THRESHOLD="${CB_THRESHOLD:-6}" # consecutive failures before BLOCKED (default: 6)
105
- # Effective CB threshold: doubled when consensus mode active
106
- if [[ "$CONSENSUS_MODE" != "off" ]]; then
107
- EFFECTIVE_CB_THRESHOLD=$(( CB_THRESHOLD * 2 ))
108
- else
109
- EFFECTIVE_CB_THRESHOLD=$CB_THRESHOLD
110
- fi
111
- _API_MAX_RETRIES="${_API_MAX_RETRIES:-5}"
112
- _API_RETRY_INTERVAL_S="${_API_RETRY_INTERVAL_S:-30}"
113
-
114
- # --- Derived Paths ---
115
- DESK="$ROOT/.claude/ralph-desk"
116
- PROMPTS_DIR="$DESK/prompts"
117
- CONTEXT_DIR="$DESK/context"
118
- MEMOS_DIR="$DESK/memos"
119
- LOGS_DIR="$DESK/logs/$SLUG"
120
- RUNTIME_DIR="$LOGS_DIR/runtime"
121
- PRD_FILE="$DESK/plans/prd-$SLUG.md"
122
- TEST_SPEC_FILE="$DESK/plans/test-spec-$SLUG.md"
123
- # --- Analytics Directory (user-level, cross-project) ---
124
- ANALYTICS_SLUG_HASH=$(echo -n "$ROOT" | md5 -q 2>/dev/null || md5sum <<< "$ROOT" | cut -d' ' -f1)
125
- ANALYTICS_DIR="$HOME/.claude/ralph-desk/analytics/${SLUG}--${ANALYTICS_SLUG_HASH:0:8}"
126
- CAMPAIGN_JSONL="$ANALYTICS_DIR/campaign.jsonl"
127
- METADATA_FILE="$ANALYTICS_DIR/metadata.json"
128
- WORKER_PROMPT_BASE="$PROMPTS_DIR/${SLUG}.worker.prompt.md"
129
- VERIFIER_PROMPT_BASE="$PROMPTS_DIR/${SLUG}.verifier.prompt.md"
130
- CONTEXT_FILE="$CONTEXT_DIR/${SLUG}-latest.md"
131
- MEMORY_FILE="$MEMOS_DIR/${SLUG}-memory.md"
132
- SIGNAL_FILE="$MEMOS_DIR/${SLUG}-iter-signal.json"
133
- DONE_CLAIM_FILE="$MEMOS_DIR/${SLUG}-done-claim.json"
134
- VERDICT_FILE="$MEMOS_DIR/${SLUG}-verify-verdict.json"
135
- COMPLETE_SENTINEL="$MEMOS_DIR/${SLUG}-complete.md"
136
- BLOCKED_SENTINEL="$MEMOS_DIR/${SLUG}-blocked.md"
137
- LOCKFILE_PATH="$DESK/logs/.rlp-desk-${SLUG}.lock"
138
- STATUS_FILE="$RUNTIME_DIR/status.json"
139
- SESSION_CONFIG="$RUNTIME_DIR/session-config.json"
140
- WORKER_HEARTBEAT="$RUNTIME_DIR/worker-heartbeat.json"
141
- VERIFIER_HEARTBEAT="$RUNTIME_DIR/verifier-heartbeat.json"
142
- COST_LOG="$LOGS_DIR/cost-log.jsonl"
143
-
144
- # --- Session Naming ---
145
- TIMESTAMP=$(date +%Y%m%d-%H%M%S)
146
- SESSION_NAME="rlp-desk-${SLUG}-${TIMESTAMP}"
147
-
148
- # --- State Tracking ---
149
- typeset -A LAST_PANE_CONTENT
150
- typeset -A PANE_IDLE_SINCE
151
- typeset -A WORKER_RESTARTS
152
- typeset -A US_FAIL_HISTORY
153
- STALE_CONTEXT_COUNT=0
154
- HEARTBEAT_STALE_COUNT=0
155
- MONITOR_FAILURE_COUNT=0
156
- CONSECUTIVE_FAILURES=0
157
- PREV_CONTEXT_HASH=""
158
- PREV_PRD_HASH=""
159
- PREV_PRD_US_LIST=""
160
- _PRD_CHANGED=0
161
- ITERATION=0
162
- START_TIME=$(date +%s)
163
- BASELINE_COMMIT="" # git HEAD at campaign start (captured before loop)
164
- CAMPAIGN_REPORT_GENERATED=0 # guard against double-generation in cleanup trap
165
- SV_REPORT_GENERATED=0 # guard against double-generation in generate_sv_report
166
- VERIFIED_US="" # comma-separated list of verified US IDs (per-us mode)
167
- CONSENSUS_ROUND=0 # current consensus round for current US
168
- US_LIST="" # comma-separated US IDs from PRD (per-us mode)
169
- LOCKFILE_ACQUIRED=0
170
- LOCK_WORKER_MODEL="${LOCK_WORKER_MODEL:-0}" # 0|1 — set by --lock-worker-model; disables progressive upgrade
171
- _SAME_US_FAIL_COUNT=0 # consecutive same-US fail counter (upgrade trigger at >= 2)
172
- _LAST_FAILED_US="" # last failed US ID (same-US tracking for upgrade logic)
173
- _MODEL_UPGRADED=0 # 1 if Worker model was auto-upgraded during campaign
174
- _ORIGINAL_WORKER_MODEL="" # WORKER_MODEL saved before first upgrade (for restore on pass)
175
- _ORIGINAL_WORKER_CODEX_REASONING="" # WORKER_CODEX_REASONING saved before first upgrade
176
-
177
- # =============================================================================
178
- # Utility Functions
179
- # =============================================================================
180
-
181
- DEBUG="${DEBUG:-0}"
182
- DEBUG_LOG="$ANALYTICS_DIR/debug.log"
183
-
184
- # Source shared business logic
185
- LIB_DIR="$(cd "$(dirname "$0")" && pwd)"
186
- source "$LIB_DIR/lib_ralph_desk.zsh"
187
-
188
- # A16: Warn if running in foreground (may conflict with Claude Code pane)
189
- if [[ -z "${RLP_BACKGROUND:-}" ]]; then
190
- echo "⚠ WARNING: Running in foreground. This may conflict with Claude Code's pane." >&2
191
- echo " Recommended: launch via Bash tool with run_in_background: true" >&2
192
- echo " Set RLP_BACKGROUND=1 to suppress this warning." >&2
193
- fi
194
-
195
- # check_dead_pane() — determine if pane command indicates a dead/exited process
196
- # Engine-aware: bash is normal for codex workers (trigger runs in bash),
197
- # but indicates dead pane for claude workers.
198
- # Args: $1=pane_current_command $2=engine (claude|codex) $3=role (worker|verifier)
199
- # Returns: 0 if dead, 1 if alive
200
- check_dead_pane() {
201
- local poll_cmd="$1"
202
- local engine="${2:-claude}"
203
- local role="${3:-worker}"
204
-
205
- if [[ -z "$poll_cmd" ]]; then
206
- return 0 # empty = dead
207
- elif [[ "$poll_cmd" == "zsh" ]]; then
208
- return 0 # bare zsh = dead
209
- elif [[ "$poll_cmd" == "bash" && "$engine" != "codex" ]]; then
210
- return 0 # bash = dead for claude (codex uses bash trigger)
211
- fi
212
- return 1 # alive
213
- }
214
-
215
- # launch_worker_codex() — launch codex Worker TUI, send instruction, verify submission
216
- # Matches launch_worker_claude() pattern for consistent tmux-visible execution.
217
- # Args: $1=pane_id $2=prompt_file $3=iteration $4=worker_launch_cmd
218
- # Returns: 0 on success, 1 on fatal failure
219
- launch_worker_codex() {
220
- local pane_id="$1"
221
- local prompt_file="$2"
222
- local iter="$3"
223
- local worker_launch="$4"
224
-
225
- log " Launching Worker codex TUI in pane $pane_id..."
226
- paste_to_pane "$pane_id" "$worker_launch"
227
- tmux send-keys -t "$pane_id" C-m
228
-
229
- # Wait for codex TUI to be ready
230
- if ! wait_for_pane_ready "$pane_id" 30; then
231
- log_error "Worker codex failed to start"
232
- return 1
233
- fi
234
-
235
- # Send instruction to codex TUI
236
- sleep 3
237
- local worker_instruction="Read and execute the instructions in $prompt_file"
238
- paste_to_pane "$pane_id" "$worker_instruction"
239
- tmux send-keys -t "$pane_id" C-m
240
- log_debug "Worker codex instruction sent (${#worker_instruction} chars)"
241
-
242
- # Submit loop — verify codex started working
243
- local submit_attempts=0
244
- while (( submit_attempts < 15 )); do
245
- sleep 2
246
- local pane_check
247
- pane_check=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null)
248
- if echo "$pane_check" | grep -qi "working\|thinking\|Exploring\|Running\|reading\|searching\|editing\|writing" 2>/dev/null; then
249
- log_debug "Worker codex started working after $((submit_attempts + 1)) checks"
250
- break
251
- fi
252
- if (( submit_attempts == 8 )); then
253
- log_debug "Adaptive instruction retry: clearing line and re-typing"
254
- tmux send-keys -t "$pane_id" C-u 2>/dev/null
255
- sleep 0.1
256
- paste_to_pane "$pane_id" "$worker_instruction"
257
- tmux send-keys -t "$pane_id" C-m
258
- fi
259
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
260
- sleep 0.3
261
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
262
- (( submit_attempts++ ))
263
- done
264
- return 0
265
- }
266
-
267
- # launch_worker_claude() — launch claude Worker TUI, send instruction, verify submission
268
- # Handles: TUI startup, wait_for_pane_ready, instruction send, 15-iteration submit loop,
269
- # restart recovery on submit failure.
270
- # Args: $1=pane_id $2=prompt_file $3=iteration $4=worker_launch_cmd
271
- # Returns: 0 on success, 1 on fatal failure (caller writes BLOCKED)
272
- launch_worker_claude() {
273
- local pane_id="$1"
274
- local prompt_file="$2"
275
- local iter="$3"
276
- local worker_launch="$4"
277
-
278
- log " Launching Worker claude in pane $pane_id..."
279
- paste_to_pane "$pane_id" "$worker_launch"
280
- tmux send-keys -t "$pane_id" C-m
281
-
282
- # Wait for claude TUI to be ready
283
- if ! wait_for_pane_ready "$pane_id" 30; then
284
- log_error "Worker claude failed to start"
285
- return 1
286
- fi
287
-
288
- # Send instruction to claude TUI
289
- sleep 3
290
- local worker_instruction="Read and execute the instructions in $prompt_file"
291
- paste_to_pane "$pane_id" "$worker_instruction"
292
- tmux send-keys -t "$pane_id" C-m
293
- log_debug "Worker instruction sent directly (${#worker_instruction} chars)"
294
-
295
- # 15-iteration submit loop — verify claude started working
296
- local submit_attempts=0
297
- while (( submit_attempts < 15 )); do
298
- sleep 2
299
- local pane_check
300
- pane_check=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null)
301
- if echo "$pane_check" | grep -qi "esc to interrupt\|thinking\|working\|kneading\|crunching\|clauding\|billowing\|brewing\|tinkering\|burrowing\|saut\|Exploring\|Running\|exec\|Explored\|Prestidigitating\|Undulating\|Reading\|Bash\|Edit\|Write\|Grep\|Glob" 2>/dev/null; then
302
- log_debug "Worker started working after $((submit_attempts + 1)) submit checks"
303
- log_debug "[FLOW] iter=$iter worker_submit_check=OK attempts=$((submit_attempts + 1))"
304
- break
305
- fi
306
- # Every 3 failed attempts, re-send full instruction
307
- if (( submit_attempts > 0 && submit_attempts % 3 == 0 )); then
308
- log_debug "Re-sending full worker instruction (attempt $submit_attempts)"
309
- tmux send-keys -t "$pane_id" C-u 2>/dev/null
310
- sleep 0.2
311
- paste_to_pane "$pane_id" "$worker_instruction"
312
- sleep 0.15
313
- tmux send-keys -t "$pane_id" C-m
314
- sleep 1
315
- fi
316
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
317
- sleep 0.3
318
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
319
- (( submit_attempts++ ))
320
- done
321
-
322
- # If 15 attempts failed, restart claude and retry
323
- if (( submit_attempts >= 15 )); then
324
- log " WARNING: Worker instruction not consumed after 15 attempts — restarting claude"
325
- log_debug "[GOV] iter=$iter worker_instruction_failed=true attempts=15 action=restart_claude"
326
- tmux send-keys -t "$pane_id" C-c 2>/dev/null
327
- sleep 0.5
328
- tmux send-keys -t "$pane_id" "/exit" C-m 2>/dev/null
329
- sleep 2
330
- wait_for_pane_ready "$pane_id" 10 2>/dev/null || true
331
- paste_to_pane "$pane_id" "$worker_launch"
332
- tmux send-keys -t "$pane_id" C-m
333
- if wait_for_pane_ready "$pane_id" 30; then
334
- sleep 3
335
- paste_to_pane "$pane_id" "$worker_instruction"
336
- tmux send-keys -t "$pane_id" C-m
337
- log " Worker restarted and instruction re-sent"
338
- log_debug "[FLOW] iter=$iter worker_restart_recovery=success"
339
- else
340
- log_error "Worker restart failed — pane not ready"
341
- log_debug "[FLOW] iter=$iter worker_restart_recovery=failed"
342
- fi
343
- fi
344
-
345
- return 0
346
- }
347
-
348
- # launch_verifier_codex() — launch codex Verifier TUI, send instruction, verify submission
349
- # Matches launch_verifier_claude() pattern for consistent tmux-visible execution.
350
- # Args: $1=pane_id $2=prompt_file $3=iteration $4=launch_cmd
351
- # Returns: 0 on success
352
- launch_verifier_codex() {
353
- local pane_id="$1"
354
- local prompt_file="$2"
355
- local iter="$3"
356
- local verifier_launch="$4"
357
-
358
- log " Launching Verifier codex TUI in pane $pane_id..."
359
- paste_to_pane "$pane_id" "$verifier_launch"
360
- tmux send-keys -t "$pane_id" C-m
361
-
362
- if ! wait_for_pane_ready "$pane_id" 30; then
363
- log_error "Verifier codex failed to start"
364
- return 1
365
- fi
366
-
367
- sleep 3
368
- local verifier_instruction="Read and execute the instructions in $prompt_file"
369
- paste_to_pane "$pane_id" "$verifier_instruction"
370
- tmux send-keys -t "$pane_id" C-m
371
- log_debug "Verifier codex instruction sent"
372
-
373
- # Submit loop — verify codex started working
374
- local submit_attempts=0
375
- while (( submit_attempts < 15 )); do
376
- sleep 2
377
- local vs_check
378
- vs_check=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null)
379
- if echo "$vs_check" | grep -qi "working\|thinking\|Exploring\|Running\|reading\|searching\|editing\|writing" 2>/dev/null; then
380
- log_debug "Verifier codex started working after $((submit_attempts + 1)) checks"
381
- break
382
- fi
383
- if (( submit_attempts == 8 )); then
384
- log_debug "Adaptive instruction retry: clearing line and re-typing"
385
- tmux send-keys -t "$pane_id" C-u 2>/dev/null
386
- sleep 0.1
387
- paste_to_pane "$pane_id" "$verifier_instruction"
388
- tmux send-keys -t "$pane_id" C-m
389
- fi
390
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
391
- sleep 0.3
392
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
393
- (( submit_attempts++ ))
394
- done
395
- return 0
396
- }
397
-
398
- # launch_verifier_claude() — launch claude Verifier TUI, send instruction, verify submission
399
- # Args: $1=pane_id $2=prompt_file $3=iteration $4=launch_cmd
400
- # Returns: 0 on success
401
- launch_verifier_claude() {
402
- local pane_id="$1"
403
- local prompt_file="$2"
404
- local iter="$3"
405
- local verifier_launch="$4"
406
-
407
- log " Launching Verifier claude in pane $pane_id..."
408
- paste_to_pane "$pane_id" "$verifier_launch"
409
- tmux send-keys -t "$pane_id" C-m
410
-
411
- if ! wait_for_pane_ready "$pane_id" 30; then
412
- log_error "Verifier failed to start"
413
- return 1
414
- fi
415
-
416
- sleep 3
417
- local verifier_instruction="Read and execute the instructions in $prompt_file"
418
- paste_to_pane "$pane_id" "$verifier_instruction"
419
- tmux send-keys -t "$pane_id" C-m
420
- log_debug "Verifier instruction sent directly"
421
-
422
- # Submit loop — verify verifier started working
423
- local submit_attempts=0
424
- while (( submit_attempts < 15 )); do
425
- sleep 2
426
- local vs_check
427
- vs_check=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null)
428
- if echo "$vs_check" | grep -qi "esc to interrupt\|thinking\|working\|kneading\|crunching\|clauding\|billowing\|brewing\|tinkering\|burrowing\|saut\|Exploring\|Running\|exec\|Explored" 2>/dev/null; then
429
- log_debug "Verifier started working after $((submit_attempts + 1)) checks"
430
- break
431
- fi
432
- if (( submit_attempts == 8 )); then
433
- log_debug "Adaptive instruction retry: clearing line and re-typing"
434
- tmux send-keys -t "$pane_id" C-u 2>/dev/null
435
- sleep 0.1
436
- paste_to_pane "$pane_id" "$verifier_instruction"
437
- tmux send-keys -t "$pane_id" C-m
438
- fi
439
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
440
- sleep 0.3
441
- tmux send-keys -t "$pane_id" C-m 2>/dev/null
442
- (( submit_attempts++ ))
443
- done
444
- return 0
445
- }
446
-
447
- # handle_worker_exit_codex() — handle codex worker process exit (1-shot exec)
448
- # On exit: check done-claim, auto-generate iter-signal.
449
- # Args: $1=iteration $2=signal_file
450
- # Returns: 0 (signal generated), 1 (error)
451
- handle_worker_exit_codex() {
452
- local iter="$1"
453
- local signal_file="$2"
454
-
455
- log " Codex worker process exited. Checking for done-claim..."
456
- if [[ -f "$DONE_CLAIM_FILE" ]]; then
457
- local dc_us_id
458
- dc_us_id=$(jq -r '.us_id // "unknown"' "$DONE_CLAIM_FILE" 2>/dev/null)
459
- log " Codex worker completed with done-claim (us_id=$dc_us_id). Auto-generating signal."
460
- echo '{"iteration":'"$iter"',"status":"verify","us_id":"'"$dc_us_id"'","summary":"auto-generated after codex exit","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$signal_file"
461
- else
462
- log " WARNING: Codex worker exited without done-claim. Generating verify signal for current US."
463
- local current_us
464
- current_us=$(jq -r '.us_id // "US-001"' "$DESK/memos/${SLUG}-iter-signal.json" 2>/dev/null || echo "US-001")
465
- local mem_us
466
- mem_us=$(sed -n 's/.*Next.*US-\([0-9]*\).*/US-\1/p' "$DESK/memos/${SLUG}-memory.md" 2>/dev/null | head -1)
467
- [[ -n "$mem_us" ]] && current_us="$mem_us"
468
- echo '{"iteration":'"$iter"',"status":"verify","us_id":"'"$current_us"'","summary":"auto-generated after codex exit (no done-claim)","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$signal_file"
469
- fi
470
- return 0
471
- }
472
-
473
- # handle_worker_exit_claude() — handle claude worker process exit (restart with backoff)
474
- # Args: $1=pane_id $2=iteration $3=trigger_file
475
- # Returns: 0 (restarted), 1 (max restarts exceeded)
476
- handle_worker_exit_claude() {
477
- local pane_id="$1"
478
- local iter="$2"
479
- local trigger_file="$3"
480
-
481
- log_error "Worker exited without writing signal file"
482
- if restart_worker "$pane_id" "$iter" "$trigger_file"; then
483
- return 0
484
- else
485
- return 1
486
- fi
487
- }
488
-
489
- # --- omc-teams pattern: Kill-and-replace dead/stuck worker panes ---
490
- replace_worker_pane() {
491
- local old_pane="$1"
492
- local role="$2" # "worker" or "verifier"
493
-
494
- log " Replacing dead $role pane $old_pane..."
495
- tmux kill-pane -t "$old_pane" 2>/dev/null
496
-
497
- # Create fresh pane maintaining original layout: worker(top-right) / verifier(bottom-right)
498
- local new_pane
499
- if [[ "$role" == "verifier" ]]; then
500
- # Verifier goes below worker: split vertically from worker pane
501
- if tmux display-message -t "$WORKER_PANE" -p '#{pane_id}' &>/dev/null; then
502
- new_pane=$(tmux split-window -v -d -t "$WORKER_PANE" -P -F '#{pane_id}' -c "$ROOT")
503
- else
504
- # Fallback: worker pane also dead, split horizontally from leader
505
- new_pane=$(tmux split-window -h -d -t "$LEADER_PANE" -P -F '#{pane_id}' -c "$ROOT")
506
- fi
507
- else
508
- # Worker goes above verifier: split vertically before verifier pane
509
- if tmux display-message -t "$VERIFIER_PANE" -p '#{pane_id}' &>/dev/null; then
510
- new_pane=$(tmux split-window -v -b -d -t "$VERIFIER_PANE" -P -F '#{pane_id}' -c "$ROOT")
511
- else
512
- # Fallback: verifier pane also dead, split horizontally from leader
513
- new_pane=$(tmux split-window -h -d -t "$LEADER_PANE" -P -F '#{pane_id}' -c "$ROOT")
514
- fi
515
- fi
516
-
517
- log " New $role pane: $new_pane (replaced $old_pane)"
518
- log_debug "[FLOW] iter=$ITERATION pane_replaced=${role} old=$old_pane new=$new_pane"
519
-
520
- # Update session-config.json with new pane ID
521
- if [[ -f "$SESSION_CONFIG" ]]; then
522
- jq --arg role "$role" --arg pane "$new_pane" \
523
- '.panes[$role] = $pane' "$SESSION_CONFIG" | atomic_write "$SESSION_CONFIG"
524
- log_debug "Updated session-config.json: $role pane → $new_pane"
525
- fi
526
-
527
- echo "$new_pane"
528
- }
529
-
530
- # =============================================================================
531
- # Dependency Checks
532
- # =============================================================================
533
-
534
- # --- governance.md s7 step 1: Validate prerequisites before starting ---
535
- check_dependencies() {
536
- local missing=0
537
-
538
- if ! command -v tmux >/dev/null 2>&1; then
539
- log_error "tmux is required but not found. Install with: brew install tmux"
540
- missing=1
541
- fi
542
-
543
- # claude required only when claude engine is used for Worker or Verifier execution;
544
- # codex-only campaigns can run without claude — generate_sv_report degrades gracefully
545
- if [[ "$WORKER_ENGINE" != "codex" || "$VERIFIER_ENGINE" != "codex" ]]; then
546
- if ! command -v claude >/dev/null 2>&1; then
547
- log_error "claude CLI is required but not found. See: https://docs.anthropic.com/en/docs/claude-cli"
548
- missing=1
549
- fi
550
- fi
551
-
552
- if ! command -v jq >/dev/null 2>&1; then
553
- log_error "jq is required but not found. Install with: brew install jq"
554
- missing=1
555
- fi
556
-
557
- # Codex binary required only when engine=codex or consensus verification is enabled
558
- if [[ "$WORKER_ENGINE" = "codex" || "$VERIFIER_ENGINE" = "codex" || "$CONSENSUS_MODE" != "off" ]]; then
559
- if ! command -v codex >/dev/null 2>&1; then
560
- log_error "codex CLI not found. Install: npm install -g @openai/codex"
561
- missing=1
562
- fi
563
- fi
564
-
565
- if (( missing )); then
566
- exit 1
567
- fi
568
-
569
- # Resolve full path to claude binary when claude engine is in use
570
- if [[ "$WORKER_ENGINE" != "codex" || "$VERIFIER_ENGINE" != "codex" ]]; then
571
- CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "claude")
572
- log " Claude binary: $CLAUDE_BIN"
573
- fi
574
-
575
- # Resolve codex binary if needed
576
- if [[ "$WORKER_ENGINE" = "codex" || "$VERIFIER_ENGINE" = "codex" || "$CONSENSUS_MODE" != "off" ]]; then
577
- CODEX_BIN=$(command -v codex 2>/dev/null || echo "codex")
578
- log " Codex binary: $CODEX_BIN"
579
- fi
580
- }
581
-
582
- # =============================================================================
583
- # Session Management (tmux pattern: pane IDs)
584
- # =============================================================================
585
-
586
- # --- governance.md s7 step 1: Check for existing sessions ---
587
- check_existing_sessions() {
588
- local current_session
589
- current_session=$(tmux display-message -p '#{session_name}' 2>/dev/null || echo "")
590
- local existing
591
- existing=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^rlp-desk-${SLUG}-" | grep -v "^${current_session}$" || true)
592
- if [[ -n "$existing" ]]; then
593
- log_error "Existing tmux session(s) found for slug '$SLUG':"
594
- echo "$existing" | while read -r s; do
595
- echo " - $s"
596
- done
597
- echo ""
598
- echo "Kill existing session first:"
599
- echo " tmux kill-session -t <session-name>"
600
- exit 1
601
- fi
602
- }
603
-
604
- # --- governance.md s7 step 1: Create tmux session with pane IDs (%N) ---
605
- create_session() {
606
- log "Creating tmux session: $SESSION_NAME"
607
-
608
- # tmux split-pane pattern
609
- if [[ -n "${TMUX:-}" ]]; then
610
- # Inside tmux: split CURRENT pane in place
611
- # Current pane stays as-is (leader/user stays here)
612
- # Worker/Verifier appear on the RIGHT, user sees them immediately
613
- LEADER_PANE=$(tmux display-message -p '#{pane_id}')
614
- SESSION_NAME=$(tmux display-message -p '#{session_name}')
615
- log " Splitting current pane in session: $SESSION_NAME"
616
-
617
- # -h off current pane → right column (worker)
618
- WORKER_PANE=$(tmux split-window -h -d -t "$LEADER_PANE" -P -F '#{pane_id}' -c "$ROOT")
619
- # -v off worker → stacked below on right (verifier)
620
- VERIFIER_PANE=$(tmux split-window -v -d -t "$WORKER_PANE" -P -F '#{pane_id}' -c "$ROOT")
621
- else
622
- # Outside tmux: wrap current terminal into a new tmux session and attach
623
- # tmux pattern: user sees panes immediately, no separate attach needed
624
- tmux new-session -d -s "$SESSION_NAME" -x 200 -y 50 -c "$ROOT"
625
- LEADER_PANE=$(tmux display-message -p -t "$SESSION_NAME" '#{pane_id}')
626
- WORKER_PANE=$(tmux split-window -h -d -t "$LEADER_PANE" -P -F '#{pane_id}' -c "$ROOT")
627
- VERIFIER_PANE=$(tmux split-window -v -d -t "$WORKER_PANE" -P -F '#{pane_id}' -c "$ROOT")
628
-
629
- fi
630
-
631
- # Set pane titles and enable border labels for visual distinction
632
- local worker_label="Worker ($WORKER_ENGINE:$WORKER_MODEL)"
633
- local verifier_label="Verifier ($VERIFIER_ENGINE:$VERIFIER_MODEL)"
634
- [[ "$CONSENSUS_MODE" != "off" ]] && verifier_label="Verifier ($VERIFIER_ENGINE:$VERIFIER_MODEL + consensus)"
635
- tmux select-pane -t "$LEADER_PANE" -T "Leader" 2>/dev/null
636
- tmux select-pane -t "$WORKER_PANE" -T "$worker_label" 2>/dev/null
637
- tmux select-pane -t "$VERIFIER_PANE" -T "$verifier_label" 2>/dev/null
638
- # Color-coded pane borders: green=leader, blue=worker, yellow=verifier
639
- tmux set-option -p -t "$LEADER_PANE" pane-border-style "fg=green" 2>/dev/null
640
- tmux set-option -p -t "$WORKER_PANE" pane-border-style "fg=blue" 2>/dev/null
641
- tmux set-option -p -t "$VERIFIER_PANE" pane-border-style "fg=yellow" 2>/dev/null
642
- # Show pane titles in border
643
- tmux set-option pane-border-status top 2>/dev/null
644
- tmux set-option pane-border-format "#{?pane_active,#[fg=white bold],#[fg=grey]} #{pane_title} " 2>/dev/null
645
-
646
- log " Leader pane: $LEADER_PANE"
647
- log " Worker pane: $WORKER_PANE"
648
- log " Verifier pane: $VERIFIER_PANE"
649
-
650
- # AC12: Capture baseline commit before writing session config
651
- BASELINE_COMMIT=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo "none")
652
-
653
- # Truncate cost-log for fresh run (previous data in versioned campaign reports)
654
- > "$COST_LOG"
655
-
656
- # SV flag warning for tmux mode
657
- if (( WITH_SELF_VERIFICATION )); then
658
- log " NOTE: --with-self-verification recorded but SV report generation is Agent-mode only"
659
- fi
660
-
661
- # Write session config (atomic write)
662
- echo '{
663
- "session_name": "'"$SESSION_NAME"'",
664
- "slug": "'"$SLUG"'",
665
- "created_at": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
666
- "baseline_commit": "'"$BASELINE_COMMIT"'",
667
- "panes": {
668
- "leader": "'"$LEADER_PANE"'",
669
- "worker": "'"$WORKER_PANE"'",
670
- "verifier": "'"$VERIFIER_PANE"'"
671
- },
672
- "pid": '$$',
673
- "root": "'"$ROOT"'",
674
- "models": {
675
- "worker": "'"$WORKER_MODEL"'",
676
- "verifier": "'"$VERIFIER_MODEL"'"
677
- },
678
- "engines": {
679
- "worker": "'"$WORKER_ENGINE"'",
680
- "verifier": "'"$VERIFIER_ENGINE"'",
681
- "worker_codex_model": "'"$WORKER_CODEX_MODEL"'",
682
- "worker_codex_reasoning": "'"$WORKER_CODEX_REASONING"'",
683
- "verifier_codex_model": "'"$VERIFIER_CODEX_MODEL"'",
684
- "verifier_codex_reasoning": "'"$VERIFIER_CODEX_REASONING"'"
685
- },
686
- "verification": {
687
- "verify_mode": "'"$VERIFY_MODE"'",
688
- "consensus_mode": "'"$CONSENSUS_MODE"'"
689
- },
690
- "config": {
691
- "max_iter": '"$MAX_ITER"',
692
- "poll_interval": '"$POLL_INTERVAL"',
693
- "iter_timeout": '"$ITER_TIMEOUT"',
694
- "heartbeat_stale_threshold": '"$HEARTBEAT_STALE_THRESHOLD"',
695
- "max_restarts": '"$MAX_RESTARTS"',
696
- "idle_nudge_threshold": '"$IDLE_NUDGE_THRESHOLD"',
697
- "max_nudges": '"$MAX_NUDGES"',
698
- "cb_threshold": '"$CB_THRESHOLD"',
699
- "effective_cb_threshold": '"$EFFECTIVE_CB_THRESHOLD"',
700
- "with_self_verification": '"$WITH_SELF_VERIFICATION"'
701
- }
702
- }' | atomic_write "$SESSION_CONFIG"
703
-
704
- log " Session config: $SESSION_CONFIG"
705
- }
706
-
707
- # =============================================================================
708
- # Copy-Mode Guard (tmux pattern)
709
- # =============================================================================
710
-
711
- # --- governance.md s7 step 5: Check pane_in_mode before every send-keys ---
712
- check_copy_mode() {
713
- local pane_id="$1"
714
- local in_mode
715
- in_mode=$(tmux display-message -p -t "$pane_id" '#{pane_in_mode}' 2>/dev/null) || return 1
716
- if [[ "$in_mode" -eq 1 ]]; then
717
- return 1 # pane is in copy mode, cannot send keys
718
- fi
719
- return 0
720
- }
721
-
722
- # =============================================================================
723
- # Verification-Based Send Retry (tmux pattern)
724
- # =============================================================================
725
-
726
- # --- Reliable text paste via tmux buffer (avoids send-keys -l char-by-char issues) ---
727
- paste_to_pane() {
728
- local pane_id="$1"
729
- local text="$2"
730
- local tmpbuf="/tmp/.rlp-desk-paste-$$.tmp"
731
- echo -n "$text" > "$tmpbuf"
732
- tmux load-buffer -b rlp-paste "$tmpbuf" 2>/dev/null
733
- tmux paste-buffer -b rlp-paste -d -t "$pane_id" 2>/dev/null
734
- rm -f "$tmpbuf"
735
- }
736
-
737
- # --- governance.md s7 step 5: Send with copy-mode guard and retry ---
738
- safe_send_keys() {
739
- local pane_id="$1"
740
- local text="$2"
741
-
742
- # --- Exact tmux sendToWorker pattern (tmux-session.js:527-626) ---
743
-
744
- # Guard: copy-mode captures keys; skip entirely
745
- if ! check_copy_mode "$pane_id"; then
746
- log_debug " Pane $pane_id in copy mode, skipping send"
747
- return 1
748
- fi
749
-
750
- # Check for trust prompt and auto-dismiss
751
- local initial_capture
752
- initial_capture=$(tmux capture-pane -t "$pane_id" -p -S -20 2>/dev/null)
753
- local pane_busy=0
754
- if echo "$initial_capture" | grep -q "esc to interrupt" 2>/dev/null; then
755
- pane_busy=1
756
- fi
757
- if echo "$initial_capture" | grep -q "Do you trust" 2>/dev/null; then
758
- log_debug " Trust prompt detected, dismissing"
759
- tmux send-keys -t "$pane_id" C-m
760
- sleep 0.12
761
- fi
762
- # Auto-approve permission prompts ("Do you want to create/overwrite X?")
763
- if echo "$initial_capture" | grep -q "Do you want to" 2>/dev/null; then
764
- log_debug " Permission prompt detected, auto-approving"
765
- tmux send-keys -t "$pane_id" C-m
766
- sleep 0.3
767
- fi
768
- # Auto-dismiss codex update prompt (select Skip)
769
- if echo "$initial_capture" | grep -qi "new version\|update.*codex\|codex.*update" 2>/dev/null; then
770
- log_debug " Codex update prompt detected, selecting Skip"
771
- tmux send-keys -t "$pane_id" "2" C-m
772
- sleep 0.2
773
- fi
774
- # Send text via buffer paste (reliable for long strings)
775
- log_debug " Pasting text to pane $pane_id (${#text} chars)"
776
- paste_to_pane "$pane_id" "$text"
777
-
778
- # Allow input buffer to settle (tmux: 150ms)
779
- sleep 0.15
780
-
781
- # Submit: up to 6 rounds of C-m double-press
782
- local round=0
783
- while (( round < 6 )); do
784
- sleep 0.1
785
- if (( round == 0 && pane_busy )); then
786
- # Busy pane: just C-m (DO NOT send Tab — it toggles Claude Code permission mode)
787
- tmux send-keys -t "$pane_id" C-m
788
- else
789
- tmux send-keys -t "$pane_id" C-m
790
- sleep 0.2
791
- tmux send-keys -t "$pane_id" C-m
792
- fi
793
- sleep 0.14
794
-
795
- # Check if text was consumed
796
- local check_capture
797
- check_capture=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null | tail -5)
798
- if ! echo "$check_capture" | grep -qF "$text" 2>/dev/null; then
799
- log_debug " Text consumed after round $((round + 1))"
800
- return 0
801
- fi
802
- sleep 0.14
803
- (( round++ ))
804
- done
805
-
806
- # Safety gate: copy-mode check
807
- if ! check_copy_mode "$pane_id"; then
808
- log_debug " Copy mode activated during send, aborting"
809
- return 1
810
- fi
811
-
812
- # Adaptive fallback: C-u clear line, resend (tmux pattern)
813
- log_debug " Adaptive retry — clearing line and resending"
814
- tmux send-keys -t "$pane_id" C-u
815
- sleep 0.08
816
- if ! check_copy_mode "$pane_id"; then
817
- return 1
818
- fi
819
- paste_to_pane "$pane_id" "$text"
820
- sleep 0.12
821
- local retry_round=0
822
- while (( retry_round < 4 )); do
823
- tmux send-keys -t "$pane_id" C-m
824
- sleep 0.18
825
- tmux send-keys -t "$pane_id" C-m
826
- sleep 0.14
827
- local retry_capture
828
- retry_capture=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null | tail -5)
829
- if ! echo "$retry_capture" | grep -qF "$text" 2>/dev/null; then
830
- log_debug " Text consumed after adaptive retry round $((retry_round + 1))"
831
- return 0
832
- fi
833
- (( retry_round++ ))
834
- done
835
-
836
- # Fail-open: one last nudge
837
- if ! check_copy_mode "$pane_id"; then
838
- return 1
839
- fi
840
- tmux send-keys -t "$pane_id" C-m
841
- sleep 0.12
842
- tmux send-keys -t "$pane_id" C-m
843
- log_debug " Fail-open — text may or may not have been submitted"
844
- return 0
845
- }
846
-
847
- # =============================================================================
848
- # Wait for Pane Ready (tmux pattern: paneLooksReady)
849
- # =============================================================================
850
-
851
- wait_for_pane_ready() {
852
- local pane_id="$1"
853
- local timeout="${2:-10}" # tmux default: 10s
854
- local start=$(date +%s)
855
- log " Waiting for pane $pane_id ready..."
856
- while (( $(date +%s) - start < timeout )); do
857
- local captured
858
- captured=$(tmux capture-pane -t "$pane_id" -p -S -20 2>/dev/null)
859
-
860
- # Auto-dismiss trust prompt (tmux pattern: paneHasTrustPrompt)
861
- if echo "$captured" | grep -q "Do you trust" 2>/dev/null; then
862
- log " Trust prompt detected, auto-dismissing..."
863
- tmux send-keys -t "$pane_id" C-m
864
- sleep 0.12
865
- tmux send-keys -t "$pane_id" C-m
866
- sleep 2
867
- continue
868
- fi
869
-
870
- # Auto-approve permission prompts ("Do you want to create/overwrite X?")
871
- if echo "$captured" | grep -q "Do you want to" 2>/dev/null; then
872
- log " Permission prompt detected, auto-approving..."
873
- tmux send-keys -t "$pane_id" C-m
874
- sleep 0.5
875
- continue
876
- fi
877
-
878
- # Auto-dismiss codex update prompt (select Skip = option 2)
879
- if echo "$captured" | grep -qi "new version\|update.*codex\|codex.*update" 2>/dev/null; then
880
- log " Codex update prompt detected, selecting Skip..."
881
- tmux send-keys -t "$pane_id" "2" C-m
882
- sleep 0.5
883
- continue
884
- fi
885
-
886
- # tmux paneLooksReady: check each line for prompt char at line start
887
- local ready=0
888
- echo "$captured" | while IFS= read -r line; do
889
- local trimmed="${line## }"
890
- if [[ "$trimmed" == ❯* || "$trimmed" == \>* || "$trimmed" == ›* || "$trimmed" == »* ]]; then
891
- ready=1
892
- break
893
- fi
894
- done 2>/dev/null
895
-
896
- # Also check via grep as fallback
897
- if echo "$captured" | tail -5 | grep -qE '^\s*[❯›]' 2>/dev/null; then
898
- ready=1
899
- fi
900
-
901
- if (( ready )) || echo "$captured" | tail -3 | grep -qE '^\s*[❯›>]' 2>/dev/null; then
902
- # Check no active task running
903
- if ! echo "$captured" | grep -q "esc to interrupt" 2>/dev/null; then
904
- log " Pane $pane_id is ready."
905
- return 0
906
- fi
907
- fi
908
- sleep 0.25
909
- done
910
- # Timeout — return success anyway (fail-open, let safe_send_keys handle it)
911
- log " Pane $pane_id ready timeout after ${timeout}s (proceeding anyway)"
912
- return 0
913
- }
914
-
915
- # =============================================================================
916
- # Heartbeat Monitoring (tmux pattern)
917
- # =============================================================================
918
-
919
- # --- governance.md s7 step 5+6: Check heartbeat freshness ---
920
- check_heartbeat() {
921
- local hb_file="$1"
922
- local threshold="$HEARTBEAT_STALE_THRESHOLD"
923
-
924
- if [[ ! -f "$hb_file" ]]; then
925
- return 1
926
- fi
927
-
928
- local hb_epoch now_epoch
929
- # Read epoch seconds directly (avoids timezone parsing bugs)
930
- hb_epoch=$(jq -r '.epoch // empty' "$hb_file" 2>/dev/null) || return 1
931
-
932
- if [[ -z "$hb_epoch" ]]; then
933
- return 1
934
- fi
935
-
936
- now_epoch=$(date +%s)
937
- (( now_epoch - hb_epoch < threshold ))
938
- }
939
-
940
- # Check if heartbeat indicates process has exited
941
- check_heartbeat_exited() {
942
- local hb_file="$1"
943
- if [[ ! -f "$hb_file" ]]; then
944
- return 1
945
- fi
946
- local hb_status
947
- hb_status=$(jq -r '.status // empty' "$hb_file" 2>/dev/null)
948
- [[ "$hb_status" == "exited" ]]
949
- }
950
-
951
- # =============================================================================
952
- # Idle Pane Nudging (tmux pattern)
953
- # =============================================================================
954
-
955
- # --- governance.md s7 step 5+6: Nudge idle panes ---
956
- check_and_nudge_idle_pane() {
957
- local pane_id="$1"
958
- local nudge_count_var="$2"
959
- local current_content
960
- current_content=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null | tail -3)
961
-
962
- if [[ "$current_content" == "${LAST_PANE_CONTENT[$pane_id]:-}" ]]; then
963
- local idle_since="${PANE_IDLE_SINCE[$pane_id]:-$(date +%s)}"
964
- local now
965
- now=$(date +%s)
966
- if (( now - idle_since > IDLE_NUDGE_THRESHOLD )); then
967
- # A12 fix: NEVER nudge if pane is busy (thinking/working) — nudge interrupts claude
968
- local _nudge_capture
969
- _nudge_capture=$(tmux capture-pane -t "$pane_id" -p -S -5 2>/dev/null)
970
- if echo "$_nudge_capture" | grep -qi "esc to interrupt\|thinking\|working\|kneading\|crunching\|clauding\|billowing\|brewing\|tinkering\|burrowing\|saut\|razzle\|bunning\|zesting\|fermenting\|actualizing\|composing\|evaporating\|churning" 2>/dev/null; then
971
- log_debug " Pane $pane_id appears busy (thinking/working), skipping nudge"
972
- else
973
- local count=${(P)nudge_count_var}
974
- if (( count < MAX_NUDGES )); then
975
- log " Nudging idle pane $pane_id (nudge $((count + 1))/$MAX_NUDGES)"
976
- safe_send_keys "$pane_id" ""
977
- (( count++ ))
978
- eval "$nudge_count_var=$count"
979
- fi
980
- fi
981
- fi
982
- else
983
- LAST_PANE_CONTENT[$pane_id]="$current_content"
984
- PANE_IDLE_SINCE[$pane_id]=$(date +%s)
985
- fi
986
- }
987
-
988
- # =============================================================================
989
- # Exponential Backoff Restart (tmux pattern)
990
- # =============================================================================
991
-
992
- # --- governance.md s7 step 5: Restart dead workers with backoff ---
993
- restart_worker() {
994
- local pane_id="$1"
995
- local iter="$2"
996
- local trigger_file="$3"
997
-
998
- # Codex workers are 1-shot exec; restart is not applicable
999
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
1000
- log_debug "restart_worker called for codex engine — no-op (1-shot exec)"
1001
- return 1
1002
- fi
1003
-
1004
- local restart_count="${WORKER_RESTARTS[$iter]:-0}"
1005
-
1006
- if (( restart_count >= MAX_RESTARTS )); then
1007
- log_error "Worker exceeded max restarts ($MAX_RESTARTS) for iteration $iter"
1008
- return 1 # caller writes BLOCKED
1009
- fi
1010
-
1011
- # Exponential backoff: 5s, 10s, 20s, 60s (cap)
1012
- local -a delays=(5 10 20 60)
1013
- local delay=${delays[$((restart_count + 1))]:-60}
1014
- log " Restarting worker (attempt $((restart_count + 1))/$MAX_RESTARTS) after ${delay}s backoff..."
1015
- sleep "$delay"
1016
-
1017
- # Kill existing claude, wait for shell prompt
1018
- tmux send-keys -t "$pane_id" C-c 2>/dev/null
1019
- tmux send-keys -t "$pane_id" "/exit" C-m 2>/dev/null
1020
- sleep 2
1021
-
1022
- # Re-launch worker (tmux interactive pattern)
1023
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
1024
- safe_send_keys "$pane_id" "${CODEX_BIN:-codex} -m $WORKER_CODEX_MODEL -c model_reasoning_effort=\"$WORKER_CODEX_REASONING\" --disable plugins --dangerously-bypass-approvals-and-sandbox"
1025
- else
1026
- safe_send_keys "$pane_id" "$(build_claude_cmd tui "$WORKER_MODEL")"
1027
- fi
1028
- WORKER_RESTARTS[$iter]=$((restart_count + 1))
1029
- return 0
1030
- }
1031
-
1032
- # =============================================================================
1033
- # Write-Then-Notify: Trigger Script Generation (tmux CRITICAL pattern)
1034
- # =============================================================================
1035
-
1036
- # Per-US PRD injection helper
1037
- # Substitutes the full PRD path with a per-US split path in the Worker prompt base.
1038
- # Falls back to the full PRD with a stderr warning if the split file is missing.
1039
- # Args: $1=prompt_base_file $2=full_prd_path $3=per_us_prd_path (empty = no substitution)
1040
- inject_per_us_prd() {
1041
- local prompt_base="$1"
1042
- local full_prd="$2"
1043
- local per_us_prd="${3:-}"
1044
-
1045
- if [[ -n "$per_us_prd" && -f "$per_us_prd" ]]; then
1046
- sed "s|$full_prd|$per_us_prd|g" "$prompt_base"
1047
- else
1048
- if [[ -n "$per_us_prd" ]]; then
1049
- echo "WARNING: per-US split file not found: $per_us_prd — falling back to full PRD injection" >&2
1050
- fi
1051
- cat "$prompt_base"
1052
- fi
1053
- }
1054
-
1055
- # --- governance.md s7 step 4+5: Write prompt and trigger to files ---
1056
- # NEVER send prompt content through tmux send-keys.
1057
- # Write payloads to files, send only short trigger commands (<200 chars).
1058
- write_worker_trigger() {
1059
- local iter="$1"
1060
- local prompt_file="$LOGS_DIR/iter-$(printf '%03d' $iter).worker-prompt.md"
1061
- local trigger_file="$LOGS_DIR/iter-$(printf '%03d' $iter).worker-trigger.sh"
1062
- local output_log="$LOGS_DIR/iter-$(printf '%03d' $iter).worker-output.log"
1063
-
1064
- # Build the worker prompt: base prompt + iteration context
1065
- local contract
1066
- contract=$(sed -n '/^## Next Iteration Contract$/,/^## /{ /^## Next/d; /^## [^N]/d; p; }' "$MEMORY_FILE" 2>/dev/null | head -5)
1067
-
1068
- # Check for fix contract from previous verifier failure
1069
- local prev_iter=$((iter - 1))
1070
- local fix_contract_file="$LOGS_DIR/iter-$(printf '%03d' $prev_iter).fix-contract.md"
1071
-
1072
- # Compute next unverified US before prompt assembly (required for per-US PRD injection)
1073
- local next_us=""
1074
- if [[ "$VERIFY_MODE" = "per-us" && -n "$US_LIST" ]]; then
1075
- for us in $(echo "$US_LIST" | tr ',' ' '); do
1076
- if ! echo ",$VERIFIED_US," | grep -q ",$us,"; then
1077
- next_us="$us"
1078
- break
1079
- fi
1080
- done
1081
- fi
1082
-
1083
- {
1084
- # Per-US PRD injection: substitute full PRD path with per-US split path when available
1085
- local per_us_prd=""
1086
- [[ -n "$next_us" ]] && per_us_prd="$DESK/plans/prd-${SLUG}-${next_us}.md"
1087
- inject_per_us_prd "$WORKER_PROMPT_BASE" "$DESK/plans/prd-${SLUG}.md" "$per_us_prd"
1088
- echo ""
1089
- echo "---"
1090
- echo "## Iteration Context"
1091
- echo "- **Iteration**: $iter"
1092
- echo "- **Memory Stop Status**: $(sed -n '/^## Stop Status$/,/^$/{ /^## /d; /^$/d; p; }' "$MEMORY_FILE" 2>/dev/null | head -1)"
1093
- echo "- **Next Iteration Contract**: ${contract:-Start from the beginning}"
1094
- if (( _PRD_CHANGED )); then
1095
- echo "NOTE: PRD was updated since last iteration. New/changed US may exist."
1096
- fi
1097
-
1098
- # Include fix contract if previous verifier failed
1099
- if [[ -f "$fix_contract_file" ]]; then
1100
- echo ""
1101
- echo "---"
1102
- echo "## IMPORTANT: Fix Contract from Verifier (iteration $prev_iter)"
1103
- echo "The Verifier REJECTED your previous work. You MUST fix the issues below."
1104
- echo "Do NOT just resubmit — actually change the code to address each issue."
1105
- echo ""
1106
- cat "$fix_contract_file"
1107
- fi
1108
-
1109
- # Per-US mode: tell Worker exactly which US to work on
1110
- if [[ "$VERIFY_MODE" = "per-us" && -n "$US_LIST" ]]; then
1111
- if [[ -n "$next_us" ]]; then
1112
- echo ""
1113
- echo "---"
1114
- echo "## PER-US SCOPE LOCK (this iteration) — OVERRIDES memory contract"
1115
- echo "**IGNORE the 'Next Iteration Contract' from memory if it references a different story.**"
1116
- echo "The Leader has determined that **${next_us}** is the next unverified story."
1117
- echo "You MUST implement ONLY **${next_us}** in this iteration."
1118
- echo "Do NOT implement any other user stories."
1119
- # Per-US test-spec injection: point Worker to scoped test-spec if available
1120
- local per_us_test_spec="$DESK/plans/test-spec-${SLUG}-${next_us}.md"
1121
- if [[ -f "$per_us_test_spec" ]]; then
1122
- echo "- **Test Spec**: Read ONLY \`$per_us_test_spec\` (scoped to ${next_us})"
1123
- else
1124
- echo "- **Test Spec**: Read \`$DESK/plans/test-spec-${SLUG}.md\` (full — find ${next_us} section)"
1125
- fi
1126
- echo "When done, signal verify with us_id=\"${next_us}\" (not \"ALL\")."
1127
- echo "Signal format: {\"iteration\": N, \"status\": \"verify\", \"us_id\": \"${next_us}\", ...}"
1128
- echo ""
1129
- echo "**Update the campaign memory's 'Next Iteration Contract' to reflect ${next_us}.**"
1130
- elif [[ -n "$VERIFIED_US" ]]; then
1131
- # All individual US verified — this is the final full verify iteration
1132
- echo ""
1133
- echo "---"
1134
- echo "## FINAL VERIFICATION ITERATION"
1135
- echo "All individual US have been verified: $VERIFIED_US"
1136
- echo "Run all tests and verification commands to confirm everything works together."
1137
- echo "Signal verify with us_id=\"ALL\" for the final full verification."
1138
- fi
1139
- elif [[ "$VERIFY_MODE" = "batch" ]]; then
1140
- echo ""
1141
- echo "---"
1142
- if [[ -n "$VERIFIED_US" ]]; then
1143
- echo "## BATCH MODE — CONTINUE FROM PARTIAL PROGRESS"
1144
- echo "The following US have already been verified: **$VERIFIED_US**"
1145
- echo "- Do NOT re-implement these — they are done."
1146
- echo "- Focus ONLY on the remaining unverified user stories."
1147
- echo '- Signal verify with us_id="ALL" when the remaining stories are complete.'
1148
- else
1149
- echo "## BATCH MODE OVERRIDE"
1150
- echo "Ignore any per-US signal instructions above. In batch mode:"
1151
- echo "- Implement ALL user stories in this iteration"
1152
- echo '- Signal verify with us_id="ALL" only when ALL stories are complete'
1153
- echo "- Do NOT signal verify after individual stories"
1154
- fi
1155
- fi
1156
- } | atomic_write "$prompt_file"
1157
-
1158
- # Write trigger script (DO NOT use exec -- breaks heartbeat cleanup)
1159
- # Engine-specific launch command (expanded at write time)
1160
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
1161
- local engine_cmd="${CODEX_BIN:-codex} \\
1162
- -m $WORKER_CODEX_MODEL \\
1163
- -c model_reasoning_effort=\"$WORKER_CODEX_REASONING\" \\
1164
- --disable plugins --dangerously-bypass-approvals-and-sandbox \\
1165
- \"\$(cat $prompt_file)\""
1166
- local engine_comment="# Run codex with fresh context (fallback trigger — TUI primary launch via launch_worker_codex)"
1167
- else
1168
- local engine_cmd
1169
- engine_cmd=$(build_claude_cmd print "$WORKER_MODEL" "$prompt_file" "$output_log")
1170
- local engine_comment="# Run claude with fresh context, no MCP/skills (governance.md s7 step 5)"
1171
- fi
1172
-
1173
- {
1174
- cat <<TRIGGER_EOF
1175
- #!/bin/zsh
1176
- # Trigger for iteration $iter worker - generated by run_ralph_desk.zsh
1177
- # DO NOT use exec here -- it breaks heartbeat cleanup
1178
-
1179
- HEARTBEAT_FILE="$WORKER_HEARTBEAT"
1180
-
1181
- # Background heartbeat writer (tmux pattern)
1182
- (
1183
- while true; do
1184
- echo '{"epoch":'\$(date +%s)',"pid":'"\$\$"'}' > "\${HEARTBEAT_FILE}.tmp.\$\$"
1185
- mv "\${HEARTBEAT_FILE}.tmp.\$\$" "\$HEARTBEAT_FILE"
1186
- sleep 15
1187
- done
1188
- ) &
1189
- HEARTBEAT_PID=\$!
1190
-
1191
- $engine_comment
1192
- $engine_cmd
1193
-
1194
- # Cleanup heartbeat writer
1195
- kill \$HEARTBEAT_PID 2>/dev/null
1196
- wait \$HEARTBEAT_PID 2>/dev/null
1197
- echo '{"epoch":'\$(date +%s)',"status":"exited"}' > "\${HEARTBEAT_FILE}.tmp.\$\$"
1198
- mv "\${HEARTBEAT_FILE}.tmp.\$\$" "\$HEARTBEAT_FILE"
1199
- TRIGGER_EOF
1200
- } | atomic_write "$trigger_file"
1201
- chmod +x "$trigger_file"
1202
-
1203
- log " Worker prompt: $prompt_file"
1204
- log " Worker trigger: $trigger_file"
1205
- }
1206
-
1207
- write_verifier_trigger() {
1208
- local iter="$1"
1209
- local verifier_engine="${2:-$VERIFIER_ENGINE}" # allow override for consensus
1210
- local verifier_model="${3:-$VERIFIER_MODEL}"
1211
- local suffix="${4:-}" # optional suffix for consensus (e.g., "-claude", "-codex")
1212
- local prompt_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier${suffix}-prompt.md"
1213
- local trigger_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier${suffix}-trigger.sh"
1214
- local output_log="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier${suffix}-output.log"
1215
-
1216
- # Read us_id from iter-signal.json for per-US scoping
1217
- local us_id=""
1218
- if [[ -f "$SIGNAL_FILE" ]]; then
1219
- us_id=$(jq -r '.us_id // empty' "$SIGNAL_FILE" 2>/dev/null)
1220
- fi
1221
-
1222
- # Build verifier prompt from base with US scope
1223
- {
1224
- cat "$VERIFIER_PROMPT_BASE"
1225
- echo ""
1226
- echo "---"
1227
- echo "## Verification Context"
1228
- echo "- **Iteration**: $iter"
1229
- echo "- **Done Claim**: $DONE_CLAIM_FILE"
1230
- echo "- **Verify Mode**: $VERIFY_MODE"
1231
- if [[ -n "$us_id" ]]; then
1232
- if [[ "$us_id" = "ALL" ]]; then
1233
- echo "- **Scope**: FULL VERIFY — check ALL acceptance criteria from the PRD"
1234
- else
1235
- echo "- **Scope**: Verify ONLY the acceptance criteria for **${us_id}**"
1236
- fi
1237
- if [[ -n "$VERIFIED_US" ]]; then
1238
- echo "- **Previously verified US**: $VERIFIED_US"
1239
- echo "- **Note**: Skip re-verifying the above US. Focus on unverified stories."
1240
- fi
1241
- fi
1242
- } | atomic_write "$prompt_file"
1243
-
1244
- # Write trigger script (DO NOT use exec -- breaks heartbeat cleanup)
1245
- # Engine-specific launch command (expanded at write time)
1246
- if [[ "$verifier_engine" = "codex" ]]; then
1247
- local engine_cmd="${CODEX_BIN:-codex} -m $VERIFIER_CODEX_MODEL \\
1248
- -c model_reasoning_effort=\"$VERIFIER_CODEX_REASONING\" \\
1249
- --disable plugins --dangerously-bypass-approvals-and-sandbox \\
1250
- \"\$(cat $prompt_file)\" \\
1251
- 2>&1 | tee $output_log"
1252
- local engine_comment="# Run codex with fresh context (governance.md s7 step 7)"
1253
- else
1254
- local engine_cmd
1255
- engine_cmd=$(build_claude_cmd print "$verifier_model" "$prompt_file" "$output_log")
1256
- local engine_comment="# Run claude with fresh context, no MCP/skills (governance.md s7 step 7)"
1257
- fi
1258
-
1259
- {
1260
- cat <<TRIGGER_EOF
1261
- #!/bin/zsh
1262
- # Trigger for iteration $iter verifier${suffix} - generated by run_ralph_desk.zsh
1263
- # DO NOT use exec here -- it breaks heartbeat cleanup
1264
-
1265
- HEARTBEAT_FILE="$VERIFIER_HEARTBEAT"
1266
-
1267
- # Background heartbeat writer (tmux pattern)
1268
- (
1269
- while true; do
1270
- echo '{"epoch":'\$(date +%s)',"pid":'"\$\$"'}' > "\${HEARTBEAT_FILE}.tmp.\$\$"
1271
- mv "\${HEARTBEAT_FILE}.tmp.\$\$" "\$HEARTBEAT_FILE"
1272
- sleep 15
1273
- done
1274
- ) &
1275
- HEARTBEAT_PID=\$!
1276
-
1277
- $engine_comment
1278
- $engine_cmd
1279
-
1280
- # Cleanup heartbeat writer
1281
- kill \$HEARTBEAT_PID 2>/dev/null
1282
- wait \$HEARTBEAT_PID 2>/dev/null
1283
- echo '{"epoch":'\$(date +%s)',"status":"exited"}' > "\${HEARTBEAT_FILE}.tmp.\$\$"
1284
- mv "\${HEARTBEAT_FILE}.tmp.\$\$" "\$HEARTBEAT_FILE"
1285
- TRIGGER_EOF
1286
- } | atomic_write "$trigger_file"
1287
- chmod +x "$trigger_file"
1288
-
1289
- log " Verifier prompt: $prompt_file"
1290
- log " Verifier trigger: $trigger_file"
1291
- }
1292
-
1293
- # =============================================================================
1294
- # Cleanup (trap handler)
1295
- # =============================================================================
1296
-
1297
- cleanup() {
1298
- log "Cleaning up..."
1299
-
1300
- # Remove lockfile
1301
- if (( LOCKFILE_ACQUIRED )); then
1302
- rm -f "$LOCKFILE_PATH" 2>/dev/null
1303
- else
1304
- log_debug "cleanup: lockfile not owned by this process, skipping removal"
1305
- fi
1306
-
1307
- # Kill claude processes then kill panes
1308
- log_debug "cleanup: WORKER_PANE=${WORKER_PANE:-unset} VERIFIER_PANE=${VERIFIER_PANE:-unset}"
1309
- if [[ -n "${WORKER_PANE:-}" ]]; then
1310
- tmux send-keys -t "$WORKER_PANE" C-c 2>/dev/null
1311
- tmux send-keys -t "$WORKER_PANE" "/exit" C-m 2>/dev/null
1312
- fi
1313
- if [[ -n "${VERIFIER_PANE:-}" ]]; then
1314
- tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null
1315
- tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null
1316
- fi
1317
- sleep 2
1318
- # Kill panes on completion
1319
- if [[ -n "${WORKER_PANE:-}" ]]; then
1320
- tmux kill-pane -t "$WORKER_PANE" 2>/dev/null
1321
- fi
1322
- if [[ -n "${VERIFIER_PANE:-}" ]]; then
1323
- tmux kill-pane -t "$VERIFIER_PANE" 2>/dev/null
1324
- fi
1325
- log " Panes cleaned up."
1326
-
1327
- # Remove any leftover tmp files (setopt nonomatch to avoid zsh glob errors)
1328
- setopt local_options nonomatch 2>/dev/null
1329
- rm -f "$LOGS_DIR"/*.tmp.* "$MEMOS_DIR"/*.tmp.* 2>/dev/null
1330
-
1331
- # AC4: Generate campaign report on all terminal states (always-on)
1332
- generate_campaign_report
1333
-
1334
- # US-001: Generate SV report after campaign report (tmux mode)
1335
- generate_sv_report
1336
-
1337
- # Print summary
1338
- local end_time
1339
- end_time=$(date +%s)
1340
- local elapsed=$(( end_time - START_TIME ))
1341
- local minutes=$(( elapsed / 60 ))
1342
- local seconds=$(( elapsed % 60 ))
1343
-
1344
- local final_status="UNKNOWN"
1345
- if [[ -f "$COMPLETE_SENTINEL" ]]; then final_status="COMPLETE"
1346
- elif [[ -f "$BLOCKED_SENTINEL" ]]; then final_status="BLOCKED"
1347
- else final_status="TIMEOUT"; fi
1348
-
1349
- # --- Update metadata.json with final status ---
1350
- if [[ -f "$METADATA_FILE" ]]; then
1351
- jq --arg status "$final_status" --arg end_time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
1352
- '.campaign_status = $status | .end_time = $end_time' \
1353
- "$METADATA_FILE" > "${METADATA_FILE}.tmp" && mv "${METADATA_FILE}.tmp" "$METADATA_FILE"
1354
- fi
1355
-
1356
- if (( DEBUG )); then
1357
- local end_ts=$(date +%s)
1358
- local elapsed=$((end_ts - START_TIME))
1359
-
1360
- log_debug "[FLOW] final status=$final_status iterations=$ITERATION elapsed=${elapsed}s"
1361
-
1362
- # --- Validation ---
1363
- log_debug "[FLOW] === Execution Validation ==="
1364
-
1365
- # 1. Did the correct verify mode run?
1366
- log_debug "[FLOW] verify_mode=$VERIFY_MODE configured=true"
1367
-
1368
- # 2. Per-US: were all US individually verified?
1369
- if [[ "$VERIFY_MODE" = "per-us" ]]; then
1370
- local prd_file="$DESK/plans/prd-$SLUG.md"
1371
- local expected_us=""
1372
- if [[ -f "$prd_file" ]]; then
1373
- expected_us=$(grep -oE 'US-[0-9]+' "$prd_file" | sort -u | tr '\n' ',' | sed 's/,$//')
1374
- fi
1375
- local verified_count=$(echo "$VERIFIED_US" | tr ',' '\n' | grep -c 'US-' 2>/dev/null || echo 0)
1376
- local expected_count=$(echo "$expected_us" | tr ',' '\n' | grep -c 'US-' 2>/dev/null || echo 0)
1377
-
1378
- if [[ "$final_status" = "COMPLETE" ]]; then
1379
- if (( verified_count >= expected_count )); then
1380
- log_debug "[FLOW] per_us_coverage=PASS verified=$verified_count/$expected_count us=$VERIFIED_US"
1381
- else
1382
- log_debug "[FLOW] per_us_coverage=FAIL verified=$verified_count/$expected_count expected=$expected_us got=$VERIFIED_US"
1383
- fi
1384
- else
1385
- log_debug "[FLOW] per_us_coverage=INCOMPLETE verified=$verified_count/$expected_count status=$final_status"
1386
- fi
1387
- fi
1388
-
1389
- # 3. Consensus: were both engines used?
1390
- if [[ "$CONSENSUS_MODE" != "off" ]]; then
1391
- if [[ -n "${CLAUDE_VERDICT:-}" && -n "${CODEX_VERDICT:-}" ]]; then
1392
- log_debug "[FLOW] consensus=USED mode=$CONSENSUS_MODE claude=$CLAUDE_VERDICT codex=$CODEX_VERDICT rounds=$CONSENSUS_ROUND"
1393
- else
1394
- log_debug "[FLOW] consensus=NOT_TRIGGERED mode=$CONSENSUS_MODE claude=${CLAUDE_VERDICT:-none} codex=${CODEX_VERDICT:-none}"
1395
- fi
1396
- fi
1397
-
1398
- # 4. Engine match: did the configured engines actually run?
1399
- local worker_dispatches=$(grep -c '\[FLOW\].*phase=worker.*dispatched=true' "$DEBUG_LOG" 2>/dev/null || echo 0)
1400
- local verifier_dispatches=$(grep -c '\[FLOW\].*phase=verifier.*dispatched=true' "$DEBUG_LOG" 2>/dev/null || echo 0)
1401
- log_debug "[FLOW] dispatches worker=$worker_dispatches verifier=$verifier_dispatches"
1402
-
1403
- # 5. Fix loops: how many fix contracts were generated?
1404
- local fix_count=$(grep -c '\[DECIDE\].*phase=fix_loop' "$DEBUG_LOG" 2>/dev/null || echo 0)
1405
- log_debug "[FLOW] fix_loops=$fix_count consecutive_failures=$CONSECUTIVE_FAILURES"
1406
-
1407
- # 6. Circuit breakers: any triggered?
1408
- local cb_count=$(grep -c '\[GOV\].*circuit_breaker=' "$DEBUG_LOG" 2>/dev/null || echo 0)
1409
- log_debug "[FLOW] circuit_breakers_triggered=$cb_count"
1410
-
1411
- # 7. Overall result
1412
- log_debug "[FLOW] result=$final_status iterations=$ITERATION elapsed=${elapsed}s verified_us=$VERIFIED_US"
1413
- fi
1414
-
1415
- echo ""
1416
- echo "============================================================"
1417
- echo " Ralph Desk Tmux Runner - Session Complete"
1418
- echo "============================================================"
1419
- echo " Session: $SESSION_NAME"
1420
- echo " Slug: $SLUG"
1421
- echo " Iterations: $ITERATION / $MAX_ITER"
1422
- echo " Elapsed: ${minutes}m ${seconds}s"
1423
- echo ""
1424
-
1425
- if [[ -f "$COMPLETE_SENTINEL" ]]; then
1426
- echo " Final State: COMPLETE"
1427
- elif [[ -f "$BLOCKED_SENTINEL" ]]; then
1428
- echo " Final State: BLOCKED"
1429
- else
1430
- echo " Final State: STOPPED (interrupted or timeout)"
1431
- fi
1432
-
1433
- echo ""
1434
- echo " Tmux session left alive for inspection:"
1435
- echo " tmux attach -t $SESSION_NAME"
1436
- echo " tmux kill-session -t $SESSION_NAME"
1437
- echo "============================================================"
1438
- }
1439
-
1440
- # =============================================================================
1441
- # Poll Loop (used for both Worker and Verifier)
1442
- # =============================================================================
1443
-
1444
- # --- governance.md s7 step 5+6: Poll for signal file with heartbeat monitoring ---
1445
- poll_for_signal() {
1446
- local signal_file="$1"
1447
- local heartbeat_file="$2"
1448
- local pane_id="$3"
1449
- local trigger_file="$4"
1450
- local role="$5" # "worker" or "verifier"
1451
- local nudge_count=0
1452
- local api_retry_count=0
1453
- local poll_start
1454
- poll_start=$(date +%s)
1455
-
1456
- # Initialize idle tracking for this pane
1457
- LAST_PANE_CONTENT[$pane_id]=""
1458
- PANE_IDLE_SINCE[$pane_id]=$(date +%s)
1459
-
1460
- while true; do
1461
- local now
1462
- now=$(date +%s)
1463
- local elapsed=$(( now - poll_start ))
1464
-
1465
- # Per-iteration timeout check
1466
- if (( elapsed >= ITER_TIMEOUT )); then
1467
- log_error "$role timed out after ${ITER_TIMEOUT}s for iteration $ITERATION"
1468
- return 1 # timeout
1469
- fi
1470
-
1471
- # Check if signal file appeared
1472
- if [[ -f "$signal_file" ]]; then
1473
- log " Signal file detected: $signal_file"
1474
- return 0 # success
1475
- fi
1476
-
1477
- # A4 fallback: done-claim exists but no signal → Worker forgot iter-signal
1478
- # ONLY for Worker polling — Verifier waits for verdict file, not done-claim
1479
- if [[ "$role" != *erifier* && -f "$DONE_CLAIM_FILE" && ! -f "$signal_file" ]]; then
1480
- local dc_us_id
1481
- dc_us_id=$(jq -r '.us_id // "unknown"' "$DONE_CLAIM_FILE" 2>/dev/null)
1482
- if [[ -n "$dc_us_id" && "$dc_us_id" != "null" ]]; then
1483
- log " WARNING: done-claim exists for $dc_us_id but no iter-signal. Auto-generating signal (A4 fallback)."
1484
- log_debug "[GOV] iter=$ITERATION done_claim_without_signal=true us_id=$dc_us_id action=auto_generate_signal"
1485
- echo '{"iteration":'"$ITERATION"',"status":"verify","us_id":"'"$dc_us_id"'","summary":"auto-generated by A4 fallback (done-claim without signal)","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$signal_file"
1486
- return 0
1487
- fi
1488
- fi
1489
-
1490
- # API transient-error recovery with bounded backoff
1491
- local pane_output_for_retry
1492
- pane_output_for_retry=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null || true)
1493
- local is_api_text_retry=0
1494
- if [[ -n "$pane_output_for_retry" ]] &&
1495
- ( echo "$pane_output_for_retry" | grep -qiE '(^|[^[:digit:]])500([^[:digit:]]|$)' \
1496
- || echo "$pane_output_for_retry" | grep -qiE '(^|[^[:digit:]])529([^[:digit:]]|$)' \
1497
- || echo "$pane_output_for_retry" | grep -qi 'overloaded' \
1498
- || echo "$pane_output_for_retry" | grep -qi 'too many requests' \
1499
- || echo "$pane_output_for_retry" | grep -qi 'service unavailable' ); then
1500
- is_api_text_retry=1
1501
- fi
1502
-
1503
- if (( is_api_text_retry )) || is_api_error "$pane_id"; then
1504
- (( api_retry_count++ ))
1505
- log_debug "[FLOW] iter=$ITERATION api_retry=${api_retry_count}/${_API_MAX_RETRIES} role=${role} reason=tmux_pane_api_error"
1506
- if (( api_retry_count >= _API_MAX_RETRIES )); then
1507
- log_error "API unavailable after ${_API_MAX_RETRIES} retries"
1508
- write_blocked_sentinel "API unavailable after ${_API_MAX_RETRIES} retries"
1509
- return 2
1510
- fi
1511
- # A5: If pane shows "queued messages" or rate-limit corruption, restart pane
1512
- if echo "$pane_output_for_retry" | grep -qi 'queued messages'; then
1513
- log " A5: Rate-limited pane shows 'queued messages' — restarting $role pane"
1514
- log_debug "[GOV] iter=$ITERATION phase=rate_limit_pane_restart role=$role reason=queued_messages"
1515
- tmux send-keys -t "$pane_id" C-c 2>/dev/null; sleep 0.5
1516
- tmux send-keys -t "$pane_id" "/exit" C-m 2>/dev/null; sleep 2
1517
- wait_for_pane_ready "$pane_id" 10 2>/dev/null || true
1518
- fi
1519
- sleep "$_API_RETRY_INTERVAL_S"
1520
- continue
1521
- else
1522
- api_retry_count=0
1523
- fi
1524
-
1525
- # Check heartbeat freshness (tmux pattern)
1526
- if [[ -f "$heartbeat_file" ]]; then
1527
- if check_heartbeat_exited "$heartbeat_file"; then
1528
- # Process exited but no signal file -- give a brief grace period
1529
- sleep 3
1530
- if [[ -f "$signal_file" ]]; then
1531
- log " Signal file detected after process exit: $signal_file"
1532
- return 0
1533
- fi
1534
- # Dispatch to engine-specific exit handler
1535
- if [[ "$WORKER_ENGINE" = "codex" && "$role" != *erifier* ]]; then
1536
- handle_worker_exit_codex "$ITERATION" "$signal_file"
1537
- return 0
1538
- fi
1539
- # Claude path (or verifier of any engine)
1540
- if handle_worker_exit_claude "$pane_id" "$ITERATION" "$trigger_file"; then
1541
- # Reset poll timer for the restart
1542
- poll_start=$(date +%s)
1543
- nudge_count=0
1544
- LAST_PANE_CONTENT[$pane_id]=""
1545
- PANE_IDLE_SINCE[$pane_id]=$(date +%s)
1546
- sleep "$POLL_INTERVAL"
1547
- continue
1548
- else
1549
- return 1 # max restarts exceeded
1550
- fi
1551
- fi
1552
-
1553
- if ! check_heartbeat "$heartbeat_file"; then
1554
- log " WARNING: $role heartbeat stale (>${HEARTBEAT_STALE_THRESHOLD}s)"
1555
- (( HEARTBEAT_STALE_COUNT++ ))
1556
- # Circuit breaker: 3 consecutive heartbeat stale events
1557
- if (( HEARTBEAT_STALE_COUNT >= 3 )); then
1558
- log_debug "[GOV] iter=$ITERATION circuit_breaker=heartbeat_stale detail=\"3 consecutive heartbeat stale events\""
1559
- log_error "Circuit breaker: 3 consecutive heartbeat stale events"
1560
- return 1
1561
- fi
1562
- # Attempt restart
1563
- if restart_worker "$pane_id" "$ITERATION" "$trigger_file"; then
1564
- poll_start=$(date +%s)
1565
- nudge_count=0
1566
- continue
1567
- else
1568
- return 1
1569
- fi
1570
- else
1571
- # Heartbeat is fresh, reset stale counter
1572
- HEARTBEAT_STALE_COUNT=0
1573
- fi
1574
- fi
1575
-
1576
- # Dead pane detection during poll: check if claude/codex process died
1577
- local poll_cmd
1578
- poll_cmd=$(tmux display-message -p -t "$pane_id" '#{pane_current_command}' 2>/dev/null)
1579
- # Dead pane detection — delegates to check_dead_pane() for engine-aware logic
1580
- if check_dead_pane "$poll_cmd" "$WORKER_ENGINE" "$role"; then
1581
- log " WARNING: $role pane $pane_id has bare shell ($poll_cmd) — process died during execution"
1582
- log_debug "[GOV] iter=$ITERATION pane_dead_during_poll=true pane=$pane_id cmd=$poll_cmd role=$role"
1583
- # Return failure so caller can handle recovery
1584
- return 1
1585
- fi
1586
-
1587
- # Auto-approve permission prompts during poll
1588
- local poll_capture
1589
- poll_capture=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null)
1590
- if echo "$poll_capture" | grep -q "Do you want to" 2>/dev/null; then
1591
- log " Permission prompt detected during poll, auto-approving..."
1592
- log_debug "[FLOW] iter=$ITERATION permission_prompt_auto_approved=true"
1593
- tmux send-keys -t "$pane_id" C-m
1594
- sleep 0.5
1595
- fi
1596
-
1597
- # Idle pane nudging (tmux pattern)
1598
- check_and_nudge_idle_pane "$pane_id" "nudge_count"
1599
-
1600
- sleep "$POLL_INTERVAL"
1601
- done
1602
- }
1603
-
1604
- # =============================================================================
1605
- # Consensus Verification (run two verifiers sequentially in same pane)
1606
- # =============================================================================
1607
-
1608
- # --- US-004: Run a single verifier in the Verifier pane and poll for verdict ---
1609
- run_single_verifier() {
1610
- local iter="$1"
1611
- local engine="$2" # claude|codex
1612
- local model="$3" # model for this verifier
1613
- local suffix="$4" # "-claude" or "-codex"
1614
- local verdict_dest="$5" # where to copy the verdict file
1615
-
1616
- # Write trigger for this engine
1617
- write_verifier_trigger "$iter" "$engine" "$model" "$suffix"
1618
- local trigger_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier${suffix}-trigger.sh"
1619
- local prompt_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier${suffix}-prompt.md"
1620
-
1621
- # Clean previous Verifier session (with dead pane detection)
1622
- local verifier_cmd
1623
- verifier_cmd=$(tmux display-message -p -t "$VERIFIER_PANE" '#{pane_current_command}' 2>/dev/null)
1624
- if [[ -z "$verifier_cmd" ]]; then
1625
- log " Verifier pane $VERIFIER_PANE is gone — replacing..."
1626
- log_debug "[GOV] iter=$iter pane_dead=true pane_id=$VERIFIER_PANE action=replace_pane"
1627
- replace_worker_pane "$VERIFIER_PANE" "verifier"
1628
- VERIFIER_PANE=$(jq -r '.panes.verifier' "$SESSION_CONFIG")
1629
- log " New verifier pane: $VERIFIER_PANE"
1630
- elif [[ "$verifier_cmd" == "zsh" || "$verifier_cmd" == "bash" ]]; then
1631
- log " Verifier pane $VERIFIER_PANE has bare shell ($verifier_cmd) — resetting..."
1632
- log_debug "[GOV] iter=$iter pane_dead=true pane_id=$VERIFIER_PANE cmd=$verifier_cmd action=reset_shell"
1633
- tmux send-keys -t "$VERIFIER_PANE" C-c C-u 2>/dev/null
1634
- sleep 0.2
1635
- tmux send-keys -t "$VERIFIER_PANE" "clear" C-m 2>/dev/null
1636
- sleep 0.3
1637
- elif [[ "$verifier_cmd" == "node" || "$verifier_cmd" == "claude" || "$verifier_cmd" == "codex" ]]; then
1638
- tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null
1639
- sleep 0.5
1640
- tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null
1641
- sleep 2
1642
- fi
1643
- # Always ensure clean shell state before launching new verifier
1644
- wait_for_pane_ready "$VERIFIER_PANE" 10 2>/dev/null || true
1645
- # Clear pane to avoid residual text interference
1646
- tmux send-keys -t "$VERIFIER_PANE" C-l 2>/dev/null
1647
- sleep 0.5
1648
-
1649
- # Remove previous verdict file
1650
- rm -f "$VERDICT_FILE" 2>/dev/null
1651
-
1652
- # Launch verifier — dispatch to engine-specific function
1653
- local verifier_launch
1654
- if [[ "$engine" = "codex" ]]; then
1655
- verifier_launch="${CODEX_BIN:-codex} -m $VERIFIER_CODEX_MODEL -c model_reasoning_effort=\"$VERIFIER_CODEX_REASONING\" --disable plugins --dangerously-bypass-approvals-and-sandbox"
1656
- launch_verifier_codex "$VERIFIER_PANE" "$prompt_file" "$iter" "$verifier_launch"
1657
- log_debug "Verifier$suffix codex TUI dispatched"
1658
- else
1659
- verifier_launch="$(build_claude_cmd tui "$model")"
1660
- if ! launch_verifier_claude "$VERIFIER_PANE" "$prompt_file" "$iter" "$verifier_launch"; then
1661
- log_error "Verifier$suffix failed to start"
1662
- return 1
1663
- fi
1664
- log_debug "Verifier$suffix claude dispatched"
1665
- fi
1666
-
1667
- # Poll for verdict
1668
- if [[ "$engine" = "codex" ]]; then
1669
- # Codex exec: simple file poll (non-interactive, no heartbeat/nudge needed)
1670
- log " Polling for verify-verdict.json ($suffix, codex TUI)..."
1671
- local codex_poll_start
1672
- codex_poll_start=$(date +%s)
1673
- while true; do
1674
- if [[ -f "$VERDICT_FILE" ]]; then
1675
- # Validate JSON
1676
- if jq . "$VERDICT_FILE" >/dev/null 2>&1; then
1677
- log " Verdict file detected: $VERDICT_FILE"
1678
- break
1679
- fi
1680
- fi
1681
- local codex_elapsed=$(( $(date +%s) - codex_poll_start ))
1682
- if (( codex_elapsed >= ITER_TIMEOUT )); then
1683
- log_error "Codex verifier$suffix timed out after ${ITER_TIMEOUT}s"
1684
- return 1
1685
- fi
1686
- sleep "$POLL_INTERVAL"
1687
- done
1688
- else
1689
- # Claude: use full poll_for_signal with heartbeat/nudge
1690
- log " Polling for verify-verdict.json ($suffix)..."
1691
- if ! poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier$suffix"; then
1692
- local verifier_poll_rc=$?
1693
- if (( verifier_poll_rc == 2 )); then
1694
- return 1
1695
- fi
1696
- log_error "Verifier$suffix poll failed"
1697
- return 1
1698
- fi
1699
- fi
1700
-
1701
- # Copy verdict to destination
1702
- cp "$VERDICT_FILE" "$verdict_dest"
1703
- log " Verifier$suffix verdict saved to $verdict_dest"
1704
- return 0
1705
- }
1706
-
1707
- # --- Sequential final verify: run per-US scoped verifiers instead of one big ALL verify ---
1708
- # Returns 0 if all US pass + integration check pass, 1 if any US fails, 2 if integration fails.
1709
- # Sets FAILED_US global on failure.
1710
- run_sequential_final_verify() {
1711
- local iter="$1"
1712
- FAILED_US=""
1713
-
1714
- log " Sequential final verify: ${US_LIST} (${VERIFY_MODE} mode)"
1715
- log_debug "[FLOW] iter=$iter phase=sequential_final_verify us_list=$US_LIST"
1716
-
1717
- for us in $(echo "$US_LIST" | tr ',' ' '); do
1718
- log " Final verify: checking $us..."
1719
-
1720
- # Temporarily override signal file to scope verifier to this US
1721
- local orig_signal
1722
- orig_signal=$(cat "$SIGNAL_FILE" 2>/dev/null)
1723
- echo "{\"status\":\"verify\",\"us_id\":\"$us\",\"summary\":\"sequential final verify\"}" | atomic_write "$SIGNAL_FILE"
1724
-
1725
- # Write scoped verifier trigger
1726
- write_verifier_trigger "$iter"
1727
- local verifier_prompt="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier-prompt.md"
1728
-
1729
- # Clean verifier pane
1730
- local verifier_cmd
1731
- verifier_cmd=$(tmux display-message -p -t "$VERIFIER_PANE" '#{pane_current_command}' 2>/dev/null)
1732
- if [[ "$verifier_cmd" == "node" || "$verifier_cmd" == "claude" || "$verifier_cmd" == "codex" ]]; then
1733
- tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null; sleep 0.5
1734
- tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null; sleep 2
1735
- fi
1736
- wait_for_pane_ready "$VERIFIER_PANE" 10 2>/dev/null || true
1737
-
1738
- # Launch verifier
1739
- local verifier_launch
1740
- if [[ "$VERIFIER_ENGINE" = "codex" ]]; then
1741
- verifier_launch="${CODEX_BIN:-codex} -m $VERIFIER_CODEX_MODEL -c model_reasoning_effort=\"$VERIFIER_CODEX_REASONING\" --disable plugins --dangerously-bypass-approvals-and-sandbox"
1742
- launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch"
1743
- else
1744
- verifier_launch="$(build_claude_cmd tui "$VERIFIER_MODEL")"
1745
- launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch" || {
1746
- log_error "Failed to launch verifier for $us"
1747
- FAILED_US="$us"
1748
- return 1
1749
- }
1750
- fi
1751
-
1752
- # Poll for verdict
1753
- rm -f "$VERDICT_FILE"
1754
- local poll_rc=0
1755
- poll_for_signal "$VERDICT_FILE" "$ITER_TIMEOUT" "verdict" || poll_rc=$?
1756
- if (( poll_rc != 0 )); then
1757
- log_error "Verifier poll failed for $us (rc=$poll_rc)"
1758
- FAILED_US="$us"
1759
- return 1
1760
- fi
1761
-
1762
- # Check verdict
1763
- local verdict
1764
- verdict=$(jq -r '.verdict' "$VERDICT_FILE" 2>/dev/null)
1765
- if [[ "$verdict" != "pass" ]]; then
1766
- FAILED_US="$us"
1767
- log " Sequential final verify FAILED at $us"
1768
- log_debug "[FLOW] iter=$iter phase=sequential_final_verify failed_us=$us verdict=$verdict"
1769
- return 1
1770
- fi
1771
- log " Sequential final verify: $us PASSED"
1772
-
1773
- # Archive per-US final verdict
1774
- cp "$VERDICT_FILE" "$LOGS_DIR/iter-$(printf '%03d' $iter).final-verdict-${us}.json" 2>/dev/null
1775
- done
1776
-
1777
- # Integration check: run tests if VERIFICATION_CMD is set
1778
- if [[ -n "${VERIFICATION_CMD:-}" ]]; then
1779
- log " Running integration test suite after sequential verify..."
1780
- log_debug "[FLOW] iter=$iter phase=integration_check cmd=$VERIFICATION_CMD"
1781
- if ! eval "$VERIFICATION_CMD" > /dev/null 2>&1; then
1782
- log " Integration test suite FAILED"
1783
- FAILED_US="integration"
1784
- return 2
1785
- fi
1786
- log " Integration test suite PASSED"
1787
- fi
1788
-
1789
- log " Sequential final verify: ALL PASSED"
1790
- return 0
1791
- }
1792
-
1793
- # --- US-005: Determine whether consensus verification should run for this signal ---
1794
- # Returns 0 (use consensus) or 1 (single engine).
1795
- # Uses unified CONSENSUS_MODE: off|all|final-only
1796
- _should_use_consensus() {
1797
- local signal_us_id="${1:-}"
1798
- case "$CONSENSUS_MODE" in
1799
- all) return 0 ;;
1800
- final-only) [[ "$signal_us_id" == "ALL" ]] && return 0 ;;
1801
- off|*) return 1 ;;
1802
- esac
1803
- }
1804
-
1805
- # --- US-004: Run consensus verification (claude + codex sequentially) ---
1806
- run_consensus_verification() {
1807
- local iter="$1"
1808
- local claude_verdict_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verify-verdict-claude.json"
1809
- local codex_verdict_file="$LOGS_DIR/iter-$(printf '%03d' $iter).verify-verdict-codex.json"
1810
-
1811
- CONSENSUS_ROUND=0
1812
- CLAUDE_VERDICT=""
1813
- CODEX_VERDICT=""
1814
-
1815
- while (( CONSENSUS_ROUND < 6 )); do
1816
- (( CONSENSUS_ROUND++ ))
1817
- log " Consensus round $CONSENSUS_ROUND/6..."
1818
-
1819
- # Run claude verifier first
1820
- local _claude_t0=$(date +%s)
1821
- if ! run_single_verifier "$iter" "claude" "$VERIFIER_MODEL" "-claude" "$claude_verdict_file"; then
1822
- log_error "Claude verifier failed in consensus round $CONSENSUS_ROUND"
1823
- return 1
1824
- fi
1825
- ITER_VERIFIER_CLAUDE_DURATION_S=$(( $(date +%s) - _claude_t0 ))
1826
- CLAUDE_VERDICT=$(jq -r '.verdict' "$claude_verdict_file" 2>/dev/null)
1827
- # A12 fix: validate claude verdict is not null/empty — if so, retry once before proceeding
1828
- if [[ -z "$CLAUDE_VERDICT" || "$CLAUDE_VERDICT" == "null" ]]; then
1829
- log " WARNING: Claude verdict is '$CLAUDE_VERDICT' — likely interrupted. Retrying claude verifier..."
1830
- log_debug "[GOV] iter=$iter phase=consensus_claude_retry reason=null_verdict"
1831
- rm -f "$claude_verdict_file" 2>/dev/null
1832
- if ! run_single_verifier "$iter" "claude" "$VERIFIER_MODEL" "-claude" "$claude_verdict_file"; then
1833
- log_error "Claude verifier retry also failed"
1834
- return 1
1835
- fi
1836
- CLAUDE_VERDICT=$(jq -r '.verdict' "$claude_verdict_file" 2>/dev/null)
1837
- if [[ -z "$CLAUDE_VERDICT" || "$CLAUDE_VERDICT" == "null" ]]; then
1838
- log_error "Claude verdict still null after retry — consensus cannot proceed"
1839
- return 1
1840
- fi
1841
- fi
1842
- log_debug "[GOV] iter=$iter phase=consensus_claude verdict=$CLAUDE_VERDICT model=$VERIFIER_MODEL"
1843
-
1844
- # consensus-fail-fast removed (complexity vs value too low)
1845
-
1846
- # Run codex verifier second
1847
- local _codex_t0=$(date +%s)
1848
- if ! run_single_verifier "$iter" "codex" "$VERIFIER_CODEX_MODEL" "-codex" "$codex_verdict_file"; then
1849
- log_error "Codex verifier failed in consensus round $CONSENSUS_ROUND"
1850
- return 1
1851
- fi
1852
- ITER_VERIFIER_CODEX_DURATION_S=$(( $(date +%s) - _codex_t0 ))
1853
- CODEX_VERDICT=$(jq -r '.verdict' "$codex_verdict_file" 2>/dev/null)
1854
- log_debug "[GOV] iter=$iter phase=consensus_codex verdict=$CODEX_VERDICT model=$VERIFIER_CODEX_MODEL reasoning=$VERIFIER_CODEX_REASONING"
1855
-
1856
- log " Consensus: claude=$CLAUDE_VERDICT codex=$CODEX_VERDICT"
1857
- local _combined_action="retry"
1858
- if [[ "$CLAUDE_VERDICT" = "pass" && "$CODEX_VERDICT" = "pass" ]]; then _combined_action="pass"
1859
- elif (( CONSENSUS_ROUND >= 6 )); then _combined_action="blocked"
1860
- fi
1861
- log_debug "[GOV] iter=$iter phase=consensus round=$CONSENSUS_ROUND claude=$CLAUDE_VERDICT codex=$CODEX_VERDICT combined_action=$_combined_action"
1862
-
1863
- # Both pass → success
1864
- if [[ "$CLAUDE_VERDICT" = "pass" && "$CODEX_VERDICT" = "pass" ]]; then
1865
- # Create merged verdict with per-engine details
1866
- {
1867
- echo '{'
1868
- echo ' "verdict": "pass",'
1869
- echo ' "verified_at_utc": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",'
1870
- echo ' "summary": "Consensus PASS: both claude and codex verified independently",'
1871
- echo ' "recommended_state_transition": "complete",'
1872
- echo ' "consensus": {'
1873
- echo ' "claude": { "verdict": "pass", "file": "'"$claude_verdict_file"'" },'
1874
- echo ' "codex": { "verdict": "pass", "file": "'"$codex_verdict_file"'" },'
1875
- echo ' "round": '"$CONSENSUS_ROUND"
1876
- echo ' }'
1877
- echo '}'
1878
- } | atomic_write "$VERDICT_FILE"
1879
- return 0
1880
- fi
1881
-
1882
- # Consensus disagreement
1883
- log_debug "[GOV] iter=$iter phase=consensus_disagreement round=$CONSENSUS_ROUND claude=$CLAUDE_VERDICT codex=$CODEX_VERDICT action=fix_contract"
1884
-
1885
- # NOTE: pre_existing_failure heuristic was removed (v0.3.5).
1886
- # It used unreliable grep-in-description string matching to classify
1887
- # consensus failures as "pre-existing", bypassing the consensus rule.
1888
- # Consensus disagreement now ALWAYS flows to fix contract.
1889
- # Codex CLI crash (no verdict file) is handled upstream via run_single_verifier return 1 → BLOCKED.
1890
-
1891
- # --- Consensus disagreement: build fix contract ---
1892
- local fix_contract="$LOGS_DIR/iter-$(printf '%03d' $iter).fix-contract.md"
1893
- {
1894
- echo "# Fix Contract (Consensus Round $CONSENSUS_ROUND, iteration $iter)"
1895
- echo ""
1896
- echo "## Claude Verdict: $CLAUDE_VERDICT"
1897
- if [[ "$CLAUDE_VERDICT" = "fail" ]]; then
1898
- echo "### Claude Issues"
1899
- jq -r '.issues[]? | "- [\(.severity // "unknown")] \(.criterion // "?"): \(.description // "no description")\(if .fix_hint then " (hint: \(.fix_hint))" else "" end)"' "$claude_verdict_file" 2>/dev/null || echo "- (no structured issues)"
1900
- fi
1901
- echo ""
1902
- echo "## Codex Verdict: $CODEX_VERDICT"
1903
- if [[ "$CODEX_VERDICT" = "fail" ]]; then
1904
- echo "### Codex Issues"
1905
- jq -r '.issues[]? | "- [\(.severity // "unknown")] \(.criterion // "?"): \(.description // "no description")\(if .fix_hint then " (hint: \(.fix_hint))" else "" end)"' "$codex_verdict_file" 2>/dev/null || echo "- (no structured issues)"
1906
- fi
1907
- echo ""
1908
- echo "## Traceability"
1909
- echo "Only changes that resolve a listed issue are allowed."
1910
- } | atomic_write "$fix_contract"
1911
-
1912
- log " Combined fix contract: $fix_contract"
1913
-
1914
- # If this is not the last round, the caller will dispatch the Worker with the fix contract
1915
- # For now, write a fail verdict so the main loop can handle the fix loop
1916
- if (( CONSENSUS_ROUND < 6 )); then
1917
- # Create a merged fail verdict for the main loop — include issues from BOTH verdicts
1918
- local merged_issues="[]"
1919
- local claude_issues codex_issues
1920
- claude_issues=$(jq -c '[.issues[]? | . + {"source": "claude"}]' "$claude_verdict_file" 2>/dev/null || echo '[]')
1921
- codex_issues=$(jq -c '[.issues[]? | . + {"source": "codex"}]' "$codex_verdict_file" 2>/dev/null || echo '[]')
1922
- merged_issues=$(echo "$claude_issues $codex_issues" | jq -s 'add // []')
1923
- {
1924
- echo '{'
1925
- echo ' "verdict": "fail",'
1926
- echo ' "verified_at_utc": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",'
1927
- echo ' "summary": "Consensus disagreement (round '"$CONSENSUS_ROUND"'/6): claude='"$CLAUDE_VERDICT"' codex='"$CODEX_VERDICT"'",'
1928
- echo ' "issues": '"$merged_issues"','
1929
- echo ' "recommended_state_transition": "continue",'
1930
- echo ' "consensus": { "claude": "'"$CLAUDE_VERDICT"'", "codex": "'"$CODEX_VERDICT"'", "round": '"$CONSENSUS_ROUND"' }'
1931
- echo '}'
1932
- } | atomic_write "$VERDICT_FILE"
1933
- return 2 # special return: consensus disagreement, needs retry
1934
- fi
1935
- done
1936
-
1937
- # Max consensus rounds exceeded — include issues from both verdicts
1938
- log_error "Consensus failed after 6 rounds"
1939
- local final_claude_issues final_codex_issues final_merged_issues
1940
- final_claude_issues=$(jq -c '[.issues[]? | . + {"source": "claude"}]' "$claude_verdict_file" 2>/dev/null || echo '[]')
1941
- final_codex_issues=$(jq -c '[.issues[]? | . + {"source": "codex"}]' "$codex_verdict_file" 2>/dev/null || echo '[]')
1942
- final_merged_issues=$(echo "$final_claude_issues $final_codex_issues" | jq -s 'add // []')
1943
- {
1944
- echo '{'
1945
- echo ' "verdict": "fail",'
1946
- echo ' "verified_at_utc": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",'
1947
- echo ' "summary": "Consensus failed after 6 rounds: claude='"$CLAUDE_VERDICT"' codex='"$CODEX_VERDICT"'",'
1948
- echo ' "issues": '"$final_merged_issues"','
1949
- echo ' "recommended_state_transition": "blocked",'
1950
- echo ' "consensus": { "claude": "'"$CLAUDE_VERDICT"'", "codex": "'"$CODEX_VERDICT"'", "round": 6 }'
1951
- echo '}'
1952
- } | atomic_write "$VERDICT_FILE"
1953
- return 1
1954
- }
1955
-
1956
- # =============================================================================
1957
- # Main Leader Loop
1958
- # =============================================================================
1959
-
1960
- main() {
1961
- # --- Lockfile: prevent duplicate execution ---
1962
- local lockfile="$LOCKFILE_PATH"
1963
- mkdir -p "$(dirname "$lockfile")" 2>/dev/null
1964
- if ! (set -C; echo $$ > "$lockfile") 2>/dev/null; then
1965
- local lock_pid
1966
- lock_pid=$(cat "$lockfile" 2>/dev/null)
1967
- if kill -0 "$lock_pid" 2>/dev/null; then
1968
- log_error "Another instance is already running (PID $lock_pid). Kill $lock_pid or rm $lockfile"
1969
- exit 1
1970
- fi
1971
- # Stale lock — overwrite
1972
- log "Stale lock detected (PID ${lock_pid:-unknown} not running), recovering"
1973
- echo $$ > "$lockfile"
1974
- LOCKFILE_ACQUIRED=1
1975
- else
1976
- LOCKFILE_ACQUIRED=1
1977
- fi
1978
- trap cleanup EXIT INT TERM
1979
- mkdir -p "$LOGS_DIR" "$RUNTIME_DIR" 2>/dev/null
1980
-
1981
- # --- Analytics directory: always create (campaign.jsonl + metadata.json are always-on) ---
1982
- mkdir -p "$ANALYTICS_DIR" 2>/dev/null
1983
-
1984
- # --- debug.log versioning (in analytics dir, --debug only) ---
1985
- if (( DEBUG )) && [[ -f "$DEBUG_LOG" ]]; then
1986
- local dbg_n=1
1987
- while [[ -f "${DEBUG_LOG%.log}-v${dbg_n}.log" ]]; do
1988
- (( dbg_n++ ))
1989
- done
1990
- mv "$DEBUG_LOG" "${DEBUG_LOG%.log}-v${dbg_n}.log"
1991
- fi
1992
-
1993
- # --- campaign.jsonl versioning (always-on) ---
1994
- if [[ -f "$CAMPAIGN_JSONL" ]]; then
1995
- local cj_n=1
1996
- while [[ -f "${CAMPAIGN_JSONL%.jsonl}-v${cj_n}.jsonl" ]]; do
1997
- (( cj_n++ ))
1998
- done
1999
- mv "$CAMPAIGN_JSONL" "${CAMPAIGN_JSONL%.jsonl}-v${cj_n}.jsonl"
2000
- fi
2001
-
2002
- # --- metadata.json: always write at campaign start (cross-project identification) ---
2003
- jq -n \
2004
- --arg slug "$SLUG" \
2005
- --arg project_root "$ROOT" \
2006
- --arg project_name "$(basename "$ROOT")" \
2007
- --arg campaign_status "running" \
2008
- --arg start_time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
2009
- --arg end_time "" \
2010
- --arg worker_model "$WORKER_MODEL" \
2011
- --arg verifier_model "$VERIFIER_MODEL" \
2012
- --argjson debug "$DEBUG" \
2013
- --argjson with_sv "$WITH_SELF_VERIFICATION" \
2014
- --argjson consensus "${VERIFY_CONSENSUS:-0}" \
2015
- '{slug: $slug, project_root: $project_root, project_name: $project_name, campaign_status: $campaign_status, start_time: $start_time, end_time: $end_time, worker_model: $worker_model, verifier_model: $verifier_model, debug: $debug, with_self_verification: $with_sv, consensus: $consensus}' \
2016
- > "$METADATA_FILE"
2017
-
2018
- # --- Startup ---
2019
- log "Ralph Desk Tmux Runner starting..."
2020
- log " Slug: $SLUG"
2021
- log " Root: $ROOT"
2022
- log " Max iterations: $MAX_ITER"
2023
- log " Worker model: $WORKER_MODEL"
2024
- log " Verifier model: $VERIFIER_MODEL (per-US) / $FINAL_VERIFIER_MODEL (final)"
2025
- log " Verify mode: $VERIFY_MODE"
2026
- log " Consensus mode: $CONSENSUS_MODE"
2027
- log " Consensus model: $CONSENSUS_MODEL (per-US) / $FINAL_CONSENSUS_MODEL (final)"
2028
- log " Poll interval: ${POLL_INTERVAL}s"
2029
- log " Iter timeout: ${ITER_TIMEOUT}s"
2030
- # --- Debug: Log execution plan ---
2031
- if (( DEBUG )); then
2032
- # Extract US IDs from PRD
2033
- local prd_file="$DESK/plans/prd-$SLUG.md"
2034
- local us_list=""
2035
- if [[ -f "$prd_file" ]]; then
2036
- us_list=$(grep -oE 'US-[0-9]+' "$prd_file" | sort -u | tr '\n' ',' | sed 's/,$//')
2037
- fi
2038
- local us_count=$(echo "$us_list" | tr ',' '\n' | grep -c 'US-')
2039
-
2040
- log_debug "[OPTION] slug=$SLUG us_count=$us_count us_list=$us_list"
2041
- log_debug "[OPTION] worker_engine=$WORKER_ENGINE worker_model=$WORKER_MODEL"
2042
- log_debug "[OPTION] verifier_engine=$VERIFIER_ENGINE verifier_model=$VERIFIER_MODEL"
2043
- log_debug "[OPTION] verify_mode=$VERIFY_MODE consensus_mode=$CONSENSUS_MODE max_iter=$MAX_ITER"
2044
- log_debug "[OPTION] cb_threshold=$CB_THRESHOLD effective_cb_threshold=$EFFECTIVE_CB_THRESHOLD iter_timeout=$ITER_TIMEOUT with_self_verification=$WITH_SELF_VERIFICATION debug=$DEBUG"
2045
-
2046
- if [[ "$VERIFY_MODE" = "per-us" ]]; then
2047
- # Build expected flow
2048
- local expected_flow=""
2049
- for us in $(echo "$us_list" | tr ',' ' '); do
2050
- expected_flow="${expected_flow}worker->verify($us)->"
2051
- done
2052
- expected_flow="${expected_flow}verify(ALL)->COMPLETE"
2053
- log_debug "[OPTION] expected_flow=$expected_flow"
2054
- else
2055
- log_debug "[OPTION] expected_flow=worker(all)->verify(ALL)->COMPLETE"
2056
- fi
2057
-
2058
- if [[ "${VERIFY_CONSENSUS:-0}" = "1" ]]; then
2059
- log_debug "[OPTION] consensus_flow=each_verify_runs_claude+codex_both_must_pass"
2060
- fi
2061
- fi
2062
-
2063
- # Extract US list for per-US sequencing
2064
- if [[ "$VERIFY_MODE" = "per-us" ]]; then
2065
- local prd_file="$DESK/plans/prd-$SLUG.md"
2066
- if [[ -f "$prd_file" ]]; then
2067
- US_LIST=$(grep -oE 'US-[0-9]+' "$prd_file" | sort -u | tr '\n' ',' | sed 's/,$//')
2068
- fi
2069
-
2070
- # Initialize VERIFIED_US from memory's Completed Stories (carry over previous runs)
2071
- local memory_file="$DESK/memos/${SLUG}-memory.md"
2072
- if [[ -f "$memory_file" ]]; then
2073
- local completed_us
2074
- completed_us=$(sed -n '/^## Completed Stories$/,/^## /p' "$memory_file" 2>/dev/null | grep '^- US-' | sed 's/^- \(US-[0-9]*\):.*/\1/' | sort -u | tr '\n' ',' | sed 's/,$//')
2075
- if [[ -n "$completed_us" ]]; then
2076
- VERIFIED_US="$completed_us"
2077
- log " Loaded completed stories from memory: $VERIFIED_US"
2078
- log_debug "[FLOW] loaded_verified_us_from_memory=$VERIFIED_US"
2079
- fi
2080
- fi
2081
-
2082
- # D1: Fallback — restore verified_us from status.json if memory had none
2083
- if [[ -z "$VERIFIED_US" && -f "$STATUS_FILE" ]]; then
2084
- local status_verified
2085
- status_verified=$(jq -r '.verified_us // [] | join(",")' "$STATUS_FILE" 2>/dev/null)
2086
- if [[ -n "$status_verified" ]]; then
2087
- VERIFIED_US="$status_verified"
2088
- log " Restored verified_us from status.json: $VERIFIED_US"
2089
- log_debug "[FLOW] restored_verified_us_from_status=$VERIFIED_US"
2090
- fi
2091
- fi
2092
- fi
2093
-
2094
- # Initialize PRD snapshot state for live update detection
2095
- PREV_PRD_HASH=$(compute_prd_hash)
2096
- PREV_PRD_US_LIST=$(count_prd_us)
2097
-
2098
- # Dependency checks
2099
- check_dependencies
2100
-
2101
- # Print security warning (governance.md s7: --dangerously-skip-permissions)
2102
- print_security_warning
2103
-
2104
- # Validate scaffold
2105
- validate_scaffold
2106
-
2107
- # Check for existing sessions
2108
- check_existing_sessions
2109
-
2110
- # Create tmux session with pane IDs (governance.md s7 step 1)
2111
- create_session
2112
-
2113
- # Set trap for cleanup on exit/error
2114
- trap cleanup EXIT
2115
-
2116
- # Initialize context hash for stale detection
2117
- PREV_CONTEXT_HASH=$(compute_context_hash)
2118
-
2119
- # --- governance.md s7: Leader Loop ---
2120
- local HARD_CEILING=$(( ITER_TIMEOUT * 3 )) # logged but NOT enforced — Worker extends indefinitely when active
2121
-
2122
- for (( ITERATION = 1; ITERATION <= MAX_ITER; ITERATION++ )); do
2123
- log ""
2124
- log "========== Iteration $ITERATION / $MAX_ITER =========="
2125
- local ITER_START_TIME
2126
- ITER_START_TIME=$(date +%s)
2127
- local _iter_contract=""
2128
- _iter_contract=$(sed -n '/^## Next Iteration Contract$/,/^## /{ /^## Next/d; /^## [^N]/d; p; }' "$MEMORY_FILE" 2>/dev/null | head -1 | tr '\n' ' ')
2129
- log_debug "[FLOW] iter=$ITERATION start contract=\"${_iter_contract:-none}\""
2130
-
2131
- # --- governance.md s7 step 1: Check sentinels ---
2132
- if [[ -f "$COMPLETE_SENTINEL" ]]; then
2133
- log "COMPLETE sentinel found. Campaign succeeded."
2134
- update_status "complete" "complete"
2135
- return 0
2136
- fi
2137
- if [[ -f "$BLOCKED_SENTINEL" ]]; then
2138
- log "BLOCKED sentinel found. Campaign blocked."
2139
- update_status "blocked" "blocked"
2140
- return 1
2141
- fi
2142
-
2143
- # --- governance.md s7 step 8 (cleanup): Clean previous iteration signals ---
2144
- rm -f "$SIGNAL_FILE" "$DONE_CLAIM_FILE" "$VERDICT_FILE" 2>/dev/null
2145
- rm -f "$WORKER_HEARTBEAT" "$VERIFIER_HEARTBEAT" 2>/dev/null
2146
-
2147
- # --- Clean previous claude session in panes (one-shot lifecycle) ---
2148
- # Only needed from iteration 2 onwards (iteration 1 has fresh panes)
2149
- if (( ITERATION > 1 )); then
2150
- # Send C-c first (in case claude is mid-task), then /exit
2151
- tmux send-keys -t "$WORKER_PANE" C-c 2>/dev/null
2152
- sleep 1
2153
- tmux send-keys -t "$WORKER_PANE" "/exit" C-m 2>/dev/null
2154
- sleep 2
2155
- # Wait for shell prompt before proceeding
2156
- wait_for_pane_ready "$WORKER_PANE" 10 2>/dev/null || true
2157
- fi
2158
-
2159
- # Reset per-iteration state
2160
- local worker_nudge_count=0
2161
- local verifier_nudge_count=0
2162
- ITER_VERIFIER_START=""
2163
- ITER_VERIFIER_END=""
2164
-
2165
- # --- US-004: detect PRD changes for live update + re-split ---
2166
- check_prd_update
2167
-
2168
- # --- governance.md s7 step 4: Build worker prompt + trigger ---
2169
- write_worker_trigger "$ITERATION"
2170
- local worker_prompt="$LOGS_DIR/iter-$(printf '%03d' $ITERATION).worker-prompt.md"
2171
-
2172
- # AC1: capture worker start timestamp
2173
- ITER_WORKER_START=$(date +%s)
2174
-
2175
- update_status "worker" "running"
2176
-
2177
- # --- governance.md s7 step 5: Execute Worker (dispatched to engine-specific function) ---
2178
- log_debug "[FLOW] iter=$ITERATION phase=worker engine=$WORKER_ENGINE model=$WORKER_MODEL dispatched=true"
2179
-
2180
- local worker_launch
2181
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
2182
- worker_launch="${CODEX_BIN:-codex} -m $WORKER_CODEX_MODEL -c model_reasoning_effort=\"$WORKER_CODEX_REASONING\" --disable plugins --dangerously-bypass-approvals-and-sandbox"
2183
- if ! launch_worker_codex "$WORKER_PANE" "$worker_prompt" "$ITERATION" "$worker_launch"; then
2184
- write_blocked_sentinel "Worker codex failed to start in pane"
2185
- update_status "blocked" "worker_start_failed"
2186
- return 1
2187
- fi
2188
- else
2189
- worker_launch="$(build_claude_cmd tui "$WORKER_MODEL")"
2190
- if ! launch_worker_claude "$WORKER_PANE" "$worker_prompt" "$ITERATION" "$worker_launch"; then
2191
- write_blocked_sentinel "Worker claude failed to start in pane"
2192
- update_status "blocked" "worker_start_failed"
2193
- return 1
2194
- fi
2195
- fi
2196
-
2197
- # --- governance.md s7 step 5+6: Poll for Worker completion ---
2198
- log " Polling for iter-signal.json..."
2199
- local worker_poll_done=0
2200
- while (( ! worker_poll_done )); do
2201
- local worker_poll_rc=0
2202
- if poll_for_signal "$SIGNAL_FILE" "$WORKER_HEARTBEAT" "$WORKER_PANE" "$worker_launch" "Worker"; then
2203
- worker_poll_done=1
2204
- log_debug "[FLOW] iter=$ITERATION poll_signal_received=true"
2205
- else
2206
- worker_poll_rc=$?
2207
- if (( worker_poll_rc == 2 )); then
2208
- return 1
2209
- fi
2210
- # Check if Worker is still actively running (not stuck)
2211
- local worker_cmd
2212
- worker_cmd=$(tmux display-message -p -t "$WORKER_PANE" '#{pane_current_command}' 2>/dev/null)
2213
- if [[ "$worker_cmd" == "node" || "$worker_cmd" == "claude" || "$worker_cmd" == "codex" ]]; then
2214
- # Process alive — extend indefinitely (no hard ceiling kill)
2215
- # Stale-context breaker and nudge system handle truly stuck workers
2216
- local iter_elapsed=$(( $(date +%s) - ITER_START_TIME ))
2217
- local ceiling_exceeded=""
2218
- if (( iter_elapsed >= HARD_CEILING )); then
2219
- ceiling_exceeded=" [EXCEEDED hard_ceiling=${HARD_CEILING}s — not enforced, logged only]"
2220
- log " WARNING: Worker exceeded soft hard-ceiling (${iter_elapsed}s >= ${HARD_CEILING}s) but still active. Continuing..."
2221
- log_debug "[GOV] iter=$ITERATION hard_ceiling_exceeded=true elapsed=${iter_elapsed}s ceiling=${HARD_CEILING}s process=$worker_cmd action=log_only_no_kill"
2222
- fi
2223
- log " Worker timed out but still active ($worker_cmd). Extending poll... (${iter_elapsed}s, no ceiling)${ceiling_exceeded}"
2224
- log_debug "[GOV] iter=$ITERATION timeout_active=true process=$worker_cmd elapsed=${iter_elapsed}s action=extend_indefinitely"
2225
- log_debug "[FLOW] iter=$ITERATION poll_extended=true worker_cmd=$worker_cmd"
2226
- update_status "worker" "slow"
2227
- # Loop continues — re-poll same iteration
2228
- else
2229
- # Worker is truly dead/stuck
2230
- (( MONITOR_FAILURE_COUNT++ ))
2231
- log_debug "[GOV] iter=$ITERATION monitor_failure=$MONITOR_FAILURE_COUNT/3"
2232
- if (( MONITOR_FAILURE_COUNT >= 3 )); then
2233
- log_debug "[GOV] iter=$ITERATION circuit_breaker=monitor_failures detail=\"3 consecutive monitor failures\""
2234
- write_blocked_sentinel "3 consecutive monitor failures (worker not active)"
2235
- update_status "blocked" "monitor_failures"
2236
- return 1
2237
- fi
2238
- log " WARNING: Worker poll failed (monitor failure $MONITOR_FAILURE_COUNT/3)"
2239
- update_status "worker" "poll_failed"
2240
- log_debug "[FLOW] iter=$ITERATION poll_worker_dead=true worker_cmd=$worker_cmd"
2241
- # Worker is truly dead/stuck — BLOCK and let user decide
2242
- write_blocked_sentinel "Worker process dead/stuck (poll failed). Pane preserved for inspection."
2243
- update_status "blocked" "worker_dead"
2244
- return 1
2245
- fi
2246
- fi
2247
- done
2248
-
2249
- if [[ ! -f "$SIGNAL_FILE" ]]; then
2250
- log_debug "[FLOW] iter=$ITERATION no_signal_after_poll=true continuing"
2251
- # No signal — monitor failure, go to next iteration
2252
- continue
2253
- fi
2254
-
2255
- # Reset monitor failure count on success
2256
- MONITOR_FAILURE_COUNT=0
2257
-
2258
- # AC1: capture worker end timestamp; reset consensus timing
2259
- ITER_WORKER_END=$(date +%s)
2260
- ITER_VERIFIER_CLAUDE_DURATION_S=""
2261
- ITER_VERIFIER_CODEX_DURATION_S=""
2262
-
2263
- # --- governance.md s7 step 6: Read iter-signal.json via jq (JSON only, no markdown) ---
2264
- local signal_status
2265
- signal_status=$(jq -r '.status' "$SIGNAL_FILE" 2>/dev/null)
2266
- local signal_summary
2267
- signal_summary=$(jq -r '.summary // "no summary"' "$SIGNAL_FILE" 2>/dev/null)
2268
-
2269
- log " Worker signal: status=$signal_status summary=\"$signal_summary\""
2270
-
2271
- # Read us_id early for EXEC logging (also used later in verify branch)
2272
- local signal_us_id_early=""
2273
- signal_us_id_early=$(jq -r '.us_id // empty' "$SIGNAL_FILE" 2>/dev/null)
2274
- log_debug "[FLOW] iter=$ITERATION phase=worker_signal status=$signal_status us_id=${signal_us_id_early:-none} summary=\"$signal_summary\""
2275
-
2276
- case "$signal_status" in
2277
- continue)
2278
- # --- governance.md s7 step 6: continue -> go to step 8 ---
2279
- log " Worker requests continue. Moving to next iteration."
2280
- update_status "worker" "continue"
2281
- ;;
2282
- verify)
2283
- # --- governance.md s7 step 7: Execute Verifier ---
2284
- # Read us_id from signal for per-US scoping
2285
- local signal_us_id=""
2286
- signal_us_id=$(jq -r '.us_id // empty' "$SIGNAL_FILE" 2>/dev/null)
2287
- log " Worker claims done (us_id=${signal_us_id:-all}). Dispatching Verifier..."
2288
-
2289
- # AC1: capture verifier start timestamp
2290
- ITER_VERIFIER_START=$(date +%s)
2291
-
2292
- update_status "verifier" "running"
2293
-
2294
- # --- Sequential final verify: per-US scoped checks instead of one big ALL verify ---
2295
- if [[ "$signal_us_id" == "ALL" && "$VERIFY_MODE" == "per-us" && -n "$US_LIST" ]]; then
2296
- log " Final ALL verify: using sequential per-US strategy (timeout prevention)"
2297
- local seq_rc=0
2298
- run_sequential_final_verify "$ITERATION" || seq_rc=$?
2299
- if (( seq_rc == 0 )); then
2300
- write_complete_sentinel "Sequential final verify passed (all US verified individually)"
2301
- update_status "complete" "pass"
2302
- write_campaign_jsonl "$ITERATION" "ALL" "pass"
2303
- return 0
2304
- else
2305
- # Sequential verify failed — fall through to fix loop with failed US
2306
- log " Sequential final verify failed at ${FAILED_US:-unknown}. Entering fix loop."
2307
- signal_us_id="${FAILED_US:-ALL}"
2308
- # Synthesize a fail verdict for the fix loop
2309
- echo "{\"verdict\":\"fail\",\"summary\":\"Sequential final verify failed at ${FAILED_US:-unknown}\",\"issues\":[{\"severity\":\"critical\",\"criterion\":\"${FAILED_US:-ALL}\",\"description\":\"Failed during sequential final verification\"}]}" | atomic_write "$VERDICT_FILE"
2310
- fi
2311
- fi
2312
-
2313
- # --- Consensus scope check (US-005: _should_use_consensus handles CONSENSUS_MODE) ---
2314
- local use_consensus=0
2315
- _should_use_consensus "$signal_us_id" && use_consensus=1
2316
-
2317
- # --- Consensus vs single verification ---
2318
- if (( use_consensus )); then
2319
- # US-004: Run consensus verification (claude + codex sequentially)
2320
- local consensus_rc=0
2321
- run_consensus_verification "$ITERATION" || consensus_rc=$?
2322
-
2323
- if (( consensus_rc == 2 )); then
2324
- # Consensus disagreement — treat as fail, fix loop will handle
2325
- log " Consensus disagreement, treating as fail."
2326
- elif (( consensus_rc != 0 )); then
2327
- # Consensus verification failed entirely
2328
- log_error "Consensus verification failed"
2329
- write_blocked_sentinel "Consensus verification failed after max rounds"
2330
- update_status "blocked" "consensus_failed"
2331
- return 1
2332
- fi
2333
- else
2334
- # Standard single-engine verification
2335
- write_verifier_trigger "$ITERATION"
2336
- local verifier_prompt="$LOGS_DIR/iter-$(printf '%03d' $ITERATION).verifier-prompt.md"
2337
-
2338
- # Step 7a: Clean previous Verifier session (with dead pane detection)
2339
- local verifier_cmd
2340
- verifier_cmd=$(tmux display-message -p -t "$VERIFIER_PANE" '#{pane_current_command}' 2>/dev/null)
2341
- if [[ -z "$verifier_cmd" ]]; then
2342
- log " Verifier pane $VERIFIER_PANE is gone — replacing..."
2343
- log_debug "[GOV] iter=$ITERATION pane_dead=true pane_id=$VERIFIER_PANE action=replace_pane"
2344
- replace_worker_pane "$VERIFIER_PANE" "verifier"
2345
- VERIFIER_PANE=$(jq -r '.panes.verifier' "$SESSION_CONFIG")
2346
- log " New verifier pane: $VERIFIER_PANE"
2347
- elif [[ "$verifier_cmd" == "zsh" || "$verifier_cmd" == "bash" ]]; then
2348
- log " Verifier pane $VERIFIER_PANE has bare shell ($verifier_cmd) — resetting..."
2349
- log_debug "[GOV] iter=$ITERATION pane_dead=true pane_id=$VERIFIER_PANE cmd=$verifier_cmd action=reset_shell"
2350
- tmux send-keys -t "$VERIFIER_PANE" C-c C-u 2>/dev/null
2351
- sleep 0.2
2352
- tmux send-keys -t "$VERIFIER_PANE" "clear" C-m 2>/dev/null
2353
- sleep 0.3
2354
- elif [[ "$verifier_cmd" == "node" || "$verifier_cmd" == "claude" || "$verifier_cmd" == "codex" ]]; then
2355
- tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null
2356
- sleep 0.5
2357
- tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null
2358
- sleep 2
2359
- fi
2360
- wait_for_pane_ready "$VERIFIER_PANE" 10 2>/dev/null || true
2361
-
2362
- local verifier_launch
2363
- if [[ "$VERIFIER_ENGINE" = "codex" ]]; then
2364
- verifier_launch="${CODEX_BIN:-codex} -m $VERIFIER_CODEX_MODEL -c model_reasoning_effort=\"$VERIFIER_CODEX_REASONING\" --disable plugins --dangerously-bypass-approvals-and-sandbox"
2365
- else
2366
- verifier_launch="$(build_claude_cmd tui "$VERIFIER_MODEL")"
2367
- fi
2368
- log_debug "[FLOW] iter=$ITERATION phase=verifier engine=$VERIFIER_ENGINE model=$VERIFIER_MODEL scope=${signal_us_id:-all} dispatched=true"
2369
-
2370
- if [[ "$VERIFIER_ENGINE" = "codex" ]]; then
2371
- launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$ITERATION" "$verifier_launch"
2372
- else
2373
- if ! launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$ITERATION" "$verifier_launch"; then
2374
- update_status "verifier" "start_failed"
2375
- continue
2376
- fi
2377
- fi
2378
-
2379
- # Poll for verify-verdict.json
2380
- log " Polling for verify-verdict.json..."
2381
- if ! poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier"; then
2382
- local verifier_poll_rc=$?
2383
- if (( verifier_poll_rc == 2 )); then
2384
- return 1
2385
- fi
2386
- log_error "Verifier poll failed"
2387
- # Verifier is dead/stuck — BLOCK and let user decide
2388
- write_blocked_sentinel "Verifier process dead/stuck (poll failed). Pane preserved for inspection."
2389
- update_status "blocked" "verifier_dead"
2390
- return 1
2391
- fi
2392
- fi
2393
-
2394
- # AC1: capture verifier end timestamp
2395
- ITER_VERIFIER_END=$(date +%s)
2396
-
2397
- # --- governance.md s7 step 7: Read verdict via jq ---
2398
- local verdict
2399
- verdict=$(jq -r '.verdict' "$VERDICT_FILE" 2>/dev/null)
2400
- local recommended
2401
- recommended=$(jq -r '.recommended_state_transition' "$VERDICT_FILE" 2>/dev/null)
2402
- local verdict_summary
2403
- verdict_summary=$(jq -r '.summary // "no summary"' "$VERDICT_FILE" 2>/dev/null)
2404
-
2405
- log " Verifier: verdict=$verdict recommended=$recommended"
2406
- log " Verifier summary: \"$verdict_summary\""
2407
- local _issues_count=$(jq '.issues | length' "$VERDICT_FILE" 2>/dev/null || echo 0)
2408
- log_debug "[GOV] iter=$ITERATION phase=verdict engine=$VERIFIER_ENGINE verdict=$verdict recommended=$recommended us_id=${signal_us_id:-all} issues=$_issues_count"
2409
-
2410
- case "$verdict" in
2411
- pass)
2412
- CONSECUTIVE_FAILURES=0
2413
- CONSENSUS_ROUND=0
2414
- _SAME_US_FAIL_COUNT=0
2415
- _LAST_FAILED_US=""
2416
- if (( _MODEL_UPGRADED )); then
2417
- log " Worker model restored: ${WORKER_MODEL} → ${_ORIGINAL_WORKER_MODEL} (pass verdict)"
2418
- log_debug "[DECIDE] iter=$ITERATION phase=model_select model_restore=true from=${WORKER_MODEL} to=${_ORIGINAL_WORKER_MODEL}"
2419
- WORKER_MODEL="$_ORIGINAL_WORKER_MODEL"
2420
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
2421
- WORKER_CODEX_MODEL="$WORKER_MODEL"
2422
- WORKER_CODEX_REASONING="$_ORIGINAL_WORKER_CODEX_REASONING"
2423
- fi
2424
- _MODEL_UPGRADED=0
2425
- fi
2426
-
2427
- # --- Verified US tracking (both per-us and batch modes) ---
2428
- if [[ -n "$signal_us_id" && "$signal_us_id" != "ALL" ]]; then
2429
- # Add this US to verified list
2430
- if [[ -n "$VERIFIED_US" ]]; then
2431
- VERIFIED_US="${VERIFIED_US},${signal_us_id}"
2432
- else
2433
- VERIFIED_US="$signal_us_id"
2434
- fi
2435
- log " US $signal_us_id verified. Verified so far: $VERIFIED_US"
2436
- log_debug "[FLOW] iter=$ITERATION verified_us_update=$signal_us_id verified_us_total=$VERIFIED_US"
2437
- update_status "verifier" "pass_us"
2438
- # Worker will do next US on next iteration
2439
- elif [[ "$recommended" == "complete" || "$signal_us_id" == "ALL" ]]; then
2440
- # Final full verify passed or complete recommended
2441
- write_complete_sentinel "$verdict_summary"
2442
- update_status "complete" "pass"
2443
- write_campaign_jsonl "$ITERATION" "${signal_us_id:-ALL}" "pass"
2444
- return 0
2445
- else
2446
- log " Verifier passed but did not recommend complete. Continuing."
2447
- update_status "verifier" "pass_continue"
2448
- fi
2449
- ;;
2450
- fail)
2451
- # --- governance.md s7½: Fix Loop (adapted for tmux lean mode) ---
2452
-
2453
- # Parse per_us_results from verdict to track partial progress (batch + per-us)
2454
- local _prev_verified="$VERIFIED_US"
2455
- if jq -e '.per_us_results' "$VERDICT_FILE" &>/dev/null; then
2456
- local _newly_passed
2457
- _newly_passed=$(jq -r '.per_us_results | to_entries[] | select(.value == "pass") | .key' "$VERDICT_FILE" 2>/dev/null)
2458
- for _pus in $(echo "$_newly_passed"); do
2459
- if ! echo ",$VERIFIED_US," | grep -q ",$_pus,"; then
2460
- if [[ -n "$VERIFIED_US" ]]; then
2461
- VERIFIED_US="${VERIFIED_US},${_pus}"
2462
- else
2463
- VERIFIED_US="$_pus"
2464
- fi
2465
- log " Partial progress: $_pus passed (overall FAIL). Verified so far: $VERIFIED_US"
2466
- fi
2467
- done
2468
- log_debug "[FLOW] iter=$ITERATION partial_progress prev=$_prev_verified now=$VERIFIED_US"
2469
- fi
2470
-
2471
- # Partial progress resets consecutive failures (progress was made)
2472
- if [[ "$VERIFIED_US" != "$_prev_verified" ]]; then
2473
- CONSECUTIVE_FAILURES=0
2474
- log " Progress detected — consecutive_failures reset to 0"
2475
- log_debug "[GOV] iter=$ITERATION consecutive_failures_reset=partial_progress"
2476
- fi
2477
-
2478
- (( CONSECUTIVE_FAILURES++ ))
2479
- record_us_failure "${signal_us_id:-unknown}"
2480
- check_model_upgrade "${signal_us_id:-unknown}"
2481
-
2482
- # Mid-CB warning: alert at halfway point (governance §8 early warning)
2483
- if (( CONSECUTIVE_FAILURES == EFFECTIVE_CB_THRESHOLD / 2 )); then
2484
- log " [WARN] Mid-CB: $CONSECUTIVE_FAILURES/${EFFECTIVE_CB_THRESHOLD} consecutive failures — consider reviewing AC quality"
2485
- log_debug "[GOV] iter=$ITERATION mid_cb_warning=true consecutive_failures=$CONSECUTIVE_FAILURES threshold=$EFFECTIVE_CB_THRESHOLD"
2486
- fi
2487
- local verdict_summary_fail
2488
- verdict_summary_fail=$(jq -r '.summary // "no summary"' "$VERDICT_FILE" 2>/dev/null)
2489
- log " Verifier FAILED (consecutive: $CONSECUTIVE_FAILURES). Building fix contract..."
2490
-
2491
- # Extract issues from verdict for next Worker's fix contract
2492
- local fix_contract="$LOGS_DIR/iter-$(printf '%03d' $ITERATION).fix-contract.md"
2493
- {
2494
- echo "# Fix Contract (from Verifier iteration $ITERATION)"
2495
- echo ""
2496
- if [[ -n "$VERIFIED_US" ]]; then
2497
- echo "## Verified US (do NOT re-implement these)"
2498
- echo "$VERIFIED_US" | tr ',' '\n' | sed 's/^/- /'
2499
- echo ""
2500
- echo "**Focus ONLY on unverified user stories. The above are already verified.**"
2501
- echo ""
2502
- fi
2503
- echo "## Summary"
2504
- echo "$verdict_summary_fail"
2505
- echo ""
2506
- echo "## Issues (from verify-verdict.json)"
2507
- jq -r '.issues[]? | "- [\(.severity // "unknown")] \(.criterion // "?"): \(.description // "no description")\(if .fix_hint then " (hint: \(.fix_hint))" else "" end)"' "$VERDICT_FILE" 2>/dev/null || echo "- (no structured issues available)"
2508
- echo ""
2509
- echo "## Next Iteration Contract"
2510
- jq -r '.next_iteration_contract // "Fix the issues listed above."' "$VERDICT_FILE" 2>/dev/null
2511
- } | atomic_write "$fix_contract"
2512
- log " Fix contract: $fix_contract"
2513
- log_debug "[DECIDE] iter=$ITERATION phase=fix_loop trigger=$verdict consecutive_failures=$CONSECUTIVE_FAILURES fix_contract=$fix_contract"
2514
-
2515
- # Circuit breaker: consecutive failures (with architecture escalation when at model ceiling)
2516
- if (( CONSECUTIVE_FAILURES >= EFFECTIVE_CB_THRESHOLD )); then
2517
- # For codex: use full model:reasoning string (WORKER_MODEL loses reasoning suffix after upgrade)
2518
- _ceiling_model_str="$([[ "$WORKER_ENGINE" = "codex" ]] && echo "${WORKER_CODEX_MODEL}:${WORKER_CODEX_REASONING}" || echo "$WORKER_MODEL")"
2519
- if (( _MODEL_UPGRADED )) && [[ -z "$(get_next_model "$_ceiling_model_str")" ]]; then
2520
- log_debug "[GOV] iter=$ITERATION circuit_breaker=consecutive_failures detail=\"architecture escalation: Worker at ceiling (${WORKER_MODEL}), ${EFFECTIVE_CB_THRESHOLD} consecutive failures\""
2521
- log_error "Circuit breaker: architecture escalation — Worker upgraded to ceiling (${WORKER_MODEL}), ${EFFECTIVE_CB_THRESHOLD} consecutive failures"
2522
- write_blocked_sentinel "architecture escalation: Worker upgraded to ceiling model (${WORKER_MODEL}), ${EFFECTIVE_CB_THRESHOLD} consecutive verification failures"
2523
- else
2524
- log_debug "[GOV] iter=$ITERATION circuit_breaker=consecutive_failures detail=\"${EFFECTIVE_CB_THRESHOLD} consecutive verification failures\""
2525
- log_error "Circuit breaker: ${EFFECTIVE_CB_THRESHOLD} consecutive verification failures"
2526
- write_blocked_sentinel "${EFFECTIVE_CB_THRESHOLD} consecutive verification failures"
2527
- fi
2528
- update_status "blocked" "consecutive_failures"
2529
- return 1
2530
- fi
2531
-
2532
- update_status "verifier" "fail"
2533
- ;;
2534
- request_info)
2535
- # --- governance.md s7 step 7: request_info (degraded in tmux mode) ---
2536
- local verdict_summary_ri
2537
- verdict_summary_ri=$(jq -r '.summary // "no summary"' "$VERDICT_FILE" 2>/dev/null)
2538
- log " Verifier requests info (degraded in tmux lean mode)."
2539
- log " Questions: \"$verdict_summary_ri\""
2540
- log " Treating as soft fail — Worker will see verdict in next iteration."
2541
- update_status "verifier" "request_info"
2542
- ;;
2543
- blocked)
2544
- write_blocked_sentinel "Verifier verdict: blocked - $verdict_summary"
2545
- update_status "blocked" "verifier_blocked"
2546
- return 1
2547
- ;;
2548
- *)
2549
- log_error "Unknown verdict: $verdict"
2550
- update_status "verifier" "unknown_verdict"
2551
- ;;
2552
- esac
2553
- ;;
2554
- blocked)
2555
- # --- governance.md s7 step 6: blocked -> write sentinel ---
2556
- write_blocked_sentinel "Worker reported blocked: $signal_summary"
2557
- update_status "blocked" "worker_blocked"
2558
- return 1
2559
- ;;
2560
- *)
2561
- log_error "Unknown signal status: $signal_status"
2562
- update_status "worker" "unknown_status"
2563
- ;;
2564
- esac
2565
-
2566
- # --- step 7d: Archive iteration artifacts before cleanup ---
2567
- archive_iter_artifacts "$ITERATION"
2568
-
2569
- # --- AC5: Write per-iteration cost estimate ---
2570
- write_cost_log "$ITERATION"
2571
- write_campaign_jsonl "$ITERATION" "${signal_us_id:-unknown}" "${signal_status:-unknown}"
2572
-
2573
- # --- governance.md s7 step 8: Write result log ---
2574
- write_result_log "$ITERATION" "$signal_status"
2575
-
2576
- # --- governance.md s7 step 8: Circuit breaker - stale context check ---
2577
- if ! check_stale_context; then
2578
- log_debug "[GOV] iter=$ITERATION circuit_breaker=stale_context detail=\"context unchanged for 3 consecutive iterations\""
2579
- write_blocked_sentinel "Context unchanged for 3 consecutive iterations (stale)"
2580
- update_status "blocked" "stale_context"
2581
- return 1
2582
- fi
2583
-
2584
- # --- governance.md s7 step 8: Update status ---
2585
- update_status "idle" "${signal_status:-unknown}"
2586
- done
2587
-
2588
- # Max iterations reached
2589
- log "Max iterations ($MAX_ITER) reached."
2590
- update_status "timeout" "max_iter"
2591
- return 1
2592
- }
2593
-
2594
- # =============================================================================
2595
- # Entry Point
2596
- # =============================================================================
2597
-
2598
- # --- CLI: parse --worker-model / --verifier-model flags ---
2599
- # These flags override env-var defaults (WORKER_ENGINE, WORKER_MODEL, etc.)
2600
- # Format: "model:reasoning" → codex engine; "model-name" → claude engine
2601
- _cli_i=1
2602
- while (( _cli_i <= $# )); do
2603
- case "${@[$_cli_i]}" in
2604
- --worker-model)
2605
- (( _cli_i++ ))
2606
- _cli_parsed=$(parse_model_flag "${@[$_cli_i]:-}" "worker") || exit 1
2607
- WORKER_ENGINE="${_cli_parsed%% *}"
2608
- _cli_rest="${_cli_parsed#* }"
2609
- WORKER_MODEL="${_cli_rest%% *}"
2610
- if [[ "$WORKER_ENGINE" = "codex" ]]; then
2611
- WORKER_CODEX_MODEL="$WORKER_MODEL"
2612
- WORKER_CODEX_REASONING="${_cli_rest##* }"
2613
- fi
2614
- ;;
2615
- --verifier-model)
2616
- (( _cli_i++ ))
2617
- _cli_parsed=$(parse_model_flag "${@[$_cli_i]:-}" "verifier") || exit 1
2618
- VERIFIER_ENGINE="${_cli_parsed%% *}"
2619
- _cli_rest="${_cli_parsed#* }"
2620
- VERIFIER_MODEL="${_cli_rest%% *}"
2621
- if [[ "$VERIFIER_ENGINE" = "codex" ]]; then
2622
- VERIFIER_CODEX_MODEL="$VERIFIER_MODEL"
2623
- VERIFIER_CODEX_REASONING="${_cli_rest##* }"
2624
- fi
2625
- ;;
2626
- --lock-worker-model)
2627
- LOCK_WORKER_MODEL=1
2628
- ;;
2629
- --final-verifier-model)
2630
- (( _cli_i++ ))
2631
- _cli_parsed=$(parse_model_flag "${@[$_cli_i]:-}" "final-verifier") || exit 1
2632
- FINAL_VERIFIER_ENGINE="${_cli_parsed%% *}"
2633
- _cli_rest="${_cli_parsed#* }"
2634
- FINAL_VERIFIER_MODEL="${_cli_rest%% *}"
2635
- if [[ "$FINAL_VERIFIER_ENGINE" = "codex" ]]; then
2636
- FINAL_VERIFIER_CODEX_MODEL="$FINAL_VERIFIER_MODEL"
2637
- FINAL_VERIFIER_CODEX_REASONING="${_cli_rest##* }"
2638
- fi
2639
- ;;
2640
- --consensus)
2641
- (( _cli_i++ ))
2642
- CONSENSUS_MODE="${@[$_cli_i]:-off}"
2643
- ;;
2644
- --consensus-model)
2645
- (( _cli_i++ ))
2646
- CONSENSUS_MODEL="${@[$_cli_i]:-gpt-5.4:medium}"
2647
- ;;
2648
- --final-consensus-model)
2649
- (( _cli_i++ ))
2650
- FINAL_CONSENSUS_MODEL="${@[$_cli_i]:-gpt-5.4:high}"
2651
- ;;
2652
- --final-consensus)
2653
- # Legacy: map to new --consensus final-only
2654
- CONSENSUS_MODE="final-only"
2655
- ;;
2656
- --verify-consensus)
2657
- # Legacy: map to new --consensus all
2658
- CONSENSUS_MODE="all"
2659
- ;;
2660
- esac
2661
- (( _cli_i++ ))
2662
- done
2663
- unset _cli_i _cli_parsed _cli_rest
2664
-
2665
- # Require tmux — tmux mode only works inside an active tmux session
2666
- if [[ -z "${TMUX:-}" ]]; then
2667
- echo "ERROR: tmux mode requires running inside a tmux session."
2668
- echo ""
2669
- echo " Start tmux first, then retry:"
2670
- echo " tmux"
2671
- echo " LOOP_NAME=$SLUG $0"
2672
- echo ""
2673
- echo " Or use Agent() mode instead (no tmux needed):"
2674
- echo " /rlp-desk run $SLUG"
2675
- exit 1
2676
- fi
2677
-
2678
- main "$@"