prizmkit 1.1.19 → 1.1.21

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.
@@ -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
 
@@ -262,8 +262,15 @@ if incomplete:
262
262
  sys.exit(1)
263
263
  print('ALL_COMPLETE')
264
264
  sys.exit(0)
265
- " "$checkpoint_file" 2>&1)
265
+ " "$checkpoint_file" 2>&1) || checkpoint_result="CHECK_FAILED"
266
266
  local check_exit=$?
267
+ if [[ "$checkpoint_result" == "CHECK_FAILED" ]]; then
268
+ check_exit=2
269
+ elif [[ "$checkpoint_result" == *"INCOMPLETE"* ]]; then
270
+ check_exit=1
271
+ else
272
+ check_exit=0
273
+ fi
267
274
  if [[ $check_exit -eq 2 ]]; then
268
275
  log_warn "CHECKPOINT_CORRUPTED: workflow-checkpoint.json is not valid JSON"
269
276
  elif [[ $check_exit -eq 1 ]]; then
@@ -297,7 +304,7 @@ sys.exit(0)
297
304
  fi
298
305
  fi
299
306
 
300
- # Update refactor status
307
+ # Update refactor status (do NOT commit on dev branch — commit happens after merge)
301
308
  python3 "$SCRIPTS_DIR/update-refactor-status.py" \
302
309
  --refactor-list "$refactor_list" \
303
310
  --state-dir "$STATE_DIR" \
@@ -307,12 +314,6 @@ sys.exit(0)
307
314
  --max-retries "$max_retries" \
308
315
  --action update >/dev/null 2>&1 || true
309
316
 
310
- # Commit .prizmkit/plans/refactor-list.json status update (pipeline management commit)
311
- if ! git -C "$project_root" diff --quiet "$refactor_list" 2>/dev/null; then
312
- git -C "$project_root" add "$refactor_list"
313
- git -C "$project_root" commit --no-verify -m "chore($refactor_id): update refactor status" 2>/dev/null || true
314
- fi
315
-
316
317
  _SPAWN_RESULT="$session_status"
317
318
  }
318
319
 
@@ -333,13 +334,40 @@ cleanup() {
333
334
  log_info "Original branch was: $_ORIGINAL_BRANCH"
334
335
  fi
335
336
 
337
+ # Update status of currently in-progress refactor to interrupted
336
338
  if [[ -n "$REFACTOR_LIST" && -f "$REFACTOR_LIST" ]]; then
339
+ # Find any in-progress refactor and mark it as failed
340
+ local _interrupted_id
341
+ _interrupted_id=$(python3 -c "
342
+ import json, sys
343
+ with open(sys.argv[1]) as f:
344
+ data = json.load(f)
345
+ for item in data.get('refactors', []):
346
+ if item.get('status') == 'in_progress':
347
+ print(item['id'])
348
+ break
349
+ " "$REFACTOR_LIST" 2>/dev/null || echo "")
350
+
351
+ if [[ -n "$_interrupted_id" ]]; then
352
+ python3 "$SCRIPTS_DIR/update-refactor-status.py" \
353
+ --refactor-list "$REFACTOR_LIST" \
354
+ --state-dir "$STATE_DIR" \
355
+ --refactor-id "$_interrupted_id" \
356
+ --session-status "failed" \
357
+ --action update 2>/dev/null || true
358
+ log_info "Refactor $_interrupted_id marked as failed due to interrupt"
359
+ fi
360
+
361
+ # Pause the pipeline
337
362
  python3 "$SCRIPTS_DIR/update-refactor-status.py" \
338
363
  --refactor-list "$REFACTOR_LIST" \
339
364
  --state-dir "$STATE_DIR" \
340
365
  --action pause 2>/dev/null || true
341
366
  fi
342
367
 
368
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
369
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
370
+
343
371
  log_info "Refactor pipeline paused. Run './run-refactor.sh run' to resume."
344
372
  exit 130
345
373
  }
@@ -669,6 +697,20 @@ else:
669
697
  log_info "Development was on branch: $_DEV_BRANCH_NAME"
670
698
  fi
671
699
  log_info "Session log: $session_dir/logs/session.log"
700
+
701
+ # Update refactor status to failed on interrupt
702
+ if [[ -n "$refactor_list" && -f "$refactor_list" ]]; then
703
+ python3 "$SCRIPTS_DIR/update-refactor-status.py" \
704
+ --refactor-list "$refactor_list" \
705
+ --state-dir "$STATE_DIR" \
706
+ --refactor-id "$refactor_id" \
707
+ --session-status "failed" \
708
+ --action update 2>/dev/null || true
709
+ log_info "Refactor $refactor_id marked as failed due to interrupt"
710
+ fi
711
+
712
+ # GUARANTEED: always return to original branch (save WIP on dev branch first)
713
+ branch_ensure_return "$(cd "$SCRIPT_DIR/.." && pwd)" "$_ORIGINAL_BRANCH" "$_DEV_BRANCH_NAME"
672
714
  exit 130
673
715
  }
