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
|
@@ -153,24 +153,20 @@ def now_iso():
|
|
|
153
153
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
def load_feature_status(state_dir, feature_id
|
|
157
|
-
"""Load the status.json for a feature.
|
|
156
|
+
def load_feature_status(state_dir, feature_id):
|
|
157
|
+
"""Load the runtime state from status.json for a feature.
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
it overrides the status field from status.json. This makes .prizmkit/plans/feature-list.json
|
|
163
|
-
the single source of truth for terminal statuses, while all other fields
|
|
164
|
-
(retry_count, sessions, etc.) still come from status.json.
|
|
159
|
+
Returns runtime fields only (retry_count, sessions, etc.).
|
|
160
|
+
The 'status' field is NOT included — status lives exclusively
|
|
161
|
+
in feature-list.json.
|
|
165
162
|
"""
|
|
166
163
|
status_path = os.path.join(
|
|
167
164
|
state_dir, "features", feature_id, "status.json"
|
|
168
165
|
)
|
|
169
166
|
if not os.path.isfile(status_path):
|
|
170
167
|
now = now_iso()
|
|
171
|
-
|
|
168
|
+
return {
|
|
172
169
|
"feature_id": feature_id,
|
|
173
|
-
"status": feature_list_status if feature_list_status else "pending",
|
|
174
170
|
"retry_count": 0,
|
|
175
171
|
"max_retries": 3,
|
|
176
172
|
"sessions": [],
|
|
@@ -179,14 +175,11 @@ def load_feature_status(state_dir, feature_id, feature_list_status=None):
|
|
|
179
175
|
"created_at": now,
|
|
180
176
|
"updated_at": now,
|
|
181
177
|
}
|
|
182
|
-
return data
|
|
183
178
|
data, err = load_json_file(status_path)
|
|
184
179
|
if err:
|
|
185
|
-
# If we can't read it, treat as pending
|
|
186
180
|
now = now_iso()
|
|
187
|
-
|
|
181
|
+
return {
|
|
188
182
|
"feature_id": feature_id,
|
|
189
|
-
"status": feature_list_status if feature_list_status else "pending",
|
|
190
183
|
"retry_count": 0,
|
|
191
184
|
"max_retries": 3,
|
|
192
185
|
"sessions": [],
|
|
@@ -195,20 +188,32 @@ def load_feature_status(state_dir, feature_id, feature_list_status=None):
|
|
|
195
188
|
"created_at": now,
|
|
196
189
|
"updated_at": now,
|
|
197
190
|
}
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
data["status"] = feature_list_status
|
|
191
|
+
# Defensively remove status if present (legacy data)
|
|
192
|
+
data.pop("status", None)
|
|
201
193
|
return data
|
|
202
194
|
|
|
203
195
|
|
|
204
196
|
def save_feature_status(state_dir, feature_id, status_data):
|
|
205
|
-
"""Write the status.json for a feature."""
|
|
197
|
+
"""Write the status.json for a feature (runtime fields only)."""
|
|
198
|
+
# Defensively strip status — it belongs in feature-list.json
|
|
199
|
+
status_data.pop("status", None)
|
|
206
200
|
status_path = os.path.join(
|
|
207
201
|
state_dir, "features", feature_id, "status.json"
|
|
208
202
|
)
|
|
209
203
|
return write_json_file(status_path, status_data)
|
|
210
204
|
|
|
211
205
|
|
|
206
|
+
def get_feature_status_from_list(feature_list_path, feature_id):
|
|
207
|
+
"""Read a single feature's status from feature-list.json."""
|
|
208
|
+
data, err = load_json_file(feature_list_path)
|
|
209
|
+
if err:
|
|
210
|
+
return "pending"
|
|
211
|
+
for f in data.get("features", []):
|
|
212
|
+
if isinstance(f, dict) and f.get("id") == feature_id:
|
|
213
|
+
return f.get("status", "pending")
|
|
214
|
+
return "pending"
|
|
215
|
+
|
|
216
|
+
|
|
212
217
|
def update_feature_in_list(feature_list_path, feature_id, new_status):
|
|
213
218
|
"""Update a feature's status field in .prizmkit/plans/feature-list.json.
|
|
214
219
|
|
|
@@ -389,10 +394,9 @@ def auto_skip_blocked_features(feature_list_path, state_dir, failed_feature_id):
|
|
|
389
394
|
f["status"] = "auto_skipped"
|
|
390
395
|
write_json_file(feature_list_path, data)
|
|
391
396
|
|
|
392
|
-
#
|
|
397
|
+
# Update timestamps in status.json for each auto-skipped feature
|
|
393
398
|
for fid in to_skip:
|
|
394
399
|
fs = load_feature_status(state_dir, fid)
|
|
395
|
-
fs["status"] = "auto_skipped"
|
|
396
400
|
fs["updated_at"] = now_iso()
|
|
397
401
|
save_feature_status(state_dir, fid, fs)
|
|
398
402
|
|
|
@@ -454,18 +458,19 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
|
|
|
454
458
|
return
|
|
455
459
|
|
|
456
460
|
# Build status map from ALL features (for dependency checking).
|
|
461
|
+
# Status comes from feature-list.json (the single source of truth).
|
|
457
462
|
# This must happen BEFORE the feature filter is applied, because
|
|
458
463
|
# filtered features may depend on features outside the filter.
|
|
459
464
|
status_map = {} # feature_id -> status string
|
|
460
|
-
status_data_map = {} # feature_id ->
|
|
465
|
+
status_data_map = {} # feature_id -> runtime status data (retry_count, etc.)
|
|
461
466
|
for feature in features:
|
|
462
467
|
if not isinstance(feature, dict):
|
|
463
468
|
continue
|
|
464
469
|
fid = feature.get("id")
|
|
465
470
|
if not fid:
|
|
466
471
|
continue
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
status_map[fid] = feature.get("status", "pending")
|
|
473
|
+
fs = load_feature_status(state_dir, fid)
|
|
469
474
|
status_data_map[fid] = fs
|
|
470
475
|
|
|
471
476
|
# Apply feature filter: only consider these features as candidates
|
|
@@ -576,12 +581,16 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
576
581
|
return
|
|
577
582
|
|
|
578
583
|
fs = load_feature_status(state_dir, feature_id)
|
|
584
|
+
current_list_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
585
|
+
|
|
586
|
+
# Track what status we write to feature-list.json
|
|
587
|
+
new_status = current_list_status
|
|
579
588
|
|
|
580
589
|
if session_status == "success":
|
|
581
590
|
# No-op guard: if this exact successful session was already recorded,
|
|
582
591
|
# avoid rewriting state files again (prevents post-commit dirty changes).
|
|
583
592
|
existing_sessions = fs.get("sessions", [])
|
|
584
|
-
already_completed =
|
|
593
|
+
already_completed = current_list_status == "completed" and fs.get("resume_from_phase") is None
|
|
585
594
|
same_session_already_recorded = (
|
|
586
595
|
session_id
|
|
587
596
|
and session_id in existing_sessions
|
|
@@ -592,7 +601,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
592
601
|
"action": "update",
|
|
593
602
|
"feature_id": feature_id,
|
|
594
603
|
"session_status": session_status,
|
|
595
|
-
"new_status":
|
|
604
|
+
"new_status": "completed",
|
|
596
605
|
"retry_count": fs.get("retry_count", 0),
|
|
597
606
|
"resume_from_phase": fs.get("resume_from_phase"),
|
|
598
607
|
"updated_at": fs.get("updated_at"),
|
|
@@ -601,7 +610,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
601
610
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
602
611
|
return
|
|
603
612
|
|
|
604
|
-
|
|
613
|
+
new_status = "completed"
|
|
605
614
|
fs["resume_from_phase"] = None
|
|
606
615
|
err = update_feature_in_list(feature_list_path, feature_id, "completed")
|
|
607
616
|
if err:
|
|
@@ -614,42 +623,33 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
614
623
|
fs["retry_count"] = fs.get("retry_count", 0) + 1
|
|
615
624
|
|
|
616
625
|
if fs["retry_count"] >= max_retries:
|
|
617
|
-
|
|
618
|
-
target_status = "failed"
|
|
626
|
+
new_status = "failed"
|
|
619
627
|
else:
|
|
620
|
-
# status.json keeps the granular degraded reason for diagnostics
|
|
621
|
-
fs["status"] = session_status
|
|
622
628
|
# feature-list.json gets schema-valid "pending" (will be retried)
|
|
623
|
-
|
|
629
|
+
new_status = "pending"
|
|
624
630
|
|
|
625
631
|
fs["degraded_reason"] = session_status
|
|
626
632
|
fs["resume_from_phase"] = None
|
|
627
633
|
fs["sessions"] = []
|
|
628
634
|
fs["last_session_id"] = None
|
|
629
635
|
|
|
630
|
-
err = update_feature_in_list(feature_list_path, feature_id,
|
|
636
|
+
err = update_feature_in_list(feature_list_path, feature_id, new_status)
|
|
631
637
|
if err:
|
|
632
638
|
error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
|
|
633
639
|
return
|
|
634
640
|
else:
|
|
635
641
|
# crashed / failed / timed_out — preserve all artifacts for debugging.
|
|
636
|
-
# Previous behavior deleted everything via cleanup_feature_artifacts(),
|
|
637
|
-
# making it impossible to diagnose why the session failed.
|
|
638
642
|
fs["retry_count"] = fs.get("retry_count", 0) + 1
|
|
639
643
|
|
|
640
644
|
if fs["retry_count"] >= max_retries:
|
|
641
|
-
|
|
642
|
-
target_status = "failed"
|
|
645
|
+
new_status = "failed"
|
|
643
646
|
else:
|
|
644
|
-
|
|
645
|
-
target_status = "pending"
|
|
647
|
+
new_status = "pending"
|
|
646
648
|
|
|
647
649
|
fs["resume_from_phase"] = None
|
|
648
650
|
# Keep sessions list and last_session_id for debugging
|
|
649
|
-
# fs["sessions"] = []
|
|
650
|
-
# fs["last_session_id"] = None
|
|
651
651
|
|
|
652
|
-
err = update_feature_in_list(feature_list_path, feature_id,
|
|
652
|
+
err = update_feature_in_list(feature_list_path, feature_id, new_status)
|
|
653
653
|
if err:
|
|
654
654
|
error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
|
|
655
655
|
return
|
|
@@ -670,7 +670,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
670
670
|
|
|
671
671
|
# Auto-skip downstream features when this feature is marked as failed or skipped
|
|
672
672
|
auto_skipped_features = []
|
|
673
|
-
if
|
|
673
|
+
if new_status in ("failed", "skipped"):
|
|
674
674
|
auto_skipped_features = auto_skip_blocked_features(
|
|
675
675
|
feature_list_path, state_dir, feature_id
|
|
676
676
|
)
|
|
@@ -679,7 +679,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
679
679
|
"action": "update",
|
|
680
680
|
"feature_id": feature_id,
|
|
681
681
|
"session_status": session_status,
|
|
682
|
-
"new_status":
|
|
682
|
+
"new_status": new_status,
|
|
683
683
|
"retry_count": fs["retry_count"],
|
|
684
684
|
"resume_from_phase": fs.get("resume_from_phase"),
|
|
685
685
|
"updated_at": fs["updated_at"],
|
|
@@ -776,12 +776,12 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
776
776
|
# Complexity weights (used for estimation when no historical data is available)
|
|
777
777
|
COMPLEXITY_WEIGHT = {"low": 1.0, "medium": 2.0, "high": 4.0}
|
|
778
778
|
|
|
779
|
-
# Build feature-list status map
|
|
779
|
+
# Build feature-list status map (status lives in feature-list.json)
|
|
780
780
|
fl_status_map = {}
|
|
781
781
|
if feature_list_data:
|
|
782
782
|
for f in feature_list_data.get("features", []):
|
|
783
783
|
if isinstance(f, dict) and f.get("id"):
|
|
784
|
-
fl_status_map[f["id"]] = f.get("status")
|
|
784
|
+
fl_status_map[f["id"]] = f.get("status", "pending")
|
|
785
785
|
|
|
786
786
|
# Collect completed feature durations grouped by complexity
|
|
787
787
|
duration_by_complexity = {} # complexity -> [duration_seconds]
|
|
@@ -803,8 +803,7 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
803
803
|
fid = feature.get("id")
|
|
804
804
|
if not fid:
|
|
805
805
|
continue
|
|
806
|
-
|
|
807
|
-
if fs.get("status") != "completed":
|
|
806
|
+
if fl_status_map.get(fid) != "completed":
|
|
808
807
|
continue
|
|
809
808
|
duration = _calc_feature_duration(state_dir, fid)
|
|
810
809
|
if duration is None:
|
|
@@ -833,8 +832,7 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
833
832
|
fid = feature.get("id")
|
|
834
833
|
if not fid:
|
|
835
834
|
continue
|
|
836
|
-
|
|
837
|
-
fstatus = fs.get("status", "pending")
|
|
835
|
+
fstatus = fl_status_map.get(fid, "pending")
|
|
838
836
|
if fstatus in TERMINAL_STATUSES:
|
|
839
837
|
continue
|
|
840
838
|
remaining_count += 1
|
|
@@ -910,7 +908,7 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
910
908
|
fid = feature.get("id")
|
|
911
909
|
if not fid:
|
|
912
910
|
continue
|
|
913
|
-
fs = load_feature_status(state_dir, fid
|
|
911
|
+
fs = load_feature_status(state_dir, fid)
|
|
914
912
|
dr = fs.get("degraded_reason")
|
|
915
913
|
if dr:
|
|
916
914
|
degraded_reason_map[fid] = dr
|
|
@@ -1089,9 +1087,8 @@ def action_start(args, feature_list_path, state_dir):
|
|
|
1089
1087
|
return
|
|
1090
1088
|
|
|
1091
1089
|
fs = load_feature_status(state_dir, feature_id)
|
|
1092
|
-
old_status =
|
|
1090
|
+
old_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
1093
1091
|
|
|
1094
|
-
fs["status"] = "in_progress"
|
|
1095
1092
|
fs["updated_at"] = now_iso()
|
|
1096
1093
|
|
|
1097
1094
|
err = save_feature_status(state_dir, feature_id, fs)
|
|
@@ -1121,7 +1118,7 @@ def action_start(args, feature_list_path, state_dir):
|
|
|
1121
1118
|
def action_reset(args, feature_list_path, state_dir):
|
|
1122
1119
|
"""Reset a feature to pending state.
|
|
1123
1120
|
|
|
1124
|
-
Resets status.json
|
|
1121
|
+
Resets status.json runtime fields (retry_count -> 0, clear sessions,
|
|
1125
1122
|
clear resume_from_phase) and updates .prizmkit/plans/feature-list.json status to pending.
|
|
1126
1123
|
Does NOT delete any files on disk.
|
|
1127
1124
|
"""
|
|
@@ -1132,11 +1129,10 @@ def action_reset(args, feature_list_path, state_dir):
|
|
|
1132
1129
|
|
|
1133
1130
|
# Load current status to preserve created_at
|
|
1134
1131
|
fs = load_feature_status(state_dir, feature_id)
|
|
1135
|
-
old_status =
|
|
1132
|
+
old_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
1136
1133
|
old_retry = fs.get("retry_count", 0)
|
|
1137
1134
|
|
|
1138
|
-
# Reset fields
|
|
1139
|
-
fs["status"] = "pending"
|
|
1135
|
+
# Reset runtime fields
|
|
1140
1136
|
fs["retry_count"] = 0
|
|
1141
1137
|
fs["sessions"] = []
|
|
1142
1138
|
fs["last_session_id"] = None
|
|
@@ -1227,10 +1223,9 @@ def action_clean(args, feature_list_path, state_dir):
|
|
|
1227
1223
|
|
|
1228
1224
|
# 5. Reset status (reuse reset logic)
|
|
1229
1225
|
fs = load_feature_status(state_dir, feature_id)
|
|
1230
|
-
old_status =
|
|
1226
|
+
old_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
1231
1227
|
old_retry = fs.get("retry_count", 0)
|
|
1232
1228
|
|
|
1233
|
-
fs["status"] = "pending"
|
|
1234
1229
|
fs["retry_count"] = 0
|
|
1235
1230
|
fs["sessions"] = []
|
|
1236
1231
|
fs["last_session_id"] = None
|
|
@@ -1387,10 +1382,9 @@ def action_unskip(args, feature_list_path, state_dir):
|
|
|
1387
1382
|
error_out("Failed to write .prizmkit/plans/feature-list.json: {}".format(err))
|
|
1388
1383
|
return
|
|
1389
1384
|
|
|
1390
|
-
# Reset status.json for each feature
|
|
1385
|
+
# Reset runtime fields in status.json for each feature
|
|
1391
1386
|
for fid in to_reset:
|
|
1392
1387
|
fs = load_feature_status(state_dir, fid)
|
|
1393
|
-
fs["status"] = "pending"
|
|
1394
1388
|
fs["retry_count"] = 0
|
|
1395
1389
|
fs["sessions"] = []
|
|
1396
1390
|
fs["last_session_id"] = None
|
|
@@ -96,11 +96,10 @@ def now_iso():
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _default_status(refactor_id):
|
|
99
|
-
"""Create a default refactor status object."""
|
|
99
|
+
"""Create a default refactor runtime status object (no status field)."""
|
|
100
100
|
now = now_iso()
|
|
101
101
|
return {
|
|
102
102
|
"refactor_id": refactor_id,
|
|
103
|
-
"status": "pending",
|
|
104
103
|
"retry_count": 0,
|
|
105
104
|
"max_retries": 3,
|
|
106
105
|
"sessions": [],
|
|
@@ -112,20 +111,42 @@ def _default_status(refactor_id):
|
|
|
112
111
|
|
|
113
112
|
|
|
114
113
|
def load_refactor_status(state_dir, refactor_id):
|
|
114
|
+
"""Load runtime state from status.json for a refactor.
|
|
115
|
+
|
|
116
|
+
Returns runtime fields only (retry_count, sessions, etc.).
|
|
117
|
+
The 'status' field is NOT included — status lives exclusively
|
|
118
|
+
in refactor-list.json.
|
|
119
|
+
"""
|
|
115
120
|
status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
|
|
116
121
|
if not os.path.isfile(status_path):
|
|
117
122
|
return _default_status(refactor_id)
|
|
118
123
|
data, err = load_json_file(status_path)
|
|
119
124
|
if err:
|
|
120
125
|
return _default_status(refactor_id)
|
|
126
|
+
# Defensively remove status if present (legacy data)
|
|
127
|
+
data.pop("status", None)
|
|
121
128
|
return data
|
|
122
129
|
|
|
123
130
|
|
|
124
131
|
def save_refactor_status(state_dir, refactor_id, status_data):
|
|
132
|
+
"""Write the status.json for a refactor (runtime fields only)."""
|
|
133
|
+
# Defensively strip status — it belongs in refactor-list.json
|
|
134
|
+
status_data.pop("status", None)
|
|
125
135
|
status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
|
|
126
136
|
return write_json_file(status_path, status_data)
|
|
127
137
|
|
|
128
138
|
|
|
139
|
+
def get_refactor_status_from_list(refactor_list_path, refactor_id):
|
|
140
|
+
"""Read a single refactor's status from refactor-list.json."""
|
|
141
|
+
data, err = load_json_file(refactor_list_path)
|
|
142
|
+
if err:
|
|
143
|
+
return "pending"
|
|
144
|
+
for r in data.get("refactors", []):
|
|
145
|
+
if isinstance(r, dict) and r.get("id") == refactor_id:
|
|
146
|
+
return r.get("status", "pending")
|
|
147
|
+
return "pending"
|
|
148
|
+
|
|
149
|
+
|
|
129
150
|
def update_refactor_in_list(refactor_list_path, refactor_id, new_status):
|
|
130
151
|
data, err = load_json_file(refactor_list_path)
|
|
131
152
|
if err:
|
|
@@ -179,7 +200,7 @@ def action_get_next(refactor_list_data, state_dir):
|
|
|
179
200
|
print("PIPELINE_COMPLETE")
|
|
180
201
|
return
|
|
181
202
|
|
|
182
|
-
# Build status map
|
|
203
|
+
# Build status map from refactor-list.json (single source of truth)
|
|
183
204
|
status_map = {}
|
|
184
205
|
status_data_map = {}
|
|
185
206
|
for r in refactors:
|
|
@@ -188,8 +209,8 @@ def action_get_next(refactor_list_data, state_dir):
|
|
|
188
209
|
rid = r.get("id")
|
|
189
210
|
if not rid:
|
|
190
211
|
continue
|
|
212
|
+
status_map[rid] = r.get("status", "pending")
|
|
191
213
|
rs = load_refactor_status(state_dir, rid)
|
|
192
|
-
status_map[rid] = rs.get("status", "pending")
|
|
193
214
|
status_data_map[rid] = rs
|
|
194
215
|
|
|
195
216
|
completed_set = {rid for rid, st in status_map.items() if st in TERMINAL_STATUSES}
|
|
@@ -270,35 +291,30 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
270
291
|
|
|
271
292
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
272
293
|
|
|
294
|
+
# Track what status we write to refactor-list.json
|
|
295
|
+
new_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
296
|
+
|
|
273
297
|
if session_status == "success":
|
|
274
|
-
|
|
298
|
+
new_status = "completed"
|
|
275
299
|
rs["resume_from_phase"] = None
|
|
276
300
|
err = update_refactor_in_list(refactor_list_path, refactor_id, "completed")
|
|
277
301
|
if err:
|
|
278
302
|
error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
279
303
|
return
|
|
280
304
|
elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
281
|
-
# Degraded outcome: keep artifacts for retry.
|
|
282
|
-
# Write schema-valid status to refactor-list.json ("pending" for retry,
|
|
283
|
-
# "failed" if max retries exceeded). Store the granular degraded reason
|
|
284
|
-
# in status.json only (internal pipeline state, not schema-bound).
|
|
285
305
|
rs["retry_count"] = rs.get("retry_count", 0) + 1
|
|
286
306
|
|
|
287
307
|
if rs["retry_count"] >= max_retries:
|
|
288
|
-
|
|
289
|
-
target_status = "failed"
|
|
308
|
+
new_status = "failed"
|
|
290
309
|
else:
|
|
291
|
-
|
|
292
|
-
rs["status"] = session_status
|
|
293
|
-
# refactor-list.json gets schema-valid "pending" (will be retried)
|
|
294
|
-
target_status = "pending"
|
|
310
|
+
new_status = "pending"
|
|
295
311
|
|
|
296
312
|
rs["degraded_reason"] = session_status
|
|
297
313
|
rs["resume_from_phase"] = None
|
|
298
314
|
rs["sessions"] = []
|
|
299
315
|
rs["last_session_id"] = None
|
|
300
316
|
|
|
301
|
-
err = update_refactor_in_list(refactor_list_path, refactor_id,
|
|
317
|
+
err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
|
|
302
318
|
if err:
|
|
303
319
|
error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
304
320
|
return
|
|
@@ -312,17 +328,15 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
312
328
|
)
|
|
313
329
|
|
|
314
330
|
if rs["retry_count"] >= max_retries:
|
|
315
|
-
|
|
316
|
-
target_status = "failed"
|
|
331
|
+
new_status = "failed"
|
|
317
332
|
else:
|
|
318
|
-
|
|
319
|
-
target_status = "pending"
|
|
333
|
+
new_status = "pending"
|
|
320
334
|
|
|
321
335
|
rs["resume_from_phase"] = None
|
|
322
336
|
rs["sessions"] = []
|
|
323
337
|
rs["last_session_id"] = None
|
|
324
338
|
|
|
325
|
-
err = update_refactor_in_list(refactor_list_path, refactor_id,
|
|
339
|
+
err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
|
|
326
340
|
if err:
|
|
327
341
|
error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
328
342
|
return
|
|
@@ -343,7 +357,7 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
343
357
|
|
|
344
358
|
# Auto-skip downstream refactors when this refactor is marked as failed or skipped
|
|
345
359
|
auto_skipped_refactors = []
|
|
346
|
-
if
|
|
360
|
+
if new_status in ("failed", "skipped"):
|
|
347
361
|
auto_skipped_refactors = auto_skip_blocked_refactors(
|
|
348
362
|
refactor_list_path, state_dir, refactor_id
|
|
349
363
|
)
|
|
@@ -352,7 +366,7 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
352
366
|
"action": "update",
|
|
353
367
|
"refactor_id": refactor_id,
|
|
354
368
|
"session_status": session_status,
|
|
355
|
-
"new_status":
|
|
369
|
+
"new_status": new_status,
|
|
356
370
|
"retry_count": rs["retry_count"],
|
|
357
371
|
"resume_from_phase": rs.get("resume_from_phase"),
|
|
358
372
|
"updated_at": rs["updated_at"],
|
|
@@ -496,10 +510,9 @@ def auto_skip_blocked_refactors(refactor_list_path, state_dir, failed_refactor_i
|
|
|
496
510
|
r["status"] = "auto_skipped"
|
|
497
511
|
write_json_file(refactor_list_path, data)
|
|
498
512
|
|
|
499
|
-
#
|
|
513
|
+
# Update timestamps in status.json for each auto-skipped refactor
|
|
500
514
|
for rid in to_skip:
|
|
501
515
|
rs = load_refactor_status(state_dir, rid)
|
|
502
|
-
rs["status"] = "auto_skipped"
|
|
503
516
|
rs["updated_at"] = now_iso()
|
|
504
517
|
save_refactor_status(state_dir, rid, rs)
|
|
505
518
|
|
|
@@ -589,8 +602,8 @@ def action_status(refactor_list_data, state_dir):
|
|
|
589
602
|
if not rid:
|
|
590
603
|
continue
|
|
591
604
|
|
|
605
|
+
rstatus = r.get("status", "pending")
|
|
592
606
|
rs = load_refactor_status(state_dir, rid)
|
|
593
|
-
rstatus = rs.get("status", "pending")
|
|
594
607
|
retry_count = rs.get("retry_count", 0)
|
|
595
608
|
max_retries_val = rs.get("max_retries", 3)
|
|
596
609
|
resume_phase = rs.get("resume_from_phase")
|
|
@@ -688,10 +701,9 @@ def action_reset(args, refactor_list_path, state_dir):
|
|
|
688
701
|
return
|
|
689
702
|
|
|
690
703
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
691
|
-
old_status =
|
|
704
|
+
old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
692
705
|
old_retry = rs.get("retry_count", 0)
|
|
693
706
|
|
|
694
|
-
rs["status"] = "pending"
|
|
695
707
|
rs["retry_count"] = 0
|
|
696
708
|
rs["sessions"] = []
|
|
697
709
|
rs["last_session_id"] = None
|
|
@@ -760,10 +772,9 @@ def action_clean(args, refactor_list_path, state_dir):
|
|
|
760
772
|
|
|
761
773
|
# 4. Reset status
|
|
762
774
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
763
|
-
old_status =
|
|
775
|
+
old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
764
776
|
old_retry = rs.get("retry_count", 0)
|
|
765
777
|
|
|
766
|
-
rs["status"] = "pending"
|
|
767
778
|
rs["retry_count"] = 0
|
|
768
779
|
rs["sessions"] = []
|
|
769
780
|
rs["last_session_id"] = None
|
|
@@ -834,9 +845,8 @@ def action_start(args, refactor_list_path, state_dir):
|
|
|
834
845
|
return
|
|
835
846
|
|
|
836
847
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
837
|
-
old_status =
|
|
848
|
+
old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
838
849
|
|
|
839
|
-
rs["status"] = "in_progress"
|
|
840
850
|
rs["updated_at"] = now_iso()
|
|
841
851
|
|
|
842
852
|
err = save_refactor_status(state_dir, refactor_id, rs)
|
|
@@ -988,10 +998,9 @@ def action_unskip(args, refactor_list_path, state_dir):
|
|
|
988
998
|
error_out("Failed to write .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
989
999
|
return
|
|
990
1000
|
|
|
991
|
-
# Reset status.json for each refactor
|
|
1001
|
+
# Reset runtime fields in status.json for each refactor
|
|
992
1002
|
for rid in to_reset:
|
|
993
1003
|
rs = load_refactor_status(state_dir, rid)
|
|
994
|
-
rs["status"] = "pending"
|
|
995
1004
|
rs["retry_count"] = 0
|
|
996
1005
|
rs["sessions"] = []
|
|
997
1006
|
rs["last_session_id"] = None
|
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
305
|
-
|
|
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
|
|
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:
|
|
340
|
+
URL: http://localhost:$DEV_PORT
|
|
326
341
|
Dev Server Command: <actual command used>
|
|
327
342
|
Steps executed: [list]
|
|
328
343
|
Screenshot: [path]
|