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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/lib/branch.sh +187 -23
- package/bundled/dev-pipeline/reset-bug.sh +21 -13
- package/bundled/dev-pipeline/reset-feature.sh +21 -13
- package/bundled/dev-pipeline/reset-refactor.sh +21 -13
- package/bundled/dev-pipeline/run-bugfix.sh +113 -26
- package/bundled/dev-pipeline/run-feature.sh +113 -27
- package/bundled/dev-pipeline/run-refactor.sh +113 -26
- package/bundled/dev-pipeline/scripts/detect-stuck.py +25 -14
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +32 -0
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +0 -5
- package/bundled/dev-pipeline/scripts/init-pipeline.py +0 -5
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +0 -5
- package/bundled/dev-pipeline/scripts/update-bug-status.py +40 -31
- package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -60
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +43 -34
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +22 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +22 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +22 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +22 -7
- package/bundled/dev-pipeline/tests/test_auto_skip.py +10 -3
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
|
@@ -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 —
|
|
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
|
-
#
|
|
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 —
|
|
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" \
|
|
@@ -226,15 +226,16 @@ def check_stuck_checkpoint(item_dir):
|
|
|
226
226
|
return None
|
|
227
227
|
|
|
228
228
|
|
|
229
|
-
def check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold):
|
|
229
|
+
def check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold, task_list_status=None):
|
|
230
230
|
"""Check 3: Is the heartbeat stale or missing for an in_progress item?
|
|
231
231
|
|
|
232
232
|
Only applies to items whose status indicates active work.
|
|
233
|
-
|
|
233
|
+
Status is read from task_list_status (task list JSON, single source of truth).
|
|
234
|
+
Uses last_session_id from the item's own status.json to find the active session.
|
|
234
235
|
|
|
235
236
|
Returns a stuck-report dict or None.
|
|
236
237
|
"""
|
|
237
|
-
status =
|
|
238
|
+
status = task_list_status
|
|
238
239
|
# All pipelines now use "in_progress" as the active status
|
|
239
240
|
in_progress_statuses = {"in_progress"}
|
|
240
241
|
if status not in in_progress_statuses:
|
|
@@ -287,6 +288,8 @@ def check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_t
|
|
|
287
288
|
def check_dependency_deadlock(item_id, task_list_data, state_dir, items_subdir, items_key):
|
|
288
289
|
"""Check 4: Does this item depend on a failed item?
|
|
289
290
|
|
|
291
|
+
Reads dependency status from task list JSON (single source of truth).
|
|
292
|
+
|
|
290
293
|
Returns a stuck-report dict or None.
|
|
291
294
|
"""
|
|
292
295
|
if task_list_data is None:
|
|
@@ -296,6 +299,12 @@ def check_dependency_deadlock(item_id, task_list_data, state_dir, items_subdir,
|
|
|
296
299
|
if not isinstance(items, list):
|
|
297
300
|
return None
|
|
298
301
|
|
|
302
|
+
# Build status map from task list
|
|
303
|
+
status_map = {}
|
|
304
|
+
for item in items:
|
|
305
|
+
if isinstance(item, dict) and item.get("id"):
|
|
306
|
+
status_map[item["id"]] = item.get("status", "pending")
|
|
307
|
+
|
|
299
308
|
# Find this item in the list to get its dependencies
|
|
300
309
|
deps = None
|
|
301
310
|
for item in items:
|
|
@@ -308,15 +317,9 @@ def check_dependency_deadlock(item_id, task_list_data, state_dir, items_subdir,
|
|
|
308
317
|
if not deps or not isinstance(deps, list):
|
|
309
318
|
return None
|
|
310
319
|
|
|
311
|
-
# Check each dependency's status
|
|
320
|
+
# Check each dependency's status from the task list
|
|
312
321
|
for dep_id in deps:
|
|
313
|
-
|
|
314
|
-
state_dir, items_subdir, dep_id, "status.json"
|
|
315
|
-
)
|
|
316
|
-
dep_status = load_json(dep_status_path)
|
|
317
|
-
if dep_status is None:
|
|
318
|
-
continue
|
|
319
|
-
dep_state = dep_status.get("status")
|
|
322
|
+
dep_state = status_map.get(dep_id)
|
|
320
323
|
if dep_state == "failed":
|
|
321
324
|
return {
|
|
322
325
|
"reason": "dependency_failed",
|
|
@@ -376,8 +379,16 @@ def check_item(item_id, state_dir, items_subdir, items_key, task_list_data, max_
|
|
|
376
379
|
item_status = load_json(status_path)
|
|
377
380
|
|
|
378
381
|
if item_status is None:
|
|
379
|
-
#
|
|
380
|
-
|
|
382
|
+
# Create a minimal runtime dict so checks can proceed
|
|
383
|
+
item_status = {}
|
|
384
|
+
|
|
385
|
+
# Look up item status from task list (single source of truth)
|
|
386
|
+
task_list_status = None
|
|
387
|
+
if task_list_data:
|
|
388
|
+
for item in task_list_data.get(items_key, []):
|
|
389
|
+
if isinstance(item, dict) and item.get("id") == item_id:
|
|
390
|
+
task_list_status = item.get("status", "pending")
|
|
391
|
+
break
|
|
381
392
|
|
|
382
393
|
reports = []
|
|
383
394
|
|
|
@@ -392,7 +403,7 @@ def check_item(item_id, state_dir, items_subdir, items_key, task_list_data, max_
|
|
|
392
403
|
reports.append(result)
|
|
393
404
|
|
|
394
405
|
# Check 3: Stale heartbeat
|
|
395
|
-
result = check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold)
|
|
406
|
+
result = check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold, task_list_status)
|
|
396
407
|
if result is not None:
|
|
397
408
|
reports.append(result)
|
|
398
409
|
|
|
@@ -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
|
|
@@ -249,13 +249,8 @@ def create_state_directory(state_dir, bug_list_path, bugs):
|
|
|
249
249
|
sessions_dir = os.path.join(bug_dir, "sessions")
|
|
250
250
|
os.makedirs(sessions_dir, exist_ok=True)
|
|
251
251
|
|
|
252
|
-
# Respect existing terminal status from bug-fix-list.json
|
|
253
|
-
bl_status = bug.get("status", "pending")
|
|
254
|
-
init_status = bl_status if bl_status in TERMINAL_STATUSES else "pending"
|
|
255
|
-
|
|
256
252
|
bug_status = {
|
|
257
253
|
"bug_id": bid,
|
|
258
|
-
"status": init_status,
|
|
259
254
|
"retry_count": 0,
|
|
260
255
|
"max_retries": 3,
|
|
261
256
|
"sessions": [],
|
|
@@ -283,13 +283,8 @@ def create_state_directory(state_dir, feature_list_path, features):
|
|
|
283
283
|
sessions_dir = os.path.join(feature_dir, "sessions")
|
|
284
284
|
os.makedirs(sessions_dir, exist_ok=True)
|
|
285
285
|
|
|
286
|
-
# Respect existing terminal status from .prizmkit/plans/feature-list.json
|
|
287
|
-
fl_status = feature.get("status", "pending")
|
|
288
|
-
init_status = fl_status if fl_status in TERMINAL_STATUSES else "pending"
|
|
289
|
-
|
|
290
286
|
feature_status = {
|
|
291
287
|
"feature_id": fid,
|
|
292
|
-
"status": init_status,
|
|
293
288
|
"retry_count": 0,
|
|
294
289
|
"max_retries": 3,
|
|
295
290
|
"sessions": [],
|
|
@@ -325,13 +325,8 @@ def create_state_directory(state_dir, refactor_list_path, refactors):
|
|
|
325
325
|
sessions_dir = os.path.join(refactor_dir, "sessions")
|
|
326
326
|
os.makedirs(sessions_dir, exist_ok=True)
|
|
327
327
|
|
|
328
|
-
# Respect existing terminal status from refactor-list.json
|
|
329
|
-
rl_status = refactor.get("status", "pending")
|
|
330
|
-
init_status = rl_status if rl_status in TERMINAL_STATUSES else "pending"
|
|
331
|
-
|
|
332
328
|
refactor_status = {
|
|
333
329
|
"refactor_id": rid,
|
|
334
|
-
"status": init_status,
|
|
335
330
|
"retry_count": 0,
|
|
336
331
|
"max_retries": 3,
|
|
337
332
|
"sessions": [],
|
|
@@ -84,12 +84,17 @@ def now_iso():
|
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def load_bug_status(state_dir, bug_id):
|
|
87
|
+
"""Load runtime state from status.json for a bug.
|
|
88
|
+
|
|
89
|
+
Returns runtime fields only (retry_count, sessions, etc.).
|
|
90
|
+
The 'status' field is NOT included — status lives exclusively
|
|
91
|
+
in bug-fix-list.json.
|
|
92
|
+
"""
|
|
87
93
|
status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
|
|
88
94
|
if not os.path.isfile(status_path):
|
|
89
95
|
now = now_iso()
|
|
90
96
|
return {
|
|
91
97
|
"bug_id": bug_id,
|
|
92
|
-
"status": "pending",
|
|
93
98
|
"retry_count": 0,
|
|
94
99
|
"max_retries": 3,
|
|
95
100
|
"sessions": [],
|
|
@@ -103,7 +108,6 @@ def load_bug_status(state_dir, bug_id):
|
|
|
103
108
|
now = now_iso()
|
|
104
109
|
return {
|
|
105
110
|
"bug_id": bug_id,
|
|
106
|
-
"status": "pending",
|
|
107
111
|
"retry_count": 0,
|
|
108
112
|
"max_retries": 3,
|
|
109
113
|
"sessions": [],
|
|
@@ -112,14 +116,30 @@ def load_bug_status(state_dir, bug_id):
|
|
|
112
116
|
"created_at": now,
|
|
113
117
|
"updated_at": now,
|
|
114
118
|
}
|
|
119
|
+
# Defensively remove status if present (legacy data)
|
|
120
|
+
data.pop("status", None)
|
|
115
121
|
return data
|
|
116
122
|
|
|
117
123
|
|
|
118
124
|
def save_bug_status(state_dir, bug_id, status_data):
|
|
125
|
+
"""Write the status.json for a bug (runtime fields only)."""
|
|
126
|
+
# Defensively strip status — it belongs in bug-fix-list.json
|
|
127
|
+
status_data.pop("status", None)
|
|
119
128
|
status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
|
|
120
129
|
return write_json_file(status_path, status_data)
|
|
121
130
|
|
|
122
131
|
|
|
132
|
+
def get_bug_status_from_list(bug_list_path, bug_id):
|
|
133
|
+
"""Read a single bug's status from bug-fix-list.json."""
|
|
134
|
+
data, err = load_json_file(bug_list_path)
|
|
135
|
+
if err:
|
|
136
|
+
return "pending"
|
|
137
|
+
for b in data.get("bugs", []):
|
|
138
|
+
if isinstance(b, dict) and b.get("id") == bug_id:
|
|
139
|
+
return b.get("status", "pending")
|
|
140
|
+
return "pending"
|
|
141
|
+
|
|
142
|
+
|
|
123
143
|
def update_bug_in_list(bug_list_path, bug_id, new_status):
|
|
124
144
|
data, err = load_json_file(bug_list_path)
|
|
125
145
|
if err:
|
|
@@ -153,7 +173,7 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
153
173
|
print("PIPELINE_COMPLETE")
|
|
154
174
|
return
|
|
155
175
|
|
|
156
|
-
# Build status map
|
|
176
|
+
# Build status map from bug-fix-list.json (single source of truth)
|
|
157
177
|
status_map = {}
|
|
158
178
|
status_data_map = {}
|
|
159
179
|
for bug in bugs:
|
|
@@ -162,8 +182,8 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
162
182
|
bid = bug.get("id")
|
|
163
183
|
if not bid:
|
|
164
184
|
continue
|
|
185
|
+
status_map[bid] = bug.get("status", "pending")
|
|
165
186
|
bs = load_bug_status(state_dir, bid)
|
|
166
|
-
status_map[bid] = bs.get("status", "pending")
|
|
167
187
|
status_data_map[bid] = bs
|
|
168
188
|
|
|
169
189
|
# Check if all bugs are terminal
|
|
@@ -237,35 +257,30 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
237
257
|
|
|
238
258
|
bs = load_bug_status(state_dir, bug_id)
|
|
239
259
|
|
|
260
|
+
# Track what status we write to bug-fix-list.json
|
|
261
|
+
new_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
262
|
+
|
|
240
263
|
if session_status == "success":
|
|
241
|
-
|
|
264
|
+
new_status = "completed"
|
|
242
265
|
bs["resume_from_phase"] = None
|
|
243
266
|
err = update_bug_in_list(bug_list_path, bug_id, "completed")
|
|
244
267
|
if err:
|
|
245
268
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
246
269
|
return
|
|
247
270
|
elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
248
|
-
# Degraded outcome: keep artifacts for retry (branch preserves work).
|
|
249
|
-
# Write schema-valid status to bug-fix-list.json ("pending" for retry,
|
|
250
|
-
# "failed" if max retries exceeded). Store the granular degraded reason
|
|
251
|
-
# in status.json only (internal pipeline state, not schema-bound).
|
|
252
271
|
bs["retry_count"] = bs.get("retry_count", 0) + 1
|
|
253
272
|
|
|
254
273
|
if bs["retry_count"] >= max_retries:
|
|
255
|
-
|
|
256
|
-
target_status = "failed"
|
|
274
|
+
new_status = "failed"
|
|
257
275
|
else:
|
|
258
|
-
|
|
259
|
-
bs["status"] = session_status
|
|
260
|
-
# bug-fix-list.json gets schema-valid "pending" (will be retried)
|
|
261
|
-
target_status = "pending"
|
|
276
|
+
new_status = "pending"
|
|
262
277
|
|
|
263
278
|
bs["degraded_reason"] = session_status
|
|
264
279
|
bs["resume_from_phase"] = None
|
|
265
280
|
bs["sessions"] = []
|
|
266
281
|
bs["last_session_id"] = None
|
|
267
282
|
|
|
268
|
-
err = update_bug_in_list(bug_list_path, bug_id,
|
|
283
|
+
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
269
284
|
if err:
|
|
270
285
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
271
286
|
return
|
|
@@ -279,17 +294,15 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
279
294
|
)
|
|
280
295
|
|
|
281
296
|
if bs["retry_count"] >= max_retries:
|
|
282
|
-
|
|
283
|
-
target_status = "failed"
|
|
297
|
+
new_status = "failed"
|
|
284
298
|
else:
|
|
285
|
-
|
|
286
|
-
target_status = "pending"
|
|
299
|
+
new_status = "pending"
|
|
287
300
|
|
|
288
301
|
bs["resume_from_phase"] = None
|
|
289
302
|
bs["sessions"] = []
|
|
290
303
|
bs["last_session_id"] = None
|
|
291
304
|
|
|
292
|
-
err = update_bug_in_list(bug_list_path, bug_id,
|
|
305
|
+
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
293
306
|
if err:
|
|
294
307
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
295
308
|
return
|
|
@@ -312,7 +325,7 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
312
325
|
"action": "update",
|
|
313
326
|
"bug_id": bug_id,
|
|
314
327
|
"session_status": session_status,
|
|
315
|
-
"new_status":
|
|
328
|
+
"new_status": new_status,
|
|
316
329
|
"retry_count": bs["retry_count"],
|
|
317
330
|
"resume_from_phase": bs.get("resume_from_phase"),
|
|
318
331
|
"updated_at": bs["updated_at"],
|
|
@@ -432,8 +445,8 @@ def action_status(bug_list_data, state_dir):
|
|
|
432
445
|
if not bid:
|
|
433
446
|
continue
|
|
434
447
|
|
|
448
|
+
bstatus = bug.get("status", "pending")
|
|
435
449
|
bs = load_bug_status(state_dir, bid)
|
|
436
|
-
bstatus = bs.get("status", "pending")
|
|
437
450
|
retry_count = bs.get("retry_count", 0)
|
|
438
451
|
max_retries_val = bs.get("max_retries", 3)
|
|
439
452
|
resume_phase = bs.get("resume_from_phase")
|
|
@@ -537,10 +550,9 @@ def action_reset(args, bug_list_path, state_dir):
|
|
|
537
550
|
return
|
|
538
551
|
|
|
539
552
|
bs = load_bug_status(state_dir, bug_id)
|
|
540
|
-
old_status =
|
|
553
|
+
old_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
541
554
|
old_retry = bs.get("retry_count", 0)
|
|
542
555
|
|
|
543
|
-
bs["status"] = "pending"
|
|
544
556
|
bs["retry_count"] = 0
|
|
545
557
|
bs["sessions"] = []
|
|
546
558
|
bs["last_session_id"] = None
|
|
@@ -613,10 +625,9 @@ def action_clean(args, bug_list_path, state_dir):
|
|
|
613
625
|
|
|
614
626
|
# 5. Reset status
|
|
615
627
|
bs = load_bug_status(state_dir, bug_id)
|
|
616
|
-
old_status =
|
|
628
|
+
old_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
617
629
|
old_retry = bs.get("retry_count", 0)
|
|
618
630
|
|
|
619
|
-
bs["status"] = "pending"
|
|
620
631
|
bs["retry_count"] = 0
|
|
621
632
|
bs["sessions"] = []
|
|
622
633
|
bs["last_session_id"] = None
|
|
@@ -687,9 +698,8 @@ def action_start(args, bug_list_path, state_dir):
|
|
|
687
698
|
return
|
|
688
699
|
|
|
689
700
|
bs = load_bug_status(state_dir, bug_id)
|
|
690
|
-
old_status =
|
|
701
|
+
old_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
691
702
|
|
|
692
|
-
bs["status"] = "in_progress"
|
|
693
703
|
bs["updated_at"] = now_iso()
|
|
694
704
|
|
|
695
705
|
err = save_bug_status(state_dir, bug_id, bs)
|
|
@@ -779,10 +789,9 @@ def action_unskip(args, bug_list_path, state_dir):
|
|
|
779
789
|
error_out("Failed to write .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
780
790
|
return
|
|
781
791
|
|
|
782
|
-
# Reset status.json for each bug
|
|
792
|
+
# Reset runtime fields in status.json for each bug
|
|
783
793
|
for bid in to_reset:
|
|
784
794
|
bs = load_bug_status(state_dir, bid)
|
|
785
|
-
bs["status"] = "pending"
|
|
786
795
|
bs["retry_count"] = 0
|
|
787
796
|
bs["sessions"] = []
|
|
788
797
|
bs["last_session_id"] = None
|