prizmkit 1.1.152 → 1.1.154
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/README.md +100 -87
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +10 -12
- package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
- package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
- package/bundled/dev-pipeline/scripts/utils.py +119 -0
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
- package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
- package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
- package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
- package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
- package/bundled/skills/recovery-workflow/SKILL.md +7 -5
- package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
- package/bundled/skills/recovery-workflow/references/detection.md +3 -3
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
- package/bundled/templates/project-memory-template.md +19 -11
- package/package.json +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
|
@@ -9,12 +9,12 @@ Handles eight actions:
|
|
|
9
9
|
- pause: Save pipeline state for graceful shutdown
|
|
10
10
|
- reset: Reset a bug to pending (status + retry count)
|
|
11
11
|
- clean: Reset + delete session history + delete bugfix artifacts
|
|
12
|
-
-
|
|
12
|
+
- reset_state: Recover blocking roots and causally recoverable auto-skipped descendants
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
python3 update-bug-status.py \
|
|
16
16
|
--bug-list <path> --state-dir <path> \
|
|
17
|
-
--action <get_next|start|update|status|pause|reset|clean|
|
|
17
|
+
--action <get_next|start|update|status|pause|reset|clean|reset_state> \
|
|
18
18
|
[--bug-id <id>] [--session-status <status>] \
|
|
19
19
|
[--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
|
|
20
20
|
"""
|
|
@@ -23,6 +23,7 @@ import argparse
|
|
|
23
23
|
import json
|
|
24
24
|
import os
|
|
25
25
|
import shutil
|
|
26
|
+
import sys
|
|
26
27
|
from datetime import datetime, timezone
|
|
27
28
|
|
|
28
29
|
from utils import (
|
|
@@ -33,6 +34,7 @@ from utils import (
|
|
|
33
34
|
_build_progress_bar,
|
|
34
35
|
read_checkpoint_cursor,
|
|
35
36
|
dependency_rank_map,
|
|
37
|
+
causal_reset_selection,
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
|
|
@@ -53,7 +55,8 @@ SESSION_STATUS_VALUES = [
|
|
|
53
55
|
"merge_conflict",
|
|
54
56
|
]
|
|
55
57
|
|
|
56
|
-
TERMINAL_STATUSES = {"completed", "failed", "skipped", "needs_info"}
|
|
58
|
+
TERMINAL_STATUSES = {"completed", "failed", "skipped", "needs_info", "auto_skipped"}
|
|
59
|
+
RESET_BLOCKING_ROOT_STATUSES = {"failed", "skipped", "needs_info"}
|
|
57
60
|
|
|
58
61
|
# Severity priority (lower value = higher priority)
|
|
59
62
|
SEVERITY_PRIORITY = {
|
|
@@ -72,7 +75,7 @@ def parse_args():
|
|
|
72
75
|
parser.add_argument("--state-dir", required=True, help="Path to the state directory (default: .prizmkit/state/bugfix)")
|
|
73
76
|
parser.add_argument(
|
|
74
77
|
"--action", required=True,
|
|
75
|
-
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "
|
|
78
|
+
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "reset_state", "complete"],
|
|
76
79
|
help="Action to perform",
|
|
77
80
|
)
|
|
78
81
|
parser.add_argument("--bug-id", default=None, help="Bug ID (required for 'update'/'reset'/'clean' actions)")
|
|
@@ -383,19 +386,117 @@ def continuation_metadata_summary(status_data):
|
|
|
383
386
|
}
|
|
384
387
|
|
|
385
388
|
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# Auto-skip: cascade terminal blockers to pending downstream bugs
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
def auto_skip_blocked_bugs(bug_list_path, state_dir, blocking_bug_id=None):
|
|
394
|
+
"""Mark pending descendants of terminal Bugfix blockers as auto_skipped."""
|
|
395
|
+
data, err = load_json_file(bug_list_path)
|
|
396
|
+
if err:
|
|
397
|
+
error_out("Cannot load bug fix list for auto-skip: {}".format(err))
|
|
398
|
+
return []
|
|
399
|
+
bugs = data.get("bugs", [])
|
|
400
|
+
status_map = {
|
|
401
|
+
bug.get("id"): bug.get("status", "pending")
|
|
402
|
+
for bug in bugs
|
|
403
|
+
if isinstance(bug, dict) and bug.get("id")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
to_skip = set()
|
|
407
|
+
changed = True
|
|
408
|
+
while changed:
|
|
409
|
+
changed = False
|
|
410
|
+
for bug in bugs:
|
|
411
|
+
if not isinstance(bug, dict):
|
|
412
|
+
continue
|
|
413
|
+
bug_id = bug.get("id")
|
|
414
|
+
if not bug_id or bug_id in to_skip:
|
|
415
|
+
continue
|
|
416
|
+
if status_map.get(bug_id, "pending") != "pending":
|
|
417
|
+
continue
|
|
418
|
+
dependencies = bug.get("dependencies", [])
|
|
419
|
+
if not isinstance(dependencies, list):
|
|
420
|
+
continue
|
|
421
|
+
if any(
|
|
422
|
+
status_map.get(dependency_id, "pending")
|
|
423
|
+
in RESET_BLOCKING_ROOT_STATUSES | {"auto_skipped"}
|
|
424
|
+
or dependency_id in to_skip
|
|
425
|
+
for dependency_id in dependencies
|
|
426
|
+
):
|
|
427
|
+
to_skip.add(bug_id)
|
|
428
|
+
status_map[bug_id] = "auto_skipped"
|
|
429
|
+
changed = True
|
|
430
|
+
|
|
431
|
+
if not to_skip:
|
|
432
|
+
return []
|
|
433
|
+
|
|
434
|
+
for bug in bugs:
|
|
435
|
+
if isinstance(bug, dict) and bug.get("id") in to_skip:
|
|
436
|
+
bug["status"] = "auto_skipped"
|
|
437
|
+
err = write_json_file(bug_list_path, data)
|
|
438
|
+
if err:
|
|
439
|
+
error_out("Failed to write auto-skipped bugs: {}".format(err))
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
for bug_id in to_skip:
|
|
443
|
+
bs = load_bug_status(state_dir, bug_id)
|
|
444
|
+
bs["updated_at"] = now_iso()
|
|
445
|
+
err = save_bug_status(state_dir, bug_id, bs)
|
|
446
|
+
if err:
|
|
447
|
+
error_out("Failed to save auto-skipped bug status: {}".format(err))
|
|
448
|
+
return []
|
|
449
|
+
|
|
450
|
+
skipped_info = []
|
|
451
|
+
for bug in bugs:
|
|
452
|
+
if not isinstance(bug, dict) or bug.get("id") not in to_skip:
|
|
453
|
+
continue
|
|
454
|
+
dependencies = bug.get("dependencies", [])
|
|
455
|
+
blockers = [
|
|
456
|
+
dependency_id
|
|
457
|
+
for dependency_id in dependencies
|
|
458
|
+
if dependency_id == blocking_bug_id
|
|
459
|
+
or status_map.get(dependency_id, "pending")
|
|
460
|
+
in RESET_BLOCKING_ROOT_STATUSES | {"auto_skipped"}
|
|
461
|
+
]
|
|
462
|
+
skipped_info.append({
|
|
463
|
+
"bug_id": bug["id"],
|
|
464
|
+
"title": bug.get("title", ""),
|
|
465
|
+
"blocked_by": blockers,
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
cause = blocking_bug_id or "existing terminal blockers"
|
|
469
|
+
print(
|
|
470
|
+
"[auto-skip] {} bug(s) auto-skipped due to {}".format(
|
|
471
|
+
len(skipped_info), cause
|
|
472
|
+
),
|
|
473
|
+
file=sys.stderr,
|
|
474
|
+
)
|
|
475
|
+
return skipped_info
|
|
476
|
+
|
|
477
|
+
|
|
386
478
|
# ---------------------------------------------------------------------------
|
|
387
479
|
# Action: get_next
|
|
388
480
|
# ---------------------------------------------------------------------------
|
|
389
481
|
|
|
390
|
-
def action_get_next(bug_list_data, state_dir, project_root=None):
|
|
482
|
+
def action_get_next(bug_list_data, state_dir, project_root=None, bug_list_path=None):
|
|
391
483
|
"""Find the next bug to process.
|
|
392
484
|
|
|
393
485
|
Priority logic:
|
|
394
|
-
1.
|
|
395
|
-
2.
|
|
396
|
-
3.
|
|
397
|
-
4.
|
|
486
|
+
1. Lazily normalize pending descendants of terminal blockers to auto_skipped
|
|
487
|
+
2. Skip terminal statuses (completed, failed, skipped, needs_info, auto_skipped)
|
|
488
|
+
3. Only consider bugs whose dependencies are all completed
|
|
489
|
+
4. Prefer in_progress bugs (interrupted session resume) over pending
|
|
490
|
+
5. Within each status class, use stable topological list order
|
|
398
491
|
"""
|
|
492
|
+
if bug_list_path:
|
|
493
|
+
auto_skip_blocked_bugs(bug_list_path, state_dir)
|
|
494
|
+
refreshed, refresh_error = load_json_file(bug_list_path)
|
|
495
|
+
if refresh_error:
|
|
496
|
+
error_out("Cannot reload bug fix list after auto-skip: {}".format(refresh_error))
|
|
497
|
+
return
|
|
498
|
+
bug_list_data = refreshed
|
|
499
|
+
|
|
399
500
|
bugs = bug_list_data.get("bugs", [])
|
|
400
501
|
if not bugs:
|
|
401
502
|
print("PIPELINE_COMPLETE")
|
|
@@ -513,6 +614,8 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
513
614
|
bs = load_bug_status(state_dir, bug_id)
|
|
514
615
|
|
|
515
616
|
current_list_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
617
|
+
bs["max_retries"] = max_retries
|
|
618
|
+
bs["max_infra_retries"] = max_infra_retries
|
|
516
619
|
|
|
517
620
|
# Track what status we write to bug-fix-list.json
|
|
518
621
|
new_status = current_list_status
|
|
@@ -677,6 +780,12 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
677
780
|
error_out("Failed to save bug status: {}".format(err))
|
|
678
781
|
return
|
|
679
782
|
|
|
783
|
+
auto_skipped_bugs = []
|
|
784
|
+
if new_status in RESET_BLOCKING_ROOT_STATUSES:
|
|
785
|
+
auto_skipped_bugs = auto_skip_blocked_bugs(
|
|
786
|
+
bug_list_path, state_dir, bug_id
|
|
787
|
+
)
|
|
788
|
+
|
|
680
789
|
summary = {
|
|
681
790
|
"action": "update",
|
|
682
791
|
"bug_id": bug_id,
|
|
@@ -689,6 +798,8 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
689
798
|
"checkpoint_state": semantic_checkpoint,
|
|
690
799
|
"updated_at": bs["updated_at"],
|
|
691
800
|
}
|
|
801
|
+
if auto_skipped_bugs:
|
|
802
|
+
summary["auto_skipped"] = [info["bug_id"] for info in auto_skipped_bugs]
|
|
692
803
|
if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
693
804
|
summary["degraded_reason"] = session_status
|
|
694
805
|
summary["restart_policy"] = "finalization_retry"
|
|
@@ -863,7 +974,20 @@ def action_status(bug_list_data, state_dir, project_root=None):
|
|
|
863
974
|
project_name = bug_list_data.get("project_name", "Unknown")
|
|
864
975
|
project_root = os.path.abspath(project_root) if project_root else _default_project_root()
|
|
865
976
|
|
|
866
|
-
counts = {
|
|
977
|
+
counts = {
|
|
978
|
+
"completed": 0,
|
|
979
|
+
"in_progress": 0,
|
|
980
|
+
"failed": 0,
|
|
981
|
+
"pending": 0,
|
|
982
|
+
"needs_info": 0,
|
|
983
|
+
"skipped": 0,
|
|
984
|
+
"auto_skipped": 0,
|
|
985
|
+
}
|
|
986
|
+
status_map = {
|
|
987
|
+
bug.get("id"): bug.get("status", "pending")
|
|
988
|
+
for bug in bugs
|
|
989
|
+
if isinstance(bug, dict) and bug.get("id")
|
|
990
|
+
}
|
|
867
991
|
bug_lines = []
|
|
868
992
|
|
|
869
993
|
for bug in bugs:
|
|
@@ -901,6 +1025,8 @@ def action_status(bug_list_data, state_dir, project_root=None):
|
|
|
901
1025
|
icon = COLOR_MAGENTA + "[?]" + COLOR_RESET
|
|
902
1026
|
elif bstatus == "skipped":
|
|
903
1027
|
icon = COLOR_GRAY + "[—]" + COLOR_RESET
|
|
1028
|
+
elif bstatus == "auto_skipped":
|
|
1029
|
+
icon = COLOR_GRAY + "[⊘]" + COLOR_RESET
|
|
904
1030
|
else:
|
|
905
1031
|
icon = COLOR_GRAY + "[ ]" + COLOR_RESET
|
|
906
1032
|
|
|
@@ -921,6 +1047,16 @@ def action_status(bug_list_data, state_dir, project_root=None):
|
|
|
921
1047
|
detail = " (failed after {} retries)".format(retry_count)
|
|
922
1048
|
elif bstatus == "needs_info":
|
|
923
1049
|
detail = " (needs more info)"
|
|
1050
|
+
elif bstatus == "auto_skipped":
|
|
1051
|
+
blockers = [
|
|
1052
|
+
dependency_id
|
|
1053
|
+
for dependency_id in bug.get("dependencies", [])
|
|
1054
|
+
if status_map.get(dependency_id, "pending")
|
|
1055
|
+
in RESET_BLOCKING_ROOT_STATUSES | {"auto_skipped"}
|
|
1056
|
+
]
|
|
1057
|
+
detail = " (blocked by {})".format(
|
|
1058
|
+
", ".join(blockers) if blockers else "dependency chain"
|
|
1059
|
+
)
|
|
924
1060
|
|
|
925
1061
|
# Colorize
|
|
926
1062
|
if bstatus == "completed":
|
|
@@ -954,8 +1090,12 @@ def action_status(bug_list_data, state_dir, project_root=None):
|
|
|
954
1090
|
summary_line = "Total: {} bugs | Completed: {} | In Progress: {}".format(
|
|
955
1091
|
total, completed, counts["in_progress"]
|
|
956
1092
|
)
|
|
957
|
-
summary_line2 = "Failed: {} | Pending: {} | Needs Info: {} | Skipped: {}".format(
|
|
958
|
-
counts["failed"],
|
|
1093
|
+
summary_line2 = "Failed: {} | Pending: {} | Needs Info: {} | Skipped: {} | Auto-skipped: {}".format(
|
|
1094
|
+
counts["failed"],
|
|
1095
|
+
counts["pending"],
|
|
1096
|
+
counts["needs_info"],
|
|
1097
|
+
counts["skipped"],
|
|
1098
|
+
counts["auto_skipped"],
|
|
959
1099
|
)
|
|
960
1100
|
|
|
961
1101
|
inner = BOX_WIDTH - 2
|
|
@@ -1146,6 +1286,8 @@ def action_start(args, bug_list_path, state_dir):
|
|
|
1146
1286
|
bs = load_bug_status(state_dir, bug_id)
|
|
1147
1287
|
old_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
1148
1288
|
|
|
1289
|
+
bs["max_retries"] = args.max_retries
|
|
1290
|
+
bs["max_infra_retries"] = args.max_infra_retries
|
|
1149
1291
|
bs["updated_at"] = now_iso()
|
|
1150
1292
|
|
|
1151
1293
|
err = save_bug_status(state_dir, bug_id, bs)
|
|
@@ -1172,64 +1314,37 @@ def action_start(args, bug_list_path, state_dir):
|
|
|
1172
1314
|
|
|
1173
1315
|
|
|
1174
1316
|
# ---------------------------------------------------------------------------
|
|
1175
|
-
# Action:
|
|
1317
|
+
# Action: reset_state
|
|
1176
1318
|
# ---------------------------------------------------------------------------
|
|
1177
1319
|
|
|
1178
|
-
def
|
|
1179
|
-
"""Reset
|
|
1180
|
-
|
|
1181
|
-
Two modes:
|
|
1182
|
-
- --bug-id B-001: Reset the specified skipped bug to pending.
|
|
1183
|
-
- No --bug-id: Reset ALL skipped bugs to pending.
|
|
1184
|
-
"""
|
|
1185
|
-
bug_id = args.bug_id
|
|
1186
|
-
|
|
1320
|
+
def action_reset_state(args, bug_list_path, state_dir):
|
|
1321
|
+
"""Reset one causal bug closure, or every recoverable closure, to pending."""
|
|
1187
1322
|
data, err = load_json_file(bug_list_path)
|
|
1188
1323
|
if err:
|
|
1189
1324
|
error_out("Cannot load bug fix list: {}".format(err))
|
|
1190
1325
|
return
|
|
1191
1326
|
bugs = data.get("bugs", [])
|
|
1192
1327
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if not target:
|
|
1203
|
-
error_out("Bug '{}' not found in .prizmkit/plans/bug-fix-list.json".format(bug_id))
|
|
1204
|
-
return
|
|
1205
|
-
if target.get("status") not in ("failed", "skipped", "needs_info"):
|
|
1206
|
-
error_out(
|
|
1207
|
-
"Bug '{}' has status '{}', expected 'failed', 'skipped', or 'needs_info'".format(
|
|
1208
|
-
bug_id, target.get("status", "unknown")
|
|
1209
|
-
)
|
|
1210
|
-
)
|
|
1211
|
-
return
|
|
1212
|
-
to_reset.add(bug_id)
|
|
1213
|
-
else:
|
|
1214
|
-
# No bug-id: reset ALL skipped/failed/needs_info bugs
|
|
1215
|
-
for b in bugs:
|
|
1216
|
-
if isinstance(b, dict) and b.get("id"):
|
|
1217
|
-
if b.get("status") in ("failed", "skipped", "needs_info"):
|
|
1218
|
-
to_reset.add(b["id"])
|
|
1219
|
-
|
|
1220
|
-
if not to_reset:
|
|
1221
|
-
error_out("No bugs to unskip")
|
|
1328
|
+
try:
|
|
1329
|
+
recovery_ids = causal_reset_selection(
|
|
1330
|
+
bugs,
|
|
1331
|
+
selected_id=args.bug_id,
|
|
1332
|
+
blocking_root_statuses=RESET_BLOCKING_ROOT_STATUSES,
|
|
1333
|
+
item_label="Bug",
|
|
1334
|
+
)
|
|
1335
|
+
except ValueError as exc:
|
|
1336
|
+
error_out(str(exc))
|
|
1222
1337
|
return
|
|
1223
1338
|
|
|
1224
|
-
|
|
1339
|
+
to_reset = set(recovery_ids)
|
|
1225
1340
|
reset_details = []
|
|
1226
|
-
for
|
|
1227
|
-
if isinstance(
|
|
1228
|
-
old_status =
|
|
1229
|
-
|
|
1341
|
+
for bug in bugs:
|
|
1342
|
+
if isinstance(bug, dict) and bug.get("id") in to_reset:
|
|
1343
|
+
old_status = bug.get("status", "unknown")
|
|
1344
|
+
bug["status"] = "pending"
|
|
1230
1345
|
reset_details.append({
|
|
1231
|
-
"bug_id":
|
|
1232
|
-
"title":
|
|
1346
|
+
"bug_id": bug["id"],
|
|
1347
|
+
"title": bug.get("title", ""),
|
|
1233
1348
|
"old_status": old_status,
|
|
1234
1349
|
})
|
|
1235
1350
|
|
|
@@ -1238,22 +1353,31 @@ def action_unskip(args, bug_list_path, state_dir):
|
|
|
1238
1353
|
error_out("Failed to write .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
1239
1354
|
return
|
|
1240
1355
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1356
|
+
for bug_id in recovery_ids:
|
|
1357
|
+
bs = load_bug_status(state_dir, bug_id)
|
|
1358
|
+
checkout_identity = {
|
|
1359
|
+
key: bs[key]
|
|
1360
|
+
for key in ("active_dev_branch", "base_branch")
|
|
1361
|
+
if key in bs
|
|
1362
|
+
}
|
|
1244
1363
|
bs["retry_count"] = 0
|
|
1245
1364
|
bs["infra_error_count"] = 0
|
|
1246
1365
|
bs["last_infra_error_session_id"] = None
|
|
1247
1366
|
bs["sessions"] = []
|
|
1248
1367
|
bs["last_session_id"] = None
|
|
1368
|
+
bs["last_failed_session_id"] = None
|
|
1249
1369
|
bs["resume_from_phase"] = None
|
|
1250
1370
|
reset_continuation_metadata(bs)
|
|
1371
|
+
bs.update(checkout_identity)
|
|
1251
1372
|
bs["updated_at"] = now_iso()
|
|
1252
|
-
save_bug_status(state_dir,
|
|
1373
|
+
err = save_bug_status(state_dir, bug_id, bs)
|
|
1374
|
+
if err:
|
|
1375
|
+
error_out("Failed to save bug status: {}".format(err))
|
|
1376
|
+
return
|
|
1253
1377
|
|
|
1254
1378
|
result = {
|
|
1255
|
-
"action": "
|
|
1256
|
-
"reset_count": len(
|
|
1379
|
+
"action": "reset_state",
|
|
1380
|
+
"reset_count": len(recovery_ids),
|
|
1257
1381
|
"bugs": reset_details,
|
|
1258
1382
|
}
|
|
1259
1383
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -1283,7 +1407,12 @@ def main():
|
|
|
1283
1407
|
error_out("Cannot load bug fix list: {}".format(err))
|
|
1284
1408
|
|
|
1285
1409
|
if args.action == "get_next":
|
|
1286
|
-
action_get_next(
|
|
1410
|
+
action_get_next(
|
|
1411
|
+
bug_list_data,
|
|
1412
|
+
args.state_dir,
|
|
1413
|
+
args.project_root,
|
|
1414
|
+
args.bug_list,
|
|
1415
|
+
)
|
|
1287
1416
|
elif args.action == "start":
|
|
1288
1417
|
action_start(args, args.bug_list, args.state_dir)
|
|
1289
1418
|
elif args.action == "update":
|
|
@@ -1296,8 +1425,8 @@ def main():
|
|
|
1296
1425
|
action_clean(args, args.bug_list, args.state_dir)
|
|
1297
1426
|
elif args.action == "pause":
|
|
1298
1427
|
action_pause(args.state_dir)
|
|
1299
|
-
elif args.action == "
|
|
1300
|
-
|
|
1428
|
+
elif args.action == "reset_state":
|
|
1429
|
+
action_reset_state(args, args.bug_list, args.state_dir)
|
|
1301
1430
|
elif args.action == "complete":
|
|
1302
1431
|
# Shortcut: 'complete' is equivalent to 'update --session-status success'
|
|
1303
1432
|
args.session_status = "success"
|
|
@@ -153,7 +153,18 @@ def _validate_semantic_update(step, semantic_update):
|
|
|
153
153
|
terminal_status = semantic_update.get("terminal_status")
|
|
154
154
|
if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
|
|
155
155
|
return "Invalid terminal_status '{}'".format(terminal_status)
|
|
156
|
-
|
|
156
|
+
blocked_reason = semantic_update.get("blocked_reason")
|
|
157
|
+
blocked_policy_result = stage_result in {"TEST_NEEDS_FIXES", "TEST_BLOCKED"}
|
|
158
|
+
if terminal_status == "WORKFLOW_COMPLETED" and blocked_policy_result:
|
|
159
|
+
return "WORKFLOW_COMPLETED conflicts with blocking stage result {}".format(stage_result)
|
|
160
|
+
if blocked_reason is not None:
|
|
161
|
+
if not isinstance(blocked_reason, str) or not blocked_reason.strip():
|
|
162
|
+
return "blocked_reason must be a non-empty string"
|
|
163
|
+
if terminal_status == "WORKFLOW_COMPLETED":
|
|
164
|
+
return "WORKFLOW_COMPLETED cannot retain blocked_reason"
|
|
165
|
+
if terminal_status != "WORKFLOW_BLOCKED" and not blocked_policy_result:
|
|
166
|
+
return "blocked_reason requires terminal_status=WORKFLOW_BLOCKED"
|
|
167
|
+
if terminal_status == "WORKFLOW_BLOCKED" and not blocked_reason:
|
|
157
168
|
return "WORKFLOW_BLOCKED requires blocked_reason"
|
|
158
169
|
return None
|
|
159
170
|
|
|
@@ -361,11 +372,13 @@ def update_checkpoint(
|
|
|
361
372
|
exists=True,
|
|
362
373
|
project_root=_checkpoint_project_root(checkpoint_path),
|
|
363
374
|
)
|
|
364
|
-
if not final_state.valid:
|
|
375
|
+
if not final_state.valid or final_state.semantic.error_code:
|
|
376
|
+
error_code = final_state.error_code or final_state.semantic.error_code
|
|
377
|
+
error_message = final_state.error_message or final_state.semantic.error_message
|
|
365
378
|
return {
|
|
366
379
|
"ok": False,
|
|
367
380
|
"error": "Update would create invalid checkpoint ({}): {}".format(
|
|
368
|
-
|
|
381
|
+
error_code, error_message
|
|
369
382
|
),
|
|
370
383
|
}
|
|
371
384
|
|