prizmkit 1.1.21 → 1.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/lib/heartbeat.sh +50 -7
  3. package/bundled/dev-pipeline/reset-bug.sh +21 -13
  4. package/bundled/dev-pipeline/reset-feature.sh +21 -13
  5. package/bundled/dev-pipeline/reset-refactor.sh +21 -13
  6. package/bundled/dev-pipeline/run-bugfix.sh +40 -2
  7. package/bundled/dev-pipeline/run-feature.sh +41 -1
  8. package/bundled/dev-pipeline/run-refactor.sh +40 -2
  9. package/bundled/dev-pipeline/scripts/detect-stuck.py +25 -14
  10. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +0 -5
  11. package/bundled/dev-pipeline/scripts/init-pipeline.py +0 -5
  12. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +0 -5
  13. package/bundled/dev-pipeline/scripts/update-bug-status.py +40 -31
  14. package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -60
  15. package/bundled/dev-pipeline/scripts/update-refactor-status.py +43 -34
  16. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +50 -7
  17. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +50 -7
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +50 -7
  19. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +20 -0
  20. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +84 -5
  21. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -0
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -0
  23. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +7 -0
  24. package/bundled/dev-pipeline/tests/test_auto_skip.py +10 -3
  25. package/bundled/skills/_metadata.json +1 -1
  26. package/package.json +1 -1
@@ -283,13 +283,8 @@ def create_state_directory(state_dir, feature_list_path, features):
283
283
  sessions_dir = os.path.join(feature_dir, "sessions")
284
284
  os.makedirs(sessions_dir, exist_ok=True)
285
285
 
286
- # Respect existing terminal status from .prizmkit/plans/feature-list.json
287
- fl_status = feature.get("status", "pending")
288
- init_status = fl_status if fl_status in TERMINAL_STATUSES else "pending"
289
-
290
286
  feature_status = {
291
287
  "feature_id": fid,
292
- "status": init_status,
293
288
  "retry_count": 0,
294
289
  "max_retries": 3,
295
290
  "sessions": [],
@@ -325,13 +325,8 @@ def create_state_directory(state_dir, refactor_list_path, refactors):
325
325
  sessions_dir = os.path.join(refactor_dir, "sessions")
326
326
  os.makedirs(sessions_dir, exist_ok=True)
327
327
 
328
- # Respect existing terminal status from refactor-list.json
329
- rl_status = refactor.get("status", "pending")
330
- init_status = rl_status if rl_status in TERMINAL_STATUSES else "pending"
331
-
332
328
  refactor_status = {
333
329
  "refactor_id": rid,
334
- "status": init_status,
335
330
  "retry_count": 0,
336
331
  "max_retries": 3,
337
332
  "sessions": [],
@@ -84,12 +84,17 @@ def now_iso():
84
84
 
85
85
 
86
86
  def load_bug_status(state_dir, bug_id):
87
+ """Load runtime state from status.json for a bug.
88
+
89
+ Returns runtime fields only (retry_count, sessions, etc.).
90
+ The 'status' field is NOT included — status lives exclusively
91
+ in bug-fix-list.json.
92
+ """
87
93
  status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
88
94
  if not os.path.isfile(status_path):
89
95
  now = now_iso()
90
96
  return {
91
97
  "bug_id": bug_id,
92
- "status": "pending",
93
98
  "retry_count": 0,
94
99
  "max_retries": 3,
95
100
  "sessions": [],
@@ -103,7 +108,6 @@ def load_bug_status(state_dir, bug_id):
103
108
  now = now_iso()
104
109
  return {
105
110
  "bug_id": bug_id,
106
- "status": "pending",
107
111
  "retry_count": 0,
108
112
  "max_retries": 3,
109
113
  "sessions": [],
@@ -112,14 +116,30 @@ def load_bug_status(state_dir, bug_id):
112
116
  "created_at": now,
113
117
  "updated_at": now,
114
118
  }
119
+ # Defensively remove status if present (legacy data)
120
+ data.pop("status", None)
115
121
  return data
116
122
 
117
123
 
118
124
  def save_bug_status(state_dir, bug_id, status_data):
125
+ """Write the status.json for a bug (runtime fields only)."""
126
+ # Defensively strip status — it belongs in bug-fix-list.json
127
+ status_data.pop("status", None)
119
128
  status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
120
129
  return write_json_file(status_path, status_data)
121
130
 
122
131
 
132
+ def get_bug_status_from_list(bug_list_path, bug_id):
133
+ """Read a single bug's status from bug-fix-list.json."""
134
+ data, err = load_json_file(bug_list_path)
135
+ if err:
136
+ return "pending"
137
+ for b in data.get("bugs", []):
138
+ if isinstance(b, dict) and b.get("id") == bug_id:
139
+ return b.get("status", "pending")
140
+ return "pending"
141
+
142
+
123
143
  def update_bug_in_list(bug_list_path, bug_id, new_status):
124
144
  data, err = load_json_file(bug_list_path)
125
145
  if err:
@@ -153,7 +173,7 @@ def action_get_next(bug_list_data, state_dir):
153
173
  print("PIPELINE_COMPLETE")
154
174
  return
155
175
 
156
- # Build status map
176
+ # Build status map from bug-fix-list.json (single source of truth)
157
177
  status_map = {}
158
178
  status_data_map = {}
159
179
  for bug in bugs:
@@ -162,8 +182,8 @@ def action_get_next(bug_list_data, state_dir):
162
182
  bid = bug.get("id")
163
183
  if not bid:
164
184
  continue
185
+ status_map[bid] = bug.get("status", "pending")
165
186
  bs = load_bug_status(state_dir, bid)
166
- status_map[bid] = bs.get("status", "pending")
167
187
  status_data_map[bid] = bs
168
188
 
169
189
  # Check if all bugs are terminal
@@ -237,35 +257,30 @@ def action_update(args, bug_list_path, state_dir):
237
257
 
238
258
  bs = load_bug_status(state_dir, bug_id)
239
259
 
260
+ # Track what status we write to bug-fix-list.json
261
+ new_status = get_bug_status_from_list(bug_list_path, bug_id)
262
+
240
263
  if session_status == "success":
241
- 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