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
|
@@ -10,12 +10,12 @@ Handles nine actions:
|
|
|
10
10
|
- reset: Reset a feature to pending (status + retry count)
|
|
11
11
|
- clean: Reset + delete session history + delete prizmkit artifacts
|
|
12
12
|
- complete: Shortcut for manually marking a feature as completed
|
|
13
|
-
-
|
|
13
|
+
- reset_state: Recover blocking roots and causally recoverable auto-skipped descendants
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
python3 update-feature-status.py \
|
|
17
17
|
--feature-list <path> --state-dir <path> \
|
|
18
|
-
--action <get_next|start|update|status|pause|reset|clean|complete|
|
|
18
|
+
--action <get_next|start|update|status|pause|reset|clean|complete|reset_state> \
|
|
19
19
|
[--feature-id <id>] [--session-status <status>] \
|
|
20
20
|
[--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>] \
|
|
21
21
|
[--features <filter>]
|
|
@@ -37,6 +37,7 @@ from utils import (
|
|
|
37
37
|
_build_progress_bar,
|
|
38
38
|
read_checkpoint_cursor,
|
|
39
39
|
dependency_rank_map,
|
|
40
|
+
causal_reset_selection,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
|
|
@@ -58,6 +59,7 @@ SESSION_STATUS_VALUES = [
|
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped", "split"}
|
|
62
|
+
RESET_BLOCKING_ROOT_STATUSES = {"failed", "skipped"}
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
def parse_args():
|
|
@@ -77,7 +79,7 @@ def parse_args():
|
|
|
77
79
|
parser.add_argument(
|
|
78
80
|
"--action",
|
|
79
81
|
required=True,
|
|
80
|
-
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "complete", "
|
|
82
|
+
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "complete", "reset_state"],
|
|
81
83
|
help="Action to perform",
|
|
82
84
|
)
|
|
83
85
|
parser.add_argument(
|
|
@@ -909,6 +911,8 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
909
911
|
|
|
910
912
|
fs = load_feature_status(state_dir, feature_id)
|
|
911
913
|
current_list_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
914
|
+
fs["max_retries"] = max_retries
|
|
915
|
+
fs["max_infra_retries"] = max_infra_retries
|
|
912
916
|
|
|
913
917
|
# Track what status we write to feature-list.json
|
|
914
918
|
new_status = current_list_status
|
|
@@ -1568,6 +1572,8 @@ def action_start(args, feature_list_path, state_dir):
|
|
|
1568
1572
|
fs = load_feature_status(state_dir, feature_id)
|
|
1569
1573
|
old_status = get_feature_status_from_list(feature_list_path, feature_id)
|
|
1570
1574
|
|
|
1575
|
+
fs["max_retries"] = args.max_retries
|
|
1576
|
+
fs["max_infra_retries"] = args.max_infra_retries
|
|
1571
1577
|
fs["updated_at"] = now_iso()
|
|
1572
1578
|
|
|
1573
1579
|
err = save_feature_status(state_dir, feature_id, fs)
|
|
@@ -1749,124 +1755,37 @@ def action_clean(args, feature_list_path, state_dir):
|
|
|
1749
1755
|
|
|
1750
1756
|
|
|
1751
1757
|
# ---------------------------------------------------------------------------
|
|
1752
|
-
# Action:
|
|
1758
|
+
# Action: reset_state
|
|
1753
1759
|
# ---------------------------------------------------------------------------
|
|
1754
1760
|
|
|
1755
|
-
def
|
|
1756
|
-
"""
|
|
1757
|
-
|
|
1758
|
-
Two modes:
|
|
1759
|
-
- --feature-id F-032: Reset the specified failed/skipped feature + all auto_skipped
|
|
1760
|
-
features whose dependency chain includes it.
|
|
1761
|
-
- No --feature-id: Reset ALL failed, skipped, and auto_skipped features to pending.
|
|
1762
|
-
"""
|
|
1763
|
-
feature_id = args.feature_id
|
|
1764
|
-
|
|
1761
|
+
def action_reset_state(args, feature_list_path, state_dir):
|
|
1762
|
+
"""Reset one causal feature closure, or every recoverable closure, to pending."""
|
|
1765
1763
|
data, err = load_json_file(feature_list_path)
|
|
1766
1764
|
if err:
|
|
1767
1765
|
error_out("Cannot load feature list: {}".format(err))
|
|
1768
1766
|
return
|
|
1769
1767
|
features = data.get("features", [])
|
|
1770
1768
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
if not target:
|
|
1781
|
-
error_out("Feature '{}' not found in .prizmkit/plans/feature-list.json".format(feature_id))
|
|
1782
|
-
return
|
|
1783
|
-
if target.get("status") not in ("failed", "skipped", "auto_skipped"):
|
|
1784
|
-
error_out(
|
|
1785
|
-
"Feature '{}' has status '{}', expected 'failed', 'skipped', or 'auto_skipped'".format(
|
|
1786
|
-
feature_id, target.get("status", "unknown")
|
|
1787
|
-
)
|
|
1788
|
-
)
|
|
1789
|
-
return
|
|
1790
|
-
|
|
1791
|
-
# If target is failed or skipped, reset it and find all auto_skipped descendants
|
|
1792
|
-
if target.get("status") in ("failed", "skipped"):
|
|
1793
|
-
to_reset.add(feature_id)
|
|
1794
|
-
# Find all auto_skipped features that depend (transitively) on this one
|
|
1795
|
-
changed = True
|
|
1796
|
-
while changed:
|
|
1797
|
-
changed = False
|
|
1798
|
-
for f in features:
|
|
1799
|
-
if not isinstance(f, dict):
|
|
1800
|
-
continue
|
|
1801
|
-
fid = f.get("id")
|
|
1802
|
-
if not fid or fid in to_reset:
|
|
1803
|
-
continue
|
|
1804
|
-
if f.get("status") != "auto_skipped":
|
|
1805
|
-
continue
|
|
1806
|
-
deps = f.get("dependencies", [])
|
|
1807
|
-
if any(d in to_reset for d in deps):
|
|
1808
|
-
to_reset.add(fid)
|
|
1809
|
-
changed = True
|
|
1810
|
-
|
|
1811
|
-
# If target is auto_skipped, reset it and its failed upstream + siblings
|
|
1812
|
-
elif target.get("status") == "auto_skipped":
|
|
1813
|
-
to_reset.add(feature_id)
|
|
1814
|
-
# Transitively walk upstream to find ALL failed/auto_skipped ancestors
|
|
1815
|
-
# (e.g., F-001 failed → F-002 auto_skipped → F-003 auto_skipped;
|
|
1816
|
-
# unskip F-003 must also find and reset F-001)
|
|
1817
|
-
upstream_changed = True
|
|
1818
|
-
while upstream_changed:
|
|
1819
|
-
upstream_changed = False
|
|
1820
|
-
for f in features:
|
|
1821
|
-
if not isinstance(f, dict):
|
|
1822
|
-
continue
|
|
1823
|
-
fid = f.get("id")
|
|
1824
|
-
if not fid or fid not in to_reset:
|
|
1825
|
-
continue
|
|
1826
|
-
for dep_id in f.get("dependencies", []):
|
|
1827
|
-
if dep_id in to_reset:
|
|
1828
|
-
continue
|
|
1829
|
-
for dep_f in features:
|
|
1830
|
-
if isinstance(dep_f, dict) and dep_f.get("id") == dep_id:
|
|
1831
|
-
if dep_f.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1832
|
-
to_reset.add(dep_id)
|
|
1833
|
-
upstream_changed = True
|
|
1834
|
-
# Also reset downstream auto_skipped features blocked by the same upstreams
|
|
1835
|
-
changed = True
|
|
1836
|
-
while changed:
|
|
1837
|
-
changed = False
|
|
1838
|
-
for f in features:
|
|
1839
|
-
if not isinstance(f, dict):
|
|
1840
|
-
continue
|
|
1841
|
-
fid = f.get("id")
|
|
1842
|
-
if not fid or fid in to_reset:
|
|
1843
|
-
continue
|
|
1844
|
-
if f.get("status") != "auto_skipped":
|
|
1845
|
-
continue
|
|
1846
|
-
fdeps = f.get("dependencies", [])
|
|
1847
|
-
if any(d in to_reset for d in fdeps):
|
|
1848
|
-
to_reset.add(fid)
|
|
1849
|
-
changed = True
|
|
1850
|
-
else:
|
|
1851
|
-
# No feature-id: reset ALL failed + skipped + auto_skipped
|
|
1852
|
-
for f in features:
|
|
1853
|
-
if isinstance(f, dict) and f.get("id"):
|
|
1854
|
-
if f.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1855
|
-
to_reset.add(f["id"])
|
|
1856
|
-
|
|
1857
|
-
if not to_reset:
|
|
1858
|
-
error_out("No features to unskip")
|
|
1769
|
+
try:
|
|
1770
|
+
recovery_ids = causal_reset_selection(
|
|
1771
|
+
features,
|
|
1772
|
+
selected_id=args.feature_id,
|
|
1773
|
+
blocking_root_statuses=RESET_BLOCKING_ROOT_STATUSES,
|
|
1774
|
+
item_label="Feature",
|
|
1775
|
+
)
|
|
1776
|
+
except ValueError as exc:
|
|
1777
|
+
error_out(str(exc))
|
|
1859
1778
|
return
|
|
1860
1779
|
|
|
1861
|
-
|
|
1780
|
+
to_reset = set(recovery_ids)
|
|
1862
1781
|
reset_details = []
|
|
1863
|
-
for
|
|
1864
|
-
if isinstance(
|
|
1865
|
-
old_status =
|
|
1866
|
-
|
|
1782
|
+
for feature in features:
|
|
1783
|
+
if isinstance(feature, dict) and feature.get("id") in to_reset:
|
|
1784
|
+
old_status = feature.get("status", "unknown")
|
|
1785
|
+
feature["status"] = "pending"
|
|
1867
1786
|
reset_details.append({
|
|
1868
|
-
"feature_id":
|
|
1869
|
-
"title":
|
|
1787
|
+
"feature_id": feature["id"],
|
|
1788
|
+
"title": feature.get("title", ""),
|
|
1870
1789
|
"old_status": old_status,
|
|
1871
1790
|
})
|
|
1872
1791
|
|
|
@@ -1875,22 +1794,31 @@ def action_unskip(args, feature_list_path, state_dir):
|
|
|
1875
1794
|
error_out("Failed to write .prizmkit/plans/feature-list.json: {}".format(err))
|
|
1876
1795
|
return
|
|
1877
1796
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1797
|
+
for feature_id in recovery_ids:
|
|
1798
|
+
fs = load_feature_status(state_dir, feature_id)
|
|
1799
|
+
checkout_identity = {
|
|
1800
|
+
key: fs[key]
|
|
1801
|
+
for key in ("active_dev_branch", "base_branch")
|
|
1802
|
+
if key in fs
|
|
1803
|
+
}
|
|
1881
1804
|
fs["retry_count"] = 0
|
|
1882
1805
|
fs["infra_error_count"] = 0
|
|
1883
1806
|
fs["last_infra_error_session_id"] = None
|
|
1884
1807
|
fs["sessions"] = []
|
|
1885
1808
|
fs["last_session_id"] = None
|
|
1809
|
+
fs["last_failed_session_id"] = None
|
|
1886
1810
|
fs["resume_from_phase"] = None
|
|
1887
1811
|
reset_continuation_metadata(fs)
|
|
1812
|
+
fs.update(checkout_identity)
|
|
1888
1813
|
fs["updated_at"] = now_iso()
|
|
1889
|
-
save_feature_status(state_dir,
|
|
1814
|
+
err = save_feature_status(state_dir, feature_id, fs)
|
|
1815
|
+
if err:
|
|
1816
|
+
error_out("Failed to save feature status: {}".format(err))
|
|
1817
|
+
return
|
|
1890
1818
|
|
|
1891
1819
|
result = {
|
|
1892
|
-
"action": "
|
|
1893
|
-
"reset_count": len(
|
|
1820
|
+
"action": "reset_state",
|
|
1821
|
+
"reset_count": len(recovery_ids),
|
|
1894
1822
|
"features": reset_details,
|
|
1895
1823
|
}
|
|
1896
1824
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -1977,8 +1905,8 @@ def main():
|
|
|
1977
1905
|
action_update(args, args.feature_list, args.state_dir)
|
|
1978
1906
|
elif args.action == "pause":
|
|
1979
1907
|
action_pause(args.state_dir)
|
|
1980
|
-
elif args.action == "
|
|
1981
|
-
|
|
1908
|
+
elif args.action == "reset_state":
|
|
1909
|
+
action_reset_state(args, args.feature_list, args.state_dir)
|
|
1982
1910
|
|
|
1983
1911
|
|
|
1984
1912
|
if __name__ == "__main__":
|
|
@@ -9,12 +9,12 @@ Handles eight actions:
|
|
|
9
9
|
- pause: Save pipeline state for graceful shutdown
|
|
10
10
|
- reset: Reset a refactor to pending (status + retry count)
|
|
11
11
|
- clean: Reset + delete session history + delete refactor artifacts
|
|
12
|
-
-
|
|
12
|
+
- reset_state: Recover blocking roots and causally recoverable auto-skipped descendants
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
python3 update-refactor-status.py \
|
|
16
16
|
--refactor-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
|
[--refactor-id <id>] [--session-status <status>] \
|
|
19
19
|
[--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
|
|
20
20
|
"""
|
|
@@ -34,6 +34,7 @@ from utils import (
|
|
|
34
34
|
_build_progress_bar,
|
|
35
35
|
read_checkpoint_cursor,
|
|
36
36
|
dependency_rank_map,
|
|
37
|
+
causal_reset_selection,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
|
|
@@ -55,6 +56,7 @@ SESSION_STATUS_VALUES = [
|
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped"}
|
|
59
|
+
RESET_BLOCKING_ROOT_STATUSES = {"failed", "skipped"}
|
|
58
60
|
|
|
59
61
|
# Artifact directory names (relative to project root)
|
|
60
62
|
REFACTOR_ARTIFACTS_REL = os.path.join(".prizmkit", "refactor")
|
|
@@ -84,7 +86,7 @@ def parse_args():
|
|
|
84
86
|
parser.add_argument("--state-dir", required=True, help="Path to the state directory (default: .prizmkit/state/refactor)")
|
|
85
87
|
parser.add_argument(
|
|
86
88
|
"--action", required=True,
|
|
87
|
-
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "
|
|
89
|
+
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "reset_state", "complete"],
|
|
88
90
|
help="Action to perform",
|
|
89
91
|
)
|
|
90
92
|
parser.add_argument("--refactor-id", default=None, help="Refactor ID (required for 'update'/'reset'/'clean' actions)")
|
|
@@ -517,6 +519,8 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
517
519
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
518
520
|
|
|
519
521
|
current_list_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
522
|
+
rs["max_retries"] = max_retries
|
|
523
|
+
rs["max_infra_retries"] = max_infra_retries
|
|
520
524
|
|
|
521
525
|
# Track what status we write to refactor-list.json
|
|
522
526
|
new_status = current_list_status
|
|
@@ -1263,6 +1267,8 @@ def action_start(args, refactor_list_path, state_dir):
|
|
|
1263
1267
|
rs = load_refactor_status(state_dir, refactor_id)
|
|
1264
1268
|
old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
|
|
1265
1269
|
|
|
1270
|
+
rs["max_retries"] = args.max_retries
|
|
1271
|
+
rs["max_infra_retries"] = args.max_infra_retries
|
|
1266
1272
|
rs["updated_at"] = now_iso()
|
|
1267
1273
|
|
|
1268
1274
|
err = save_refactor_status(state_dir, refactor_id, rs)
|
|
@@ -1289,126 +1295,37 @@ def action_start(args, refactor_list_path, state_dir):
|
|
|
1289
1295
|
|
|
1290
1296
|
|
|
1291
1297
|
# ---------------------------------------------------------------------------
|
|
1292
|
-
# Action:
|
|
1298
|
+
# Action: reset_state
|
|
1293
1299
|
# ---------------------------------------------------------------------------
|
|
1294
1300
|
|
|
1295
|
-
def
|
|
1296
|
-
"""
|
|
1297
|
-
|
|
1298
|
-
Two modes:
|
|
1299
|
-
- --refactor-id R-001: Reset the specified failed/skipped/auto_skipped refactor + all
|
|
1300
|
-
downstream auto_skipped refactors whose dependency chain includes it.
|
|
1301
|
-
If the target is auto_skipped, also walk upstream to find and reset the
|
|
1302
|
-
failed/skipped ancestor that caused the cascade.
|
|
1303
|
-
- No --refactor-id: Reset ALL failed, skipped, and auto_skipped refactors to pending.
|
|
1304
|
-
"""
|
|
1305
|
-
refactor_id = args.refactor_id
|
|
1306
|
-
|
|
1301
|
+
def action_reset_state(args, refactor_list_path, state_dir):
|
|
1302
|
+
"""Reset one causal refactor closure, or every recoverable closure, to pending."""
|
|
1307
1303
|
data, err = load_json_file(refactor_list_path)
|
|
1308
1304
|
if err:
|
|
1309
1305
|
error_out("Cannot load refactor list: {}".format(err))
|
|
1310
1306
|
return
|
|
1311
1307
|
refactors = data.get("refactors", [])
|
|
1312
1308
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if not target:
|
|
1323
|
-
error_out("Refactor '{}' not found in .prizmkit/plans/refactor-list.json".format(refactor_id))
|
|
1324
|
-
return
|
|
1325
|
-
if target.get("status") not in ("failed", "skipped", "auto_skipped"):
|
|
1326
|
-
error_out(
|
|
1327
|
-
"Refactor '{}' has status '{}', expected 'failed', 'skipped', or 'auto_skipped'".format(
|
|
1328
|
-
refactor_id, target.get("status", "unknown")
|
|
1329
|
-
)
|
|
1330
|
-
)
|
|
1331
|
-
return
|
|
1332
|
-
|
|
1333
|
-
# If target is failed or skipped, reset it and find all auto_skipped descendants
|
|
1334
|
-
if target.get("status") in ("failed", "skipped"):
|
|
1335
|
-
to_reset.add(refactor_id)
|
|
1336
|
-
# Find all auto_skipped refactors that depend (transitively) on this one
|
|
1337
|
-
changed = True
|
|
1338
|
-
while changed:
|
|
1339
|
-
changed = False
|
|
1340
|
-
for r in refactors:
|
|
1341
|
-
if not isinstance(r, dict):
|
|
1342
|
-
continue
|
|
1343
|
-
rid = r.get("id")
|
|
1344
|
-
if not rid or rid in to_reset:
|
|
1345
|
-
continue
|
|
1346
|
-
if r.get("status") != "auto_skipped":
|
|
1347
|
-
continue
|
|
1348
|
-
deps = r.get("dependencies", [])
|
|
1349
|
-
if any(d in to_reset for d in deps):
|
|
1350
|
-
to_reset.add(rid)
|
|
1351
|
-
changed = True
|
|
1352
|
-
|
|
1353
|
-
# If target is auto_skipped, reset it and its failed upstream + siblings
|
|
1354
|
-
elif target.get("status") == "auto_skipped":
|
|
1355
|
-
to_reset.add(refactor_id)
|
|
1356
|
-
# Transitively walk upstream to find ALL failed/auto_skipped ancestors
|
|
1357
|
-
# (e.g., R-001 failed → R-002 auto_skipped → R-003 auto_skipped;
|
|
1358
|
-
# unskip R-003 must also find and reset R-001)
|
|
1359
|
-
upstream_changed = True
|
|
1360
|
-
while upstream_changed:
|
|
1361
|
-
upstream_changed = False
|
|
1362
|
-
for r in refactors:
|
|
1363
|
-
if not isinstance(r, dict):
|
|
1364
|
-
continue
|
|
1365
|
-
rid = r.get("id")
|
|
1366
|
-
if not rid or rid not in to_reset:
|
|
1367
|
-
continue
|
|
1368
|
-
for dep_id in r.get("dependencies", []):
|
|
1369
|
-
if dep_id in to_reset:
|
|
1370
|
-
continue
|
|
1371
|
-
for dep_r in refactors:
|
|
1372
|
-
if isinstance(dep_r, dict) and dep_r.get("id") == dep_id:
|
|
1373
|
-
if dep_r.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1374
|
-
to_reset.add(dep_id)
|
|
1375
|
-
upstream_changed = True
|
|
1376
|
-
# Also reset downstream auto_skipped refactors blocked by the same upstreams
|
|
1377
|
-
changed = True
|
|
1378
|
-
while changed:
|
|
1379
|
-
changed = False
|
|
1380
|
-
for r in refactors:
|
|
1381
|
-
if not isinstance(r, dict):
|
|
1382
|
-
continue
|
|
1383
|
-
rid = r.get("id")
|
|
1384
|
-
if not rid or rid in to_reset:
|
|
1385
|
-
continue
|
|
1386
|
-
if r.get("status") != "auto_skipped":
|
|
1387
|
-
continue
|
|
1388
|
-
rdeps = r.get("dependencies", [])
|
|
1389
|
-
if any(d in to_reset for d in rdeps):
|
|
1390
|
-
to_reset.add(rid)
|
|
1391
|
-
changed = True
|
|
1392
|
-
else:
|
|
1393
|
-
# No refactor-id: reset ALL failed + skipped + auto_skipped
|
|
1394
|
-
for r in refactors:
|
|
1395
|
-
if isinstance(r, dict) and r.get("id"):
|
|
1396
|
-
if r.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1397
|
-
to_reset.add(r["id"])
|
|
1398
|
-
|
|
1399
|
-
if not to_reset:
|
|
1400
|
-
error_out("No refactors to unskip")
|
|
1309
|
+
try:
|
|
1310
|
+
recovery_ids = causal_reset_selection(
|
|
1311
|
+
refactors,
|
|
1312
|
+
selected_id=args.refactor_id,
|
|
1313
|
+
blocking_root_statuses=RESET_BLOCKING_ROOT_STATUSES,
|
|
1314
|
+
item_label="Refactor",
|
|
1315
|
+
)
|
|
1316
|
+
except ValueError as exc:
|
|
1317
|
+
error_out(str(exc))
|
|
1401
1318
|
return
|
|
1402
1319
|
|
|
1403
|
-
|
|
1320
|
+
to_reset = set(recovery_ids)
|
|
1404
1321
|
reset_details = []
|
|
1405
|
-
for
|
|
1406
|
-
if isinstance(
|
|
1407
|
-
old_status =
|
|
1408
|
-
|
|
1322
|
+
for refactor in refactors:
|
|
1323
|
+
if isinstance(refactor, dict) and refactor.get("id") in to_reset:
|
|
1324
|
+
old_status = refactor.get("status", "unknown")
|
|
1325
|
+
refactor["status"] = "pending"
|
|
1409
1326
|
reset_details.append({
|
|
1410
|
-
"refactor_id":
|
|
1411
|
-
"title":
|
|
1327
|
+
"refactor_id": refactor["id"],
|
|
1328
|
+
"title": refactor.get("title", ""),
|
|
1412
1329
|
"old_status": old_status,
|
|
1413
1330
|
})
|
|
1414
1331
|
|
|
@@ -1417,22 +1334,31 @@ def action_unskip(args, refactor_list_path, state_dir):
|
|
|
1417
1334
|
error_out("Failed to write .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
1418
1335
|
return
|
|
1419
1336
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1337
|
+
for refactor_id in recovery_ids:
|
|
1338
|
+
rs = load_refactor_status(state_dir, refactor_id)
|
|
1339
|
+
checkout_identity = {
|
|
1340
|
+
key: rs[key]
|
|
1341
|
+
for key in ("active_dev_branch", "base_branch")
|
|
1342
|
+
if key in rs
|
|
1343
|
+
}
|
|
1423
1344
|
rs["retry_count"] = 0
|
|
1424
1345
|
rs["infra_error_count"] = 0
|
|
1425
1346
|
rs["last_infra_error_session_id"] = None
|
|
1426
1347
|
rs["sessions"] = []
|
|
1427
1348
|
rs["last_session_id"] = None
|
|
1349
|
+
rs["last_failed_session_id"] = None
|
|
1428
1350
|
rs["resume_from_phase"] = None
|
|
1429
1351
|
reset_continuation_metadata(rs)
|
|
1352
|
+
rs.update(checkout_identity)
|
|
1430
1353
|
rs["updated_at"] = now_iso()
|
|
1431
|
-
save_refactor_status(state_dir,
|
|
1354
|
+
err = save_refactor_status(state_dir, refactor_id, rs)
|
|
1355
|
+
if err:
|
|
1356
|
+
error_out("Failed to save refactor status: {}".format(err))
|
|
1357
|
+
return
|
|
1432
1358
|
|
|
1433
1359
|
result = {
|
|
1434
|
-
"action": "
|
|
1435
|
-
"reset_count": len(
|
|
1360
|
+
"action": "reset_state",
|
|
1361
|
+
"reset_count": len(recovery_ids),
|
|
1436
1362
|
"refactors": reset_details,
|
|
1437
1363
|
}
|
|
1438
1364
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -1475,8 +1401,8 @@ def main():
|
|
|
1475
1401
|
action_start(args, args.refactor_list, args.state_dir)
|
|
1476
1402
|
elif args.action == "pause":
|
|
1477
1403
|
action_pause(args.state_dir)
|
|
1478
|
-
elif args.action == "
|
|
1479
|
-
|
|
1404
|
+
elif args.action == "reset_state":
|
|
1405
|
+
action_reset_state(args, args.refactor_list, args.state_dir)
|
|
1480
1406
|
elif args.action == "complete":
|
|
1481
1407
|
# Shortcut: 'complete' is equivalent to 'update --session-status success'
|
|
1482
1408
|
args.session_status = "success"
|
|
@@ -228,6 +228,125 @@ def dependency_rank_map(items, id_key="id", dependencies_key="dependencies", ite
|
|
|
228
228
|
return {item[id_key]: index for index, item in enumerate(ordered) if id_key in item}, errors
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def causal_reset_selection(
|
|
232
|
+
items,
|
|
233
|
+
selected_id=None,
|
|
234
|
+
blocking_root_statuses=None,
|
|
235
|
+
id_key="id",
|
|
236
|
+
dependencies_key="dependencies",
|
|
237
|
+
status_key="status",
|
|
238
|
+
auto_skipped_status="auto_skipped",
|
|
239
|
+
item_label="Item",
|
|
240
|
+
):
|
|
241
|
+
"""Return the exact state-only recovery closure for one dependency graph.
|
|
242
|
+
|
|
243
|
+
A selected terminal root recovers itself and only ``auto_skipped`` items
|
|
244
|
+
whose complete transitive blocking-root set is contained by that selection.
|
|
245
|
+
Selecting an ``auto_skipped`` item first expands the selection to every
|
|
246
|
+
blocking root required by that item. ``selected_id=None`` means recover all
|
|
247
|
+
supported blocking roots. Invalid graph or target state raises ``ValueError``
|
|
248
|
+
before callers perform any mutation.
|
|
249
|
+
"""
|
|
250
|
+
if not isinstance(items, list):
|
|
251
|
+
raise ValueError("{} list must be an array".format(item_label))
|
|
252
|
+
|
|
253
|
+
roots_statuses = set(blocking_root_statuses or ())
|
|
254
|
+
if not roots_statuses:
|
|
255
|
+
raise ValueError("Blocking root statuses must not be empty")
|
|
256
|
+
|
|
257
|
+
item_by_id = {}
|
|
258
|
+
ordered_ids = []
|
|
259
|
+
for index, item in enumerate(items):
|
|
260
|
+
if not isinstance(item, dict):
|
|
261
|
+
raise ValueError("{} at index {} is not an object".format(item_label, index))
|
|
262
|
+
item_id = item.get(id_key)
|
|
263
|
+
if not isinstance(item_id, str) or not item_id:
|
|
264
|
+
raise ValueError("{} at index {} has no valid id".format(item_label, index))
|
|
265
|
+
if item_id in item_by_id:
|
|
266
|
+
raise ValueError("Duplicate {} id: {}".format(item_label.lower(), item_id))
|
|
267
|
+
item_by_id[item_id] = item
|
|
268
|
+
ordered_ids.append(item_id)
|
|
269
|
+
|
|
270
|
+
_ordered, graph_errors = stable_topological_order(
|
|
271
|
+
items,
|
|
272
|
+
id_key=id_key,
|
|
273
|
+
dependencies_key=dependencies_key,
|
|
274
|
+
item_label=item_label,
|
|
275
|
+
)
|
|
276
|
+
if graph_errors:
|
|
277
|
+
raise ValueError(graph_errors[0])
|
|
278
|
+
|
|
279
|
+
blocker_cache = {}
|
|
280
|
+
|
|
281
|
+
def transitive_blockers(item_id):
|
|
282
|
+
cached = blocker_cache.get(item_id)
|
|
283
|
+
if cached is not None:
|
|
284
|
+
cached_roots, cached_unresolved = cached
|
|
285
|
+
return set(cached_roots), set(cached_unresolved)
|
|
286
|
+
item = item_by_id[item_id]
|
|
287
|
+
roots = set()
|
|
288
|
+
unresolved = set()
|
|
289
|
+
item_status = item.get(status_key, "pending")
|
|
290
|
+
if item_status in roots_statuses:
|
|
291
|
+
roots.add(item_id)
|
|
292
|
+
for dependency_id in item.get(dependencies_key, []) or []:
|
|
293
|
+
dependency_roots, dependency_unresolved = transitive_blockers(dependency_id)
|
|
294
|
+
roots.update(dependency_roots)
|
|
295
|
+
unresolved.update(dependency_unresolved)
|
|
296
|
+
if item_status == auto_skipped_status and not roots and not unresolved:
|
|
297
|
+
unresolved.add(item_id)
|
|
298
|
+
blocker_cache[item_id] = (frozenset(roots), frozenset(unresolved))
|
|
299
|
+
return roots, unresolved
|
|
300
|
+
|
|
301
|
+
if selected_id is None:
|
|
302
|
+
selected_roots = {
|
|
303
|
+
item_id
|
|
304
|
+
for item_id in ordered_ids
|
|
305
|
+
if item_by_id[item_id].get(status_key, "pending") in roots_statuses
|
|
306
|
+
}
|
|
307
|
+
if not selected_roots:
|
|
308
|
+
raise ValueError("No recoverable blocking roots found")
|
|
309
|
+
else:
|
|
310
|
+
target = item_by_id.get(selected_id)
|
|
311
|
+
if target is None:
|
|
312
|
+
raise ValueError("{} '{}' not found".format(item_label, selected_id))
|
|
313
|
+
target_status = target.get(status_key, "pending")
|
|
314
|
+
if target_status in roots_statuses:
|
|
315
|
+
selected_roots = {selected_id}
|
|
316
|
+
elif target_status == auto_skipped_status:
|
|
317
|
+
selected_roots, unresolved = transitive_blockers(selected_id)
|
|
318
|
+
if unresolved:
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"{} '{}' has unresolved auto_skipped blocker(s): {}".format(
|
|
321
|
+
item_label, selected_id, ", ".join(sorted(unresolved))
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
if not selected_roots:
|
|
325
|
+
raise ValueError(
|
|
326
|
+
"{} '{}' is auto_skipped but has no transitive blocking root".format(
|
|
327
|
+
item_label, selected_id
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
supported = sorted(roots_statuses | {auto_skipped_status})
|
|
332
|
+
raise ValueError(
|
|
333
|
+
"{} '{}' has status '{}', which is not recoverable; expected one of {}".format(
|
|
334
|
+
item_label, selected_id, target_status, ", ".join(supported)
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
selected = set(selected_roots)
|
|
339
|
+
for item_id in ordered_ids:
|
|
340
|
+
item = item_by_id[item_id]
|
|
341
|
+
if item.get(status_key, "pending") != auto_skipped_status:
|
|
342
|
+
continue
|
|
343
|
+
blockers, unresolved = transitive_blockers(item_id)
|
|
344
|
+
if blockers and not unresolved and blockers.issubset(selected_roots):
|
|
345
|
+
selected.add(item_id)
|
|
346
|
+
|
|
347
|
+
return tuple(item_id for item_id in ordered_ids if item_id in selected)
|
|
348
|
+
|
|
349
|
+
|
|
231
350
|
def setup_logging(name="prizmkit.dev_pipeline", level=None):
|
|
232
351
|
"""Configure and return a standard logger for pipeline scripts.
|
|
233
352
|
|
|
@@ -177,9 +177,10 @@
|
|
|
177
177
|
"completed",
|
|
178
178
|
"failed",
|
|
179
179
|
"skipped",
|
|
180
|
-
"needs_info"
|
|
180
|
+
"needs_info",
|
|
181
|
+
"auto_skipped"
|
|
181
182
|
],
|
|
182
|
-
"description": "Bug fix status.
|
|
183
|
+
"description": "Bug fix status. auto_skipped is runtime-managed for descendants blocked by failed, skipped, or needs_info dependencies."
|
|
183
184
|
},
|
|
184
185
|
"user_context": {
|
|
185
186
|
"type": "array",
|