prizmkit 1.1.20 → 1.1.23

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.
@@ -254,8 +254,15 @@ if incomplete:
254
254
  sys.exit(1)
255
255
  print('ALL_COMPLETE')
256
256
  sys.exit(0)
257
- " "$checkpoint_file" 2>&1)
257
+ " "$checkpoint_file" 2>&1) || checkpoint_result="CHECK_FAILED"
258
258
  local check_exit=$?
259
+ if [[ "$checkpoint_result" == "CHECK_FAILED" ]]; then
260
+ check_exit=2
261
+ elif [[ "$checkpoint_result" == *"INCOMPLETE"* ]]; then
262
+ check_exit=1
263
+ else
264
+ check_exit=0
265
+ fi
259
266
  if [[ $check_exit -eq 2 ]]; then
260
267
  log_warn "CHECKPOINT_CORRUPTED: workflow-checkpoint.json is not valid JSON"
261
268
  elif [[ $check_exit -eq 1 ]]; then
@@ -270,7 +277,7 @@ sys.exit(0)
270
277
  # Subagent detection
271
278
  prizm_detect_subagents "$session_log"
272
279
 
273
- # Update bug status
280
+ # Update bug status (do NOT commit on dev branch — commit happens after merge)
274
281
  python3 "$SCRIPTS_DIR/update-bug-status.py" \
275
282
  --bug-list "$bug_list" \
276
283
  --state-dir "$STATE_DIR" \
@@ -280,12 +287,6 @@ sys.exit(0)
280
287
  --max-retries "$max_retries" \
281
288
  --action update >/dev/null 2>&1 || true
282
289
 
283
- # Commit .prizmkit/plans/bug-fix-list.json status update (pipeline management commit)
284
- if ! git -C "$project_root" diff --quiet "$bug_list" 2>/dev/null; then
285
- git -C "$project_root" add "$bug_list"
286
- git -C "$project_root" commit --no-verify -m "chore($bug_id): update bug status" 2>/dev/null || true
287
- fi
288
-
289
290
  _SPAWN_RESULT="$session_status"
290
291
  }
291
292
 
@@ -306,13 +307,40 @@ cleanup() {
306
307
  log_info "Original branch was: $_ORIGINAL_BRANCH"
307
308
  fi
308
309
 
310
+ # Update status of currently in-progress bug to interrupted
309
311
  if [[ -n "$BUG_LIST" && -f "$BUG_LIST" ]]; then
312
+ # Find any in-progress bug and mark it as failed
313
+ local _interrupted_id
314
+ _interrupted_id=$(python3 -c "
315
+ import json, sys
316
+ with open(sys.argv[1]) as f:
317
+ data = json.load(f)
318
+ for bug in data.get('bugs', []):
319
+ if bug.get('status') == 'in_progress':
320
+ print(bug['id'])
321
+ break
322
+ " "$BUG_LIST" 2>/dev/null || echo "")
323
+
324
+ if [[ -n "$_interrupted_id" ]]; then
325
+ python3 "$SCRIPTS_DIR/update-bug-status.py" \
326
+ --bug-list "$BUG_LIST" \
327
+ --state-dir "$STATE_DIR" \
328
+ --bug-id "$_interrupted_id" \
329
+ --session-status "failed" \
330
+ --action update 2>/dev/null || true
331
+ log_info "Bug $_interrupted_id marked as failed due to interrupt"
332
+ fi
333
+
334
+ # Pause the pipeline
310
335
  python3 "$SCRIPTS_DIR/update-bug-status.py" \
311
336
  --bug-list "$BUG_LIST" \
312
337
  --state-dir "$STATE_DIR" \
313
338
  --action pause 2>/dev/null || true
314
339
  fi
315
340
 
341
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
342
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
343
+
316
344
  log_info "Bug fix pipeline paused. Run './run-bugfix.sh run' to resume."
317
345
  exit 130
318
346
  }
@@ -639,6 +667,20 @@ else:
639
667
  log_info "Development was on branch: $_DEV_BRANCH_NAME"
640
668
  fi
641
669
  log_info "Session log: $session_dir/logs/session.log"
670
+
671
+ # Update bug status to failed on interrupt
672
+ if [[ -n "$bug_list" && -f "$bug_list" ]]; then
673
+ python3 "$SCRIPTS_DIR/update-bug-status.py" \
674
+ --bug-list "$bug_list" \
675
+ --state-dir "$STATE_DIR" \
676
+ --bug-id "$bug_id" \
677
+ --session-status "failed" \
678
+ --action update 2>/dev/null || true
679
+ log_info "Bug $bug_id marked as failed due to interrupt"
680
+ fi
681
+
682
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
683
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
642
684
  exit 130
643
685
  }