674
716
  trap cleanup_single_refactor SIGINT SIGTERM
@@ -682,6 +724,20 @@ else:
682
724
  _source_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
683
725
  _ORIGINAL_BRANCH="$_source_branch"
684
726
 
727
+ # Mark refactor as in-progress BEFORE creating dev branch
728
+ # This ensures the in_progress status commit lands on the original branch,
729
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
730
+ python3 "$SCRIPTS_DIR/update-refactor-status.py" \
731
+ --refactor-list "$refactor_list" \
732
+ --state-dir "$STATE_DIR" \
733
+ --refactor-id "$refactor_id" \
734
+ --action start >/dev/null 2>&1 || true
735
+ # Commit the in_progress status on the original branch
736
+ if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
737
+ git -C "$_proj_root" add "$refactor_list" 2>/dev/null || true
738
+ git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): mark in_progress" 2>/dev/null || true
739
+ fi
740
+
685
741
  local _branch_name="${DEV_BRANCH:-refactor/${refactor_id}-$(date +%s)}"
686
742
  if branch_create "$_proj_root" "$_branch_name" "$_source_branch"; then
687
743
  _DEV_BRANCH_NAME="$_branch_name"
@@ -701,18 +757,23 @@ else:
701
757
  else
702
758
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
703
759
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
704
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
705
760
  _DEV_BRANCH_NAME=""
706
761
  fi
707
762
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
708
- # Session failed — return to original branch, preserve dev branch for inspection
709
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
710
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
711
- fi
763
+ # Session failed — preserve dev branch for inspection
712
764
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
713
765
  _DEV_BRANCH_NAME=""
714
766
  fi
715
767
 
768
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
769
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
770
+
771
+ # Commit refactor status update on the original branch (after guaranteed return)
772
+ if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
773
+ git -C "$_proj_root" add "$refactor_list"
774
+ git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): update refactor status" 2>/dev/null || true
775
+ fi
776
+
716
777
  echo ""
717
778
  if [[ "$session_status" == "success" ]]; then
718
779
  log_success "════════════════════════════════════════════════════"
