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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +0 -4
- package/bundled/dev-pipeline/README.md +9 -39
- package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +0 -4
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -2
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -1
- package/bundled/dev-pipeline/launch-feature-daemon.sh +2 -3
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -1
- package/bundled/dev-pipeline/lib/branch.sh +8 -5
- package/bundled/dev-pipeline/lib/common.sh +122 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +13 -3
- package/bundled/dev-pipeline/run-bugfix.sh +149 -98
- package/bundled/dev-pipeline/run-feature.sh +90 -103
- package/bundled/dev-pipeline/run-recovery.sh +1 -32
- package/bundled/dev-pipeline/run-refactor.sh +148 -99
- package/bundled/dev-pipeline/scripts/update-bug-status.py +39 -4
- package/bundled/dev-pipeline/scripts/update-feature-status.py +43 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +36 -4
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +3 -3
- package/bundled/dev-pipeline/tests/test_auto_skip.py +92 -3
- package/bundled/dev-pipeline-windows/.env.example +0 -4
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -4
- package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -2
- package/bundled/dev-pipeline-windows/lib/branch.ps1 +17 -4
- package/bundled/dev-pipeline-windows/lib/common.ps1 +9 -0
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +89 -108
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +4 -19
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +39 -4
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +43 -6
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +36 -4
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +5 -15
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +4 -4
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +3 -13
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +3 -3
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +4 -14
- package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +1 -1
- package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +4 -14
- package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +4 -4
- package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +2 -12
- package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +3 -3
- package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +4 -14
- package/package.json +1 -1
- package/bundled/dev-pipeline/scripts/cleanup-logs.py +0 -192
- 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
|
-
|
|
290
|
-
|
|
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
|
-
#
|
|
658
|
-
#
|
|
659
|
-
|
|
660
|
-
fs["infra_error_count"] =
|
|
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
|
-
|
|
324
|
-
|
|
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:
|
|
84
|
-
echo "[Test 4] '
|
|
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": "
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
84
|
-
|
|
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
|