prizmkit 1.1.21 → 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/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/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/tests/test_auto_skip.py +10 -3
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -139,22 +139,20 @@ fi
|
|
|
139
139
|
BUG_IDS=()
|
|
140
140
|
|
|
141
141
|
if [[ -n "$FILTER_MODE" ]]; then
|
|
142
|
-
# Filter by status from .
|
|
142
|
+
# Filter by status from bug-fix-list.json (single source of truth)
|
|
143
143
|
while IFS= read -r bid; do
|
|
144
144
|
[[ -n "$bid" ]] && BUG_IDS+=("$bid")
|
|
145
145
|
done < <(python3 -c "
|
|
146
|
-
import json,
|
|
147
|
-
state_dir = '$STATE_DIR'
|
|
146
|
+
import json, sys
|
|
148
147
|
filter_mode = '$FILTER_MODE'
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for
|
|
153
|
-
|
|
154
|
-
if not os.path.isfile(status_file):
|
|
148
|
+
bug_list = '$BUG_LIST'
|
|
149
|
+
with open(bug_list) as f:
|
|
150
|
+
data = json.load(f)
|
|
151
|
+
for bug in data.get('bugs', []):
|
|
152
|
+
if not isinstance(bug, dict):
|
|
155
153
|
continue
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
bid = bug.get('id', '')
|
|
155
|
+
status = bug.get('status', '')
|
|
158
156
|
if filter_mode == 'auto_skipped' and status == 'auto_skipped':
|
|
159
157
|
print(bid)
|
|
160
158
|
elif filter_mode == 'failed' and status == 'failed':
|
|
@@ -244,13 +242,23 @@ sys.exit(1)
|
|
|
244
242
|
echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
|
|
245
243
|
|
|
246
244
|
STATUS_FILE="$STATE_DIR/bugs/$CUR_BUG_ID/status.json"
|
|
245
|
+
# Read status from bug-fix-list.json (single source of truth)
|
|
246
|
+
CURRENT_STATUS=$(python3 -c "
|
|
247
|
+
import json, sys
|
|
248
|
+
with open('$BUG_LIST') as f:
|
|
249
|
+
data = json.load(f)
|
|
250
|
+
for bug in data.get('bugs', []):
|
|
251
|
+
if isinstance(bug, dict) and bug.get('id') == '$CUR_BUG_ID':
|
|
252
|
+
print(bug.get('status', '?'))
|
|
253
|
+
sys.exit(0)
|
|
254
|
+
print('?')
|
|
255
|
+
" 2>/dev/null || echo "?")
|
|
247
256
|
if [[ -f "$STATUS_FILE" ]]; then
|
|
248
|
-
CURRENT_STATUS=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('status','?'))")
|
|
249
257
|
CURRENT_RETRY=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('retry_count',0))")
|
|
250
258
|
SESSION_COUNT=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(len(d.get('sessions',[])))")
|
|
251
259
|
log_info "Current status: $CURRENT_STATUS (retry $CURRENT_RETRY, $SESSION_COUNT sessions)"
|
|
252
260
|
else
|
|
253
|
-
log_info "
|
|
261
|
+
log_info "Current status: $CURRENT_STATUS (no runtime state file)"
|
|
254
262
|
fi
|
|
255
263
|
|
|
256
264
|
BUGFIX_DIR="$PROJECT_ROOT/.prizmkit/bugfix/$CUR_BUG_ID"
|
|
@@ -139,22 +139,20 @@ fi
|
|
|
139
139
|
FEATURE_IDS=()
|
|
140
140
|
|
|
141
141
|
if [[ -n "$FILTER_MODE" ]]; then
|
|
142
|
-
# Filter by status from
|
|
142
|
+
# Filter by status from feature-list.json (single source of truth)
|
|
143
143
|
while IFS= read -r fid; do
|
|
144
144
|
[[ -n "$fid" ]] && FEATURE_IDS+=("$fid")
|
|
145
145
|
done < <(python3 -c "
|
|
146
|
-
import json,
|
|
147
|
-
state_dir = '$STATE_DIR'
|
|
146
|
+
import json, sys
|
|
148
147
|
filter_mode = '$FILTER_MODE'
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for
|
|
153
|
-
|
|
154
|
-
if not os.path.isfile(status_file):
|
|
148
|
+
feature_list = '$FEATURE_LIST'
|
|
149
|
+
with open(feature_list) as f:
|
|
150
|
+
data = json.load(f)
|
|
151
|
+
for feat in data.get('features', []):
|
|
152
|
+
if not isinstance(feat, dict):
|
|
155
153
|
continue
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
fid = feat.get('id', '')
|
|
155
|
+
status = feat.get('status', '')
|
|
158
156
|
if filter_mode == 'auto_skipped' and status == 'auto_skipped':
|
|
159
157
|
print(fid)
|
|
160
158
|
elif filter_mode == 'failed' and status == 'failed':
|
|
@@ -253,13 +251,23 @@ sys.exit(1)
|
|
|
253
251
|
echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
|
|
254
252
|
|
|
255
253
|
STATUS_FILE="$STATE_DIR/features/$CUR_FEATURE_ID/status.json"
|
|
254
|
+
# Read status from feature-list.json (single source of truth)
|
|
255
|
+
CURRENT_STATUS=$(python3 -c "
|
|
256
|
+
import json, sys
|
|
257
|
+
with open('$FEATURE_LIST') as f:
|
|
258
|
+
data = json.load(f)
|
|
259
|
+
for feat in data.get('features', []):
|
|
260
|
+
if isinstance(feat, dict) and feat.get('id') == '$CUR_FEATURE_ID':
|
|
261
|
+
print(feat.get('status', '?'))
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
print('?')
|
|
264
|
+
" 2>/dev/null || echo "?")
|
|
256
265
|
if [[ -f "$STATUS_FILE" ]]; then
|
|
257
|
-
CURRENT_STATUS=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('status','?'))")
|
|
258
266
|
CURRENT_RETRY=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('retry_count',0))")
|
|
259
267
|
SESSION_COUNT=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(len(d.get('sessions',[])))")
|
|
260
268
|
log_info "Current status: $CURRENT_STATUS (retry $CURRENT_RETRY, $SESSION_COUNT sessions)"
|
|
261
269
|
else
|
|
262
|
-
log_info "
|
|
270
|
+
log_info "Current status: $CURRENT_STATUS (no runtime state file)"
|
|
263
271
|
fi
|
|
264
272
|
|
|
265
273
|
SPECS_DIR="$PROJECT_ROOT/.prizmkit/specs/$FEATURE_SLUG"
|
|
@@ -129,22 +129,20 @@ fi
|
|
|
129
129
|
REFACTOR_IDS=()
|
|
130
130
|
|
|
131
131
|
if [[ -n "$FILTER_MODE" ]]; then
|
|
132
|
-
# Filter by status from
|
|
132
|
+
# Filter by status from refactor-list.json (single source of truth)
|
|
133
133
|
while IFS= read -r rid; do
|
|
134
134
|
[[ -n "$rid" ]] && REFACTOR_IDS+=("$rid")
|
|
135
135
|
done < <(python3 -c "
|
|
136
|
-
import json,
|
|
137
|
-
state_dir = '$STATE_DIR'
|
|
136
|
+
import json, sys
|
|
138
137
|
filter_mode = '$FILTER_MODE'
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for
|
|
143
|
-
|
|
144
|
-
if not os.path.isfile(status_file):
|
|
138
|
+
refactor_list = '$REFACTOR_LIST'
|
|
139
|
+
with open(refactor_list) as f:
|
|
140
|
+
data = json.load(f)
|
|
141
|
+
for r in data.get('refactors', []):
|
|
142
|
+
if not isinstance(r, dict):
|
|
145
143
|
continue
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
rid = r.get('id', '')
|
|
145
|
+
status = r.get('status', '')
|
|
148
146
|
if filter_mode == 'auto_skipped' and status == 'auto_skipped':
|
|
149
147
|
print(rid)
|
|
150
148
|
elif filter_mode == 'failed' and status == 'failed':
|
|
@@ -242,13 +240,23 @@ sys.exit(1)
|
|
|
242
240
|
echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
|
|
243
241
|
|
|
244
242
|
STATUS_FILE="$STATE_DIR/refactors/$CUR_REFACTOR_ID/status.json"
|
|
243
|
+
# Read status from refactor-list.json (single source of truth)
|
|
244
|
+
CURRENT_STATUS=$(python3 -c "
|
|
245
|
+
import json, sys
|
|
246
|
+
with open('$REFACTOR_LIST') as f:
|
|
247
|
+
data = json.load(f)
|
|
248
|
+
for r in data.get('refactors', []):
|
|
249
|
+
if isinstance(r, dict) and r.get('id') == '$CUR_REFACTOR_ID':
|
|
250
|
+
print(r.get('status', '?'))
|
|
251
|
+
sys.exit(0)
|
|
252
|
+
print('?')
|
|
253
|
+
" 2>/dev/null || echo "?")
|
|
245
254
|
if [[ -f "$STATUS_FILE" ]]; then
|
|
246
|
-
CURRENT_STATUS=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('status','?'))")
|
|
247
255
|
CURRENT_RETRY=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(d.get('retry_count',0))")
|
|
248
256
|
SESSION_COUNT=$(python3 -c "import json; d=json.load(open('$STATUS_FILE')); print(len(d.get('sessions',[])))")
|
|
249
257
|
log_info "Current status: $CURRENT_STATUS (retry $CURRENT_RETRY, $SESSION_COUNT sessions)"
|
|
250
258
|
else
|
|
251
|
-
log_info "
|
|
259
|
+
log_info "Current status: $CURRENT_STATUS (no runtime state file)"
|
|
252
260
|
fi
|
|
253
261
|
|
|
254
262
|
SPECS_DIR="$PROJECT_ROOT/.prizmkit/specs/$REFACTOR_SLUG"
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -122,8 +122,13 @@ class TestAutoSkipLinearChain:
|
|
|
122
122
|
|
|
123
123
|
auto_skip_blocked_features(fl_path, state_dir, "F-001")
|
|
124
124
|
|
|
125
|
+
# status lives in feature-list.json, not status.json
|
|
126
|
+
statuses = _read_statuses(fl_path)
|
|
127
|
+
assert statuses["F-002"] == "auto_skipped"
|
|
128
|
+
# status.json should have updated_at but no status field
|
|
125
129
|
fs = load_feature_status(state_dir, "F-002")
|
|
126
|
-
assert
|
|
130
|
+
assert "status" not in fs
|
|
131
|
+
assert "updated_at" in fs
|
|
127
132
|
|
|
128
133
|
def test_failing_mid_skips_only_downstream(self, tmp_path):
|
|
129
134
|
features = [
|
|
@@ -434,7 +439,6 @@ class TestAutoSkipIntegration:
|
|
|
434
439
|
with open(fs_path) as f:
|
|
435
440
|
fs = json.load(f)
|
|
436
441
|
fs["retry_count"] = 3
|
|
437
|
-
fs["status"] = "failed"
|
|
438
442
|
with open(fs_path, "w") as f:
|
|
439
443
|
json.dump(fs, f)
|
|
440
444
|
|
|
@@ -443,4 +447,7 @@ class TestAutoSkipIntegration:
|
|
|
443
447
|
with open(fs_path) as f:
|
|
444
448
|
fs = json.load(f)
|
|
445
449
|
assert fs["retry_count"] == 0
|
|
446
|
-
|
|
450
|
+
# status lives in feature-list.json, not status.json
|
|
451
|
+
assert "status" not in fs
|
|
452
|
+
statuses = _read_statuses(fl_path)
|
|
453
|
+
assert statuses["F-001"] == "pending"
|