644
686
  trap cleanup_single_bug SIGINT SIGTERM
@@ -652,6 +694,20 @@ else:
652
694
  _source_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
653
695
  _ORIGINAL_BRANCH="$_source_branch"
654
696
 
697
+ # Mark bug as in-progress BEFORE creating dev branch
698
+ # This ensures the in_progress status commit lands on the original branch,
699
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
700
+ python3 "$SCRIPTS_DIR/update-bug-status.py" \
701
+ --bug-list "$bug_list" \
702
+ --state-dir "$STATE_DIR" \
703
+ --bug-id "$bug_id" \
704
+ --action start >/dev/null 2>&1 || true
705
+ # Commit the in_progress status on the original branch
706
+ if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
707
+ git -C "$_proj_root" add "$bug_list" 2>/dev/null || true
708
+ git -C "$_proj_root" commit --no-verify -m "chore($bug_id): mark in_progress" 2>/dev/null || true
709
+ fi
710
+
655
711
  local _branch_name="${DEV_BRANCH:-bugfix/${bug_id}-$(date +%s)}"
656
712
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
657
713
  _DEV_BRANCH_NAME="$_branch_name"
@@ -671,18 +727,23 @@ else:
671
727
  else
672
728
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
673
729
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
674
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
675
730
  _DEV_BRANCH_NAME=""
676
731
  fi
677
732
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
678
- # Session failed — return to original branch, preserve dev branch for inspection
679
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
680
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
681
- fi
733
+ # Session failed — preserve dev branch for inspection
682
734
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
683
735
  _DEV_BRANCH_NAME=""
684
736
  fi
685
737
 
738
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
739
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
740
+
741
+ # Commit bug status update on the original branch (after guaranteed return)
742
+ if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
743
+ git -C "$_proj_root" add "$bug_list"
744
+ git -C "$_proj_root" commit --no-verify -m "chore($bug_id): update bug status" 2>/dev/null || true
745
+ fi
746
+
686
747
  echo ""
687
748
  if [[ "$session_status" == "success" ]]; then
688
749
  log_success "════════════════════════════════════════════════════"
