prizmkit 1.1.91 → 1.1.93

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.
Files changed (46) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +0 -4
  3. package/bundled/dev-pipeline/README.md +9 -39
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +0 -4
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -2
  6. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -1
  7. package/bundled/dev-pipeline/launch-feature-daemon.sh +2 -3
  8. package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -1
  9. package/bundled/dev-pipeline/lib/branch.sh +8 -5
  10. package/bundled/dev-pipeline/lib/common.sh +122 -0
  11. package/bundled/dev-pipeline/lib/heartbeat.sh +13 -3
  12. package/bundled/dev-pipeline/run-bugfix.sh +149 -98
  13. package/bundled/dev-pipeline/run-feature.sh +90 -103
  14. package/bundled/dev-pipeline/run-recovery.sh +1 -32
  15. package/bundled/dev-pipeline/run-refactor.sh +148 -99
  16. package/bundled/dev-pipeline/scripts/update-bug-status.py +39 -4
  17. package/bundled/dev-pipeline/scripts/update-feature-status.py +43 -6
  18. package/bundled/dev-pipeline/scripts/update-refactor-status.py +36 -4
  19. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +3 -3
  20. package/bundled/dev-pipeline/tests/test_auto_skip.py +92 -3
  21. package/bundled/dev-pipeline-windows/.env.example +0 -4
  22. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -4
  23. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -2
  24. package/bundled/dev-pipeline-windows/lib/branch.ps1 +17 -4
  25. package/bundled/dev-pipeline-windows/lib/common.ps1 +9 -0
  26. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +89 -108
  27. package/bundled/dev-pipeline-windows/run-recovery.ps1 +4 -19
  28. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +39 -4
  29. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +43 -6
  30. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +36 -4
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +1 -1
  33. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +5 -15
  34. package/bundled/skills/feature-pipeline-launcher/SKILL.md +4 -4
  35. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +3 -13
  36. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +3 -3
  37. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +4 -14
  38. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +1 -1
  39. package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +4 -14
  40. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +4 -4
  41. package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +2 -12
  42. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +3 -3
  43. package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +4 -14
  44. package/package.json +1 -1
  45. package/bundled/dev-pipeline/scripts/cleanup-logs.py +0 -192
  46. package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +0 -192
@@ -9,14 +9,14 @@ set -euo pipefail
9
9
  # from a feature list.
10
10
  #
11
11
  # Usage:
12
- # ./run-feature.sh run [.prizmkit/plans/feature-list.json] Run all features
12
+ # ./run-feature.sh run [.prizmkit/plans/feature-list.json] [options] Run all features
13
13
  # ./run-feature.sh run <feature-id> [options] Run a single feature
14
14
  # ./run-feature.sh status [.prizmkit/plans/feature-list.json] Show pipeline status
15
15
  # ./run-feature.sh reset Clear all state
16
16
  #
17
17
  # Environment Variables:
18
- # MAX_RETRIES Max retries per feature (default: 3)
19
- # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
18
+ # MAX_RETRIES Max code retries per feature (default: 3)
19
+ # MAX_INFRA_RETRIES Max infrastructure/provider retries per feature (default: 3)
20
20
  # AI_CLI AI CLI command name (override; also readable from .prizmkit/config.json)
21
21
  # CODEBUDDY_CLI Legacy alias for AI_CLI (deprecated, use AI_CLI instead)
22
22
  # PRIZMKIT_PLATFORM Platform hint for custom AI_CLI wrappers: codebuddy, claude, or codex
@@ -25,9 +25,6 @@ set -euo pipefail
25
25
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
26
26
  # STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)
27
27
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
28
- # LOG_CLEANUP_ENABLED Run periodic log cleanup (default: 1)
29
- # LOG_RETENTION_DAYS Delete logs older than N days (default: 14)
30
- # LOG_MAX_TOTAL_MB Keep total logs under N MB via oldest-first cleanup (default: 1024)
31
28
  # PIPELINE_MODE Override mode for all features: lite|standard|full (used by daemon)
