prizmkit 1.1.91 → 1.1.93

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 (46) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +0 -4
  3. package/bundled/dev-pipeline/README.md +9 -39
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +0 -4
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -2
  6. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -1
  7. package/bundled/dev-pipeline/launch-feature-daemon.sh +2 -3
  8. package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -1
  9. package/bundled/dev-pipeline/lib/branch.sh +8 -5
  10. package/bundled/dev-pipeline/lib/common.sh +122 -0
  11. package/bundled/dev-pipeline/lib/heartbeat.sh +13 -3
  12. package/bundled/dev-pipeline/run-bugfix.sh +149 -98
  13. package/bundled/dev-pipeline/run-feature.sh +90 -103
  14. package/bundled/dev-pipeline/run-recovery.sh +1 -32
  15. package/bundled/dev-pipeline/run-refactor.sh +148 -99
  16. package/bundled/dev-pipeline/scripts/update-bug-status.py +39 -4
  17. package/bundled/dev-pipeline/scripts/update-feature-status.py +43 -6
  18. package/bundled/dev-pipeline/scripts/update-refactor-status.py +36 -4
  19. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +3 -3
  20. package/bundled/dev-pipeline/tests/test_auto_skip.py +92 -3
  21. package/bundled/dev-pipeline-windows/.env.example +0 -4
  22. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -4
  23. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -2
  24. package/bundled/dev-pipeline-windows/lib/branch.ps1 +17 -4
  25. package/bundled/dev-pipeline-windows/lib/common.ps1 +9 -0
  26. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +89 -108
  27. package/bundled/dev-pipeline-windows/run-recovery.ps1 +4 -19
  28. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +39 -4
  29. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +43 -6
  30. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +36 -4
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +1 -1
  33. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +5 -15
  34. package/bundled/skills/feature-pipeline-launcher/SKILL.md +4 -4
  35. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +3 -13
  36. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +3 -3
  37. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +4 -14
  38. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +1 -1
  39. package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +4 -14
  40. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +4 -4
  41. package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +2 -12
  42. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +3 -3
  43. package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +4 -14
  44. package/package.json +1 -1
  45. package/bundled/dev-pipeline/scripts/cleanup-logs.py +0 -192
  46. package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +0 -192
@@ -16,7 +16,7 @@ Usage:
16
16
  --bug-list <path> --state-dir <path> \
17
17
  --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
18
  [--bug-id <id>] [--session-status <status>] \
