@ai-dev-methodologies/rlp-desk 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,30 @@ For pre-v0.15.4 versions, refer to `git log` and individual GitHub release notes
11
11
  - Post-v0.15.6: remove `RLP_LIFECYCLE_METRICS` flag entirely (per plan v3 ADR follow-ups).
12
12
  - Phase D.1 (handoff documents) + Phase D.2 (per-stage agent role specialization) — both deferred per `docs/plans/v0.15.4-release-runbook.md` §7.6.
13
13
 
14
+ ## [0.18.2] — 2026-06-25
15
+
16
+ Five leader-resilience fixes (per-us, consensus, and config-validation paths), each
17
+ validated by a real-LLM dogfood. Consensus is opt-in; default per-us campaigns benefit
18
+ from the final-verify and config-validation fixes.
19
+
20
+ ### Fixed
21
+ - Final verification is resilient to verifier non-determinism: a user story that already
22
+ passed its per-US check is re-verified (up to a small bound) on a fail verdict, so a
23
+ flaky false-fail must reproduce before it charges a fix-loop failure. A single
24
+ non-deterministic false-fail can no longer block a complete, correct campaign.
25
+ - The `claude` rate-limit banner ("API Error: … temporarily limiting requests … · Rate
26
+ limited") is now recognized as a transient API condition and routed to the bounded
27
+ backoff, instead of being misclassified as a frozen-pane deadlock.
28
+ - Numeric configuration knobs (e.g. `--max-iter`, `--cb-threshold`, timeouts) are now
29
+ validated: a non-integer or out-of-range value falls back to its default with a warning,
30
+ instead of silently mis-evaluating (a bad `--max-iter` previously could make a campaign
31
+ run zero iterations).
32
+ - Leader auto-commit recovery no longer blocks a campaign whose work the worker already
33
+ committed (a commit-timing race previously surfaced as "nothing to commit" → blocked).
34
+ - `--consensus final-only` now actually runs at the final verification in the default
35
+ per-us verify mode (it was previously bypassed by the sequential final-verify path, so
36
+ the recommended consensus configuration was a silent no-op in the default mode).
37
+
14
38
  ## [0.18.1] — 2026-06-25
15
39
 
16
40
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-dev-methodologies/rlp-desk",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Fresh-context iterative loops for Claude Code — autonomous task completion with independent verification",
5
5
  "scripts": {
6
6
  "postinstall": "node scripts/postinstall.js",
@@ -3,6 +3,31 @@ set -uo pipefail
3
3
  # NOTE: We use set -u (undefined var check) and pipefail, but NOT set -e
4
4
  # because the main loop uses explicit error checks throughout.
5
5
 
6
+ # D-19: validate an env-overridable INTEGER knob. A non-integer value (operator
7
+ # typo, or a bad CLI arg like `--max-iter abc` threaded into the env) otherwise
8
+ # mis-evaluates under `set -u` inside (( )) arithmetic — e.g. a non-integer
9
+ # MAX_ITER makes the main-loop bound error so the campaign silently runs ZERO
10
+ # iterations; a non-integer CB_THRESHOLD breaks the circuit breaker. The `<->`
11
+ # integer glob is checked FIRST (and short-circuits) so the arithmetic never runs
12
+ # on a non-integer. A malformed / below-min / above-max value → the default.
13
+ _validate_int_knob() {
14
+ local _name="$1" _default="$2" _min="${3:-0}" _max="${4:-0}"
15
+ local _val="${(P)_name}"
16
+ local _bad=0
17
+ if ! [[ "$_val" == <-> ]]; then
18
+ _bad=1
19
+ elif (( _val < _min )); then
20
+ _bad=1
21
+ elif (( _max > 0 && _val > _max )); then
22
+ _bad=1
23
+ fi
24
+ if (( _bad )); then
25
+ local _range="min=$_min"; (( _max > 0 )) && _range="$_range, max=$_max"
26
+ print -r -- "WARNING: $_name='$_val' is not a valid integer ($_range) — using default $_default" >&2
27
+ eval "$_name=$_default"
28
+ fi
29
+ }
30
+
6
31
  # =============================================================================
7
32
  # Ralph Desk Tmux Runner
8
33
  #
@@ -56,6 +81,14 @@ HEARTBEAT_STALE_THRESHOLD="${HEARTBEAT_STALE_THRESHOLD:-120}"
56
81
  MAX_RESTARTS="${MAX_RESTARTS:-3}"
57
82
  IDLE_NUDGE_THRESHOLD="${IDLE_NUDGE_THRESHOLD:-30}"
58
83
  MAX_NUDGES="${MAX_NUDGES:-3}"
84
+ # D-19: validate the numeric knobs above (set -u + (( )) arithmetic safety).
85
+ _validate_int_knob MAX_ITER 20 1
86
+ _validate_int_knob POLL_INTERVAL 5 1
87
+ _validate_int_knob ITER_TIMEOUT 600 1
88
+ _validate_int_knob HEARTBEAT_STALE_THRESHOLD 120 1
89
+ _validate_int_knob MAX_RESTARTS 3 0
90
+ _validate_int_knob IDLE_NUDGE_THRESHOLD 30 1
91
+ _validate_int_knob MAX_NUDGES 3 0
59
92
  WITH_SELF_VERIFICATION="${WITH_SELF_VERIFICATION:-0}"
60
93
  WITH_SELF_VERIFICATION_REQUESTED="$WITH_SELF_VERIFICATION" # preserves original user intent for traceability (governance §1f)
61
94
  SV_SKIPPED_REASON="" # set when SV is disabled despite user request
@@ -88,6 +121,7 @@ TEST_DENSITY_MODE="${TEST_DENSITY_MODE:-warn}"
88
121
  # .sisyphus/mission-abort.json and exits non-zero so contract defects don't
89
122
  # silently loop. infra_failure category and the very first iteration are exempt.
90
123
  BLOCK_CB_THRESHOLD="${BLOCK_CB_THRESHOLD:-3}"
124
+ _validate_int_knob BLOCK_CB_THRESHOLD 3 1 # D-19
91
125
  CONSECUTIVE_BLOCKS=0
92
126
  LAST_BLOCK_REASON=""
93
127
 
@@ -229,6 +263,18 @@ FINAL_VERIFIER_ENGINE="${FINAL_VERIFIER_ENGINE:-claude}"
229
263
  WORKER_EFFORT="${WORKER_EFFORT:-}"
230
264
  VERIFIER_EFFORT="${VERIFIER_EFFORT:-}"
231
265
  FINAL_VERIFIER_EFFORT="${FINAL_VERIFIER_EFFORT:-}"
266
+ # D-18: max final-verify attempts for a US that ALREADY passed per-US. A verifier
267
+ # fail verdict on already-per-US-passed work must REPRODUCE across all attempts
268
+ # (first pass wins) before it charges a fix-loop failure — guards against verifier
269
+ # non-determinism defeating a complete, correct campaign. A genuinely-regressed US
270
+ # (or one never per-US-passed) still fails on the first attempt.
271
+ FINAL_VERIFY_MAX_ATTEMPTS="${FINAL_VERIFY_MAX_ATTEMPTS:-3}"
272
+ # D-18/D-19: a non-integer value ("abc") would mis-evaluate under set -u in the
273
+ # (( )) retry-loop arithmetic — skipping the loop and silently FALSE-FAILING a US
274
+ # — and an unbounded value would be ruinously expensive. Validate to an integer
275
+ # in 1..10 via the shared _validate_int_knob helper (D-19 generalized this
276
+ # per-knob fix into one validator used by every numeric knob).
277
+ _validate_int_knob FINAL_VERIFY_MAX_ATTEMPTS 3 1 10
232
278
 
233
279
  # Auto-detect engine from model format for env var path (CLI path uses parse_model_flag)
234
280
  _auto_detect_engine WORKER_MODEL WORKER_ENGINE WORKER_CODEX_MODEL WORKER_CODEX_REASONING WORKER_EFFORT
@@ -260,6 +306,7 @@ elif [[ "${FINAL_CONSENSUS:-0}" = "1" ]]; then
260
306
  fi
261
307
  CONSENSUS_SCOPE="${CONSENSUS_SCOPE:-${CONSENSUS_MODE}}"
262
308
  CB_THRESHOLD="${CB_THRESHOLD:-6}" # consecutive failures before BLOCKED (default: 6)
309
+ _validate_int_knob CB_THRESHOLD 6 1 # D-19: must be valid before the (( *2 )) below
263
310
  # Effective CB threshold: doubled when consensus mode active
264
311
  if [[ "$CONSENSUS_MODE" != "off" ]]; then
265
312
  EFFECTIVE_CB_THRESHOLD=$(( CB_THRESHOLD * 2 ))
@@ -268,6 +315,8 @@ else
268
315
  fi
269
316
  _API_MAX_RETRIES="${_API_MAX_RETRIES:-5}"
270
317
  _API_RETRY_INTERVAL_S="${_API_RETRY_INTERVAL_S:-30}"
318
+ _validate_int_knob _API_MAX_RETRIES 5 1 # D-19
319
+ _validate_int_knob _API_RETRY_INTERVAL_S 30 1 # D-19
271
320
 
272
321
  # --- Derived Paths ---
273
322
  DESK="$ROOT/${RLP_DESK_RUNTIME_DIR:-.rlp-desk}"
@@ -847,7 +896,25 @@ _bug8_check_synth_allowed() {
847
896
  log " Bug #8 F-8 recovery: done-claim + Worker's uncommitted tracked changes — auto-committing $us_id work (files: $_bug8_first5)."
848
897
  log_debug "[GOV] iter=$iter bug8=recover_autocommit us_id=$us_id files='$_bug8_first5'"
849
898
  local -a _bug8_add=("${(@f)_bug8_worker_files}")
850
- if git -C "$ROOT" add -- "${_bug8_add[@]}" && git -C "$ROOT" commit -q -m "chore(leader-recovery): commit Worker's uncommitted $us_id changes (Bug #8 F-8)"; then
899
+ # D-20 (codex LOW): fail-safe on an empty file list. The upstream
900
+ # `[[ -z "$_bug8_worker_files" ]]` guard already makes this unreachable, but
901
+ # never let an empty array turn `git diff --quiet HEAD --` into a whole-tree
902
+ # check (which could falsely read "already committed" → false PASS). BLOCK.
903
+ if (( ${#_bug8_add} == 0 )); then
904
+ log_error " Bug #8: empty worker-file list at auto-commit (unexpected) — refusing synthesis."
905
+ write_blocked_sentinel "worker_incomplete_uncommitted: empty file list at auto-commit" "$us_id" "metric_failure"
906
+ return 1
907
+ fi
908
+ if git -C "$ROOT" diff --quiet HEAD -- "${_bug8_add[@]}" 2>/dev/null; then
909
+ # D-20: the Worker committed these files itself in the window between the
910
+ # dirty-detection above and now (a reap/commit race) — the working tree is
911
+ # already clean vs HEAD for them, i.e. the work IS committed. The old code
912
+ # ran `git add … && git commit`, which exited non-zero ("nothing to commit")
913
+ # and BLOCKED a correct, fully-committed campaign. Treat "already committed"
914
+ # as success: proceed to synthesis (the Verifier still gates correctness).
915
+ log " Bug #8 F-8 (D-20): Worker files already committed (commit race) — nothing to auto-commit; proceeding."
916
+ log_debug "[GOV] iter=$iter bug8=autocommit_noop_already_committed us_id=$us_id files='$_bug8_first5'"
917
+ elif git -C "$ROOT" add -- "${_bug8_add[@]}" && git -C "$ROOT" commit -q -m "chore(leader-recovery): commit Worker's uncommitted $us_id changes (Bug #8 F-8)"; then
851
918
  log " Leader-recovery auto-commit OK (Worker files only) — Verifier will gate correctness."
852
919
  else
853
920
  log_error " Bug #8: leader-recovery auto-commit failed. Refusing synthesis. files: $_bug8_first5"
@@ -1434,6 +1501,8 @@ typeset -gA PANE_PROMPT_STUCK_SINCE
1434
1501
  typeset -gA PANE_DISMISS_FAILED_COUNT
1435
1502
  PROMPT_STALL_TIMEOUT="${PROMPT_STALL_TIMEOUT:-300}" # 5 min default
1436
1503
  PROMPT_DISMISS_FAIL_LIMIT="${PROMPT_DISMISS_FAIL_LIMIT:-20}" # ~100s of fruitless dismiss attempts
1504
+ _validate_int_knob PROMPT_STALL_TIMEOUT 300 1 # D-19
1505
+ _validate_int_knob PROMPT_DISMISS_FAIL_LIMIT 20 1 # D-19
1437
1506
 
1438
1507
  # v5.7 §4.17: generic no-progress timeout (codex Critic HIGH — closes the gap
1439
1508
  # where an undetected prompt or alive-but-frozen Worker bypasses Layer 4).
@@ -1441,6 +1510,7 @@ PROMPT_DISMISS_FAIL_LIMIT="${PROMPT_DISMISS_FAIL_LIMIT:-20}" # ~100s of fruitle
1441
1510
  # seconds AND signal file still missing, write BLOCKED `infra_failure` reason
1442
1511
  # `worker_no_progress` so silent infinite-wait is impossible.
1443
1512
  PROGRESS_NO_CHANGE_TIMEOUT="${PROGRESS_NO_CHANGE_TIMEOUT:-600}" # 10 min default
1513
+ _validate_int_knob PROGRESS_NO_CHANGE_TIMEOUT 600 1 # D-19
1444
1514
  typeset -gA PANE_LAST_CHANGE_TS # epoch when content last changed
1445
1515
  typeset -gA PANE_LAST_CONTENT_FOR_PROGRESS # captured content for diff
1446
1516
 
@@ -1449,6 +1519,7 @@ typeset -gA PANE_LAST_CONTENT_FOR_PROGRESS # captured content for diff
1449
1519
  # CODEX_IDLE_GRACE_S (default 120s) before BLOCK. Per-pane bookkeeping to
1450
1520
  # avoid granting it repeatedly. Bug Report #3 (BOS 2026-05-04).
1451
1521
  CODEX_IDLE_GRACE_S="${CODEX_IDLE_GRACE_S:-120}"
1522
+ _validate_int_knob CODEX_IDLE_GRACE_S 120 1 # D-19
1452
1523
  typeset -gA PANE_CODEX_IDLE_GRACED
1453
1524
  # v0.14.2: per-verifier-pane trace flag — log the verdict-lookup outcome
1454
1525
  # exactly once per byte-stasis transition. Bug Report #4 (BOS 2026-05-05).
@@ -2520,9 +2591,22 @@ poll_for_signal() {
2520
2591
  if [[ -n "$pane_output_for_retry" ]] &&
2521
2592
  ( echo "$pane_output_for_retry" | grep -qiE '(^|[^[:digit:]])500([^[:digit:]]|$)' \
2522
2593
  || echo "$pane_output_for_retry" | grep -qiE '(^|[^[:digit:]])529([^[:digit:]]|$)' \
2594
+ || echo "$pane_output_for_retry" | grep -qiE '(^|[^[:digit:]])429([^[:digit:]]|$)' \
2523
2595
  || echo "$pane_output_for_retry" | grep -qi 'overloaded' \
2524
2596
  || echo "$pane_output_for_retry" | grep -qi 'too many requests' \
2525
- || echo "$pane_output_for_retry" | grep -qi 'service unavailable' ); then
2597
+ || echo "$pane_output_for_retry" | grep -qi 'service unavailable' \
2598
+ || echo "$pane_output_for_retry" | grep -qiE 'api error.*temporarily limiting requests' ); then
2599
+ # D-17a: the last pattern catches the claude TUI rate-limit banner
2600
+ # ("API Error: Server is temporarily limiting requests (not your usage
2601
+ # limit) · Rate limited") that previously fell through to the 600s
2602
+ # frozen-pane BLOCK with a misleading "deadlock" reason. It requires BOTH
2603
+ # the "API Error" banner prefix AND the distinctive multi-word phrase
2604
+ # "temporarily limiting requests" on the SAME line (codex MEDIUM): a Worker
2605
+ # implementing a rate-limiter feature, quoting the phrase, or merely
2606
+ # discussing API rate-limit handling does NOT false-trigger backoff — only
2607
+ # the actual error banner does. Routes to the bounded API backoff below
2608
+ # (5×30s) → recovers a transient limit, else BLOCKs as infra (recoverable),
2609
+ # not as a misleading frozen-pane deadlock.
2526
2610
  is_api_text_retry=1
2527
2611
  fi
2528
2612
 
@@ -2832,6 +2916,94 @@ _all_us_verified() {
2832
2916
  return 0
2833
2917
  }
2834
2918
 
2919
+ # D-18 helper: one final-verify pass for a single US. Returns 0=pass verdict,
2920
+ # 1=fail verdict, 2=infra-terminal (launch/poll hard fail — sentinel handling
2921
+ # already done by the poll). Reads/updates globals (VERIFIER_PANE, FINAL_VERIFIER_*,
2922
+ # SIGNAL_FILE, VERDICT_FILE, SESSION_CONFIG). Extracted from run_sequential_final_verify
2923
+ # so the caller can re-verify a per-US-passed US on a flake without duplicating the
2924
+ # dispatch/poll logic. Does NOT set FAILED_US (the caller owns that).
2925
+ _final_verify_one_us() {
2926
+ local us="$1" iter="$2"
2927
+
2928
+ # Temporarily override signal file to scope verifier to this US
2929
+ local orig_signal
2930
+ orig_signal=$(cat "$SIGNAL_FILE" 2>/dev/null)
2931
+ echo "{\"status\":\"verify\",\"us_id\":\"$us\",\"summary\":\"sequential final verify\"}" | atomic_write "$SIGNAL_FILE"
2932
+
2933
+ # Write scoped verifier trigger
2934
+ write_verifier_trigger "$iter"
2935
+ local verifier_prompt="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier-prompt.md"
2936
+
2937
+ # Clean verifier pane
2938
+ local verifier_cmd
2939
+ verifier_cmd=$(tmux display-message -p -t "$VERIFIER_PANE" '#{pane_current_command}' 2>/dev/null)
2940
+ if [[ "$verifier_cmd" == "node" || "$verifier_cmd" == "claude" || "$verifier_cmd" == "codex" ]]; then
2941
+ tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null; sleep 0.5
2942
+ tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null; sleep 2
2943
+ fi
2944
+ wait_for_pane_ready "$VERIFIER_PANE" 10 2>/dev/null || true
2945
+
2946
+ # Launch verifier. D-1: the FINAL (ALL) verify uses FINAL_VERIFIER_* (the
2947
+ # "final 엄격" knob — a configured stronger model, e.g. opus, for the final
2948
+ # gate), NOT the lighter per-US VERIFIER_*. This is the configured-final-model
2949
+ # distinction, distinct from the removed per-iteration verifier auto-upgrade.
2950
+ local verifier_launch
2951
+ if [[ "$FINAL_VERIFIER_ENGINE" = "codex" ]]; then
2952
+ verifier_launch="${CODEX_BIN:-codex} -m $FINAL_VERIFIER_CODEX_MODEL -c model_reasoning_effort=\"$FINAL_VERIFIER_CODEX_REASONING\" -c mcp_servers='{}' --disable plugins --dangerously-bypass-approvals-and-sandbox"
2953
+ launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch"
2954
+ else
2955
+ verifier_launch="$(build_claude_cmd tui "$FINAL_VERIFIER_MODEL" "" "" "$FINAL_VERIFIER_EFFORT")"
2956
+ launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch" || {
2957
+ log_error "Failed to launch final verifier for $us"
2958
+ return 2
2959
+ }
2960
+ fi
2961
+
2962
+ # Poll for verdict. D-4: distinguish rc==2 (hard-fail, sentinel already
2963
+ # written → terminal) from rc==1 (transient pane race/timeout) and give ONE
2964
+ # replace-pane + re-dispatch retry before failing the US — the F-10 retry
2965
+ # parity the per-US main verifier site has but this final-verify path lacked
2966
+ # (a single transient poll miss falsely failed a US at the most expensive
2967
+ # end-of-campaign moment, charging a bogus consecutive failure).
2968
+ rm -f "$VERDICT_FILE"
2969
+ local poll_rc=0
2970
+ poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier-final" || poll_rc=$?
2971
+ if (( poll_rc == 2 )); then
2972
+ log_error "Verifier hard-fail (rc=2, sentinel written) for $us in final verify"
2973
+ return 2
2974
+ fi
2975
+ if (( poll_rc == 1 )); then
2976
+ log " Verifier-final transient poll fail for $us — replacing pane + retrying once (D-4)"
2977
+ replace_worker_pane "$VERIFIER_PANE" "verifier"
2978
+ VERIFIER_PANE=$(jq -r '.panes.verifier' "$SESSION_CONFIG")
2979
+ if [[ "$FINAL_VERIFIER_ENGINE" = "codex" ]]; then
2980
+ launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch"
2981
+ else
2982
+ launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch" || return 2
2983
+ fi
2984
+ rm -f "$VERDICT_FILE"; poll_rc=0
2985
+ poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier-final" || poll_rc=$?
2986
+ if (( poll_rc != 0 )); then
2987
+ log_error "Verifier poll failed for $us after replace+retry (rc=$poll_rc)"
2988
+ return 2
2989
+ fi
2990
+ fi
2991
+
2992
+ # Bug #7 Fix-Q/R: reap verifier pane between per-US final verifications so
2993
+ # the previous codex/claude TUI cannot continue running while the next per-
2994
+ # US verifier dispatch reuses the same pane.
2995
+ _kill_pane_process "$VERIFIER_PANE" "verifier-final"
2996
+ _lock_sentinel "$VERDICT_FILE"
2997
+ # PR-0b-narrow: stamp leader handshake ack on the verdict (audit-only).
2998
+ _stamp_ack_field "$VERDICT_FILE"
2999
+
3000
+ # Read verdict
3001
+ local verdict
3002
+ verdict=$(jq -r '.verdict' "$VERDICT_FILE" 2>/dev/null)
3003
+ [[ "$verdict" == "pass" ]] && return 0
3004
+ return 1
3005
+ }
3006
+
2835
3007
  run_sequential_final_verify() {
2836
3008
  local iter="$1"
2837
3009
  FAILED_US=""
@@ -2842,91 +3014,38 @@ run_sequential_final_verify() {
2842
3014
  for us in $(echo "$US_LIST" | tr ',' ' '); do
2843
3015
  log " Final verify: checking $us..."
2844
3016
 
2845
- # Temporarily override signal file to scope verifier to this US
2846
- local orig_signal
2847
- orig_signal=$(cat "$SIGNAL_FILE" 2>/dev/null)
2848
- echo "{\"status\":\"verify\",\"us_id\":\"$us\",\"summary\":\"sequential final verify\"}" | atomic_write "$SIGNAL_FILE"
2849
-
2850
- # Write scoped verifier trigger
2851
- write_verifier_trigger "$iter"
2852
- local verifier_prompt="$LOGS_DIR/iter-$(printf '%03d' $iter).verifier-prompt.md"
2853
-
2854
- # Clean verifier pane
2855
- local verifier_cmd
2856
- verifier_cmd=$(tmux display-message -p -t "$VERIFIER_PANE" '#{pane_current_command}' 2>/dev/null)
2857
- if [[ "$verifier_cmd" == "node" || "$verifier_cmd" == "claude" || "$verifier_cmd" == "codex" ]]; then
2858
- tmux send-keys -t "$VERIFIER_PANE" C-c 2>/dev/null; sleep 0.5
2859
- tmux send-keys -t "$VERIFIER_PANE" "/exit" C-m 2>/dev/null; sleep 2
2860
- fi
2861
- wait_for_pane_ready "$VERIFIER_PANE" 10 2>/dev/null || true
2862
-
2863
- # Launch verifier. D-1: the FINAL (ALL) verify uses FINAL_VERIFIER_* (the
2864
- # "final 엄격" knob — a configured stronger model, e.g. opus, for the final
2865
- # gate), NOT the lighter per-US VERIFIER_*. This is the configured-final-model
2866
- # distinction, distinct from the removed per-iteration verifier auto-upgrade.
2867
- local verifier_launch
2868
- if [[ "$FINAL_VERIFIER_ENGINE" = "codex" ]]; then
2869
- verifier_launch="${CODEX_BIN:-codex} -m $FINAL_VERIFIER_CODEX_MODEL -c model_reasoning_effort=\"$FINAL_VERIFIER_CODEX_REASONING\" -c mcp_servers='{}' --disable plugins --dangerously-bypass-approvals-and-sandbox"
2870
- launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch"
2871
- else
2872
- verifier_launch="$(build_claude_cmd tui "$FINAL_VERIFIER_MODEL" "" "" "$FINAL_VERIFIER_EFFORT")"
2873
- launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch" || {
2874
- log_error "Failed to launch final verifier for $us"
3017
+ # D-18: a US that already passed per-US gets up to FINAL_VERIFY_MAX_ATTEMPTS
3018
+ # final-verify attempts on a FAIL verdict (first pass wins). A verifier
3019
+ # false-fail (non-determinism) on already-correct, per-US-passed work must
3020
+ # REPRODUCE across all attempts before it charges a fix-loop failure — else a
3021
+ # single flake defeats a complete, correct campaign (D-16 dogfood: codex
3022
+ # false-failed pytest-36/36 work → fix-loop churn → stale BLOCK). A US that
3023
+ # never passed per-US (or a genuine regression) fails on the first attempt.
3024
+ local _fv_max=1
3025
+ # FINAL_VERIFY_MAX_ATTEMPTS is validated to 1..10 at declaration, so no clamp here.
3026
+ if echo ",$VERIFIED_US," | grep -q ",$us,"; then _fv_max=$FINAL_VERIFY_MAX_ATTEMPTS; fi
3027
+ local _fv_attempt=0 _fv_rc=1
3028
+ while (( _fv_attempt < _fv_max )); do
3029
+ (( _fv_attempt++ ))
3030
+ _final_verify_one_us "$us" "$iter"; _fv_rc=$?
3031
+ if (( _fv_rc == 2 )); then # infra-terminal (launch/poll hard fail) — no retry
2875
3032
  FAILED_US="$us"
3033
+ log " Sequential final verify FAILED at $us (infra)"
3034
+ log_debug "[FLOW] iter=$iter phase=sequential_final_verify failed_us=$us reason=infra attempts=$_fv_attempt"
2876
3035
  return 1
2877
- }
2878
- fi
2879
-
2880
- # Poll for verdict. D-4: distinguish rc==2 (hard-fail, sentinel already
2881
- # written → terminal) from rc==1 (transient pane race/timeout) and give ONE
2882
- # replace-pane + re-dispatch retry before failing the US — the F-10 retry
2883
- # parity the per-US main verifier site has but this final-verify path lacked
2884
- # (a single transient poll miss falsely failed a US at the most expensive
2885
- # end-of-campaign moment, charging a bogus consecutive failure).
2886
- rm -f "$VERDICT_FILE"
2887
- local poll_rc=0
2888
- poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier-final" || poll_rc=$?
2889
- if (( poll_rc == 2 )); then
2890
- log_error "Verifier hard-fail (rc=2, sentinel written) for $us in final verify"
2891
- FAILED_US="$us"
2892
- return 1
2893
- fi
2894
- if (( poll_rc == 1 )); then
2895
- log " Verifier-final transient poll fail for $us — replacing pane + retrying once (D-4)"
2896
- replace_worker_pane "$VERIFIER_PANE" "verifier"
2897
- VERIFIER_PANE=$(jq -r '.panes.verifier' "$SESSION_CONFIG")
2898
- if [[ "$FINAL_VERIFIER_ENGINE" = "codex" ]]; then
2899
- launch_verifier_codex "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch"
2900
- else
2901
- launch_verifier_claude "$VERIFIER_PANE" "$verifier_prompt" "$iter" "$verifier_launch" || { FAILED_US="$us"; return 1; }
2902
3036
  fi
2903
- rm -f "$VERDICT_FILE"; poll_rc=0
2904
- poll_for_signal "$VERDICT_FILE" "$VERIFIER_HEARTBEAT" "$VERIFIER_PANE" "$verifier_launch" "Verifier-final" || poll_rc=$?
2905
- if (( poll_rc != 0 )); then
2906
- log_error "Verifier poll failed for $us after replace+retry (rc=$poll_rc)"
2907
- FAILED_US="$us"
2908
- return 1
2909
- fi
2910
- fi
2911
-
2912
- # Bug #7 Fix-Q/R: reap verifier pane between per-US final verifications so
2913
- # the previous codex/claude TUI cannot continue running while the next per-
2914
- # US verifier dispatch reuses the same pane.
2915
- _kill_pane_process "$VERIFIER_PANE" "verifier-final"
2916
- _lock_sentinel "$VERDICT_FILE"
2917
- # PR-0b-narrow: stamp leader handshake ack on the verdict (audit-only).
2918
- _stamp_ack_field "$VERDICT_FILE"
2919
-
2920
- # Check verdict
2921
- local verdict
2922
- verdict=$(jq -r '.verdict' "$VERDICT_FILE" 2>/dev/null)
2923
- if [[ "$verdict" != "pass" ]]; then
3037
+ (( _fv_rc == 0 )) && break # pass verdict
3038
+ log " Sequential final verify: $us verdict=fail (attempt $_fv_attempt/$_fv_max)"
3039
+ log_debug "[FLOW] iter=$iter phase=sequential_final_verify us=$us attempt=$_fv_attempt/$_fv_max verdict=fail"
3040
+ (( _fv_attempt < _fv_max )) && log " D-18: re-verifying $us a per-US-passed US's fail must reproduce to count (verifier flake guard)."
3041
+ done
3042
+ if (( _fv_rc != 0 )); then
2924
3043
  FAILED_US="$us"
2925
- log " Sequential final verify FAILED at $us"
2926
- log_debug "[FLOW] iter=$iter phase=sequential_final_verify failed_us=$us verdict=$verdict"
3044
+ log " Sequential final verify FAILED at $us (failed all $_fv_attempt/$_fv_max attempt(s))"
3045
+ log_debug "[FLOW] iter=$iter phase=sequential_final_verify failed_us=$us verdict=fail attempts=$_fv_attempt max=$_fv_max"
2927
3046
  return 1
2928
3047
  fi
2929
- log " Sequential final verify: $us PASSED"
3048
+ log " Sequential final verify: $us PASSED$([[ $_fv_attempt -gt 1 ]] && echo " (after $_fv_attempt attempts — earlier verdict was a verifier flake)")"
2930
3049
 
2931
3050
  # Archive per-US final verdict
2932
3051
  cp "$VERDICT_FILE" "$LOGS_DIR/iter-$(printf '%03d' $iter).final-verdict-${us}.json" 2>/dev/null
@@ -3760,7 +3879,15 @@ main() {
3760
3879
  update_status "verifier" "running"
3761
3880
 
3762
3881
  # --- Sequential final verify: per-US scoped checks instead of one big ALL verify ---
3763
- if [[ "$signal_us_id" == "ALL" && "$VERIFY_MODE" == "per-us" && -n "$US_LIST" ]]; then
3882
+ # D-21: do NOT take the sequential single-verifier path when consensus applies to
3883
+ # the final verify — it would BYPASS consensus entirely (run_sequential_final_verify
3884
+ # returns 0 before the consensus check below), so `--consensus final-only` was a
3885
+ # silent no-op in the DEFAULT per-us mode (the recommended consensus config). When
3886
+ # consensus is on, fall through to run_consensus_verification "ALL" (which uses the
3887
+ # stricter FINAL_VERIFIER_MODEL + FINAL_CONSENSUS_MODEL and the designed
3888
+ # claude+codex final path). The per-US timeout-prevention split is the off-consensus
3889
+ # optimization only.
3890
+ if [[ "$signal_us_id" == "ALL" && "$VERIFY_MODE" == "per-us" && -n "$US_LIST" ]] && ! _should_use_consensus "$signal_us_id"; then
3764
3891
  log " Final ALL verify: using sequential per-US strategy (timeout prevention)"
3765
3892
  local seq_rc=0
3766
3893
  run_sequential_final_verify "$ITERATION" || seq_rc=$?