prizmkit 1.1.21 → 1.1.24
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/heartbeat.sh +50 -7
- 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 +40 -2
- package/bundled/dev-pipeline/run-feature.sh +41 -1
- package/bundled/dev-pipeline/run-refactor.sh +40 -2
- package/bundled/dev-pipeline/scripts/detect-stuck.py +25 -14
- 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 +50 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +50 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +50 -7
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +20 -0
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +84 -5
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +7 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +10 -3
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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
|