19
- [--session-id <id>] [--max-retries <n>]
19
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
20
20
  """
21
21
 
22
22
  import argparse
@@ -75,7 +75,8 @@ def parse_args():
75
75
  help="Session outcome status (required for 'update' action)",
76
76
  )
77
77
  parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
78
- parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
78
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum code retry count (default: 3)")
79
+ parser.add_argument("--max-infra-retries", type=int, default=3, help="Maximum infrastructure retry count (default: 3)")
79
80
  parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
80
81
  return parser.parse_args()
81
82
 
@@ -98,6 +99,9 @@ def load_bug_status(state_dir, bug_id):
98
99
  "bug_id": bug_id,
99
100
  "retry_count": 0,
100
101
  "max_retries": 3,
102
+ "max_infra_retries": 3,
103
+ "infra_error_count": 0,
104
+ "last_infra_error_session_id": None,
101
105
  "sessions": [],
102
106
  "last_session_id": None,
103
107
  "resume_from_phase": None,
@@ -111,6 +115,9 @@ def load_bug_status(state_dir, bug_id):
111
115
  "bug_id": bug_id,
112
116
  "retry_count": 0,
113
117
  "max_retries": 3,
118
+ "max_infra_retries": 3,
119
+ "infra_error_count": 0,
120
+ "last_infra_error_session_id": None,
114
121
  "sessions": [],
115
122
  "last_session_id": None,
116
123
  "resume_from_phase": None,
@@ -234,6 +241,7 @@ def action_get_next(bug_list_data, state_dir):
234
241
  "title": chosen.get("title", ""),
235
242
  "severity": chosen.get("severity", "medium"),
236
243
  "retry_count": chosen_status_data.get("retry_count", 0),
244
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
237
245
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
238
246
  }
239
247
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -248,6 +256,7 @@ def action_update(args, bug_list_path, state_dir):
248
256
  session_status = args.session_status
249
257
  session_id = args.session_id
250
258
  max_retries = args.max_retries
259
+ max_infra_retries = args.max_infra_retries
251
260
 
252
261
  if not bug_id:
253
262
  error_out("--bug-id is required for 'update' action")
@@ -262,6 +271,8 @@ def action_update(args, bug_list_path, state_dir):
262
271
  new_status = get_bug_status_from_list(bug_list_path, bug_id)
263
272
 
264
273
  if session_status == "success":
274
+ bs["infra_error_count"] = 0
275
+ bs["last_infra_error_session_id"] = None
265
276
  new_status = "completed"
266
277
  bs["resume_from_phase"] = None
267
278
  err = update_bug_in_list(bug_list_path, bug_id, "completed")
@@ -286,11 +297,25 @@ def action_update(args, bug_list_path, state_dir):
286
297
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
287
298
  return
288
299
  elif session_status == "infra_error":
289
- new_status = "pending"
290
- bs["infra_error_count"] = bs.get("infra_error_count", 0) + 1
300
+ # AI CLI/provider outage, auth failure, gateway error, etc.
301
+ # Infra failures do not consume the code retry budget, but they still
302
+ # need their own bounded budget so a flaky provider cannot loop forever.
303
+ infra_error_count = bs.get("infra_error_count", 0) + 1
304
+ bs["infra_error_count"] = infra_error_count
291
305
  bs["last_infra_error_session_id"] = session_id
306
+ bs["max_infra_retries"] = max_infra_retries
307
+ bs["degraded_reason"] = "infra_error"
308
+ if session_id:
309
+ bs["last_session_id"] = session_id
292
310
  bs["resume_from_phase"] = None
293
311
 
312
+ if infra_error_count >= max_infra_retries:
313
+ new_status = "failed"
314
+ if session_id:
315
+ bs["last_failed_session_id"] = session_id
316
+ else:
317
+ new_status = "pending"
318
+
294
319
  err = update_bug_in_list(bug_list_path, bug_id, new_status)
295
320
  if err:
296
321
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
@@ -338,6 +363,8 @@ def action_update(args, bug_list_path, state_dir):
338
363
  "session_status": session_status,
339
364
  "new_status": new_status,
340
365
  "retry_count": bs["retry_count"],
366
+ "infra_error_count": bs.get("infra_error_count", 0),
367
+ "max_infra_retries": max_infra_retries,
341
368
  "resume_from_phase": bs.get("resume_from_phase"),
342
369
  "updated_at": bs["updated_at"],
343
370
  }
@@ -577,6 +604,8 @@ def action_reset(args, bug_list_path, state_dir):
577
604
  old_retry = bs.get("retry_count", 0)
578
605
 
579
606
  bs["retry_count"] = 0
607
+ bs["infra_error_count"] = 0
608
+ bs["last_infra_error_session_id"] = None
580
609
  bs["sessions"] = []
581
610
  bs["last_session_id"] = None
582
611
  bs["resume_from_phase"] = None
@@ -652,6 +681,8 @@ def action_clean(args, bug_list_path, state_dir):
652
681
  old_retry = bs.get("retry_count", 0)
653
682
 
654
683
  bs["retry_count"] = 0
684
+ bs["infra_error_count"] = 0
685
+ bs["last_infra_error_session_id"] = None
655
686
  bs["sessions"] = []
656
687
  bs["last_session_id"] = None
657
688
  bs["resume_from_phase"] = None
@@ -740,6 +771,8 @@ def action_start(args, bug_list_path, state_dir):
740
771
  "bug_id": bug_id,
741
772
  "old_status": old_status,
742
773
  "new_status": "in_progress",
774
+ "retry_count": bs.get("retry_count", 0),
775
+ "infra_error_count": bs.get("infra_error_count", 0),
743
776
  "updated_at": bs["updated_at"],
744
777
  }
745
778
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -816,6 +849,8 @@ def action_unskip(args, bug_list_path, state_dir):
816
849
  for bid in to_reset:
817
850
  bs = load_bug_status(state_dir, bid)
818
851
  bs["retry_count"] = 0
852
+ bs["infra_error_count"] = 0
853
+ bs["last_infra_error_session_id"] = None
819
854
  bs["sessions"] = []
820
855
  bs["last_session_id"] = None
821
856
  bs["resume_from_phase"] = None
@@ -17,7 +17,7 @@ Usage:
17
17
  --feature-list <path> --state-dir <path> \
18
18
  --action <get_next|start|update|status|pause|reset|clean|complete|unskip> \
19
19
  [--feature-id <id>] [--session-status <status>] \
20
- [--session-id <id>] [--max-retries <n>] \
20
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>] \
21
21
  [--features <filter>]
22
22
  """
23
23
 
@@ -94,7 +94,13 @@ def parse_args():
94
94
  "--max-retries",
95
95
  type=int,
96
96
  default=3,
