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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.91",
3
- "bundledAt": "2026-07-01T17:51:47.362Z",
4
- "bundledFrom": "5dcb047"
2
+ "frameworkVersion": "1.1.92",
3
+ "bundledAt": "2026-07-02T15:14:07.667Z",
4
+ "bundledFrom": "edf0ddb"
5
5
  }
@@ -211,18 +211,21 @@ branch_save_wip() {
211
211
  return 0
212
212
  fi
213
213
 
214
- # Check if there are any uncommitted changes (tracked or untracked, excluding gitignored)
214
+ # Check if there are any uncommitted changes (tracked or untracked,
215
+ # excluding gitignored and root-level hidden tool temporary worktrees)
215
216
  local has_changes
216
- has_changes=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
217
+ has_changes=$(prizm_git_status_safe "$project_root" || true)
217
218
  if [[ -z "$has_changes" ]]; then
218
219
  return 0
219
220
  fi
220
221
 
221
222
  log_warn "Saving uncommitted work-in-progress on branch: $dev_branch"
222
223
 
223
- # Stage all changes (tracked + untracked, respects .gitignore)
224
- if ! git -C "$project_root" add -A 2>/dev/null; then
225
- log_warn "git add -A failed uncommitted work may be lost on branch switch"
224
+ # Stage all changes (tracked + untracked, respects .gitignore), excluding
225
+ # root-level hidden tool temporary worktrees so nested agent repos are not
226
+ # recorded as accidental gitlinks in the user's project.
227
+ if ! prizm_git_add_all_safe "$project_root"; then
228
+ log_warn "git add failed — uncommitted work may be lost on branch switch"
226
229
  return 0
227
230
  fi
228
231
 
@@ -23,6 +23,128 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*
23
23
  log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
24
24
  log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
25
25
 
26
+ prizm_require_positive_int() {
27
+ local option_name="$1"
28
+ local option_value="$2"
29
+ if [[ ! "$option_value" =~ ^[1-9][0-9]*$ ]]; then
30
+ log_error "$option_name must be a positive integer: $option_value"
31
+ exit 1
32
+ fi
33
+ }
34
+
35
+ # Ignore temporary git worktrees created under root-level hidden AI tool dirs
36
+ # matching .<tool>/worktree(s), without hiding normal source directories.
37
+ PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES=(
38
+ ':(top,exclude,glob).*/worktree'
39
+ ':(top,exclude,glob).*/worktree/**'
40
+ ':(top,exclude,glob).*/worktrees'
41
+ ':(top,exclude,glob).*/worktrees/**'
42
+ )
43
+
44
+ prizm_git_status_safe() {
45
+ local project_root="$1"
46
+ git -C "$project_root" status --porcelain -- . \
47
+ "${PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES[@]}" 2>/dev/null
48
+ }
49
+
50
+ prizm_git_add_all_safe() {
51
+ local project_root="$1"
52
+ git -C "$project_root" add -A -- . \
53
+ "${PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES[@]}" 2>/dev/null
54
+ }
55
+
56
+ prizm_git_relative_path() {
57
+ local project_root="$1"
58
+ local target_path="$2"
59
+ python3 - "$project_root" "$target_path" <<'PY' 2>/dev/null || true
60
+ import os
61
+ import sys
62
+ root = os.path.abspath(sys.argv[1])
63
+ target = os.path.abspath(sys.argv[2])
64
+ print(os.path.relpath(target, root).replace(os.sep, "/"))
65
+ PY
66
+ }
67
+
68
+ prizm_git_status_non_bookkeeping() {
69
+ local project_root="$1"
70
+ local state_dir="$2"
71
+ local list_path="$3"
72
+
73
+ local state_rel list_rel
74
+ state_rel="$(prizm_git_relative_path "$project_root" "$state_dir")"
75
+ list_rel="$(prizm_git_relative_path "$project_root" "$list_path")"
76
+
77
+ local line path
78
+ while IFS= read -r line; do
79
+ [[ -n "$line" ]] || continue
80
+ path="${line:3}"
81
+ if [[ "$path" == *" -> "* ]]; then
82
+ path="${path##* -> }"
83
+ fi
84
+ path="${path%\"}"
85
+ path="${path#\"}"
86
+ path="${path#./}"
87
+ if [[ -n "$list_rel" && "$path" == "$list_rel" ]]; then
88
+ continue
89
+ fi
90
+ if [[ -n "$state_rel" && ( "$path" == "$state_rel" || "$path" == "$state_rel/"* ) ]]; then
91
+ continue
92
+ fi
93
+ printf '%s\n' "$line"
94
+ done < <(prizm_git_status_safe "$project_root")
95
+ }
96
+
97
+ prizm_git_status_bookkeeping() {
98
+ local project_root="$1"
99
+ local state_dir="$2"
100
+ local list_path="$3"
101
+
102
+ local state_rel list_rel
103
+ state_rel="$(prizm_git_relative_path "$project_root" "$state_dir")"
104
+ list_rel="$(prizm_git_relative_path "$project_root" "$list_path")"
105
+
106
+ local line path
107
+ while IFS= read -r line; do
108
+ [[ -n "$line" ]] || continue
109
+ path="${line:3}"
110
+ if [[ "$path" == *" -> "* ]]; then
111
+ path="${path##* -> }"
112
+ fi
113
+ path="${path%\"}"
114
+ path="${path#\"}"
115
+ path="${path#./}"
116
+ if [[ -n "$list_rel" && "$path" == "$list_rel" ]]; then
117
+ printf '%s\n' "$line"
118
+ continue
119
+ fi
120
+ if [[ -n "$state_rel" && ( "$path" == "$state_rel" || "$path" == "$state_rel/"* ) ]]; then
121
+ printf '%s\n' "$line"
122
+ fi
123
+ done < <(prizm_git_status_safe "$project_root")
124
+ }
125
+
126
+ prizm_git_commit_paths() {
127
+ local project_root="$1"
128
+ local message="$2"
129
+ shift 2
130
+
131
+ local paths=()
132
+ local path rel_path
133
+ for path in "$@"; do
134
+ [[ -n "$path" && -e "$path" ]] || continue
135
+ rel_path="$(prizm_git_relative_path "$project_root" "$path")"
136
+ [[ -n "$rel_path" ]] || continue
137
+ paths+=("$rel_path")
138
+ done
139
+ [[ ${#paths[@]} -gt 0 ]] || return 0
140
+
141
+ git -C "$project_root" add -- "${paths[@]}" 2>/dev/null || true
142
+ if ! git -C "$project_root" diff --cached --quiet -- "${paths[@]}" 2>/dev/null; then
143
+ git -C "$project_root" commit --no-verify -m "$message" -- "${paths[@]}" 2>/dev/null || true
144
+ fi
145
+ }
146
+
147
+
26
148
  # ============================================================
27
149
  # Path resolution (single source of truth)
28
150
  # ============================================================
@@ -46,6 +46,7 @@ start_heartbeat() {
46
46
  local prev_progress_signature=""
47
47
  local prev_error_loop_signature=""
48
48
  local stale_seconds=0
49
+ local stale_reason="stale_session"
49
50
  while kill -0 "$cli_pid" 2>/dev/null; do
50
51
  sleep "$heartbeat_interval"
51
52
  elapsed=$((elapsed + heartbeat_interval))
@@ -240,16 +241,20 @@ PY
240
241
  # child activity as the primary progress signals — the parent process is
241
242
  # still running and children may still be producing output.
242
243
  if [[ "$error_loop_detected" == "true" ]]; then
244
+ stale_reason="error_loop"
243
245
  stale_seconds=$effective_stale_kill_threshold
244
246
  elif [[ "$log_truncated" == "true" ]]; then
247
+ stale_reason="stale_session"
245
248
  if [[ $progress_advanced -gt 0 || $child_growth -gt 0 ]]; then
246
249
  stale_seconds=0
247
250
  else
248
251
  stale_seconds=$((stale_seconds + heartbeat_interval))
249
252
  fi
250
253
  elif [[ $growth -le 0 && $child_growth -eq 0 && $progress_advanced -eq 0 ]]; then
254
+ stale_reason="stale_session"
251
255
  stale_seconds=$((stale_seconds + heartbeat_interval))
252
256
  else
257
+ stale_reason="stale_session"
253
258
  stale_seconds=0
254
259
  fi
255
260
 
@@ -333,14 +338,19 @@ PY
333
338
  # stale window to surface stuck agents promptly.
334
339
  if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -ge $effective_stale_kill_threshold ]]; then
335
340
  local stale_mins=$((stale_seconds / 60))
336
- echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: no progress for ${stale_mins}m (threshold: ${effective_stale_kill_threshold}s)${NC}"
337
- echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
341
+ if [[ "$stale_reason" == "error_loop" ]]; then
342
+ echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: repeated read-offset/wasted-call error loop (threshold: ${effective_stale_kill_threshold}s)${NC}"
343
+ echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (error loop)..."
344
+ else
345
+ echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: no progress for ${stale_mins}m (threshold: ${effective_stale_kill_threshold}s)${NC}"
346
+ echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
347
+ fi
338
348
  # Write the marker before killing. Some CLIs exit quickly, and the
339
349
  # parent runner may stop this heartbeat process immediately after
340
350
  # wait(1) returns.
341
351
  local _marker_dir
342
352
  _marker_dir="$(dirname "$session_log")"
343
- echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"stale_session\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
353
+ echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${stale_reason}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
344
354
  kill -TERM "$cli_pid" 2>/dev/null || true
345
355
  # Give process 10s to exit gracefully, then force kill
346
356
  local stale_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
@@ -8,13 +8,14 @@ set -euo pipefail
8
8
  # fix bugs from a .prizmkit/plans/bug-fix-list.json specification.
9
9
  #
10
10
  # Usage:
11
- # ./run-bugfix.sh run [.prizmkit/plans/bug-fix-list.json] Run all bugs
11
+ # ./run-bugfix.sh run [.prizmkit/plans/bug-fix-list.json] [options] Run all bugs
12
12
  # ./run-bugfix.sh run <bug-id> [options] Run a single bug
13
13
  # ./run-bugfix.sh status [.prizmkit/plans/bug-fix-list.json] Show pipeline status
14
14
  # ./run-bugfix.sh reset Clear all state
15
15
  #
16
16
  # Environment Variables:
17
- # MAX_RETRIES Max retries per bug (default: 3)
17
+ # MAX_RETRIES Max code retries per bug (default: 3)
18
+ # MAX_INFRA_RETRIES Max infrastructure/provider retries per bug (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)
@@ -42,6 +43,7 @@ SCRIPTS_DIR="$SCRIPT_DIR/scripts"
42
43
 
43
44
  # Configuration
44
45
  MAX_RETRIES=${MAX_RETRIES:-3}
46
+ MAX_INFRA_RETRIES=${MAX_INFRA_RETRIES:-3}
45
47
  SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
46
48
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
47
49
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
@@ -90,8 +92,9 @@ spawn_and_wait_session() {
90
92
  local bootstrap_prompt="$4"
91
93
  local session_dir="$5"
92
94
  local max_retries="$6"
93
- local item_model="${7:-}"
94
- local base_branch="${8:-main}"
95
+ local max_infra_retries="$7"
96
+ local item_model="${8:-}"
97
+ local base_branch="${9:-main}"
95
98
 
96
99
  local session_log="$session_dir/logs/session.log"
97
100
  local progress_json="$session_dir/logs/progress.json"
@@ -147,9 +150,23 @@ spawn_and_wait_session() {
147
150
  # Check for stale-kill marker (heartbeat killed the process due to no progress)
148
151
  local stale_kill_marker="$session_dir/logs/stale-kill.json"
149
152
  local was_stale_killed=false
153
+ local stale_kill_reason=""
150
154
  if [[ -f "$stale_kill_marker" ]]; then
151
155
  was_stale_killed=true
152
- log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
156
+ stale_kill_reason=$(python3 - "$stale_kill_marker" <<'PY' 2>/dev/null || true
157
+ import json, sys
158
+ try:
159
+ with open(sys.argv[1], encoding="utf-8") as fh:
160
+ print(json.load(fh).get("reason") or "")
161
+ except Exception:
162
+ pass
163
+ PY
164
+ )
165
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
166
+ log_warn "Session was killed by heartbeat monitor due to repeated read-offset/wasted-call errors"
167
+ else
168
+ log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
169
+ fi
153
170
  fi
154
171
 
155
172
  local was_infra_error=false
@@ -183,7 +200,11 @@ spawn_and_wait_session() {
183
200
  log_warn "Infrastructure errors are retried without consuming code retry budget"
184
201
  session_status="infra_error"
185
202
  elif [[ "$was_stale_killed" == true ]]; then
186
- log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
203
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
204
+ log_warn "Session killed due to repeated read-offset/wasted-call error loop"
205
+ else
206
+ log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
207
+ fi
187
208
  log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
188
209
  session_status="crashed"
189
210
  elif [[ $exit_code -ne 0 ]]; then
@@ -200,10 +221,10 @@ spawn_and_wait_session() {
200
221
  session_status="success"
201
222
  else
202
223
  local uncommitted=""
203
- uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
224
+ uncommitted=$(prizm_git_status_safe "$project_root" | head -1 || true)
204
225
  if [[ -n "$uncommitted" ]]; then
205
226
  log_warn "Session exited cleanly but produced no commits (uncommitted changes found) — auto-committing..."
206
- git -C "$project_root" add -A 2>/dev/null || true
227
+ prizm_git_add_all_safe "$project_root" || true
207
228
  if git -C "$project_root" commit --no-verify -m "chore($bug_id): auto-commit session work" 2>/dev/null; then
208
229
  log_info "Auto-commit succeeded"
209
230
  session_status="success"
@@ -222,10 +243,10 @@ spawn_and_wait_session() {
222
243
  if [[ "$session_status" == "success" ]]; then
223
244
  if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
224
245
  local dirty_files=""
225
- dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
246
+ dirty_files=$(prizm_git_status_safe "$project_root" || true)
226
247
  if [[ -n "$dirty_files" ]]; then
227
248
  log_info "Auto-committing remaining session artifacts..."
228
- git -C "$project_root" add -A 2>/dev/null || true
249
+ prizm_git_add_all_safe "$project_root" || true
229
250
  git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
230
251
  || git -C "$project_root" commit --no-verify -m "chore($bug_id): include remaining session artifacts" 2>/dev/null \
231
252
  || true
@@ -278,7 +299,20 @@ sys.exit(0)
278
299
  # Subagent detection
279
300
  prizm_detect_subagents "$session_log"
280
301
 
281
- # Update bug status (do NOT commit on dev branch commit happens after merge)
302
+ # Final status updates happen after the caller returns to the original branch.
303
+ _SPAWN_RESULT="$session_status"
304
+ }
305
+
306
+ finalize_bug_status_after_branch_return() {
307
+ local bug_id="$1"
308
+ local bug_list="$2"
309
+ local session_id="$3"
310
+ local session_status="$4"
311
+ local max_retries="$5"
312
+ local max_infra_retries="$6"
313
+
314
+ # Update bug status on the original branch. The caller commits the resulting
315
+ # bug-list diff immediately after this helper returns.
282
316
  local update_output
283
317
  update_output=$(python3 "$SCRIPTS_DIR/update-bug-status.py" \
284
318
  --bug-list "$bug_list" \
@@ -287,14 +321,13 @@ sys.exit(0)
287
321
  --session-status "$session_status" \
288
322
  --session-id "$session_id" \
289
323
  --max-retries "$max_retries" \
324
+ --max-infra-retries "$max_infra_retries" \
290
325
  --action update 2>&1) || {
291
326
  log_error "Failed to update bug status: $update_output"
292
327
  update_output=""
293
328
  }
294
329
 
295
330
  _SPAWN_ITEM_STATUS="$(printf '%s' "$update_output" | prizm_extract_update_new_status)"
296
-
297
- _SPAWN_RESULT="$session_status"
298
331
  }
299
332
 
300
333
  # ============================================================
@@ -334,6 +367,8 @@ for bug in data.get('bugs', []):
334
367
  --state-dir "$STATE_DIR" \
335
368
  --bug-id "$_interrupted_id" \
336
369
  --session-status "failed" \
370
+ --max-retries "$MAX_RETRIES" \
371
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
337
372
  --action update 2>/dev/null || true
338
373
  log_info "Bug $_interrupted_id marked as failed due to interrupt"
339
374
  fi
@@ -406,6 +441,16 @@ run_one() {
406
441
  --clean) do_clean=true; shift ;;
407
442
  --no-reset) no_reset=true; shift ;;
408
443
  --timeout) shift; SESSION_TIMEOUT="${1:-0}"; shift ;;
444
+ --max-infra-retries)
445
+ shift
446
+ if [[ $# -eq 0 ]]; then
447
+ log_error "--max-infra-retries requires a value"
448
+ exit 1
449
+ fi
450
+ prizm_require_positive_int "--max-infra-retries" "$1"
451
+ MAX_INFRA_RETRIES="$1"
452
+ shift
453
+ ;;
409
454
  --mode)
410
455
  shift
411
456
  if [[ $# -eq 0 ]]; then
@@ -695,6 +740,8 @@ else:
695
740
  --state-dir "$STATE_DIR" \
696
741
  --bug-id "$bug_id" \
697
742
  --session-status "failed" \
743
+ --max-retries "$MAX_RETRIES" \
744
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
698
745
  --action update 2>/dev/null || true
699
746
  log_info "Bug $bug_id marked as failed due to interrupt"
700
747
  fi
@@ -728,12 +775,13 @@ else:
728
775
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
729
776
  _DEV_BRANCH_NAME="$_branch_name"
730
777
  else
731
- log_warn "Failed to create branch; running session on current branch"
778
+ log_error "Failed to create branch: $_branch_name from $_source_branch — aborting to avoid running on $_source_branch"
779
+ return 1
732
780
  fi
733
781
 
734
782
  spawn_and_wait_session \
735
783
  "$bug_id" "$bug_list" "$session_id" \
736
- "$bootstrap_prompt" "$session_dir" 999 "$bug_model" "$_ORIGINAL_BRANCH"
784
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$bug_model" "$_ORIGINAL_BRANCH"
737
785
  local session_status="$_SPAWN_RESULT"
738
786
 
739
787
  # Merge dev branch back to original on success
@@ -743,7 +791,7 @@ else:
743
791
  else
744
792
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
745
793
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
746
- _DEV_BRANCH_NAME=""
794
+ session_status="merge_conflict"
747
795
  fi
748
796
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
749
797
  # Session failed — preserve dev branch for inspection
@@ -754,10 +802,12 @@ else:
754
802
  # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
755
803
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
756
804
 
805
+ finalize_bug_status_after_branch_return \
806
+ "$bug_id" "$bug_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES"
807
+
757
808
  # Commit bug status update on the original branch (after guaranteed return)
758
809
  if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
759
- git -C "$_proj_root" add "$bug_list"
760
- git -C "$_proj_root" commit --no-verify -m "chore($bug_id): update bug status" 2>/dev/null || true
810
+ prizm_git_commit_paths "$_proj_root" "chore($bug_id): update bug status" "$bug_list"
761
811
  fi
762
812
 
763
813
  echo ""
@@ -778,7 +828,41 @@ else:
778
828
  # ============================================================
779
829
 
780
830
  main() {
781
- local bug_list="${1:-$PRIZMKIT_DIR/plans/bug-fix-list.json}"
831
+ local bug_list="$PRIZMKIT_DIR/plans/bug-fix-list.json"
832
+
833
+ while [[ $# -gt 0 ]]; do
834
+ case "$1" in
835
+ --max-infra-retries)
836
+ shift
837
+ if [[ $# -eq 0 ]]; then
838
+ log_error "--max-infra-retries requires a value"
839
+ exit 1
840
+ fi
841
+ prizm_require_positive_int "--max-infra-retries" "$1"
842
+ MAX_INFRA_RETRIES="$1"
843
+ shift
844
+ ;;
845
+ --)
846
+ shift
847
+ break
848
+ ;;
849
+ -*)
850
+ log_error "Unknown option: $1"
851
+ show_help
852
+ exit 1
853
+ ;;
854
+ *)
855
+ bug_list="$1"
856
+ shift
857
+ ;;
858
+ esac
859
+ done
860
+
861
+ if [[ $# -gt 0 ]]; then
862
+ log_error "Unexpected arguments: $*"
863
+ show_help
864
+ exit 1
865
+ fi
782
866
 
783
867
  if [[ ! "$bug_list" = /* ]]; then
784
868
  bug_list="$(pwd)/$bug_list"
@@ -840,7 +924,8 @@ main() {
840
924
  echo -e "${BOLD} Bug-Fix Pipeline Runner Started${NC}"
841
925
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
842
926
  log_info "Bug fix list: $bug_list"
843
- log_info "Max retries per bug: $MAX_RETRIES"
927
+ log_info "Max code retries per bug: $MAX_RETRIES"
928
+ log_info "Max infrastructure retries per bug: $MAX_INFRA_RETRIES"
844
929
  if [[ $SESSION_TIMEOUT -gt 0 ]]; then
845
930
  log_info "Session timeout: ${SESSION_TIMEOUT}s"
846
931
  else
@@ -885,6 +970,7 @@ main() {
885
970
  --bug-list "$bug_list" \
886
971
  --state-dir "$STATE_DIR" \
887
972
  --max-retries "$MAX_RETRIES" \
973
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
888
974
  --action get_next 2>/dev/null); then
889
975
  log_error "Failed to get next bug"
890
976
  break
@@ -972,27 +1058,43 @@ DEPLOY_PROMPT_EOF
972
1058
  fi
973
1059
 
974
1060
  # Parse bug info
975
- local bug_id bug_title bug_severity retry_count resume_phase
1061
+ local bug_id bug_title bug_severity retry_count infra_error_count resume_phase
976
1062
  bug_id=$(echo "$next_bug" | jq -r '.bug_id')
977
1063
  bug_title=$(echo "$next_bug" | jq -r '.title')
978
1064
  bug_severity=$(echo "$next_bug" | jq -r '.severity')
979
1065
  retry_count=$(echo "$next_bug" | jq -r '.retry_count // 0')
1066
+ infra_error_count=$(echo "$next_bug" | jq -r '.infra_error_count // 0')
980
1067
  resume_phase=$(echo "$next_bug" | jq -r '.resume_from_phase // "null"')
981
1068
 
982
1069
  echo ""
983
1070
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
984
1071
  log_info "Bug: ${BOLD}$bug_id${NC} — $bug_title"
985
- log_info "Severity: $bug_severity | Retry: $retry_count / $MAX_RETRIES"
1072
+ log_info "Severity: $bug_severity"
1073
+ log_info "Code retry: $retry_count / $MAX_RETRIES"
1074
+ log_info "Infrastructure retry: $infra_error_count / $MAX_INFRA_RETRIES"
986
1075
  if [[ "$resume_phase" != "null" ]]; then
987
1076
  log_info "Resuming from Phase $resume_phase"
988
1077
  fi
989
1078
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
990
1079
 
991
- # Pre-commit any dirty tree from previous iteration
992
- 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
993
- log_info "Dirty working tree detected committing before $bug_id..."
994
- git -C "$_proj_root" add -A 2>/dev/null || true
995
- git -C "$_proj_root" commit --no-verify -m "chore: capture artifacts before $bug_id session" 2>/dev/null || true
1080
+ # Do not auto-commit an arbitrary dirty baseline on the original branch.
1081
+ # Only pipeline bookkeeping is committed explicitly; user/source WIP,
1082
+ # submodule dirt, specs, and root-level hidden tool temporary worktrees
1083
+ # must never be swept into main by a broad git add.
1084
+ local _dirty_files=""
1085
+ _dirty_files=$(prizm_git_status_non_bookkeeping "$_proj_root" "$STATE_DIR" "$bug_list" || true)
1086
+ if [[ -n "$_dirty_files" ]]; then
1087
+ log_error "Dirty working tree detected before $bug_id; refusing to commit it on $_ORIGINAL_BRANCH."
1088
+ log_error "Commit/stash your changes or start from a clean branch, then rerun the pipeline."
1089
+ printf '%s\n' "$_dirty_files" | sed 's/^/ /'
1090
+ break
1091
+ fi
1092
+
1093
+ local _bookkeeping_dirty=""
1094
+ _bookkeeping_dirty=$(prizm_git_status_bookkeeping "$_proj_root" "$STATE_DIR" "$bug_list" || true)
1095
+ if [[ -n "$_bookkeeping_dirty" ]]; then
1096
+ log_info "Committing pending pipeline bookkeeping before $bug_id..."
1097
+ prizm_git_commit_paths "$_proj_root" "chore($bug_id): include pipeline bookkeeping" "$bug_list" "$STATE_DIR"
996
1098
  fi
997
1099
 
998
1100
  # Mark bug as in-progress BEFORE creating dev branch
@@ -1005,8 +1107,7 @@ DEPLOY_PROMPT_EOF
1005
1107
  --action start >/dev/null 2>&1 || true
1006
1108
  # Commit the in_progress status on the original branch
1007
1109
  if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
1008
- git -C "$_proj_root" add "$bug_list" 2>/dev/null || true
1009
- git -C "$_proj_root" commit --no-verify -m "chore($bug_id): mark in_progress" 2>/dev/null || true
1110
+ prizm_git_commit_paths "$_proj_root" "chore($bug_id): mark in_progress" "$bug_list"
1010
1111
  fi
1011
1112
 
1012
1113
  # Create per-bug dev branch (from the now-updated original branch)
@@ -1015,8 +1116,9 @@ DEPLOY_PROMPT_EOF
1015
1116
  _DEV_BRANCH_NAME="$_bug_branch"
1016
1117
  log_info "Dev branch: $_bug_branch"
1017
1118
  else
1018
- log_warn "Failed to create dev branch; running on current branch: $_ORIGINAL_BRANCH"
1119
+ log_error "Failed to create dev branch: $_bug_branch from $_ORIGINAL_BRANCH — aborting to avoid running on $_ORIGINAL_BRANCH"
1019
1120
  _DEV_BRANCH_NAME=""
1121
+ break
1020
1122
  fi
1021
1123
 
1022
1124
  # Generate session
@@ -1086,10 +1188,9 @@ DEPLOY_PROMPT_EOF
1086
1188
 
1087
1189
  spawn_and_wait_session \
1088
1190
  "$bug_id" "$bug_list" "$session_id" \
1089
- "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$bug_model" "$_ORIGINAL_BRANCH"
1191
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$bug_model" "$_ORIGINAL_BRANCH"
1090
1192
 
1091
1193
  local session_status="$_SPAWN_RESULT"
1092
- local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1093
1194
 
1094
1195
  # Merge per-bug dev branch back to original on success
1095
1196
  if [[ "$session_status" == "success" && -n "$_DEV_BRANCH_NAME" ]]; then
@@ -1098,7 +1199,7 @@ DEPLOY_PROMPT_EOF
1098
1199
  else
1099
1200
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
1100
1201
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
1101
- _DEV_BRANCH_NAME=""
1202
+ session_status="merge_conflict"
1102
1203
  fi
1103
1204
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
1104
1205
  # Session failed — preserve dev branch for inspection
@@ -1109,10 +1210,13 @@ DEPLOY_PROMPT_EOF
1109
1210
  # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
1110
1211
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1111
1212
 
1213
+ finalize_bug_status_after_branch_return \
1214
+ "$bug_id" "$bug_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES"
1215
+ local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1216
+
1112
1217
  # Commit bug status update on the original branch (after guaranteed return)
1113
1218
  if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
1114
- git -C "$_proj_root" add "$bug_list"
1115
- git -C "$_proj_root" commit --no-verify -m "chore($bug_id): update bug status" 2>/dev/null || true
1219
+ prizm_git_commit_paths "$_proj_root" "chore($bug_id): update bug status" "$bug_list"
1116
1220
  fi
1117
1221
 
1118
1222
  session_count=$((session_count + 1))
@@ -1129,7 +1233,7 @@ DEPLOY_PROMPT_EOF
1129
1233
  log_error "════════════════════════════════════════════════════"
1130
1234
  break
1131
1235
  elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
1132
- log_info "STOP_ON_FAILURE: $bug_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
1236
+ log_info "STOP_ON_FAILURE: $bug_id is ${item_status_after_session:-unknown}; code/infra retry budget not exhausted, continuing."
1133
1237
  fi
1134
1238
 
1135
1239
  # Stuck detection
@@ -1166,7 +1270,7 @@ show_help() {
1166
1270
  echo "Usage: $0 <command> [options]"
1167
1271
  echo ""
1168
1272
  echo "Commands:"
1169
- echo " run [.prizmkit/plans/bug-fix-list.json] Run all bugs by severity/priority order"
1273
+ echo " run [.prizmkit/plans/bug-fix-list.json] [options] Run all bugs by severity/priority order"
1170
1274
  echo " run <bug-id> [options] Run a single bug fix"
1171
1275
  echo " status [.prizmkit/plans/bug-fix-list.json] Show bug fix pipeline status"
1172
1276
  echo " reset Clear all bugfix state"
@@ -1177,12 +1281,17 @@ show_help() {
1177
1281
  echo " --clean Delete artifacts and reset before running"
1178
1282
  echo " --no-reset Skip status reset (preserve retry count)"
1179
1283
  echo " --timeout N Session timeout in seconds (default: 0 = no limit)"
1284
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1180
1285
  echo " --mode <lite|standard|full> Override pipeline mode"
1181
1286
  echo " --critic Enable adversarial critic review"
1182
1287
  echo " --no-critic Disable adversarial critic review"
1183
1288
  echo ""
1289
+ echo "Pipeline Options (run all):"
1290
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1291
+ echo ""
1184
1292
  echo "Environment Variables:"
1185
- echo " MAX_RETRIES Max retries per bug (default: 3)"
1293
+ echo " MAX_RETRIES Max code retries per bug (default: 3)"
1294
+ echo " MAX_INFRA_RETRIES Max infrastructure/provider retries per bug (default: 3)"
1186
1295
  echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
1187
1296
  echo " MODEL Default AI model (overridden by per-bug model in bug list)"
1188
1297
  echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
@@ -1214,7 +1323,7 @@ case "${1:-run}" in
1214
1323
  if [[ "${1:-}" =~ ^[Bb]-[0-9]+ ]]; then
1215
1324
  run_one "$@"
1216
1325
  else
1217
- main "${1:-$PRIZMKIT_DIR/plans/bug-fix-list.json}"
1326
+ main "$@"
1218
1327
  fi
1219
1328
  ;;
1220
1329
  status)
@@ -1226,6 +1335,8 @@ case "${1:-run}" in
1226
1335
  python3 "$SCRIPTS_DIR/update-bug-status.py" \
1227
1336
  --bug-list "${2:-$PRIZMKIT_DIR/plans/bug-fix-list.json}" \
1228
1337
  --state-dir "$STATE_DIR" \
1338
+ --max-retries "$MAX_RETRIES" \
1339
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
1229
1340
  --action status
1230
1341
  ;;
1231
1342
  reset)