@@ -820,6 +881,18 @@ main() {
820
881
  local total_subagent_calls=0
821
882
 
822
883
  while true; do
884
+ # Safety net: ensure we're on the original branch at the start of each iteration.
885
+ # If a previous iteration's `continue` skipped branch_ensure_return, we could
886
+ # still be on a dev branch. This prevents cascading branch confusion.
887
+ if [[ -n "$_ORIGINAL_BRANCH" ]]; then
888
+ local _cur_branch
889
+ _cur_branch=$(git -C "$_proj_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
890
+ if [[ -n "$_cur_branch" && "$_cur_branch" != "$_ORIGINAL_BRANCH" ]]; then
891
+ log_warn "Still on branch $_cur_branch at loop start — returning to $_ORIGINAL_BRANCH"
892
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
893
+ fi
894
+ fi
895
+
823
896
  # Find next refactor to process (dependency-topological order)
824
897
  local next_refactor
825
898
  if ! next_refactor=$(python3 "$SCRIPTS_DIR/update-refactor-status.py" \
@@ -874,7 +947,21 @@ main() {
874
947
  git -C "$_proj_root" commit --no-verify -m "chore: capture artifacts before $refactor_id session" 2>/dev/null || true
875
948
  fi
876
949
 
877
- # Create per-refactor dev branch
950
+ # Mark refactor as in-progress BEFORE creating dev branch
951
+ # This ensures the in_progress status commit lands on the original branch,
952
+ # not the dev branch — preventing rebase conflicts in branch_merge later.
953
+ python3 "$SCRIPTS_DIR/update-refactor-status.py" \
954
+ --refactor-list "$refactor_list" \
955
+ --state-dir "$STATE_DIR" \
956
+ --refactor-id "$refactor_id" \
957
+ --action start >/dev/null 2>&1 || true
958
+ # Commit the in_progress status on the original branch
959
+ if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
960
+ git -C "$_proj_root" add "$refactor_list" 2>/dev/null || true
961
+ git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): mark in_progress" 2>/dev/null || true
962
+ fi
963
+
964
+ # Create per-refactor dev branch (from the now-updated original branch)
878
965
  local _refactor_branch="${DEV_BRANCH:-refactor/${refactor_id}-$(date +%Y%m%d%H%M)}"
879
966
  if branch_create "$_proj_root" "$_refactor_branch" "$_ORIGINAL_BRANCH"; then
880
967
  _DEV_BRANCH_NAME="$_refactor_branch"
@@ -920,6 +1007,8 @@ main() {
920
1007
  local gen_output
921
1008
  gen_output=$(python3 "$SCRIPTS_DIR/generate-refactor-prompt.py" "${main_prompt_args[@]}" 2>/dev/null) || {
922
1009
  log_error "Failed to generate bootstrap prompt for $refactor_id"
1010
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1011
+ _DEV_BRANCH_NAME=""
923
1012
  continue
924
1013
  }
925
1014
  local refactor_model pipeline_mode agent_count critic_enabled
@@ -943,13 +1032,6 @@ main() {
943
1032
  log_info "Refactor model: $refactor_model"
944
1033
  fi
945
1034
 
946
- # Mark refactor as in-progress before spawning session
947
- python3 "$SCRIPTS_DIR/update-refactor-status.py" \
948
- --refactor-list "$refactor_list" \
949
- --state-dir "$STATE_DIR" \
950
- --refactor-id "$refactor_id" \
951
- --action start >/dev/null 2>&1 || true
952
-
953
1035
  # Spawn session
954
1036
  log_info "Spawning AI CLI session: $session_id"
955
1037
  _SPAWN_RESULT=""
@@ -979,18 +1061,23 @@ main() {
979
1061
  else
980
1062
  log_warn "Auto-merge failed — dev branch preserved: $_DEV_BRANCH_NAME"
981
1063
  log_warn "Merge manually: git checkout $_ORIGINAL_BRANCH && git rebase $_DEV_BRANCH_NAME"
982
- git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null || true
983
1064
  _DEV_BRANCH_NAME=""
984
1065
  fi
985
1066
  elif [[ -n "$_DEV_BRANCH_NAME" ]]; then
986
- # Session failed — return to original branch, preserve dev branch for inspection
987
- if ! git -C "$_proj_root" checkout "$_ORIGINAL_BRANCH" 2>/dev/null; then
988
- log_warn "Failed to checkout $_ORIGINAL_BRANCH after session failure — staying on dev branch"
989
- fi
1067
+ # Session failed — preserve dev branch for inspection
990
1068
  log_warn "Session failed — dev branch preserved for inspection: $_DEV_BRANCH_NAME"
991
1069
  _DEV_BRANCH_NAME=""
992
1070
  fi
993
1071
 
1072
+ # GUARANTEED: always return to original branch regardless of success/failure/merge outcome
1073
+ branch_ensure_return "$_proj_root" "$_ORIGINAL_BRANCH"
1074
+
1075
+ # Commit refactor status update on the original branch (after guaranteed return)
1076
+ if ! git -C "$_proj_root" diff --quiet "$refactor_list" 2>/dev/null; then
1077
+ git -C "$_proj_root" add "$refactor_list"
1078
+ git -C "$_proj_root" commit --no-verify -m "chore($refactor_id): update refactor status" 2>/dev/null || true
1079
+ fi
1080
+
994
1081
  # Stuck detection
995
1082
  if python3 "$SCRIPTS_DIR/detect-stuck.py" \
996
1083
  --state-dir "$STATE_DIR" \
@@ -1314,6 +1314,36 @@ def build_replacements(args, feature, features, global_context, script_dir):
1314
1314
  if isinstance(testing_config, dict):
1315
1315
  coverage_target = str(testing_config.get("coverage_target", 80))
1316
1316
 
1317
+ # Detect dev server port from package.json
1318
+ dev_port = "3000" # Default fallback
1319
+ try:
1320
+ pkg_path = os.path.join(project_root, "package.json")
1321
+ if os.path.isfile(pkg_path):
1322
+ with open(pkg_path, "r", encoding="utf-8") as f:
1323
+ pkg = json.load(f)
1324
+ dev_script = pkg.get("scripts", {}).get("dev", "")
1325
+ # Extract -p <port> from dev script
1326
+ port_match = re.search(r"-p\s+(\d+)", dev_script)
1327
+ if port_match:
1328
+ dev_port = port_match.group(1)
1329
+ else:
1330
+ # Fallback: try NEXT_PUBLIC_SITE_URL from .env files
1331
+ for env_file in [".env.local", ".env"]:
1332
+ env_path = os.path.join(project_root, env_file)
1333
+ if os.path.isfile(env_path):
1334
+ with open(env_path, "r", encoding="utf-8") as ef:
1335
+ for line in ef:
1336
+ m = re.match(
1337
+ r"NEXT_PUBLIC_SITE_URL\s*=\s*.*?:([0-9]+)", line.strip()
1338
+ )
1339
+ if m:
1340
+ dev_port = m.group(1)
1341
+ break
1342
+ if dev_port != "3000":
1343
+ break
1344
+ except Exception:
1345
+ pass # Keep default 3000 on any error
1346
+ dev_url = f"http://localhost:{dev_port}"
1317
1347
 
1318
1348
  replacements = {
1319
1349
  "{{RUN_ID}}": args.run_id,
@@ -1356,6 +1386,8 @@ def build_replacements(args, feature, features, global_context, script_dir):
1356
1386
  "{{TEST_CMD}}": test_cmd,
1357
1387
  "{{BASELINE_FAILURES}}": baseline_failures,
1358
1388
  "{{COVERAGE_TARGET}}": coverage_target,
1389
+ "{{DEV_PORT}}": dev_port,
1390
+ "{{DEV_URL}}": dev_url,
1359
1391
  }
1360
1392
 
1361
1393
  return replacements, effective_resume, browser_enabled
@@ -194,17 +194,32 @@ You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without
194
194
  You know this project's tech stack. Detect and start the dev server yourself:
195
195
 
196
196
  1. Identify the dev server start command from project config (`package.json` scripts, `Makefile`, `docker-compose.yml`, etc.)
197
- 2. Choose an available port — check what the project defaults to, or pick one that is free:
197
+ 2. **Detect the dev server port**use the pre-detected port from pipeline if available, otherwise extract from project config. Do NOT hardcode or guess the port:
198
198
  ```bash
199
- lsof -ti:<port> 2>/dev/null && echo "PORT_IN_USE" || echo "PORT_FREE"
199
+ # Use pipeline-injected port if available, otherwise extract from package.json
200
+ DEV_PORT={{DEV_PORT}}
201
+ # If DEV_PORT is still a placeholder, detect at runtime:
202
+ if [ "$DEV_PORT" = "{{DEV_PORT}}" ]; then
203
+ DEV_PORT=$(node -e "const s=require('./package.json').scripts.dev; const m=s.match(/-p\s+(\d+)/); console.log(m?m[1]:'')")
204
+ if [ -z "$DEV_PORT" ]; then
205
+ DEV_PORT=$(echo "$NEXT_PUBLIC_SITE_URL" | sed -nE 's|.*:([0-9]+).*|\1|p')
206
+ fi
207
+ DEV_PORT=${DEV_PORT:-3000}
208
+ fi
209
+ echo "Detected DEV_PORT=$DEV_PORT"
200
210
  ```
201
- 3. Start the dev server in background, capture PID:
211
+ 3. Verify the port is available:
212
+ ```bash
213
+ lsof -ti:$DEV_PORT 2>/dev/null && echo "PORT_IN_USE" || echo "PORT_FREE"
214
+ ```
215
+ 4. Start the dev server in background, capture PID:
202
216
  ```bash
203
217
  <start-command> &
204
218
  DEV_SERVER_PID=$!
205
219
  ```
206
- 4. Wait for server to be ready: poll the target URL with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
207
- 5. If the page requires authentication, use playwright-cli to register a test user and log in first
220
+ 5. Wait for server to be ready: poll `http://localhost:$DEV_PORT` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
221
+ 6. Open the app in playwright-cli: `playwright-cli open http://localhost:$DEV_PORT`
222
+ 7. If the page requires authentication, use playwright-cli to register a test user and log in first
208
223
 
209
224
  **Step 2 — Verification**:
210
225
 
@@ -217,14 +232,14 @@ Take a final screenshot for evidence.
217
232
  **Step 3 — Cleanup (REQUIRED — you started it, you stop it)**:
218
233
 
219
234
  1. Kill the dev server process: `kill $DEV_SERVER_PID 2>/dev/null || true`
220
- 2. Verify port is released: `lsof -ti:<port> | xargs kill -9 2>/dev/null || true`
235
+ 2. Verify port is released: `lsof -ti:$DEV_PORT | xargs kill -9 2>/dev/null || true`
221
236
 
222
237
  **Step 4 — Reporting**:
223
238
 
224
239
  Append results to `context-snapshot.md`:
225
240
  ```
226
241
  ## Browser Verification
227
- URL: <actual URL used>
242
+ URL: http://localhost:$DEV_PORT
228
243
  Dev Server Command: <actual command used>
229
244
  Steps executed: [list]
230
245
  Screenshot: [path]
@@ -292,17 +292,32 @@ You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without
292
292
  You know this project's tech stack. Detect and start the dev server yourself:
293
293
 
294
294
  1. Identify the dev server start command from project config (`package.json` scripts, `Makefile`, `docker-compose.yml`, etc.)
295
- 2. Choose an available port — check what the project defaults to, or pick one that is free:
295
+ 2. **Detect the dev server port**use the pre-detected port from pipeline if available, otherwise extract from project config. Do NOT hardcode or guess the port:
296
296
  ```bash
297
- lsof -ti:<port> 2>/dev/null && echo "PORT_IN_USE" || echo "PORT_FREE"
297
+ # Use pipeline-injected port if available, otherwise extract from package.json
298
+ DEV_PORT={{DEV_PORT}}
299
+ # If DEV_PORT is still a placeholder, detect at runtime:
300
+ if [ "$DEV_PORT" = "{{DEV_PORT}}" ]; then
301
+ DEV_PORT=$(node -e "const s=require('./package.json').scripts.dev; const m=s.match(/-p\s+(\d+)/); console.log(m?m[1]:'')")
302
+ if [ -z "$DEV_PORT" ]; then
303
+ DEV_PORT=$(echo "$NEXT_PUBLIC_SITE_URL" | sed -nE 's|.*:([0-9]+).*|\1|p')
304
+ fi
305
+ DEV_PORT=${DEV_PORT:-3000}
306
+ fi
307
+ echo "Detected DEV_PORT=$DEV_PORT"
298
308
  ```
299
- 3. Start the dev server in background, capture PID:
309
+ 3. Verify the port is available:
310
+ ```bash
311
+ lsof -ti:$DEV_PORT 2>/dev/null && echo "PORT_IN_USE" || echo "PORT_FREE"
312
+ ```
313
+ 4. Start the dev server in background, capture PID:
300
314
  ```bash
301
315
  <start-command> &
302
316
  DEV_SERVER_PID=$!
303
317
  ```
304
- 4. Wait for server to be ready: poll the target URL with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
305
- 5. If the page requires authentication, use playwright-cli to register a test user and log in first
318
+ 5. Wait for server to be ready: poll `http://localhost:$DEV_PORT` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
319
+ 6. Open the app in playwright-cli: `playwright-cli open http://localhost:$DEV_PORT`
320
+ 7. If the page requires authentication, use playwright-cli to register a test user and log in first
306
321
 
307
322
  **Step 2 — Verification**:
308
323
 
@@ -315,14 +330,14 @@ Take a final screenshot for evidence.
315
330
  **Step 3 — Cleanup (REQUIRED — you started it, you stop it)**:
316
331
 
317
332
  1. Kill the dev server process: `kill $DEV_SERVER_PID 2>/dev/null || true`
318
- 2. Verify port is released: `lsof -ti:<port> | xargs kill -9 2>/dev/null || true`
333
+ 2. Verify port is released: `lsof -ti:$DEV_PORT | xargs kill -9 2>/dev/null || true`
319
334
 
320
335
  **Step 4 — Reporting**:
321
336
 
322
337
  Append results to `context-snapshot.md`:
323
338
  ```
324
339
  ## Browser Verification
325
- URL: <actual URL used>
340
+ URL: http://localhost:$DEV_PORT
326
341
  Dev Server Command: <actual command used>
327
342
  Steps executed: [list]
328
343
  Screenshot: [path]