97
- help="Maximum retry count before marking as failed (default: 3)",
97
+ help="Maximum code retry count before marking as failed (default: 3)",
98
+ )
99
+ parser.add_argument(
100
+ "--max-infra-retries",
101
+ type=int,
102
+ default=3,
103
+ help="Maximum infrastructure retry count before marking as failed (default: 3)",
98
104
  )
99
105
  parser.add_argument(
100
106
  "--feature-slug",
@@ -170,6 +176,9 @@ def load_feature_status(state_dir, feature_id):
170
176
  "feature_id": feature_id,
171
177
  "retry_count": 0,
172
178
  "max_retries": 3,
179
+ "max_infra_retries": 3,
180
+ "infra_error_count": 0,
181
+ "last_infra_error_session_id": None,
173
182
  "sessions": [],
174
183
  "last_session_id": None,
175
184
  "resume_from_phase": None,
@@ -183,6 +192,9 @@ def load_feature_status(state_dir, feature_id):
183
192
  "feature_id": feature_id,
184
193
  "retry_count": 0,
185
194
  "max_retries": 3,
195
+ "max_infra_retries": 3,
196
+ "infra_error_count": 0,
197
+ "last_infra_error_session_id": None,
186
198
  "sessions": [],
187
199
  "last_session_id": None,
188
200
  "resume_from_phase": None,
@@ -565,6 +577,7 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
565
577
  "feature_id": chosen_id,
566
578
  "title": chosen.get("title", ""),
567
579
  "retry_count": chosen_status_data.get("retry_count", 0),
580
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
568
581
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
569
582
  }
570
583
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -585,6 +598,7 @@ def action_update(args, feature_list_path, state_dir):
585
598
  session_status = args.session_status
586
599
  session_id = args.session_id
587
600
  max_retries = args.max_retries
601
+ max_infra_retries = args.max_infra_retries
588
602
 
589
603
  if not feature_id:
590
604
  error_out("--feature-id is required for 'update' action")
@@ -600,6 +614,8 @@ def action_update(args, feature_list_path, state_dir):
600
614
  new_status = current_list_status
601
615
 
602
616
  if session_status == "success":
617
+ fs["infra_error_count"] = 0
618
+ fs["last_infra_error_session_id"] = None
603
619
  # No-op guard: if this exact successful session was already recorded,
604
620
  # avoid rewriting state files again (prevents post-commit dirty changes).
605
621
  existing_sessions = fs.get("sessions", [])
@@ -616,6 +632,8 @@ def action_update(args, feature_list_path, state_dir):
616
632
  "session_status": session_status,
617
633
  "new_status": "completed",
618
634
  "retry_count": fs.get("retry_count", 0),
635
+ "infra_error_count": fs.get("infra_error_count", 0),
636
+ "max_infra_retries": max_infra_retries,
619
637
  "resume_from_phase": fs.get("resume_from_phase"),
620
638
  "updated_at": fs.get("updated_at"),
621
639
  "no_op": True,
@@ -654,15 +672,24 @@ def action_update(args, feature_list_path, state_dir):
654
672
  return
655
673
  elif session_status == "infra_error":
656
674
  # AI CLI/provider outage, auth failure, gateway error, etc.
657
- # This is outside the code's control, so keep the item pending without
658
- # consuming the task's retry budget.
659
- new_status = "pending"
660
- fs["infra_error_count"] = fs.get("infra_error_count", 0) + 1
675
+ # Infra failures do not consume the code retry budget, but they still
676
+ # need their own bounded budget so a flaky provider cannot loop forever.
677
+ infra_error_count = fs.get("infra_error_count", 0) + 1
678
+ fs["infra_error_count"] = infra_error_count
661
679
  fs["last_infra_error_session_id"] = session_id
680
+ fs["max_infra_retries"] = max_infra_retries
681
+ fs["degraded_reason"] = "infra_error"
662
682
  if session_id:
663
683
  fs["last_session_id"] = session_id
664
684
  fs["resume_from_phase"] = None
665
685
 
686
+ if infra_error_count >= max_infra_retries:
687
+ new_status = "failed"
688
+ if session_id:
689
+ fs["last_failed_session_id"] = session_id
690
+ else:
691
+ new_status = "pending"
692
+
666
693
  err = update_feature_in_list(feature_list_path, feature_id, new_status)
667
694
  if err:
668
695
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
@@ -714,6 +741,8 @@ def action_update(args, feature_list_path, state_dir):
714
741
  "session_status": session_status,
715
742
  "new_status": new_status,
716
743
  "retry_count": fs["retry_count"],
