prizmkit 1.1.91 → 1.1.92

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.
@@ -8,13 +8,14 @@ set -euo pipefail
8
8
  # execute refactors from a .prizmkit/plans/refactor-list.json specification.
9
9
  #
10
10
  # Usage:
11
- # ./run-refactor.sh run [.prizmkit/plans/refactor-list.json] Run all refactors
11
+ # ./run-refactor.sh run [.prizmkit/plans/refactor-list.json] [options] Run all refactors
12
12
  # ./run-refactor.sh run <refactor-id> [options] Run a single refactor
13
13
  # ./run-refactor.sh status [.prizmkit/plans/refactor-list.json] Show pipeline status
14
14
  # ./run-refactor.sh reset Clear all state
15
15
  #
16
16
  # Environment Variables:
17
- # MAX_RETRIES Max retries per refactor (default: 3)
17
+ # MAX_RETRIES Max code retries per refactor (default: 3)
18
+ # MAX_INFRA_RETRIES Max infrastructure/provider retries per refactor (default: 3)
18
19
  # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
19
20
  # AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)
20
21
  # CODEBUDDY_CLI Legacy alias for AI_CLI (deprecated, use AI_CLI instead)
@@ -43,6 +44,7 @@ SCRIPTS_DIR="$SCRIPT_DIR/scripts"
43
44
 
44
45
  # Configuration
45
46
  MAX_RETRIES=${MAX_RETRIES:-3}
47
+ MAX_INFRA_RETRIES=${MAX_INFRA_RETRIES:-3}
46
48
  SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