@@ -785,6 +846,18 @@ main() {
785
846
  local total_subagent_calls=0
786
847
 
787
848
  while true; do
849
+ # Safety net: ensure we're on the original branch at the start of each iteration.
850
+ # If a previous iteration's `continue` skipped branch_ensure_return, we could
851
+ # still be on a dev branch. This prevents cascading branch confusion.
852
+ if [[ -n "$_ORIGINAL_BRANCH" ]]; then
853
+ local _cur_branch
854
+ _cur_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
855
+ if [[ -n "$_cur_branch" && "$_cur_branch" != "$_ORIGINAL_BRANCH" ]]; then
856
+ log_warn "Still on branch $_cur_branch at loop start — returning to $_ORIGINAL_BRANCH"
857
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
858
+ fi
859
+ fi
860
+
788
861
  # Find next bug to process
789
862
  local next_bug
790
863
  if ! next_bug=$(python3 "$SCRIPTS_DIR/update-bug-status.py" \
@@ -839,7 +912,21 @@ main() {
839
912
  git -C "$_proj_root" commit --no-verify -m "chore: capture artifacts before $bug_id session" 2>/dev/null || true
840
913
  fi
841
914
 
842
- # Create per-bug dev branch
915
+ # Mark bug as in-progress BEFORE creating dev branch
916
+ # This ensures the in_progress status commit lands on the original branch,
917
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
918
+ python3 "$SCRIPTS_DIR/update-bug-status.py" \
919
+ --bug-list "$bug_list" \
920
+ --state-dir "$STATE_DIR" \
921
+ --bug-id "$bug_id" \
922
+ --action start >/dev/null 2>&1 || true
923
+ # Commit the in_progress status on the original branch
924
+ if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
925
+ git -C "$_proj_root" add "$bug_list" 2>/dev/null || true
926
+ git -C "$_proj_root" commit --no-verify -m "chore($bug_id): mark in_progress" 2>/dev/null || true
927
+ fi
928
+
929
+ # Create per-bug dev branch (from the now-updated original branch)
843
930
  local _bug_branch="${DEV_BRANCH:-bugfix/${bug_id}-$(date +%Y%m%d%H%M)}"
844
931
  if branch_create "$_proj_root" "$_bug_branch" "$_ORIGINAL_BRANCH"; then
845
932
  _DEV_BRANCH_NAME="$_bug_branch"
@@ -885,6 +972,8 @@ main() {
885
972
  local gen_output
886
973
  gen_output=$(python3 "$SCRIPTS_DIR/generate-bugfix-prompt.py" "${main_prompt_args[@]}" 2>/dev/null) || {
887
974
  log_error "Failed to generate bootstrap prompt for $bug_id"
975
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
976
+ _DEV_BRANCH_NAME=""
888
977
  continue
889
978
  }
890
979
  local bug_model pipeline_mode agent_count critic_enabled
@@ -907,13 +996,6 @@ main() {
907
996
  log_info "Bug model: $bug_model"
908
997
  fi
909
998
 
910
- # Mark bug as in-progress before spawning session
911
- python3 "$SCRIPTS_DIR/update-bug-status.py" \
912
- --bug-list "$bug_list" \
913
- --state-dir "$STATE_DIR" \
914
- --bug-id "$bug_id" \
915
- --action start >/dev/null 2>&1 || true
916
-
917
999
  # Spawn session
918
1000
  log_info "Spawning AI CLI session: $session_id"
919
1001
  _SPAWN_RESULT=""
@@ -931,18 +1013,23 @@ main() {
931
1013
  else
932
1014
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
933
1015
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
934
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
935
1016
  _DEV_BRANCH_NAME=""
936
1017
  fi
937
1018
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
938
- # Session failed — return to original branch, preserve dev branch for inspection
939
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
940
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
941
- fi
1019
+ # Session failed — preserve dev branch for inspection
942
1020
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
943
1021
  _DEV_BRANCH_NAME=""
944
1022
  fi
945
1023
 
1024
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
1025
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1026
+
1027
+ # Commit bug status update on the original branch (after guaranteed return)
1028
+ if ! git -C "$_proj_root" diff --quiet "$bug_list" 2>/dev/null; then
1029
+ git -C "$_proj_root" add "$bug_list"
1030
+ git -C "$_proj_root" commit --no-verify -m "chore($bug_id): update bug status" 2>/dev/null || true
1031
+ fi
1032
+
946
1033
  session_count=$((session_count + 1))
947
1034
  total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
948
1035
 
@@ -308,8 +308,15 @@ if incomplete:
308
308
  sys.exit(1)
309
309
  print('ALL_COMPLETE')
310
310
  sys.exit(0)
311
- " "$checkpoint_file" 2>&1)
311
+ " "$checkpoint_file" 2>&1) || checkpoint_result="CHECK_FAILED"
312
312
  local check_exit=$?
313
+ if [[ "$checkpoint_result" == "CHECK_FAILED" ]]; then
314
+ check_exit=2
315
+ elif [[ "$checkpoint_result" == *"INCOMPLETE"* ]]; then
316
+ check_exit=1
317
+ else
318
+ check_exit=0
319
+ fi
313
320
  if [[ $check_exit -eq 2 ]]; then
314
321
  log_warn "CHECKPOINT_CORRUPTED: workflow-checkpoint.json is not valid JSON"
315
322
  elif [[ $check_exit -eq 1 ]]; then
@@ -352,7 +359,7 @@ sys.exit(0)
352
359
  fi
353
360
  fi
354
361
 
355
- # Update feature status
362
+ # Update feature status (do NOT commit on dev branch — commit happens after merge)
356
363
  local update_output
357
364
  update_output=$(python3 "$SCRIPTS_DIR/update-feature-status.py" \
358
365
  --feature-list "$feature_list" \
@@ -366,12 +373,6 @@ sys.exit(0)
366
373
  log_error ".prizmkit/plans/feature-list.json may be out of sync. Manual intervention needed."
367
374
  }
368
375
 
369
- # Commit feature status update (pipeline management commit)
370
- if ! git -C "$project_root" diff --quiet "$feature_list" 2>/dev/null; then
371
- git -C "$project_root" add "$feature_list"
372
- git -C "$project_root" commit --no-verify -m "chore($feature_id): update feature status" 2>/dev/null || true
373
- fi
374
-
375
376
  # Return status via global variable (avoids $() swallowing stdout)
376
377
  _SPAWN_RESULT="$session_status"
377
378
  }
@@ -393,13 +394,40 @@ cleanup() {
393
394
  log_info "Original branch was: $_ORIGINAL_BRANCH"
394
395
  fi
395
396
 
397
+ # Update status of currently in-progress feature to interrupted
396
398
  if [[ -n "$FEATURE_LIST" && -f "$FEATURE_LIST" ]]; then
399
+ # Find any in-progress feature and mark it as interrupted
400
+ local _interrupted_id
401
+ _interrupted_id=$(python3 -c "
402
+ import json, sys
403
+ with open(sys.argv[1]) as f:
404
+ data = json.load(f)
405
+ for feat in data.get('features', []):
406
+ if feat.get('status') == 'in_progress':
407
+ print(feat['id'])
408
+ break
409
+ " "$FEATURE_LIST" 2>/dev/null || echo "")
410
+
411
+ if [[ -n "$_interrupted_id" ]]; then
412
+ python3 "$SCRIPTS_DIR/update-feature-status.py" \
413
+ --feature-list "$FEATURE_LIST" \
414
+ --state-dir "$STATE_DIR" \
415
+ --feature-id "$_interrupted_id" \
416
+ --session-status "failed" \
417
+ --action update 2>/dev/null || true
418
+ log_info "Feature $_interrupted_id marked as failed due to interrupt"
419
+ fi
420
+
421
+ # Pause the pipeline (mark remaining pending items)
397
422
  python3 "$SCRIPTS_DIR/update-feature-status.py" \
398
423
  --feature-list "$FEATURE_LIST" \
399
424
  --state-dir "$STATE_DIR" \
400
425
  --action pause 2>/dev/null || true
401
426
  fi
402
427
 
428
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
429
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
430
+
403
431
  log_info "Pipeline paused. Run './run-feature.sh run' to resume."
404
432
  exit 130
405
433
  }
@@ -814,6 +842,20 @@ else:
814
842
  log_info "Development was on branch: $_DEV_BRANCH_NAME"
815
843
  fi
816
844
  log_info "Session log: $session_dir/logs/session.log"
845
+
846
+ # Update feature status to failed on interrupt
847
+ if [[ -n "$feature_list" && -f "$feature_list" ]]; then
848
+ python3 "$SCRIPTS_DIR/update-feature-status.py" \
849
+ --feature-list "$feature_list" \
850
+ --state-dir "$STATE_DIR" \
851
+ --feature-id "$feature_id" \
852
+ --session-status "failed" \
853
+ --action update 2>/dev/null || true
854
+ log_info "Feature $feature_id marked as failed due to interrupt"
855
+ fi
856
+
857
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
858
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
817
859
  exit 130
818
860
  }
819
861
  trap cleanup_single_feature SIGINT SIGTERM
@@ -827,6 +869,20 @@ else:
827
869
  _source_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
828
870
  _ORIGINAL_BRANCH="$_source_branch"
829
871
 
872
+ # Mark feature as in-progress BEFORE creating dev branch
873
+ # This ensures the in_progress status commit lands on the original branch,
874
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
875
+ python3 "$SCRIPTS_DIR/update-feature-status.py" \
876
+ --feature-list "$feature_list" \
877
+ --state-dir "$STATE_DIR" \
878
+ --feature-id "$feature_id" \
879
+ --action start >/dev/null 2>&1 || true
880
+ # Commit the in_progress status on the original branch
881
+ if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
882
+ git -C "$_proj_root" add "$feature_list" 2>/dev/null || true
883
+ git -C "$_proj_root" commit --no-verify -m "chore($feature_id): mark in_progress" 2>/dev/null || true
884
+ fi
885
+
830
886
  local _branch_name="${DEV_BRANCH:-dev/${feature_id}-$(date +%Y%m%d%H%M)}"
831
887
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
832
888
  _DEV_BRANCH_NAME="$_branch_name"
@@ -846,18 +902,23 @@ else:
846
902
  else
847
903
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
848
904
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
849
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
850
905
  _DEV_BRANCH_NAME=""
851
906
  fi
852
907
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
853
- # Session failed — return to original branch, preserve dev branch for inspection
854
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
855
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
856
- fi
908
+ # Session failed — preserve dev branch for inspection
857
909
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
858
910
  _DEV_BRANCH_NAME=""
859
911
  fi
860
912
 
913
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
914
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
915
+
916
+ # Commit feature status update on the original branch (after guaranteed return)
917
+ if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
918
+ git -C "$_proj_root" add "$feature_list"
919
+ git -C "$_proj_root" commit --no-verify -m "chore($feature_id): update feature status" 2>/dev/null || true
920
+ fi
921
+
861
922
  echo ""
862
923
  if [[ "$session_status" == "success" ]]; then
863
924
  log_success "════════════════════════════════════════════════════"
@@ -980,6 +1041,18 @@ main() {
980
1041
  local total_subagent_calls=0
981
1042
 
982
1043
  while true; do
1044
+ # Safety net: ensure we're on the original branch at the start of each iteration.
1045
+ # If a previous iteration's `continue` skipped branch_ensure_return, we could
1046
+ # still be on a dev branch. This prevents cascading branch confusion.
1047
+ if [[ -n "$_ORIGINAL_BRANCH" ]]; then
1048
+ local _cur_branch
1049
+ _cur_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
1050
+ if [[ -n "$_cur_branch" && "$_cur_branch" != "$_ORIGINAL_BRANCH" ]]; then
1051
+ log_warn "Still on branch $_cur_branch at loop start — returning to $_ORIGINAL_BRANCH"
1052
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1053
+ fi
1054
+ fi
1055
+
983
1056
  # Check for stuck features
984
1057
  local stuck_result
985
1058
  stuck_result=$(python3 "$SCRIPTS_DIR/detect-stuck.py" \
@@ -1081,7 +1154,21 @@ print(count)
1081
1154
  git -C "$_proj_root" commit --no-verify -m "ready for run $feature_id" 2>/dev/null || true
1082
1155
  fi
1083
1156
 
1084
- # Create per-feature dev branch
1157
+ # Mark feature as in-progress BEFORE creating dev branch
1158
+ # This ensures the in_progress status commit lands on the original branch,
1159
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
1160
+ python3 "$SCRIPTS_DIR/update-feature-status.py" \
1161
+ --feature-list "$feature_list" \
1162
+ --state-dir "$STATE_DIR" \
1163
+ --feature-id "$feature_id" \
1164
+ --action start >/dev/null 2>&1 || true
1165
+ # Commit the in_progress status on the original branch
1166
+ if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
1167
+ git -C "$_proj_root" add "$feature_list" 2>/dev/null || true
1168
+ git -C "$_proj_root" commit --no-verify -m "chore($feature_id): mark in_progress" 2>/dev/null || true
1169
+ fi
1170
+
1171
+ # Create per-feature dev branch (from the now-updated original branch)
1085
1172
  local _feature_branch="${DEV_BRANCH:-dev/${feature_id}-$(date +%Y%m%d%H%M)}"
1086
1173
  if branch_create "$_proj_root" "$_feature_branch" "$_ORIGINAL_BRANCH"; then
1087
1174
  _DEV_BRANCH_NAME="$_feature_branch"
@@ -1127,6 +1214,8 @@ print(count)
1127
1214
  local gen_output
1128
1215
  gen_output=$(python3 "$SCRIPTS_DIR/generate-bootstrap-prompt.py" "${main_prompt_args[@]}" 2>/dev/null) || {
1129
1216
  log_error "Failed to generate bootstrap prompt for $feature_id"
1217
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1218
+ _DEV_BRANCH_NAME=""
1130
1219
  continue
1131
1220
  }
1132
1221
  local feature_model pipeline_mode agent_count critic_enabled
@@ -1146,13 +1235,6 @@ print(count)
1146
1235
  log_info "Pipeline mode: ${BOLD}$pipeline_mode${NC} ($_mode_desc)"
1147
1236
  log_info "Agents: $agent_count (critic: $([ "$critic_enabled" = "true" ] && echo "enabled" || echo "disabled"))"
1148
1237
 
1149
- # Mark feature as in-progress before spawning session
1150
- python3 "$SCRIPTS_DIR/update-feature-status.py" \
1151
- --feature-list "$feature_list" \
1152
- --state-dir "$STATE_DIR" \
1153
- --feature-id "$feature_id" \
1154
- --action start >/dev/null 2>&1 || true
1155
-
1156
1238
  # Spawn session and wait
1157
1239
  prizm_log_bootstrap_prompt "$bootstrap_prompt" "$feature_id"
1158
1240
  log_info "Spawning AI CLI session: $session_id"
@@ -1173,19 +1255,23 @@ print(count)
1173
1255
  else
1174
1256
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
1175
1257
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
1176
- # Return to original branch; state/ files are untracked and persist across checkout
1177
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
1178
1258
  _DEV_BRANCH_NAME=""
1179
1259
  fi
1180
1260
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
1181
- # Session failed — return to original branch, preserve dev branch for inspection
1182
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
1183
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
1184
- fi
1261
+ # Session failed — preserve dev branch for inspection
1185
1262
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
1186
1263
  _DEV_BRANCH_NAME=""
1187
1264
  fi
1188
1265
 
1266
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
1267
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1268
+
1269
+ # Commit feature status update on the original branch (after guaranteed return)
1270
+ if ! git -C "$_proj_root" diff --quiet "$feature_list" 2>/dev/null; then
1271
+ git -C "$_proj_root" add "$feature_list"
1272
+ git -C "$_proj_root" commit --no-verify -m "chore($feature_id): update feature status" 2>/dev/null || true
1273
+ fi
1274
+
1189
1275
  session_count=$((session_count + 1))
1190
1276
  total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
1191
1277