744
+ "infra_error_count": fs.get("infra_error_count", 0),
745
+ "max_infra_retries": max_infra_retries,
717
746
  "resume_from_phase": fs.get("resume_from_phase"),
718
747
  "updated_at": fs["updated_at"],
719
748
  }
@@ -1143,6 +1172,8 @@ def action_start(args, feature_list_path, state_dir):
1143
1172
  "feature_id": feature_id,
1144
1173
  "old_status": old_status,
1145
1174
  "new_status": "in_progress",
1175
+ "retry_count": fs.get("retry_count", 0),
1176
+ "infra_error_count": fs.get("infra_error_count", 0),
1146
1177
  "updated_at": fs["updated_at"],
1147
1178
  }
1148
1179
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -1171,6 +1202,8 @@ def action_reset(args, feature_list_path, state_dir):
1171
1202
 
1172
1203
  # Reset runtime fields
1173
1204
  fs["retry_count"] = 0
1205
+ fs["infra_error_count"] = 0
1206
+ fs["last_infra_error_session_id"] = None
1174
1207
  fs["sessions"] = []
1175
1208
  fs["last_session_id"] = None
1176
1209
  fs["resume_from_phase"] = None
@@ -1264,6 +1297,8 @@ def action_clean(args, feature_list_path, state_dir):
1264
1297
  old_retry = fs.get("retry_count", 0)
1265
1298
 
1266
1299
  fs["retry_count"] = 0
1300
+ fs["infra_error_count"] = 0
1301
+ fs["last_infra_error_session_id"] = None
1267
1302
  fs["sessions"] = []
1268
1303
  fs["last_session_id"] = None
1269
1304
  fs["resume_from_phase"] = None
@@ -1423,6 +1458,8 @@ def action_unskip(args, feature_list_path, state_dir):
1423
1458
  for fid in to_reset:
1424
1459
  fs = load_feature_status(state_dir, fid)
1425
1460
  fs["retry_count"] = 0
1461
+ fs["infra_error_count"] = 0
1462
+ fs["last_infra_error_session_id"] = None
1426
1463
  fs["sessions"] = []
1427
1464
  fs["last_session_id"] = None
1428
1465
  fs["resume_from_phase"] = None
@@ -16,7 +16,7 @@ Usage:
16
16
  --refactor-list <path> --state-dir <path> \
17
17
  --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
18
  [--refactor-id <id>] [--session-status <status>] \