47
49
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
48
50
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
@@ -92,8 +94,9 @@ spawn_and_wait_session() {
92
94
  local bootstrap_prompt="$4"
93
95
  local session_dir="$5"
94
96
  local max_retries="$6"
95
- local item_model="${7:-}"
96
- local base_branch="${8:-main}"
97
+ local max_infra_retries="$7"
98
+ local item_model="${8:-}"
99
+ local base_branch="${9:-main}"
97
100
 
98
101
  local session_log="$session_dir/logs/session.log"
99
102
  local progress_json="$session_dir/logs/progress.json"
@@ -149,9 +152,23 @@ spawn_and_wait_session() {
149
152
  # Check for stale-kill marker (heartbeat killed the process due to no progress)
150
153
  local stale_kill_marker="$session_dir/logs/stale-kill.json"
151
154
  local was_stale_killed=false
155
+ local stale_kill_reason=""
152
156
  if [[ -f "$stale_kill_marker" ]]; then
153
157
  was_stale_killed=true
154
- log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
158
+ stale_kill_reason=$(python3 - "$stale_kill_marker" <<'PY' 2>/dev/null || true
159
+ import json, sys
160
+ try:
161
+ with open(sys.argv[1], encoding="utf-8") as fh:
162
+ print(json.load(fh).get("reason") or "")
163
+ except Exception:
164
+ pass
165
+ PY
166
+ )
167
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
168
+ log_warn "Session was killed by heartbeat monitor due to repeated read-offset/wasted-call errors"
169
+ else
170
+ log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
171
+ fi
155
172
  fi
156
173
 
157
174
  local was_infra_error=false
@@ -185,7 +202,11 @@ spawn_and_wait_session() {
185
202
  log_warn "Infrastructure errors are retried without consuming code retry budget"
186
203
  session_status="infra_error"
187
204
  elif [[ "$was_stale_killed" == true ]]; then
188
- log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
205
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
206
+ log_warn "Session killed due to repeated read-offset/wasted-call error loop"
207
+ else
208
+ log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
209
+ fi
189
210
  log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
190
211
  session_status="crashed"
191
212
  elif [[ $exit_code -ne 0 ]]; then
@@ -202,10 +223,10 @@ spawn_and_wait_session() {
202
223
  session_status="success"
203
224
  else
204
225
  local uncommitted=""
205
- uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
226
+ uncommitted=$(prizm_git_status_safe "$project_root" | head -1 || true)
206
227
  if [[ -n "$uncommitted" ]]; then
207
228
  log_warn "Session exited cleanly but produced no commits (uncommitted changes found) — auto-committing..."
208
- git -C "$project_root" add -A 2>/dev/null || true
229
+ prizm_git_add_all_safe "$project_root" || true
209
230
  if git -C "$project_root" commit --no-verify -m "chore($refactor_id): auto-commit session work" 2>/dev/null; then
210
231
  log_info "Auto-commit succeeded"
211
232
  session_status="success"
@@ -224,10 +245,10 @@ spawn_and_wait_session() {
224
245
  if [[ "$session_status" == "success" ]]; then
225
246
  if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
226
247
  local dirty_files=""
227
- dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
248
+ dirty_files=$(prizm_git_status_safe "$project_root" || true)
228
249
  if [[ -n "$dirty_files" ]]; then
229
250
  log_info "Auto-committing remaining session artifacts..."
230
- git -C "$project_root" add -A 2>/dev/null || true
251
+ prizm_git_add_all_safe "$project_root" || true
231
252
  git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
232
253
  || git -C "$project_root" commit --no-verify -m "chore($refactor_id): include remaining session artifacts" 2>/dev/null \
233
254
  || true
@@ -305,7 +326,20 @@ sys.exit(0)
305
326
  fi
306
327
  fi
307
328
 
308
- # Update refactor status (do NOT commit on dev branch commit happens after merge)
329
+ # Final status updates happen after the caller returns to the original branch.
330
+ _SPAWN_RESULT="$session_status"
331
+ }
332
+
333
+ finalize_refactor_status_after_branch_return() {
334
+ local refactor_id="$1"
335
+ local refactor_list="$2"
336
+ local session_id="$3"
337
+ local session_status="$4"
338
+ local max_retries="$5"
339
+ local max_infra_retries="$6"
340
+
341
+ # Update refactor status on the original branch. The caller commits the
342
+ # resulting refactor-list diff immediately after this helper returns.
309
343
  local update_output
310
344
  update_output=$(python3 "$SCRIPTS_DIR/update-refactor-status.py" \
311
345
  --refactor-list "$refactor_list" \
@@ -314,14 +348,13 @@ sys.exit(0)
314
348
  --session-status "$session_status" \
315
349
  --session-id "$session_id" \
316
350
  --max-retries "$max_retries" \
351
+ --max-infra-retries "$max_infra_retries" \
317
352
  --action update 2>&1) || {
318
353
  log_error "Failed to update refactor status: $update_output"
319
354
  update_output=""
320
355
  }
321
356
 
322
357
  _SPAWN_ITEM_STATUS="$(printf '%s' "$update_output" | prizm_extract_update_new_status)"
323
-
324
- _SPAWN_RESULT="$session_status"
325
358
  }
326
359
 
327
360
  # ============================================================
@@ -361,6 +394,8 @@ for item in data.get('refactors', []):
361
394
  --state-dir "$STATE_DIR" \
362
395
  --refactor-id "$_interrupted_id" \
363
396
  --session-status "failed" \
397
+ --max-retries "$MAX_RETRIES" \
398
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
364
399
  --action update 2>/dev/null || true
365
400
  log_info "Refactor $_interrupted_id marked as failed due to interrupt"
366
401
  fi
@@ -433,6 +468,16 @@ run_one() {
433
468
  --clean) do_clean=true; shift ;;
434
469
  --no-reset) no_reset=true; shift ;;
435
470
  --timeout) shift; SESSION_TIMEOUT="${1:-0}"; shift ;;
471
+ --max-infra-retries)
472
+ shift
473
+ if [[ $# -eq 0 ]]; then
474
+ log_error "--max-infra-retries requires a value"
475
+ exit 1
476
+ fi
477
+ prizm_require_positive_int "--max-infra-retries" "$1"
478
+ MAX_INFRA_RETRIES="$1"
479
+ shift
480
+ ;;
436
481
  --mode)
437
482
  shift
438
483
  if [[ $# -eq 0 ]]; then
@@ -725,6 +770,8 @@ else:
725
770
  --state-dir "$STATE_DIR" \
726
771
  --refactor-id "$refactor_id" \
727
772
  --session-status "failed" \
773
+ --max-retries "$MAX_RETRIES" \
774
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
728
775
  --action update 2>/dev/null || true
729
776
  log_info "Refactor $refactor_id marked as failed due to interrupt"
730
777
  fi
@@ -758,12 +805,13 @@ else:
758
805
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
759
806
  _DEV_BRANCH_NAME="$_branch_name"
760
807
  else
761
- log_warn "Failed to create branch; running session on current branch"
808
+ log_error "Failed to create branch: $_branch_name from $_source_branch — aborting to avoid running on $_source_branch"
809
+ return 1
762
810
  fi
763
811
 
764
812
  spawn_and_wait_session \
765
813
  "$refactor_id" "$refactor_list" "$session_id" \
766
- "$bootstrap_prompt" "$session_dir" 999 "$refactor_model" "$_ORIGINAL_BRANCH"
814
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$refactor_model" "$_ORIGINAL_BRANCH"
767
815
  local session_status="$_SPAWN_RESULT"
768
816
 
769
817
  # Merge dev branch back to original on success
@@ -773,7 +821,7 @@ else:
773
821
  else
774
822
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
775
823
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
776
- _DEV_BRANCH_NAME=""
824
+ session_status="merge_conflict"
777
825
  fi
778
826
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
779
827
  # Session failed — preserve dev branch for inspection
@@ -784,10 +832,12 @@ else:
784
832
  # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
785
833
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
786
834
 
835
+ finalize_refactor_status_after_branch_return \
836
+ "$refactor_id" "$refactor_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES"
837
+
787
838
  # Commit refactor status update on the original branch (after guaranteed return)
788
839
  if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
789
- git -C "$_proj_root" add "$refactor_list"
790
- git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): update refactor status" 2>/dev/null || true
840
+ prizm_git_commit_paths "$_proj_root" "chore($refactor_id): update refactor status" "$refactor_list"
791
841
  fi
792
842
 
793
843
  echo ""
@@ -808,7 +858,41 @@ else:
808
858
  # ============================================================
809
859
 
810
860
  main() {
811
- local refactor_list="${1:-$PRIZMKIT_DIR/plans/refactor-list.json}"
861
+ local refactor_list="$PRIZMKIT_DIR/plans/refactor-list.json"
862
+
863
+ while [[ $# -gt 0 ]]; do
864
+ case "$1" in
865
+ --max-infra-retries)
866
+ shift
867
+ if [[ $# -eq 0 ]]; then
868
+ log_error "--max-infra-retries requires a value"
869
+ exit 1
870
+ fi
871
+ prizm_require_positive_int "--max-infra-retries" "$1"
872
+ MAX_INFRA_RETRIES="$1"
873
+ shift
874
+ ;;
875
+ --)
876
+ shift
877
+ break
878
+ ;;
879
+ -*)
880
+ log_error "Unknown option: $1"
881
+ show_help
882
+ exit 1
883
+ ;;
884
+ *)
885
+ refactor_list="$1"
886
+ shift
887
+ ;;
888
+ esac
889
+ done
890
+
891
+ if [[ $# -gt 0 ]]; then
892
+ log_error "Unexpected arguments: $*"
893
+ show_help
894
+ exit 1
895
+ fi
812
896
 
813
897
  if [[ ! "$refactor_list" = /* ]]; then
814
898
  refactor_list="$(pwd)/$refactor_list"
@@ -870,7 +954,8 @@ main() {
870
954
  echo -e "${BOLD} Refactor Pipeline Runner Started${NC}"
871
955
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
872
956
  log_info "Refactor list: $refactor_list"
873
- log_info "Max retries per refactor: $MAX_RETRIES"
957
+ log_info "Max code retries per refactor: $MAX_RETRIES"
958
+ log_info "Max infrastructure retries per refactor: $MAX_INFRA_RETRIES"
874
959
  if [[ $SESSION_TIMEOUT -gt 0 ]]; then
875
960
  log_info "Session timeout: ${SESSION_TIMEOUT}s"
876
961
  else
@@ -920,6 +1005,7 @@ main() {
920
1005
  --refactor-list "$refactor_list" \
921
1006
  --state-dir "$STATE_DIR" \
922
1007
  --max-retries "$MAX_RETRIES" \
1008
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
923
1009
  --action get_next 2>/dev/null); then
924
1010
  log_error "Failed to get next refactor"
925
1011
  break
@@ -1007,27 +1093,43 @@ DEPLOY_PROMPT_EOF
1007
1093
  fi
1008
1094
 
1009
1095
  # Parse refactor info
1010
- local refactor_id refactor_title refactor_complexity retry_count resume_phase
1096
+ local refactor_id refactor_title refactor_complexity retry_count infra_error_count resume_phase
1011
1097
  refactor_id=$(echo "$next_refactor" | jq -r '.refactor_id')
1012
1098
  refactor_title=$(echo "$next_refactor" | jq -r '.title')
1013
1099
  refactor_complexity=$(echo "$next_refactor" | jq -r '.complexity')
1014
1100
  retry_count=$(echo "$next_refactor" | jq -r '.retry_count // 0')
1101
+ infra_error_count=$(echo "$next_refactor" | jq -r '.infra_error_count // 0')
1015
1102
  resume_phase=$(echo "$next_refactor" | jq -r '.resume_from_phase // "null"')
1016
1103
 
1017
1104
  echo ""
1018
1105
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1019
1106
  log_info "Refactor: ${BOLD}$refactor_id${NC} — $refactor_title"
1020
- log_info "Complexity: $refactor_complexity | Retry: $retry_count / $MAX_RETRIES"
1107
+ log_info "Complexity: $refactor_complexity"
1108
+ log_info "Code retry: $retry_count / $MAX_RETRIES"
1109
+ log_info "Infrastructure retry: $infra_error_count / $MAX_INFRA_RETRIES"
1021
1110
  if [[ "$resume_phase" != "null" ]]; then
1022
1111
  log_info "Resuming from Phase $resume_phase"
1023
1112
  fi
1024
1113
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1025
1114
 
1026
- # Pre-commit any dirty tree from previous iteration
1027
- if ! git -C "$_proj_root" diff --quiet HEAD 2>/dev/null || [ -n "$(git -C "$_proj_root" ls-files --others --exclude-standard 2>/dev/null)" ]; then
1028
- log_info "Dirty working tree detected committing before $refactor_id..."
1029
- git -C "$_proj_root" add -A 2>/dev/null || true
1030
- git -C "$_proj_root" commit --no-verify -m "chore: capture artifacts before $refactor_id session" 2>/dev/null || true
1115
+ # Do not auto-commit an arbitrary dirty baseline on the original branch.
1116
+ # Only pipeline bookkeeping is committed explicitly; user/source WIP,
1117
+ # submodule dirt, specs, and root-level hidden tool temporary worktrees
1118
+ # must never be swept into main by a broad git add.
1119
+ local _dirty_files=""
1120
+ _dirty_files=$(prizm_git_status_non_bookkeeping "$_proj_root" "$STATE_DIR" "$refactor_list" || true)
1121
+ if [[ -n "$_dirty_files" ]]; then
1122
+ log_error "Dirty working tree detected before $refactor_id; refusing to commit it on $_ORIGINAL_BRANCH."
1123
+ log_error "Commit/stash your changes or start from a clean branch, then rerun the pipeline."
1124
+ printf '%s\n' "$_dirty_files" | sed 's/^/ /'
1125
+ break
1126
+ fi
1127
+
1128
+ local _bookkeeping_dirty=""
1129
+ _bookkeeping_dirty=$(prizm_git_status_bookkeeping "$_proj_root" "$STATE_DIR" "$refactor_list" || true)
1130
+ if [[ -n "$_bookkeeping_dirty" ]]; then
1131
+ log_info "Committing pending pipeline bookkeeping before $refactor_id..."
1132
+ prizm_git_commit_paths "$_proj_root" "chore($refactor_id): include pipeline bookkeeping" "$refactor_list" "$STATE_DIR"
1031
1133
  fi
1032
1134
 
1033
1135
  # Mark refactor as in-progress BEFORE creating dev branch
@@ -1040,8 +1142,7 @@ DEPLOY_PROMPT_EOF
1040
1142
  --action start >/dev/null 2>&1 || true
1041
1143
  # Commit the in_progress status on the original branch
1042
1144
  if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
1043
- git -C "$_proj_root" add "$refactor_list" 2>/dev/null || true
1044
- git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): mark in_progress" 2>/dev/null || true
1145
+ prizm_git_commit_paths "$_proj_root" "chore($refactor_id): mark in_progress" "$refactor_list"
1045
1146
  fi
1046
1147
 
1047
1148
  # Create per-refactor dev branch (from the now-updated original branch)
@@ -1050,8 +1151,9 @@ DEPLOY_PROMPT_EOF
1050
1151
  _DEV_BRANCH_NAME="$_refactor_branch"
1051
1152
  log_info "Dev branch: $_refactor_branch"
1052
1153
  else
1053
- log_warn "Failed to create dev branch; running on current branch: $_ORIGINAL_BRANCH"
1154
+ log_error "Failed to create dev branch: $_refactor_branch from $_ORIGINAL_BRANCH — aborting to avoid running on $_ORIGINAL_BRANCH"
1054
1155
  _DEV_BRANCH_NAME=""
1156
+ break
1055
1157
  fi
1056
1158
 
1057
1159
  # Generate session
@@ -1122,7 +1224,7 @@ DEPLOY_PROMPT_EOF
1122
1224
 
1123
1225
  spawn_and_wait_session \
1124
1226
  "$refactor_id" "$refactor_list" "$session_id" \
1125
- "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$refactor_model" "$_ORIGINAL_BRANCH"
1227
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$refactor_model" "$_ORIGINAL_BRANCH"
1126
1228
 
1127
1229
  # Validate key artifacts after successful session
1128
1230
  if [[ "$_SPAWN_RESULT" == "success" ]]; then
@@ -1135,7 +1237,6 @@ DEPLOY_PROMPT_EOF
1135
1237
  fi
1136
1238
 
1137
1239
  local session_status="$_SPAWN_RESULT"
1138
- local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1139
1240
 
1140
1241
  # Merge per-refactor dev branch back to original on success
1141
1242
  if [[ "$session_status" == "success" && -n "$_DEV_BRANCH_NAME" ]]; then
@@ -1144,7 +1245,7 @@ DEPLOY_PROMPT_EOF
1144
1245
  else
1145
1246
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
1146
1247
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
1147
- _DEV_BRANCH_NAME=""
1248
+ session_status="merge_conflict"
1148
1249
  fi
1149
1250
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
1150
1251
  # Session failed — preserve dev branch for inspection
@@ -1155,10 +1256,13 @@ DEPLOY_PROMPT_EOF
1155
1256
  # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
1156
1257
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1157
1258
 
1259
+ finalize_refactor_status_after_branch_return \
1260
+ "$refactor_id" "$refactor_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES"
1261
+ local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1262
+
1158
1263
  # Commit refactor status update on the original branch (after guaranteed return)
1159
1264
  if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
1160
- git -C "$_proj_root" add "$refactor_list"
1161
- git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): update refactor status" 2>/dev/null || true
1265
+ prizm_git_commit_paths "$_proj_root" "chore($refactor_id): update refactor status" "$refactor_list"
1162
1266
  fi
1163
1267
 
1164
1268
  # Stuck detection
@@ -1185,7 +1289,7 @@ DEPLOY_PROMPT_EOF
1185
1289
  log_error "════════════════════════════════════════════════════"
1186
1290
  break
1187
1291
  elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
1188
- log_info "STOP_ON_FAILURE: $refactor_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
1292
+ log_info "STOP_ON_FAILURE: $refactor_id is ${item_status_after_session:-unknown}; code/infra retry budget not exhausted, continuing."
1189
1293
  fi
1190
1294
 
1191
1295
  log_info "Pausing 5s before next refactor..."
@@ -1201,7 +1305,7 @@ show_help() {
1201
1305
  echo "Usage: $0 <command> [options]"
1202
1306
  echo ""
1203
1307
  echo "Commands:"
1204
- echo " run [.prizmkit/plans/refactor-list.json] Run all refactors in dependency-topological order"
1308
+ echo " run [.prizmkit/plans/refactor-list.json] [options] Run all refactors in dependency-topological order"
1205
1309
  echo " run <refactor-id> [options] Run a single refactor"
1206
1310
  echo " status [.prizmkit/plans/refactor-list.json] Show refactor pipeline status"
1207
1311
  echo " reset Clear all refactor state"
@@ -1212,12 +1316,17 @@ show_help() {
1212
1316
  echo " --clean Delete artifacts and reset before running"
1213
1317
  echo " --no-reset Skip status reset (preserve retry count)"
1214
1318
  echo " --timeout N Session timeout in seconds (default: 0 = no limit)"
1319
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1215
1320
  echo " --mode <lite|standard|full> Override pipeline mode"
1216
1321
  echo " --critic Enable adversarial critic review"
1217
1322
  echo " --no-critic Disable adversarial critic review"
1218
1323
  echo ""
1324
+ echo "Pipeline Options (run all):"
1325
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1326
+ echo ""
1219
1327
  echo "Environment Variables:"
1220
- echo " MAX_RETRIES Max retries per refactor (default: 3)"
1328
+ echo " MAX_RETRIES Max code retries per refactor (default: 3)"
1329
+ echo " MAX_INFRA_RETRIES Max infrastructure/provider retries per refactor (default: 3)"
1221
1330
  echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
1222
1331
  echo " MODEL Default AI model (overridden by per-refactor model in refactor list)"
1223
1332
  echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
@@ -1249,7 +1358,7 @@ case "${1:-run}" in
1249
1358
  if [[ "${1:-}" =~ ^[Rr]-[0-9]+ ]]; then
1250
1359
  run_one "$@"
1251
1360
  else
1252
- main "${1:-$PRIZMKIT_DIR/plans/refactor-list.json}"
1361
+ main "$@"
1253
1362
  fi
1254
1363
  ;;
1255
1364
  status)
@@ -1261,6 +1370,8 @@ case "${1:-run}" in
1261
1370
  python3 "$SCRIPTS_DIR/update-refactor-status.py" \
1262
1371
  --refactor-list "${2:-$PRIZMKIT_DIR/plans/refactor-list.json}" \
1263
1372
  --state-dir "$STATE_DIR" \
1373
+ --max-retries "$MAX_RETRIES" \
1374
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
1264
1375
  --action status
1265
1376
  ;;
1266
1377
  reset)
@@ -16,7 +16,7 @@ Usage:
16
16
  --bug-list <path> --state-dir <path> \
17
17
  --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
18
  [--bug-id <id>] [--session-status <status>] \
19
- [--session-id <id>] [--max-retries <n>]
19
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
20
20
  """
21
21
 
22
22
  import argparse
@@ -75,7 +75,8 @@ def parse_args():
75
75
  help="Session outcome status (required for 'update' action)",
76
76
  )
77
77
  parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
78
- parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
78
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum code retry count (default: 3)")
79
+ parser.add_argument("--max-infra-retries", type=int, default=3, help="Maximum infrastructure retry count (default: 3)")
79
80
  parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
80
81
  return parser.parse_args()
81
82
 
@@ -98,6 +99,9 @@ def load_bug_status(state_dir, bug_id):
98
99
  "bug_id": bug_id,
99
100
  "retry_count": 0,
100
101
  "max_retries": 3,
102
+ "max_infra_retries": 3,
103
+ "infra_error_count": 0,
104
+ "last_infra_error_session_id": None,
101
105
  "sessions": [],
102
106
  "last_session_id": None,
103
107
  "resume_from_phase": None,
@@ -111,6 +115,9 @@ def load_bug_status(state_dir, bug_id):
111
115
  "bug_id": bug_id,
112
116
  "retry_count": 0,
113
117
  "max_retries": 3,
118
+ "max_infra_retries": 3,
119
+ "infra_error_count": 0,
120
+ "last_infra_error_session_id": None,
114
121
  "sessions": [],
115
122
  "last_session_id": None,
116
123
  "resume_from_phase": None,
@@ -234,6 +241,7 @@ def action_get_next(bug_list_data, state_dir):
234
241
  "title": chosen.get("title", ""),
235
242
  "severity": chosen.get("severity", "medium"),
236
243
  "retry_count": chosen_status_data.get("retry_count", 0),
244
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
237
245
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
238
246
  }
239
247
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -248,6 +256,7 @@ def action_update(args, bug_list_path, state_dir):
248
256
  session_status = args.session_status
249
257
  session_id = args.session_id
250
258
  max_retries = args.max_retries
259
+ max_infra_retries = args.max_infra_retries
251
260
 
252
261
  if not bug_id:
253
262
  error_out("--bug-id is required for 'update' action")
@@ -262,6 +271,8 @@ def action_update(args, bug_list_path, state_dir):
262
271
  new_status = get_bug_status_from_list(bug_list_path, bug_id)
263
272
 
264
273
  if session_status == "success":
274
+ bs["infra_error_count"] = 0
275
+ bs["last_infra_error_session_id"] = None
265
276
  new_status = "completed"
266
277
  bs["resume_from_phase"] = None
267
278
  err = update_bug_in_list(bug_list_path, bug_id, "completed")
@@ -286,11 +297,25 @@ def action_update(args, bug_list_path, state_dir):
286
297
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
287
298
  return
288
299
  elif session_status == "infra_error":
289
- new_status = "pending"
290
- bs["infra_error_count"] = bs.get("infra_error_count", 0) + 1
300
+ # AI CLI/provider outage, auth failure, gateway error, etc.
301
+ # Infra failures do not consume the code retry budget, but they still
302
+ # need their own bounded budget so a flaky provider cannot loop forever.
303
+ infra_error_count = bs.get("infra_error_count", 0) + 1
304
+ bs["infra_error_count"] = infra_error_count
291
305
  bs["last_infra_error_session_id"] = session_id
306
+ bs["max_infra_retries"] = max_infra_retries
307
+ bs["degraded_reason"] = "infra_error"
308
+ if session_id:
309
+ bs["last_session_id"] = session_id
292
310
  bs["resume_from_phase"] = None
293
311
 
312
+ if infra_error_count >= max_infra_retries:
313
+ new_status = "failed"
314
+ if session_id:
315
+ bs["last_failed_session_id"] = session_id
316
+ else:
317
+ new_status = "pending"
318
+
294
319
  err = update_bug_in_list(bug_list_path, bug_id, new_status)
295
320
  if err:
296
321
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
@@ -338,6 +363,8 @@ def action_update(args, bug_list_path, state_dir):
338
363
  "session_status": session_status,
339
364
  "new_status": new_status,
340
365
  "retry_count": bs["retry_count"],
366
+ "infra_error_count": bs.get("infra_error_count", 0),
367
+ "max_infra_retries": max_infra_retries,
341
368
  "resume_from_phase": bs.get("resume_from_phase"),
342
369
  "updated_at": bs["updated_at"],
343
370
  }
@@ -577,6 +604,8 @@ def action_reset(args, bug_list_path, state_dir):
577
604
  old_retry = bs.get("retry_count", 0)
578
605
 
579
606
  bs["retry_count"] = 0
607
+ bs["infra_error_count"] = 0
608
+ bs["last_infra_error_session_id"] = None
580
609
  bs["sessions"] = []
581
610
  bs["last_session_id"] = None
582
611
  bs["resume_from_phase"] = None
@@ -652,6 +681,8 @@ def action_clean(args, bug_list_path, state_dir):
652
681
  old_retry = bs.get("retry_count", 0)
653
682
 
654
683
  bs["retry_count"] = 0
684
+ bs["infra_error_count"] = 0
685
+ bs["last_infra_error_session_id"] = None
655
686
  bs["sessions"] = []
656
687
  bs["last_session_id"] = None
657
688
  bs["resume_from_phase"] = None
@@ -740,6 +771,8 @@ def action_start(args, bug_list_path, state_dir):
740
771
  "bug_id": bug_id,
741
772
  "old_status": old_status,
742
773
  "new_status": "in_progress",
774
+ "retry_count": bs.get("retry_count", 0),
775
+ "infra_error_count": bs.get("infra_error_count", 0),
743
776
  "updated_at": bs["updated_at"],
744
777
  }
745
778
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -816,6 +849,8 @@ def action_unskip(args, bug_list_path, state_dir):
816
849
  for bid in to_reset:
817
850
  bs = load_bug_status(state_dir, bid)
818
851
  bs["retry_count"] = 0
852
+ bs["infra_error_count"] = 0
853
+ bs["last_infra_error_session_id"] = None
819
854
  bs["sessions"] = []
820
855
  bs["last_session_id"] = None
821
856
  bs["resume_from_phase"] = None