prizmkit 1.1.90 → 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.
@@ -9,13 +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)
18
+ # MAX_RETRIES Max code retries per feature (default: 3)
19
+ # MAX_INFRA_RETRIES Max infrastructure/provider retries per feature (default: 3)
19
20
  # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
20
21
  # AI_CLI AI CLI command name (override; also readable from .prizmkit/config.json)
21
22
  # CODEBUDDY_CLI Legacy alias for AI_CLI (deprecated, use AI_CLI instead)
@@ -45,6 +46,7 @@ SCRIPTS_DIR="$SCRIPT_DIR/scripts"
45
46
 
46
47
  # Configuration (override via environment variables)
47
48
  MAX_RETRIES=${MAX_RETRIES:-3}
49
+ MAX_INFRA_RETRIES=${MAX_INFRA_RETRIES:-3}
48
50
  SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
49
51
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
50
52
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
@@ -162,9 +164,23 @@ spawn_and_wait_session() {
162
164
  # Check for stale-kill marker (heartbeat killed the process due to no progress)
163
165
  local stale_kill_marker="$session_dir/logs/stale-kill.json"
164
166
  local was_stale_killed=false
167
+ local stale_kill_reason=""
165
168
  if [[ -f "$stale_kill_marker" ]]; then
166
169
  was_stale_killed=true
167
- log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
170
+ stale_kill_reason=$(python3 - "$stale_kill_marker" <<'PY' 2>/dev/null || true
171
+ import json, sys
172
+ try:
173
+ with open(sys.argv[1], encoding="utf-8") as fh:
174
+ print(json.load(fh).get("reason") or "")
175
+ except Exception:
176
+ pass
177
+ PY
178
+ )
179
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
180
+ log_warn "Session was killed by heartbeat monitor due to repeated read-offset/wasted-call errors"
181
+ else
182
+ log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
183
+ fi
168
184
  fi
169
185
 
170
186
  local was_infra_error=false
@@ -221,8 +237,12 @@ spawn_and_wait_session() {
221
237
  log_warn "Infrastructure errors are retried without consuming code retry budget"
222
238
  session_status="infra_error"
223
239
  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"
240
+ if [[ "$stale_kill_reason" == "error_loop" ]]; then
241
+ log_warn "Session killed due to repeated read-offset/wasted-call error loop"
242
+ else
243
+ log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
244
+ fi
245
+ log_warn "Heartbeat-killed sessions are treated as failed; dev branch is preserved for inspection"
226
246
  session_status="crashed"
227
247
  elif [[ $exit_code -ne 0 ]]; then
228
248
  log_warn "Session exited with code $exit_code"
@@ -240,10 +260,10 @@ spawn_and_wait_session() {
240
260
  # No commits found — check if there are uncommitted changes (session
241
261
  # did work but didn't commit, e.g. context window exhausted)
242
262
  local uncommitted=""
243
- uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
263
+ uncommitted=$(prizm_git_status_safe "$project_root" | head -1 || true)
244
264
  if [[ -n "$uncommitted" ]]; then
245
265
  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
266
+ prizm_git_add_all_safe "$project_root" || true
247
267
  if git -C "$project_root" commit --no-verify -m "chore($feature_id): auto-commit session work" 2>/dev/null; then
248
268
  log_info "Auto-commit succeeded"
249
269
  session_status="success"
@@ -262,10 +282,10 @@ spawn_and_wait_session() {
262
282
  if [[ "$session_status" == "success" ]]; then
263
283
  if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
264
284
  local dirty_files=""
265
- dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
285
+ dirty_files=$(prizm_git_status_safe "$project_root" || true)
266
286
  if [[ -n "$dirty_files" ]]; then
267
287
  log_info "Auto-committing remaining session artifacts..."
268
- git -C "$project_root" add -A 2>/dev/null || true
288
+ prizm_git_add_all_safe "$project_root" || true
269
289
  git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
270
290
  || git -C "$project_root" commit --no-verify -m "chore($feature_id): include remaining session artifacts" 2>/dev/null \
271
291
  || true
@@ -385,8 +405,9 @@ finalize_feature_status_after_branch_return() {
385
405
  local session_id="$3"
386
406
  local session_status="$4"
387
407
  local max_retries="$5"
388
- local session_dir="$6"
389
- local base_branch="${7:-main}"
408
+ local max_infra_retries="$6"
409
+ local session_dir="$7"
410
+ local base_branch="${8:-main}"
390
411
 
391
412
  local feature_slug="${_SPAWN_FEATURE_SLUG:-}"
392
413
  local progress_json="$session_dir/logs/progress.json"
@@ -422,6 +443,7 @@ finalize_feature_status_after_branch_return() {
422
443
  --session-status "$session_status" \
423
444
  --session-id "$session_id" \
424
445
  --max-retries "$max_retries" \
446
+ --max-infra-retries "$max_infra_retries" \
425
447
  --action update 2>&1) || {
426
448
  log_error "Failed to update feature status: $update_output"
427
449
  log_error ".prizmkit/plans/feature-list.json may be out of sync. Manual intervention needed."
@@ -467,6 +489,8 @@ for feat in data.get('features', []):
467
489
  --state-dir "$STATE_DIR" \
468
490
  --feature-id "$_interrupted_id" \
469
491
  --session-status "failed" \
492
+ --max-retries "$MAX_RETRIES" \
493
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
470
494
  --action update 2>/dev/null || true
471
495
  log_info "Feature $_interrupted_id marked as failed due to interrupt"
472
496
  fi
@@ -592,6 +616,16 @@ run_one() {
592
616
  SESSION_TIMEOUT="$1"
593
617
  shift
594
618
  ;;
619
+ --max-infra-retries)
620
+ shift
621
+ if [[ $# -eq 0 ]]; then
622
+ log_error "--max-infra-retries requires a value"
623
+ exit 1
624
+ fi
625
+ prizm_require_positive_int "--max-infra-retries" "$1"
626
+ MAX_INFRA_RETRIES="$1"
627
+ shift
628
+ ;;
595
629
  F-*|f-*)
596
630
  feature_id="$1"
597
631
  shift
@@ -916,6 +950,8 @@ else:
916
950
  --state-dir "$STATE_DIR" \
917
951
  --feature-id "$feature_id" \
918
952
  --session-status "failed" \
953
+ --max-retries "$MAX_RETRIES" \
954
+ --max-infra-retries "$MAX_INFRA_RETRIES" \
919
955
  --action update 2>/dev/null || true
920
956
  log_info "Feature $feature_id marked as failed due to interrupt"
921
957
  fi
@@ -949,12 +985,13 @@ else:
949
985
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
950
986
  _DEV_BRANCH_NAME="$_branch_name"
951
987
  else
952
- log_warn "Failed to create branch; running session on current branch"
988
+ log_error "Failed to create branch: $_branch_name from $_source_branch — aborting to avoid running on $_source_branch"
989
+ return 1
953
990
  fi
954
991
 
955
992
  spawn_and_wait_session \
956
993
  "$feature_id" "$feature_list" "$session_id" \
957
- "$bootstrap_prompt" "$session_dir" 999 "$feature_model" "$_ORIGINAL_BRANCH"
994
+ "$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$feature_model" "$_ORIGINAL_BRANCH"
958
995
  local session_status="$_SPAWN_RESULT"
959
996
 
960
997
  # Merge dev branch back to original on success
@@ -976,12 +1013,11 @@ else:
976
1013
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
977
1014
 
978
1015
  finalize_feature_status_after_branch_return \
979
- "$feature_id" "$feature_list" "$session_id" "$session_status" 999 "$session_dir" "$_ORIGINAL_BRANCH"
1016
+ "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
980
1017
 
981
1018
  # Commit feature status update on the original branch (after guaranteed return)
982
1019
  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
1020
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): update feature status" "$feature_list"
985
1021
  fi
986
1022
 
987
1023
  echo ""
@@ -1080,7 +1116,8 @@ main() {
1080
1116
  if [[ -n "$features_filter" ]]; then
1081
1117
  log_info "Features filter: $features_filter"
1082
1118
  fi
1083
- log_info "Max retries per feature: $MAX_RETRIES"
1119
+ log_info "Max code retries per feature: $MAX_RETRIES"
1120
+ log_info "Max infrastructure retries per feature: $MAX_INFRA_RETRIES"
1084
1121
  if [[ $SESSION_TIMEOUT -gt 0 ]]; then
1085
1122
  log_info "Session timeout: ${SESSION_TIMEOUT}s"
1086
1123
  else
@@ -1259,28 +1296,41 @@ DEPLOY_PROMPT_EOF
1259
1296
  fi
1260
1297
 
1261
1298
  # Parse feature info
1262
- local feature_id feature_title retry_count resume_phase
1299
+ local feature_id feature_title retry_count infra_error_count resume_phase
1263
1300
  feature_id=$(echo "$next_feature" | jq -r '.feature_id')
1264
1301
  feature_title=$(echo "$next_feature" | jq -r '.title')
1265
1302
  retry_count=$(echo "$next_feature" | jq -r '.retry_count // 0')
1303
+ infra_error_count=$(echo "$next_feature" | jq -r '.infra_error_count // 0')
1266
1304
  resume_phase=$(echo "$next_feature" | jq -r '.resume_from_phase // "null"')
1267
1305
 
1268
1306
  echo ""
1269
1307
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1270
1308
  log_info "Feature: ${BOLD}$feature_id${NC} — $feature_title"
1271
- log_info "Retry: $retry_count / $MAX_RETRIES"
1309
+ log_info "Code retry: $retry_count / $MAX_RETRIES"
1310
+ log_info "Infrastructure retry: $infra_error_count / $MAX_INFRA_RETRIES"
1272
1311
  if [[ "$resume_phase" != "null" ]]; then
1273
1312
  log_info "Resuming from Phase $resume_phase"
1274
1313
  fi
1275
1314
  echo -e "${BOLD}────────────────────────────────────────────────────${NC}"
1276
1315
 
1277
- # Commit dirty working tree before starting feature
1316
+ # Do not auto-commit an arbitrary dirty baseline on the original branch.
1317
+ # Only pipeline bookkeeping is committed explicitly; user/source WIP,
1318
+ # submodule dirt, specs, and root-level hidden tool temporary worktrees
1319
+ # must never be swept into main by a broad git add.
1278
1320
  local _dirty_files=""
1279
- _dirty_files=$(git -C "$_proj_root" status --porcelain 2>/dev/null || true)
1321
+ _dirty_files=$(prizm_git_status_non_bookkeeping "$_proj_root" "$STATE_DIR" "$feature_list" || true)
1280
1322
  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
1323
+ log_error "Dirty working tree detected before $feature_id; refusing to commit it on $_ORIGINAL_BRANCH."
1324
+ log_error "Commit/stash your changes or start from a clean branch, then rerun the pipeline."
1325
+ printf '%s\n' "$_dirty_files" | sed 's/^/ /'
1326
+ break
1327
+ fi
1328
+
1329
+ local _bookkeeping_dirty=""
1330
+ _bookkeeping_dirty=$(prizm_git_status_bookkeeping "$_proj_root" "$STATE_DIR" "$feature_list" || true)
1331
+ if [[ -n "$_bookkeeping_dirty" ]]; then
1332
+ log_info "Committing pending pipeline bookkeeping before $feature_id..."
1333
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): include pipeline bookkeeping" "$feature_list" "$STATE_DIR"
1284
1334
  fi
1285
1335
 
1286
1336
  # Mark feature as in-progress BEFORE creating dev branch
@@ -1293,8 +1343,7 @@ DEPLOY_PROMPT_EOF
1293
1343
  --action start >/dev/null 2>&1 || true
1294
1344
  # Commit the in_progress status on the original branch
1295
1345
  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
1346
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): mark in_progress" "$feature_list"
1298
1347
  fi
1299
1348
 
1300
1349
  # Create per-feature dev branch (from the now-updated original branch)
@@ -1303,8 +1352,9 @@ DEPLOY_PROMPT_EOF
1303
1352
  _DEV_BRANCH_NAME="$_feature_branch"
1304
1353
  log_info "Dev branch: $_feature_branch"
1305
1354
  else
1306
- log_warn "Failed to create dev branch; running on current branch: $_ORIGINAL_BRANCH"
1355
+ log_error "Failed to create dev branch: $_feature_branch from $_ORIGINAL_BRANCH — aborting to avoid running on $_ORIGINAL_BRANCH"
1307
1356
  _DEV_BRANCH_NAME=""
1357
+ break
1308
1358
  fi
1309
1359
 
1310
1360
  # Generate session ID and bootstrap prompt
@@ -1397,20 +1447,20 @@ DEPLOY_PROMPT_EOF
1397
1447
  branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1398
1448
 
1399
1449
  finalize_feature_status_after_branch_return \
1400
- "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
1450
+ "$feature_id" "$feature_list" "$session_id" "$session_status" "$MAX_RETRIES" "$MAX_INFRA_RETRIES" "$session_dir" "$_ORIGINAL_BRANCH"
1401
1451
  local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
1402
1452
 
1403
1453
  # Commit feature status update on the original branch (after guaranteed return)
1404
1454
  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
1455
+ prizm_git_commit_paths "$_proj_root" "chore($feature_id): update feature status" "$feature_list"
1407
1456
  fi
1408
1457
 
1409
1458
  session_count=$((session_count + 1))
1410
1459
  total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
1411
1460
 
1412
1461
  # Stop-on-failure: abort only after the task is actually marked failed.
1413
- # Pending retry outcomes, including infrastructure errors, keep running.
1462
+ # Pending retry outcomes keep running; both code and infra failures have
1463
+ # bounded budgets in update-feature-status.py.
1414
1464
  if [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" && "$item_status_after_session" == "failed" ]]; then
1415
1465
  echo ""
1416
1466
  log_error "════════════════════════════════════════════════════"
@@ -1420,7 +1470,7 @@ DEPLOY_PROMPT_EOF
1420
1470
  log_error "════════════════════════════════════════════════════"
1421
1471
  break
1422
1472
  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."
1473
+ log_info "STOP_ON_FAILURE: $feature_id is ${item_status_after_session:-unknown}; code/infra retry budget not exhausted, continuing."
1424
1474
  fi
1425
1475
 
1426
1476
  # Brief pause before next iteration
@@ -1459,9 +1509,14 @@ show_help() {
1459
1509
  echo " --clean Delete artifacts and reset before running"
1460
1510
  echo " --no-reset Skip feature status reset step"
1461
1511
  echo " --timeout N Session timeout in seconds (default: 0 = no limit)"
1512
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1513
+ echo ""
1514
+ echo "Pipeline Options (run all):"
1515
+ echo " --max-infra-retries N Max infrastructure/provider retries (default: 3)"
1462
1516
  echo ""
1463
1517
  echo "Environment Variables:"
1464
- echo " MAX_RETRIES Max retries per feature (default: 3)"
1518
+ echo " MAX_RETRIES Max code retries per feature (default: 3)"
1519
+ echo " MAX_INFRA_RETRIES Max infrastructure/provider retries per feature (default: 3)"
1465
1520
  echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
1466
1521
  echo " AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)"
1467
1522
  echo " MODEL AI model ID (e.g. claude-opus-4.6, claude-sonnet-4.6, claude-haiku-4.5)"
@@ -1512,6 +1567,16 @@ case "${1:-run}" in
1512
1567
  _run_features_filter="$1"
1513
1568
  shift
1514
1569
  ;;
1570
+ --max-infra-retries)
1571
+ shift
1572
+ if [[ $# -eq 0 ]]; then
1573
+ log_error "--max-infra-retries requires a value"
1574
+ exit 1
1575
+ fi
1576
+ prizm_require_positive_int "--max-infra-retries" "$1"
1577
+ MAX_INFRA_RETRIES="$1"
1578
+ shift
1579
+ ;;
1515
1580
  *)
1516
1581
  _run_feature_list="$1"
1517
1582
  shift