19
- [--session-id <id>] [--max-retries <n>]
19
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
20
20
  """
21
21
 
22
22
  import argparse
@@ -87,7 +87,8 @@ def parse_args():
87
87
  help="Session outcome status (required for 'update' action)",
88
88
  )
89
89
  parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
90
- parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
90
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum code retry count (default: 3)")
91
+ parser.add_argument("--max-infra-retries", type=int, default=3, help="Maximum infrastructure retry count (default: 3)")
91
92
  parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
92
93
  return parser.parse_args()
93
94
 
@@ -103,6 +104,9 @@ def _default_status(refactor_id):
103
104
  "refactor_id": refactor_id,
104
105
  "retry_count": 0,
105
106
  "max_retries": 3,
107
+ "max_infra_retries": 3,
108
+ "infra_error_count": 0,
109
+ "last_infra_error_session_id": None,
106
110
  "sessions": [],
107
111
  "last_session_id": None,
108
112
  "resume_from_phase": None,
@@ -268,6 +272,7 @@ def action_get_next(refactor_list_data, state_dir):
268
272
  "priority": chosen.get("priority", "medium"),
269
273
  "complexity": chosen.get("complexity", "medium"),
270
274
  "retry_count": chosen_status_data.get("retry_count", 0),
275
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
271
276
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
272
277
  }
273
278
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -282,6 +287,7 @@ def action_update(args, refactor_list_path, state_dir):
282
287
  session_status = args.session_status
283
288
  session_id = args.session_id
284
289
  max_retries = args.max_retries
290
+ max_infra_retries = args.max_infra_retries
285
291
 
286
292
  if not refactor_id:
287
293
  error_out("--refactor-id is required for 'update' action")
@@ -296,6 +302,8 @@ def action_update(args, refactor_list_path, state_dir):
296
302
  new_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
297
303
 
298
304
  if session_status == "success":
305
+ rs["infra_error_count"] = 0
306
+ rs["last_infra_error_session_id"] = None
299
307
  new_status = "completed"
300
308
  rs["resume_from_phase"] = None
301
309
  err = update_refactor_in_list(refactor_list_path, refactor_id, "completed")
@@ -320,11 +328,25 @@ def action_update(args, refactor_list_path, state_dir):
320
328
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
321
329
  return
322
330
  elif session_status == "infra_error":
323
- new_status = "pending"
324
- rs["infra_error_count"] = rs.get("infra_error_count", 0) + 1
331
+ # AI CLI/provider outage, auth failure, gateway error, etc.
332
+ # Infra failures do not consume the code retry budget, but they still
333
+ # need their own bounded budget so a flaky provider cannot loop forever.
334
+ infra_error_count = rs.get("infra_error_count", 0) + 1
335
+ rs["infra_error_count"] = infra_error_count
325
336
  rs["last_infra_error_session_id"] = session_id
337
+ rs["max_infra_retries"] = max_infra_retries
338
+ rs["degraded_reason"] = "infra_error"
339
+ if session_id:
340
+ rs["last_session_id"] = session_id
326
341
  rs["resume_from_phase"] = None
327
342
 
343
+ if infra_error_count >= max_infra_retries:
344
+ new_status = "failed"
345
+ if session_id:
346
+ rs["last_failed_session_id"] = session_id
347
+ else:
348
+ new_status = "pending"
349
+
328
350
  err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
329
351
  if err:
330
352
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
@@ -379,6 +401,8 @@ def action_update(args, refactor_list_path, state_dir):
379
401
  "session_status": session_status,
380
402
  "new_status": new_status,
381
403
  "retry_count": rs["retry_count"],
404
+ "infra_error_count": rs.get("infra_error_count", 0),
405
+ "max_infra_retries": max_infra_retries,
382
406
  "resume_from_phase": rs.get("resume_from_phase"),
383
407
  "updated_at": rs["updated_at"],
384
408
  }
@@ -728,6 +752,8 @@ def action_reset(args, refactor_list_path, state_dir):
728
752
  old_retry = rs.get("retry_count", 0)
729
753
 
730
754
  rs["retry_count"] = 0
755
+ rs["infra_error_count"] = 0
756
+ rs["last_infra_error_session_id"] = None
731
757
  rs["sessions"] = []
732
758
  rs["last_session_id"] = None
733
759
  rs["resume_from_phase"] = None
@@ -799,6 +825,8 @@ def action_clean(args, refactor_list_path, state_dir):
799
825
  old_retry = rs.get("retry_count", 0)
800
826
 
801
827
  rs["retry_count"] = 0
828
+ rs["infra_error_count"] = 0
829
+ rs["last_infra_error_session_id"] = None
802
830
  rs["sessions"] = []
803
831
  rs["last_session_id"] = None
804
832
  rs["resume_from_phase"] = None
@@ -887,6 +915,8 @@ def action_start(args, refactor_list_path, state_dir):
887
915
  "refactor_id": refactor_id,
888
916
  "old_status": old_status,
889
917
  "new_status": "in_progress",
918
+ "retry_count": rs.get("retry_count", 0),
919
+ "infra_error_count": rs.get("infra_error_count", 0),
890
920
  "updated_at": rs["updated_at"],
891
921
  }
892
922
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -1025,6 +1055,8 @@ def action_unskip(args, refactor_list_path, state_dir):
1025
1055
  for rid in to_reset:
1026
1056
  rs = load_refactor_status(state_dir, rid)
1027
1057
  rs["retry_count"] = 0
1058
+ rs["infra_error_count"] = 0
1059
+ rs["last_infra_error_session_id"] = None
1028
1060
  rs["sessions"] = []
1029
1061
  rs["last_session_id"] = None
1030
1062
  rs["resume_from_phase"] = None
@@ -80,10 +80,10 @@ else
80
80
  fail "incomplete_count=$count, expected 0"
81
81
  fi
82
82
 
83
- # --- Test 4: Timed-out status -> deploy blocked ---
84
- echo "[Test 4] 'timed_out' status: incomplete_count should be 1"
83
+ # --- Test 4: auto_skipped status -> deploy blocked ---
84
+ echo "[Test 4] 'auto_skipped' status: incomplete_count should be 1"
85
85
  cat > /tmp/test-deploy-timeout.json << 'JSON'
86
- {"features": [{"id": "F-A", "status": "completed"}, {"id": "F-B", "status": "timed_out"}]}
86
+ {"features": [{"id": "F-A", "status": "completed"}, {"id": "F-B", "status": "auto_skipped"}]}
87
87
  JSON
88
88
  count=$(python3 -c "
89
89
  import json
@@ -303,7 +303,7 @@ def _run_get_next(fl_path, state_dir):
303
303
  return result.stdout.strip()
304
304
 
305
305
 
306
- def _run_update(fl_path, state_dir, feature_id, session_status, session_id="session-1", max_retries=3):
306
+ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="session-1", max_retries=3, max_infra_retries=3):
307
307
  cmd = [
308
308
  "python3", _SCRIPT,
309
309
  "--feature-list", fl_path,
@@ -312,6 +312,7 @@ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="sess
312
312
  "--session-status", session_status,
313
313
  "--session-id", session_id,
314
314
  "--max-retries", str(max_retries),
315
+ "--max-infra-retries", str(max_infra_retries),
315
316
  "--action", "update",
316
317
  ]
317
318
  result = subprocess.run(cmd, capture_output=True, text=True)
@@ -320,7 +321,7 @@ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="sess
320
321
 
321
322
 
322
323
  class TestInfraErrorUpdate:
323
- def test_infra_error_keeps_pending_without_consuming_retry(self, tmp_path):
324
+ def test_infra_error_below_budget_keeps_pending_without_consuming_retry(self, tmp_path):
324
325
  features = [_make_feature("F-001", "Root", status="in_progress")]
325
326
  fl_path = _write_fl(tmp_path, features)
326
327
  state_dir = _init_state(tmp_path, ["F-001"])
@@ -329,10 +330,20 @@ class TestInfraErrorUpdate:
329
330
  fs["retry_count"] = 2
330
331
  write_json_file(status_path, fs)
331
332
 
332
- result = _run_update(fl_path, state_dir, "F-001", "infra_error", "session-infra", max_retries=3)
333
+ result = _run_update(
334
+ fl_path,
335
+ state_dir,
336
+ "F-001",
337
+ "infra_error",
338
+ "session-infra",
339
+ max_retries=3,
340
+ max_infra_retries=3,
341
+ )
333
342
 
334
343
  assert result["new_status"] == "pending"
335
344
  assert result["retry_count"] == 2
345
+ assert result["infra_error_count"] == 1
346
+ assert result["max_infra_retries"] == 3
336
347
  assert result["restart_policy"] == "infra_retry"
337
348
  assert _read_statuses(fl_path)["F-001"] == "pending"
338
349
 
@@ -340,7 +351,85 @@ class TestInfraErrorUpdate:
340
351
  assert fs["retry_count"] == 2
341
352
  assert fs["infra_error_count"] == 1
342
353
  assert fs["last_infra_error_session_id"] == "session-infra"
354
+ assert fs["max_infra_retries"] == 3
343
355
 
356
+ def test_infra_error_at_budget_marks_failed_without_consuming_code_retry(self, tmp_path):
357
+ features = [_make_feature("F-001", "Root", status="in_progress")]
358
+ fl_path = _write_fl(tmp_path, features)
359
+ state_dir = _init_state(tmp_path, ["F-001"])
360
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
361
+ fs = load_feature_status(state_dir, "F-001")
362
+ fs["retry_count"] = 1
363
+ fs["infra_error_count"] = 2
364
+ write_json_file(status_path, fs)
365
+
366
+ result = _run_update(
367
+ fl_path,
368
+ state_dir,
369
+ "F-001",
370
+ "infra_error",
371
+ "session-infra-final",
372
+ max_retries=3,
373
+ max_infra_retries=3,
374
+ )
375
+
376
+ assert result["new_status"] == "failed"
377
+ assert result["retry_count"] == 1
378
+ assert result["infra_error_count"] == 3
379
+ assert result["restart_policy"] == "infra_retry"
380
+ assert _read_statuses(fl_path)["F-001"] == "failed"
381
+
382
+ fs = load_feature_status(state_dir, "F-001")
383
+ assert fs["retry_count"] == 1
384
+ assert fs["infra_error_count"] == 3
385
+ assert fs["last_infra_error_session_id"] == "session-infra-final"
386
+ assert fs["last_failed_session_id"] == "session-infra-final"
387
+
388
+ def test_reset_clears_infra_retry_counters(self, tmp_path):
389
+ features = [_make_feature("F-001", "Root", status="failed")]
390
+ fl_path = _write_fl(tmp_path, features)
391
+ state_dir = _init_state(tmp_path, ["F-001"])
392
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
393
+ fs = load_feature_status(state_dir, "F-001")
394
+ fs["retry_count"] = 3
395
+ fs["infra_error_count"] = 3
396
+ fs["last_infra_error_session_id"] = "session-infra-final"
397
+ write_json_file(status_path, fs)
398
+
399
+ cmd = [
400
+ "python3", _SCRIPT,
401
+ "--feature-list", fl_path,
402
+ "--state-dir", state_dir,
403
+ "--feature-id", "F-001",
404
+ "--action", "reset",
405
+ ]
406
+ result = subprocess.run(cmd, capture_output=True, text=True)
407
+ assert result.returncode == 0, result.stderr
408
+
409
+ fs = load_feature_status(state_dir, "F-001")
410
+ assert fs["retry_count"] == 0
411
+ assert fs["infra_error_count"] == 0
412
+ assert fs["last_infra_error_session_id"] is None
413
+ assert _read_statuses(fl_path)["F-001"] == "pending"
414
+
415
+ def test_unskip_clears_infra_retry_counters(self, tmp_path):
416
+ features = [_make_feature("F-001", "Root", status="failed")]
417
+ fl_path = _write_fl(tmp_path, features)
418
+ state_dir = _init_state(tmp_path, ["F-001"])
419
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
420
+ fs = load_feature_status(state_dir, "F-001")
421
+ fs["retry_count"] = 3
422
+ fs["infra_error_count"] = 3
423
+ fs["last_infra_error_session_id"] = "session-infra-final"
424
+ write_json_file(status_path, fs)
425
+
426
+ _run_unskip(fl_path, state_dir, "F-001")
427
+
428
+ fs = load_feature_status(state_dir, "F-001")
429
+ assert fs["retry_count"] == 0
430
+ assert fs["infra_error_count"] == 0
431
+ assert fs["last_infra_error_session_id"] is None
432
+ assert _read_statuses(fl_path)["F-001"] == "pending"
344
433
 
345
434
  class TestUnskipByFeatureId:
346
435
  """Unskip with --feature-id targets a specific failed feature + downstream."""
@@ -19,16 +19,12 @@
19
19
 
20
20
  # ─── Pipeline Execution ──────────────────────────────────────────────
21
21
  # MAX_RETRIES=3 # Max retry attempts per task before marking failed
22
- # SESSION_TIMEOUT=0 # Session timeout in seconds (0 = no limit)
23
22
  # VERBOSE=1 # Verbose logging (1=on, 0=off)
24
23
  # HEARTBEAT_INTERVAL=30 # Poll interval for session progress/stale checks
25
24
  # STALE_KILL_THRESHOLD=900 # Auto-kill session after N seconds without parent log progress (0 = disabled)
26
25
  # STALE_KILL_GRACE_SECONDS=10 # Grace period after stale-kill before force-stopping the job
27
26
  # CODEX_WAIT_STALE_KILL_THRESHOLD=3600 # Longer no-log window while Codex waits on subagents
28
27
  # CODEX_SUBAGENT_TIMEOUT_SECONDS=3300 # Codex subagent max runtime; defaults to wait threshold - 300
29
- # LOG_CLEANUP_ENABLED=1 # Run periodic session log cleanup
30
- # LOG_RETENTION_DAYS=14 # Delete session logs older than N days
31
- # LOG_MAX_TOTAL_MB=1024 # Keep total logs under N MB via oldest-first cleanup
32
28
  # STOP_ON_FAILURE=0 # Stop after the first failed task (1=stop, 0=continue)
33
29
  # ENABLE_DEPLOY=0 # Start a deploy session after all tasks complete successfully
34
30
  # DEV_BRANCH= # Optional custom dev branch name for each task
@@ -343,16 +343,12 @@ pending, in_progress, completed, failed, skipped
343
343
  | `AI_CLI` | string | auto-detect | codex, claude, or cbc |
344
344
  | `MODEL` | string | (unset) | AI model override (per-task overrides this) |
345
345
  | `MAX_RETRIES` | integer | (not specified) | Retry attempts per task |
346
- | `SESSION_TIMEOUT` | integer | 0 | 0 = no limit |
347
346
  | `VERBOSE` | integer | (not specified) | 1=on, 0=off |
348
347
  | `HEARTBEAT_INTERVAL` | integer | 30 | Poll interval for session progress/stale checks |
349
348
  | `STALE_KILL_THRESHOLD` | integer | 900 | Auto-kill after N seconds without parent log progress; 0 disables |
350
349
  | `STALE_KILL_GRACE_SECONDS` | integer | 10 | Grace period after stale-kill before force-stopping |
351
350
  | `CODEX_WAIT_STALE_KILL_THRESHOLD` | integer | 3600 | Longer no-log stale window while Codex waits on subagents |
352
351
  | `CODEX_SUBAGENT_TIMEOUT_SECONDS` | integer | 3300 | Codex subagent max runtime |
353
- | `LOG_CLEANUP_ENABLED` | boolean | 1 | Periodic session log cleanup |
354
- | `LOG_RETENTION_DAYS` | integer | 14 | Delete session logs older than N days |
355
- | `LOG_MAX_TOTAL_MB` | integer | 1024 | Keep total logs under N MB |
356
352
  | `STOP_ON_FAILURE` | boolean | 0 | Stop after a task exhausts retries |
357
353
  | `ENABLE_DEPLOY` | boolean | 0 | Start deploy session after all tasks complete |
358
354
  | `DEV_BRANCH` | string | auto-generated | Optional custom dev branch name |
@@ -51,7 +51,6 @@ Located at `.prizmkit/dev-pipeline/scripts/`:
51
51
  | `check-session-status.py` | Read and validate session-status.json output |
52
52
  | `detect-stuck.py` | Detect stuck/hung pipeline sessions via heartbeat |
53
53
  | `parse-stream-progress.py` | Parse AI CLI output stream for progress tracking |
54
- | `cleanup-logs.py` | Clean up old pipeline logs and state files |
55
54
  | `utils.py` | Shared utility functions for pipeline scripts |
56
55
 
57
56
  ## Artifact Mapping
@@ -125,7 +124,6 @@ The agent MUST write `session-status.json` before exiting:
125
124
  | `status: "partial"`, `can_resume: false` | Retry from scratch |
126
125
  | `status: "failed"` | Retry (up to MAX_RETRIES) |
127
126
  | No status file (crash) | Treat as failed, retry |
128
- | Timeout (exit 124) | Treat as timed_out, retry |
129
127
 
130
128
  ## Team Naming Convention
131
129
 
@@ -1,6 +1,20 @@
1
1
  Set-StrictMode -Version 2.0
2
2
  $ErrorActionPreference = 'Stop'
3
3
 
4
+ function Get-PrizmGitStatusSafe {
5
+ param([string]$ProjectRoot)
6
+ $hiddenToolWorktreeExcludes = Get-PrizmHiddenToolWorktreeExcludes
7
+ $lines = & git -C $ProjectRoot status --porcelain -- . @hiddenToolWorktreeExcludes 2>$null
8
+ return @($lines)
9
+ }
10
+
11
+ function Add-PrizmGitAllSafe {
12
+ param([string]$ProjectRoot)
13
+ $hiddenToolWorktreeExcludes = Get-PrizmHiddenToolWorktreeExcludes
14
+ & git -C $ProjectRoot add -A -- . @hiddenToolWorktreeExcludes *> $null
15
+ return $LASTEXITCODE -eq 0
16
+ }
17
+
4
18
  function Get-PrizmCurrentBranch {
5
19
  param([string]$ProjectRoot)
6
20
  $branch = & git -C $ProjectRoot rev-parse --abbrev-ref HEAD 2>$null
@@ -76,13 +90,12 @@ function Save-PrizmBranchWip {
76
90
  $currentBranch = Get-PrizmCurrentBranch $ProjectRoot
77
91
  if ($currentBranch -ne $DevBranch) { return $true }
78
92
 
79
- $changes = & git -C $ProjectRoot status --porcelain 2>$null
93
+ $changes = Get-PrizmGitStatusSafe $ProjectRoot
80
94
  if ([string]::IsNullOrWhiteSpace(($changes -join "`n"))) { return $true }
81
95
 
82
96
  Write-PrizmWarn "Saving uncommitted work-in-progress on branch: $DevBranch"
83
- & git -C $ProjectRoot add -A *> $null
84
- if ($LASTEXITCODE -ne 0) {
85
- Write-PrizmWarn "git add -A failed - uncommitted work may remain on $DevBranch"
97
+ if (-not (Add-PrizmGitAllSafe $ProjectRoot)) {
98
+ Write-PrizmWarn "git add failed - uncommitted work may remain on $DevBranch"
86
99
  return $true
87
100
  }
88
101
 
@@ -51,6 +51,15 @@ function Write-PrizmWarn { param([string]$Message) Write-Host "[WARN] $(Get-D
51
51
  function Write-PrizmError { param([string]$Message) Write-Host "[ERROR] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Red }
52
52
  function Write-PrizmSuccess { param([string]$Message) Write-Host "[SUCCESS] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Green }
53
53
 
54
+ function Get-PrizmHiddenToolWorktreeExcludes {
55
+ return @(
56
+ ':(top,exclude,glob).*/worktree',
57
+ ':(top,exclude,glob).*/worktree/**',
58
+ ':(top,exclude,glob).*/worktrees',
59
+ ':(top,exclude,glob).*/worktrees/**'
60
+ )
61
+ }
62
+
54
63
  function Initialize-PrizmPaths {
55
64
  param([string]$ScriptRoot)
56
65
  $pipelineDir = Resolve-Path $ScriptRoot