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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.21",
3
- "bundledAt": "2026-04-11T11:23:13.654Z",
4
- "bundledFrom": "5137496"
2
+ "frameworkVersion": "1.1.23",
3
+ "bundledAt": "2026-04-11T14:45:05.180Z",
4
+ "bundledFrom": "fbba566"
5
5
  }
@@ -139,22 +139,20 @@ fi
139
139
  BUG_IDS=()
140
140
 
141
141
  if [[ -n "$FILTER_MODE" ]]; then
142
- # Filter by status from .prizmkit/state/bugfix/bugs/*/status.json
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, os, sys
147
- state_dir = '$STATE_DIR'
146
+ import json, sys
148
147
  filter_mode = '$FILTER_MODE'
149
- bugs_dir = os.path.join(state_dir, 'bugs')
150
- if not os.path.isdir(bugs_dir):
151
- sys.exit(0)
152
- for bid in sorted(os.listdir(bugs_dir)):
153
- status_file = os.path.join(bugs_dir, bid, 'status.json')
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
- with open(status_file) as f:
157
- status = json.load(f).get('status', '')
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 "No status file found (never executed)"
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 state/features/*/status.json
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, os, sys
147
- state_dir = '$STATE_DIR'
146
+ import json, sys
148
147
  filter_mode = '$FILTER_MODE'
149
- features_dir = os.path.join(state_dir, 'features')
150
- if not os.path.isdir(features_dir):
151
- sys.exit(0)
152
- for fid in sorted(os.listdir(features_dir)):
153
- status_file = os.path.join(features_dir, fid, 'status.json')
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
- with open(status_file) as f:
157
- status = json.load(f).get('status', '')
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 "No status file found (never executed)"
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 .prizmkit/state/refactor/refactors/*/status.json
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, os, sys
137
- state_dir = '$STATE_DIR'
136
+ import json, sys
138
137
  filter_mode = '$FILTER_MODE'
139
- refactors_dir = os.path.join(state_dir, 'refactors')
140
- if not os.path.isdir(refactors_dir):
141
- sys.exit(0)
142
- for rid in sorted(os.listdir(refactors_dir)):
143
- status_file = os.path.join(refactors_dir, rid, 'status.json')
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
- with open(status_file) as f:
147
- status = json.load(f).get('status', '')
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 "No status file found (never executed)"
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
- Uses last_session_id from the item's own status to find the active session.
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 = item_status.get("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 in state
320
+ # Check each dependency's status from the task list
312
321
  for dep_id in deps:
313
- dep_status_path = os.path.join(
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
- # Cannot read status skip silently
380
- return []
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
- bs["status"] = "completed"
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
- bs["status"] = "failed"
256
- target_status = "failed"
274
+ new_status = "failed"
257
275
  else:
258
- # status.json keeps the granular degraded reason for diagnostics
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, target_status)
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
- bs["status"] = "failed"
283
- target_status = "failed"
297
+ new_status = "failed"
284
298
  else:
285
- bs["status"] = "pending"
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, target_status)
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": bs["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 = bs.get("status", "unknown")
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 = bs.get("status", "unknown")
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 = bs.get("status", "pending")
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, feature_list_status=None):
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
- If the file does not exist, return a default pending status.
160
-
161
- If feature_list_status is a terminal status (completed, failed, skipped),
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
- data = {
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
- data = {
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
- # .prizmkit/plans/feature-list.json wins for terminal statuses
199
- if feature_list_status in TERMINAL_STATUSES:
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
- # Sync status.json for each auto-skipped feature
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 -> full status data
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
- fs = load_feature_status(state_dir, fid, feature.get("status"))
468
- status_map[fid] = fs.get("status", "pending")
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 = fs.get("status") == "completed" and fs.get("resume_from_phase") is None
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": fs.get("status", "completed"),
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
- fs["status"] = "completed"
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
- fs["status"] = "failed"
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
- target_status = "pending"
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, target_status)
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
- fs["status"] = "failed"
642
- target_status = "failed"
645
+ new_status = "failed"
643
646
  else:
644
- fs["status"] = "pending"
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, target_status)
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 fs["status"] in ("failed", "skipped"):
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": fs["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 for terminal status override
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
- fs = load_feature_status(state_dir, fid, fl_status_map.get(fid))
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
- fs = load_feature_status(state_dir, fid, fl_status_map.get(fid))
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, feature.get("status"))
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 = fs.get("status", "pending")
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 (status -> pending, retry_count -> 0, clear sessions,
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 = fs.get("status", "unknown")
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 = fs.get("status", "unknown")
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 and completed set
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
- rs["status"] = "completed"
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
- rs["status"] = "failed"
289
- target_status = "failed"
308
+ new_status = "failed"
290
309
  else:
291
- # status.json keeps the granular degraded reason for diagnostics
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, target_status)
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
- rs["status"] = "failed"
316
- target_status = "failed"
331
+ new_status = "failed"
317
332
  else:
318
- rs["status"] = "pending"
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, target_status)
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 rs["status"] in ("failed", "skipped"):
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": rs["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
- # Sync status.json for each auto-skipped refactor
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 = rs.get("status", "unknown")
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 = rs.get("status", "unknown")
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 = rs.get("status", "pending")
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 fs["status"] == "auto_skipped"
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
- assert fs["status"] == "pending"
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"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.21",
2
+ "version": "1.1.23",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {