prizmkit 1.1.153 → 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.
Files changed (43) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +100 -87
  3. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
  4. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
  6. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
  7. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
  8. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
  9. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
  10. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
  11. package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
  12. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
  13. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
  14. package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
  15. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
  16. package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
  17. package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
  18. package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
  19. package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
  20. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  21. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  22. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  23. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
  24. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
  25. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
  26. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
  27. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  28. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
  29. package/bundled/dev-pipeline/tests/test_unified_cli.py +468 -181
  30. package/bundled/skills/_metadata.json +1 -1
  31. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  32. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  33. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  34. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  35. package/bundled/skills/recovery-workflow/SKILL.md +7 -5
  36. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  37. package/bundled/skills/recovery-workflow/references/detection.md +3 -3
  38. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
  39. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  40. package/bundled/templates/project-memory-template.md +19 -11
  41. package/package.json +1 -1
  42. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  43. 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
- - unskip: Recover auto-skipped features (reset failed/skipped upstream + auto_skipped downstream)
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|unskip> \
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", "unskip"],
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: unskip
1758
+ # Action: reset_state
1753
1759
  # ---------------------------------------------------------------------------
1754
1760
 
1755
- def action_unskip(args, feature_list_path, state_dir):
1756
- """Recover auto-skipped features by resetting them and their failed upstream.
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
- to_reset = set()
1772
-
1773
- if feature_id:
1774
- # Find the target feature
1775
- target = None
1776
- for f in features:
1777
- if isinstance(f, dict) and f.get("id") == feature_id:
1778
- target = f
1779
- break
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
- # Reset all collected features in .prizmkit/plans/feature-list.json
1780
+ to_reset = set(recovery_ids)
1862
1781
  reset_details = []
1863
- for f in features:
1864
- if isinstance(f, dict) and f.get("id") in to_reset:
1865
- old_status = f.get("status", "unknown")
1866
- f["status"] = "pending"
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": f["id"],
1869
- "title": f.get("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
- # Reset runtime fields in status.json for each feature
1879
- for fid in to_reset:
1880
- fs = load_feature_status(state_dir, fid)
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, fid, fs)
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": "unskip",
1893
- "reset_count": len(to_reset),
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 == "unskip":
1981
- action_unskip(args, args.feature_list, args.state_dir)
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
- - unskip: Reset skipped refactors and their downstream dependents back to pending
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|unskip> \
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", "unskip", "complete"],
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: unskip
1298
+ # Action: reset_state
1293
1299
  # ---------------------------------------------------------------------------
1294
1300
 
1295
- def action_unskip(args, refactor_list_path, state_dir):
1296
- """Recover skipped/auto_skipped/failed refactors by resetting them and their failed upstream.
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
- to_reset = set()
1314
-
1315
- if refactor_id:
1316
- # Find the target refactor
1317
- target = None
1318
- for r in refactors:
1319
- if isinstance(r, dict) and r.get("id") == refactor_id:
1320
- target = r
1321
- break
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
- # Reset all collected refactors in refactor-list.json
1320
+ to_reset = set(recovery_ids)
1404
1321
  reset_details = []
1405
- for r in refactors:
1406
- if isinstance(r, dict) and r.get("id") in to_reset:
1407
- old_status = r.get("status", "unknown")
1408
- r["status"] = "pending"
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": r["id"],
1411
- "title": r.get("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
- # Reset runtime fields in status.json for each refactor
1421
- for rid in to_reset:
1422
- rs = load_refactor_status(state_dir, rid)
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, rid, rs)
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": "unskip",
1435
- "reset_count": len(to_reset),
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 == "unskip":
1479
- action_unskip(args, args.refactor_list, args.state_dir)
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. Subset of feature status enum plus needs_info."
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",