32
29
  # DEV_BRANCH Custom dev branch name (default: auto-generated dev/{feature_id}-YYYYMMDDHHmm)
33
30
  # AUTO_PUSH Auto-push to remote after successful feature (default: 0). Set to 1 to enable.
@@ -45,13 +42,10 @@ SCRIPTS_DIR="$SCRIPT_DIR/scripts"
45
42
 
46
43
  # Configuration (override via environment variables)
47
44
  MAX_RETRIES=${MAX_RETRIES:-3}
48
- SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
45
+ MAX_INFRA_RETRIES=${MAX_INFRA_RETRIES:-3}
49
46
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
50
47
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
51
48
  STALE_KILL_THRESHOLD=${STALE_KILL_THRESHOLD:-900}
52
- LOG_CLEANUP_ENABLED=${LOG_CLEANUP_ENABLED:-1}
53
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
54
- LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
55
49
  VERBOSE=${VERBOSE:-0}
56
50
  MODEL=${MODEL:-""}
57
51
  PRIZMKIT_EFFORT=${PRIZMKIT_EFFORT:-""}
@@ -124,13 +118,6 @@ spawn_and_wait_session() {
124
118
  start_progress_parser "$session_log" "$progress_json" "$SCRIPTS_DIR"
125
119
  local parser_pid="${_PARSER_PID:-}"
126
120
 
127
- # Timeout watchdog (only if SESSION_TIMEOUT > 0)
128
- local watcher_pid=""
129
- if [[ $SESSION_TIMEOUT -gt 0 ]]; then
130
- ( sleep "$SESSION_TIMEOUT" && kill -TERM "$cbc_pid" 2>/dev/null ) &
131
- watcher_pid=$!
132
- fi
133
-
134
121
  # Heartbeat monitor (reads progress.json when available, falls back to tail)
135
122
  # Also monitors for stale sessions and auto-kills if no progress for STALE_KILL_THRESHOLD seconds
136
123
  start_heartbeat "$cbc_pid" "$session_log" "$progress_json" "$HEARTBEAT_INTERVAL" "$STALE_KILL_THRESHOLD"
@@ -144,27 +131,35 @@ spawn_and_wait_session() {
144
131
  exit_code=$?
145
132
  fi
146
133
 
147
- # Clean up watcher, heartbeat, and parser
148
- [[ -n "$watcher_pid" ]] && kill "$watcher_pid" 2>/dev/null || true
134
+ # Clean up heartbeat and parser
149
135
  stop_heartbeat "$heartbeat_pid"
150
136
  stop_progress_parser "$parser_pid"
151
- [[ -n "$watcher_pid" ]] && wait "$watcher_pid" 2>/dev/null || true
152
137
 
153
138
  # Recover session log from backup if truncated by subagent stdout redirection
154
139
  prizm_recover_session_log "$session_log"
155
140
 
156
- # Map SIGTERM (143) to timeout code 124
157
- if [[ $exit_code -eq 143 ]]; then
158
- exit_code=124
159
- fi
160
141
  _SPAWN_EXIT_CODE="$exit_code"
161
142
 
162
143
  # Check for stale-kill marker (heartbeat killed the process due to no progress)
163
144
  local stale_kill_marker="$session_dir/logs/stale-kill.json"
164
145
  local was_stale_killed=false
146
+ local stale_kill_reason=""
165
147
  if [[ -f "$stale_kill_marker" ]]; then
166
148
  was_stale_killed=true
167
- log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
149
+ stale_kill_reason=$(python3 - "$stale_kill_marker" <<'PY' 2>/dev/null || true
150
+ import json, sys
151
+ try:
152
+ with open(sys.argv[1], encoding="utf-8") as fh:
153
+ print(json.load(fh).get("reason") or "")
154
+ except Exception:
155
+ pass
156
+ PY
157
+ )
158
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
159
+ log_warn "Session was killed by heartbeat monitor due to repeated read-offset/wasted-call errors"
160
+ else
161
+ log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
162
+ fi
168
163
  fi
169
164
 
170
165
  local was_infra_error=false
@@ -213,16 +208,17 @@ spawn_and_wait_session() {
213
208
  log_warn "Session failed due to structured AI runtime/context error"
214
209
  log_warn "AI runtime errors are retried without consuming code retry budget"
215
210
  session_status="infra_error"
216
- elif [[ $exit_code -eq 124 ]]; then
217
- log_warn "Session timed out after ${SESSION_TIMEOUT}s"
218
- session_status="timed_out"
219
211
  elif [[ "$was_infra_error" == true ]]; then
220
212
  log_warn "Session failed due to AI CLI/provider infrastructure error"
221
213
  log_warn "Infrastructure errors are retried without consuming code retry budget"
222
214
  session_status="infra_error"
223
215
  elif [[ "$was_stale_killed" == true ]]; then
224
- log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
225
- log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
216
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
217
+ log_warn "Session killed due to repeated read-offset/wasted-call error loop"
218
+ else
219
+ log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
220
+ fi
221
+ log_warn "Heartbeat-killed sessions are treated as failed; dev branch is preserved for inspection"
226
222
  session_status="crashed"
227
223
  elif [[ $exit_code -ne 0 ]]; then
228
224
  log_warn "Session exited with code $exit_code"
@@ -240,10 +236,10 @@ spawn_and_wait_session() {
240
236
  # No commits found — check if there are uncommitted changes (session
241
237
  # did work but didn't commit, e.g. context window exhausted)
242
238
  local uncommitted=""
243
- uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
239
+ uncommitted=$(prizm_git_status_safe "$project_root" | head -1 || true)
244
240
  if [[ -n "$uncommitted" ]]; then
245
241
  log_warn "Session exited cleanly but produced no commits (uncommitted changes found) — auto-committing..."
246
- git -C "$project_root" add -A 2>/dev/null || true
242
+ prizm_git_add_all_safe "$project_root" || true
247
243
  if git -C "$project_root" commit --no-verify -m "chore($feature_id): auto-commit session work" 2>/dev/null; then
248
244
  log_info "Auto-commit succeeded"
249
245
  session_status="success"
@@ -262,10 +258,10 @@ spawn_and_wait_session() {
262
258
  if [[ "$session_status" == "success" ]]; then
263
259
  if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
264
260
  local dirty_files=""
265
- dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
261
+ dirty_files=$(prizm_git_status_safe "$project_root" || true)
266
262
  if [[ -n "$dirty_files" ]]; then
267
263
  log_info "Auto-committing remaining session artifacts..."
268
- git -C "$project_root" add -A 2>/dev/null || true
264
+ prizm_git_add_all_safe "$project_root" || true
269
265
  git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
270
266
  || git -C "$project_root" commit --no-verify -m "chore($feature_id): include remaining session artifacts" 2>/dev/null \
271
267
  || true
@@ -385,8 +381,9 @@ finalize_feature_status_after_branch_return() {
385
381
  local session_id="$3"
386
382
  local session_status="$4"
387
383
  local max_retries="$5"
388
- local session_dir="$6"
389
- local base_branch="${7:-main}"
384
+ local max_infra_retries="$6"
385
+ local session_dir="$7"
386
+ local base_branch="${8:-main}"
390
387
 
391
388
  local feature_slug="${_SPAWN_FEATURE_SLUG:-}"
392
389
  local progress_json="$session_dir/logs/progress.json"
@@ -422,6 +419,7 @@ finalize_feature_status_after_branch_return() {
422
419
  --session-status "$session_status" \
423
420
  --session-id "$session_id" \
424
421
  --max-retries "$max_retries" \
422
+ --max-infra-retries "$max_infra_retries" \
425
423
  --action update 2>&1) || {
426
424
  log_error "Failed to update feature status: $update_output"
427
425
  log_error ".prizmkit/plans/feature-list.json may be out of sync. Manual intervention needed."
@@ -467,6 +465,8 @@ for feat in data.get('features', []):
467
465
  --state-dir "$STATE_DIR" \
468
466
  --feature-id "$_interrupted_id" \
469
467
  --session-status "failed" \
468
+ --max-retries "$MAX_RETRIES" \
469
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
470
470
  --action update 2>/dev/null || true
471
471
  log_info "Feature $_interrupted_id marked as failed due to interrupt"
472
472
  fi
@@ -497,29 +497,6 @@ check_dependencies() {
497
497
  prizm_ensure_git_repo "$_project_root"
498
498
  }
499
499
 
500
- run_log_cleanup() {
501
- if [[ "$LOG_CLEANUP_ENABLED" != "1" ]]; then
502
- return 0
503
- fi
504
-
505
- local cleanup_result
506
- cleanup_result=$(python3 "$SCRIPTS_DIR/cleanup-logs.py" \
507
- --state-dir "$STATE_DIR" \
508
- --retention-days "$LOG_RETENTION_DAYS" \
509
- --max-total-mb "$LOG_MAX_TOTAL_MB" 2>/dev/null) || {
510
- log_warn "Log cleanup failed (continuing)"
511
- return 0
512
- }
513
-
514
- local deleted reclaimed_kb
515
- deleted=$(echo "$cleanup_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('deleted_files', 0))" 2>/dev/null || echo "0")
516
- reclaimed_kb=$(echo "$cleanup_result" | python3 -c "import sys,json; print(int(json.load(sys.stdin).get('reclaimed_bytes', 0)/1024))" 2>/dev/null || echo "0")
517
-
518
- if [[ "$deleted" -gt 0 ]]; then
519
- log_info "Log cleanup: deleted $deleted files, reclaimed ${reclaimed_kb}KB"
520
- fi
521
- }
522
-
523
500
  # ============================================================
524
501
  # run-one: Run a single feature with full control
525
502
  # ============================================================
@@ -583,13 +560,14 @@ run_one() {
583
560
  critic_override="false"
584
561
  shift
585
562
  ;;
586
- --timeout)
563
+ --max-infra-retries)
587
564
  shift
588
565
  if [[ $# -eq 0 ]]; then
589
- log_error "--timeout requires a value in seconds"
566
+ log_error "--max-infra-retries requires a value"
590
567
  exit 1
591
568
  fi
592
- SESSION_TIMEOUT="$1"
569
+ prizm_require_positive_int "--max-infra-retries" "$1"
570
+ MAX_INFRA_RETRIES="$1"
593
571
  shift
594
572
  ;;
595
573
  F-*|f-*)
@@ -635,7 +613,6 @@ run_one() {
635
613
  fi
636
614
 
637
615
  check_dependencies
638
- run_log_cleanup
639
616
 
640
617
  # Initialize pipeline state if needed (same logic as run_all)
641
618
  if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
@@ -887,11 +864,6 @@ else:
887
864
  esac
888
865
  log_info "Pipeline mode: ${BOLD}$pipeline_mode${NC} ($_run_one_mode_desc)"
889
866
  log_info "Agents: $agent_count (critic: $([ "$critic_enabled" = "true" ] && echo "enabled" || echo "disabled"))"
890
- if [[ $SESSION_TIMEOUT -gt 0 ]]; then
891
- log_info "Session timeout: ${SESSION_TIMEOUT}s"
892
- else
893
- log_info "Session timeout: none"
894
- fi
895
867
  log_info "Prompt: $bootstrap_prompt"
896
868
  log_info "Log: $session_dir/logs/session.log"
897
869
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
@@ -916,6 +888,8 @@ else:
916
888
  --state-dir "$STATE_DIR" \
917
889
  --feature-id "$feature_id" \
918
890
  --session-status "failed" \
891
+ --max-retries "$MAX_RETRIES" \
892
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
919
893
  --action update 2>/dev/null || true
920
894
  log_info "Feature $feature_id marked as failed due to interrupt"
921
895
  fi
@@ -949,12 +923,13 @@ else:
949
923
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
950
924
  _DEV_BRANCH_NAME="$_branch_name"
951
925
  else
952
- log_warn "Failed to create branch; running session on current branch"
926
+ log_error "Failed to create branch: $_branch_name from $_source_branch — aborting to avoid running on $_source_branch"
927
+ return 1
953
928
  fi
954
929
 
955
930
  spawn_and_wait_session \
956
931
  "$feature_id" "$feature_list" "$session_id" \
957
- "$bootstrap_prompt" "$session_dir" 999 "$feature_model" "$_ORIGINAL_BRANCH"
932
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$feature_model" "$_ORIGINAL_BRANCH"
958
933
  local session_status="$_SPAWN_RESULT"
959
934
 
960
935
  # Merge dev branch back to original on success
@@ -976,12 +951,11 @@ else:
976
951
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
977
952
 
978
953
  finalize_feature_status_after_branch_return \
979
- "$feature_id" "$feature_list" "$session_id" "$session_status" 999 "$session_dir" "$_ORIGINAL_BRANCH"
954
+ "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
980
955
 
981
956
  # Commit feature status update on the original branch (after guaranteed return)
982
957
  if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
983
- git -C "$_proj_root" add "$feature_list" 2>/dev/null || true
984
- git -C "$_proj_root" commit --no-verify -m "chore($feature_id): update feature status" 2>/dev/null || true
958
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): update feature status" "$feature_list"
985
959
  fi
986
960
 
987
961
  echo ""
@@ -1029,7 +1003,6 @@ main() {
1029
1003
  fi
1030
1004
 
1031
1005
  check_dependencies
1032
- run_log_cleanup
1033
1006
 
1034
1007
  # Initialize pipeline state if needed
1035
1008
  if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
@@ -1080,12 +1053,8 @@ main() {
1080
1053
  if [[ -n "$features_filter" ]]; then
1081
1054
  log_info "Features filter: $features_filter"
1082
1055
  fi
1083
- log_info "Max retries per feature: $MAX_RETRIES"
1084
- if [[ $SESSION_TIMEOUT -gt 0 ]]; then
1085
- log_info "Session timeout: ${SESSION_TIMEOUT}s"
1086
- else
1087
- log_info "Session timeout: none"
1088
- fi
1056
+ log_info "Max code retries per feature: $MAX_RETRIES"
1057
+ log_info "Max infrastructure retries per feature: $MAX_INFRA_RETRIES"
1089
1058
  log_info "AI CLI: $CLI_CMD (platform: $PLATFORM)"
1090
1059
  if [[ -n "${MODEL:-}" ]]; then
1091
1060
  log_info "Default Model: $MODEL"
@@ -1259,28 +1228,38 @@ DEPLOY_PROMPT_EOF
1259
1228
  fi
1260
1229
 
1261
1230
  # Parse feature info
1262
- local feature_id feature_title retry_count resume_phase
1231
+ local feature_id feature_title retry_count infra_error_count resume_phase
1263
1232
  feature_id=$(echo "$next_feature" | jq -r '.feature_id')
1264
1233
  feature_title=$(echo "$next_feature" | jq -r '.title')
1265
1234
  retry_count=$(echo "$next_feature" | jq -r '.retry_count // 0')
1235
+ infra_error_count=$(echo "$next_feature" | jq -r '.infra_error_count // 0')
1266
1236
  resume_phase=$(echo "$next_feature" | jq -r '.resume_from_phase // "null"')
1267
1237
 
1268
1238
  echo ""
1269
1239
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1270
1240
  log_info "Feature: ${BOLD}$feature_id${NC} — $feature_title"
1271
- log_info "Retry: $retry_count / $MAX_RETRIES"
1241
+ log_info "Code retry: $retry_count / $MAX_RETRIES"
1242
+ log_info "Infrastructure retry: $infra_error_count / $MAX_INFRA_RETRIES"
1272
1243
  if [[ "$resume_phase" != "null" ]]; then
1273
1244
  log_info "Resuming from Phase $resume_phase"
1274
1245
  fi
1275
1246
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1276
1247
 
1277
- # Commit dirty working tree before starting feature
1248
+ # Auto-commit any uncommitted workspace changes before starting the pipeline.
1249
+ # This ensures a clean baseline without blocking execution.
1278
1250
  local _dirty_files=""
1279
- _dirty_files=$(git -C "$_proj_root" status --porcelain 2>/dev/null || true)
1251
+ _dirty_files=$(prizm_git_status_non_bookkeeping "$_proj_root" "$STATE_DIR" "$feature_list" || true)
1280
1252
  if [[ -n "$_dirty_files" ]]; then
1281
- log_info "Dirty working tree detected — committing before $feature_id..."
1282
- git -C "$_proj_root" add -A 2>/dev/null || true
1283
- git -C "$_proj_root" commit --no-verify -m "ready for run $feature_id" 2>/dev/null || true
1253
+ log_info "Auto-committing workspace changes before $feature_id..."
1254
+ prizm_git_add_all_safe "$_proj_root"
1255
+ git -C "$_proj_root" commit --no-verify -m "chore: workspace snapshot ready for run $feature_id" 2>/dev/null || true
1256
+ fi
1257
+
1258
+ local _bookkeeping_dirty=""
1259
+ _bookkeeping_dirty=$(prizm_git_status_bookkeeping "$_proj_root" "$STATE_DIR" "$feature_list" || true)
1260
+ if [[ -n "$_bookkeeping_dirty" ]]; then
1261
+ log_info "Committing pending pipeline bookkeeping before $feature_id..."
1262
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): include pipeline bookkeeping" "$feature_list" "$STATE_DIR"
1284
1263
  fi
1285
1264
 
1286
1265
  # Mark feature as in-progress BEFORE creating dev branch
@@ -1293,8 +1272,7 @@ DEPLOY_PROMPT_EOF
1293
1272
  --action start >/dev/null 2>&1 || true
1294
1273
  # Commit the in_progress status on the original branch
1295
1274
  if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
1296
- git -C "$_proj_root" add "$feature_list" 2>/dev/null || true
1297
- git -C "$_proj_root" commit --no-verify -m "chore($feature_id): mark in_progress" 2>/dev/null || true
1275
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): mark in_progress" "$feature_list"
1298
1276
  fi
1299
1277
 
1300
1278
  # Create per-feature dev branch (from the now-updated original branch)
@@ -1303,8 +1281,9 @@ DEPLOY_PROMPT_EOF
1303
1281
  _DEV_BRANCH_NAME="$_feature_branch"
1304
1282
  log_info "Dev branch: $_feature_branch"
1305
1283
  else
1306
- log_warn "Failed to create dev branch; running on current branch: $_ORIGINAL_BRANCH"
1284
+ log_error "Failed to create dev branch: $_feature_branch from $_ORIGINAL_BRANCH — aborting to avoid running on $_ORIGINAL_BRANCH"
1307
1285
  _DEV_BRANCH_NAME=""
1286
+ break
1308
1287
  fi
1309
1288
 
1310
1289
  # Generate session ID and bootstrap prompt
@@ -1397,20 +1376,20 @@ DEPLOY_PROMPT_EOF
1397
1376
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1398
1377
 
1399
1378
  finalize_feature_status_after_branch_return \
1400
- "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
1379
+ "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
1401
1380
  local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1402
1381
 
1403
1382
  # Commit feature status update on the original branch (after guaranteed return)
1404
1383
  if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
1405
- git -C "$_proj_root" add "$feature_list" 2>/dev/null || true
1406
- git -C "$_proj_root" commit --no-verify -m "chore($feature_id): update feature status" 2>/dev/null || true
1384
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): update feature status" "$feature_list"
1407
1385
  fi
1408
1386
 
1409
1387
  session_count=$((session_count + 1))
1410
1388
  total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
1411
1389
 
1412
1390
  # Stop-on-failure: abort only after the task is actually marked failed.
1413
- # Pending retry outcomes, including infrastructure errors, keep running.
1391
+ # Pending retry outcomes keep running; both code and infra failures have
1392
+ # bounded budgets in update-feature-status.py.
1414
1393
  if [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" && "$item_status_after_session" == "failed" ]]; then
1415
1394
  echo ""
1416
1395
  log_error "════════════════════════════════════════════════════"
@@ -1420,7 +1399,7 @@ DEPLOY_PROMPT_EOF
1420
1399
  log_error "════════════════════════════════════════════════════"
1421
1400
  break
1422
1401
  elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
1423
- log_info "STOP_ON_FAILURE: $feature_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
1402
+ log_info "STOP_ON_FAILURE: $feature_id is ${item_status_after_session:-unknown}; code/infra retry budget not exhausted, continuing."
1424
1403
  fi
1425
1404
 
1426
1405
  # Brief pause before next iteration
@@ -1458,20 +1437,20 @@ show_help() {
1458
1437
  echo " --no-critic Disable critic review (overrides feature-list setting)"
1459
1438
  echo " --clean Delete artifacts and reset before running"
1460
1439
  echo " --no-reset Skip feature status reset step"
1461
- echo " --timeout N Session timeout in seconds (default: 0 = no limit)"
1440
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1441
+ echo ""
1442
+ echo "Pipeline Options (run all):"
1443
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1462
1444
  echo ""
1463
1445
  echo "Environment Variables:"
1464
- echo " MAX_RETRIES Max retries per feature (default: 3)"
1465
- echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
1446
+ echo " MAX_RETRIES Max code retries per feature (default: 3)"
1447
+ echo " MAX_INFRA_RETRIES Max infrastructure/provider retries per feature (default: 3)"
1466
1448
  echo " AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)"
1467
1449
  echo " MODEL AI model ID (e.g. claude-opus-4.6, claude-sonnet-4.6, claude-haiku-4.5)"
1468
1450
  echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
1469
1451
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
1470
1452
  echo " STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)"
1471
1453
  echo " HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)"
1472
- echo " LOG_CLEANUP_ENABLED Run log cleanup before execution (default: 1)"
1473
- echo " LOG_RETENTION_DAYS Delete logs older than N days (default: 14)"
1474
- echo " LOG_MAX_TOTAL_MB Keep total logs under N MB (default: 1024)"
1475
1454
  echo " PIPELINE_MODE Override mode for all features: lite|standard|full"
1476
1455
  echo " ENABLE_CRITIC Enable critic review for all features: true|false"
1477
1456
  echo " STOP_ON_FAILURE Stop pipeline when a task exhausts retries: 0|1 (default: 0)"
@@ -1483,10 +1462,8 @@ show_help() {
1483
1462
  echo " ./run-feature.sh run F-007 --dry-run # Inspect generated prompt"
1484
1463
  echo " ./run-feature.sh run F-007 --dry-run --mode lite # Test lite mode"
1485
1464
  echo " ./run-feature.sh run F-007 --resume-phase 6 # Skip to implementation"
1486
- echo " ./run-feature.sh run F-007 --mode full --timeout 3600 # Full mode, 1h timeout"
1487
1465
  echo " ./run-feature.sh run F-007 --clean --mode standard # Clean + run standard"
1488
1466
  echo " ./run-feature.sh status # Show pipeline status"
1489
- echo " MAX_RETRIES=5 SESSION_TIMEOUT=7200 ./run-feature.sh run # Custom config"
1490
1467
  echo " MODEL=claude-sonnet-4.6 ./run-feature.sh run # Use Sonnet model"
1491
1468
  echo " MODEL=claude-haiku-4.5 ./run-feature.sh test-cli # Test with Haiku"
1492
1469
  }
@@ -1512,6 +1489,16 @@ case "${1:-run}" in
1512
1489
  _run_features_filter="$1"
1513
1490
  shift
1514
1491
  ;;
1492
+ --max-infra-retries)
1493
+ shift
1494
+ if [[ $# -eq 0 ]]; then
1495
+ log_error "--max-infra-retries requires a value"
1496
+ exit 1
1497
+ fi
1498
+ prizm_require_positive_int "--max-infra-retries" "$1"
1499
+ MAX_INFRA_RETRIES="$1"
1500
+ shift
1501
+ ;;
1515
1502
  *)
1516
1503
  _run_feature_list="$1"
1517
1504
  shift
@@ -23,7 +23,6 @@ set -euo pipefail
23
23
  # ./run-recovery.sh help Show help
24
24
  #
25
25
  # Environment Variables:
26
- # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
27
26
  # AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)
28
27
  # PRIZMKIT_PLATFORM Platform hint for custom AI_CLI wrappers: codebuddy, claude, or codex
29
28
  # MODEL AI model to use (e.g. claude-opus-4.6)
@@ -62,7 +61,6 @@ if [[ -z "$RECOVERY_DETECT_SCRIPT" ]]; then
62
61
  fi
63
62
 
64
63
  # Configuration
65
- SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
66
64
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
67
65
  STALE_KILL_THRESHOLD=${STALE_KILL_THRESHOLD:-900}
68
66
  VERBOSE=${VERBOSE:-0}
@@ -108,7 +106,6 @@ Options (for 'run' command):
108
106
  --model M Override AI model (e.g. claude-opus-4.6)
109
107
 
110
108
  Environment Variables:
111
- SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
112
109
  MODEL AI model to use
113
110
  PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)
114
111
  VERBOSE Set to 1 for verbose AI CLI output
@@ -283,13 +280,6 @@ spawn_recovery_session() {
283
280
  start_progress_parser "$session_log" "$progress_json" "$SCRIPTS_DIR"
284
281
  local parser_pid="${_PARSER_PID:-}"
285
282
 
286
- # Timeout watchdog
287
- local watcher_pid=""
288
- if [[ $SESSION_TIMEOUT -gt 0 ]]; then
289
- ( sleep "$SESSION_TIMEOUT" && kill -TERM "$_SESSION_PID" 2>/dev/null ) &
290
- watcher_pid=$!
291
- fi
292
-
293
283
  # Heartbeat monitor
294
284
  start_heartbeat "$_SESSION_PID" "$session_log" "$progress_json" "$HEARTBEAT_INTERVAL" "$STALE_KILL_THRESHOLD"
295
285
  _HEARTBEAT_PID="${_HEARTBEAT_PID:-}"
@@ -303,17 +293,10 @@ spawn_recovery_session() {
303
293
  fi
304
294
  _SESSION_PID=""
305
295
 
306
- # Cleanup background processes
307
- [[ -n "$watcher_pid" ]] && kill "$watcher_pid" 2>/dev/null || true
296
+ # Clean up heartbeat and parser
308
297
  stop_heartbeat "${_HEARTBEAT_PID:-}"
309
298
  _HEARTBEAT_PID=""
310
299
  stop_progress_parser "$parser_pid"
311
- [[ -n "$watcher_pid" ]] && wait "$watcher_pid" 2>/dev/null || true
312
-
313
- # Map SIGTERM to timeout
314
- if [[ $exit_code -eq 143 ]]; then
315
- exit_code=124
316
- fi
317
300
 
318
301
  # Check for stale-kill
319
302
  local stale_kill_marker="$session_dir/logs/stale-kill.json"
@@ -347,12 +330,6 @@ detect_session_outcome() {
347
330
  local was_stale_killed=false
348
331
  [[ -f "$stale_kill_marker" ]] && was_stale_killed=true
349
332
 
350
- if [[ $exit_code -eq 124 ]]; then
351
- log_warn "Session timed out"
352
- echo "timed_out"
353
- return
354
- fi
355
-
356
333
  if [[ "$was_stale_killed" == true ]]; then
357
334
  log_warn "Session stale-killed"
358
335
  fi
@@ -611,14 +588,6 @@ cmd_run() {
611
588
  echo " • Commit manually: git add <files> && git commit"
612
589
  echo ""
613
590
  ;;
614
- timed_out)
615
- log_warn "Session timed out before completing."
616
- echo ""
617
- echo " Options:"
618
- echo " • Run recovery again: ./run-recovery.sh"
619
- echo " • Increase timeout: SESSION_TIMEOUT=3600 ./run-recovery.sh"
620
- echo ""
621
- ;;
622
591
  no_changes)
623
592
  log_warn "Session completed but produced no changes."
624
593
  echo ""