prizmkit 1.1.149 → 1.1.151
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 +20 -17
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +234 -39
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +57 -30
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +822 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +28 -11
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +3 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +71 -28
- package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +18 -0
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +20 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +86 -13
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +959 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +362 -5
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -9
- package/bundled/templates/project-memory-template.md +38 -0
- package/package.json +1 -1
|
@@ -994,6 +994,29 @@ def test_progress_parser_does_not_mark_failed_pi_agent_end_as_success():
|
|
|
994
994
|
assert "provider request failed" in data["errors"]
|
|
995
995
|
|
|
996
996
|
|
|
997
|
+
def test_progress_parser_maps_pi_length_stop_to_context_overflow():
|
|
998
|
+
module = _load_progress_parser_module()
|
|
999
|
+
tracker = module.ProgressTracker()
|
|
1000
|
+
tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
|
|
1001
|
+
tracker.process_event({"type": "agent_start"})
|
|
1002
|
+
message = {
|
|
1003
|
+
"role": "assistant",
|
|
1004
|
+
"content": [{"type": "thinking", "thinking": "Planning the next test"}],
|
|
1005
|
+
"stopReason": "length",
|
|
1006
|
+
}
|
|
1007
|
+
tracker.process_event({"type": "message_end", "message": message})
|
|
1008
|
+
tracker.process_event({"type": "agent_end", "willRetry": False, "messages": [message]})
|
|
1009
|
+
|
|
1010
|
+
data = tracker.to_dict()
|
|
1011
|
+
|
|
1012
|
+
assert data["result_subtype"] == "error"
|
|
1013
|
+
assert data["stop_reason"] == "length"
|
|
1014
|
+
assert data["last_result_is_error"] is True
|
|
1015
|
+
assert data["api_error_code"] == "context_overflow"
|
|
1016
|
+
assert data["fatal_error_code"] == "context_overflow"
|
|
1017
|
+
assert data["terminal_success_at"] == ""
|
|
1018
|
+
|
|
1019
|
+
|
|
997
1020
|
def test_progress_parser_recovers_from_retrying_pi_attempt_before_final_success():
|
|
998
1021
|
module = _load_progress_parser_module()
|
|
999
1022
|
tracker = module.ProgressTracker()
|
|
@@ -1190,6 +1213,20 @@ def test_semantic_log_normalizer_emits_terminal_metadata_once():
|
|
|
1190
1213
|
assert "[FINAL] Done" in rendered
|
|
1191
1214
|
|
|
1192
1215
|
|
|
1216
|
+
def test_semantic_log_normalizer_labels_pi_length_as_context_overflow():
|
|
1217
|
+
module = _load_progress_parser_module()
|
|
1218
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1219
|
+
rendered = normalizer.process_event({
|
|
1220
|
+
"type": "agent_end",
|
|
1221
|
+
"willRetry": False,
|
|
1222
|
+
"messages": [{"role": "assistant", "content": [], "stopReason": "length"}],
|
|
1223
|
+
})
|
|
1224
|
+
rendered += normalizer.finish()
|
|
1225
|
+
|
|
1226
|
+
assert rendered.count("[SESSION_END]") == 1
|
|
1227
|
+
assert "status=context_overflow stop_reason=length" in rendered
|
|
1228
|
+
|
|
1229
|
+
|
|
1193
1230
|
def test_semantic_log_normalizer_preserves_identical_text_from_distinct_turns():
|
|
1194
1231
|
module = _load_progress_parser_module()
|
|
1195
1232
|
normalizer = module.SemanticLogNormalizer()
|
|
@@ -2412,8 +2449,16 @@ def test_git_branch_worktree_plans_preserve_shell_invariants(tmp_path):
|
|
|
2412
2449
|
assert any(command.args[:2] == ("rebase", "--abort") for command in ensure.commands)
|
|
2413
2450
|
assert any(command.args[:2] == ("add", "-A") for command in ensure.commands)
|
|
2414
2451
|
assert not any(command.args and command.args[0] == "stash" for command in ensure.commands)
|
|
2452
|
+
assert not any(command.args and command.args[0] in {"merge", "push"} for command in save_wip.commands + ensure.commands)
|
|
2453
|
+
assert not any(command.args[:2] == ("branch", "-d") for command in save_wip.commands + ensure.commands)
|
|
2415
2454
|
assert ensure.never_fails is False
|
|
2416
|
-
assert any("
|
|
2455
|
+
assert any(command.args == ("add", "-A", "--", ".") for command in save_wip.commands)
|
|
2456
|
+
assert not any(
|
|
2457
|
+
"exclude" in arg
|
|
2458
|
+
for command in save_wip.commands
|
|
2459
|
+
if command.args[:2] == ("add", "-A")
|
|
2460
|
+
for arg in command.args
|
|
2461
|
+
)
|
|
2417
2462
|
assert worktree.commands[0].args[:2] == ("worktree", "add")
|
|
2418
2463
|
|
|
2419
2464
|
allowed_failure_result = GitPlanResult(
|
|
@@ -2587,16 +2632,27 @@ def test_branch_return_ignores_installed_runtime_paths_without_blocking_wip_comm
|
|
|
2587
2632
|
from prizmkit_runtime.gitops import branch_ensure_return
|
|
2588
2633
|
|
|
2589
2634
|
_init_merge_repo(tmp_path)
|
|
2590
|
-
(tmp_path / ".gitignore").write_text(
|
|
2635
|
+
(tmp_path / ".gitignore").write_text(
|
|
2636
|
+
".prizmkit/*\n.claude/\n.codebuddy/\n.agents/\n.pi\n",
|
|
2637
|
+
encoding="utf-8",
|
|
2638
|
+
)
|
|
2591
2639
|
subprocess.run(["git", "add", ".gitignore"], cwd=tmp_path, check=True)
|
|
2592
2640
|
subprocess.run(["git", "commit", "-m", "ignore installed runtime"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2641
|
+
ignored_files = (
|
|
2642
|
+
tmp_path / ".prizmkit" / "dev-pipeline" / "cli.py",
|
|
2643
|
+
tmp_path / ".claude" / "runtime.json",
|
|
2644
|
+
tmp_path / ".codebuddy" / "runtime.json",
|
|
2645
|
+
tmp_path / ".agents" / "runtime.json",
|
|
2646
|
+
tmp_path / ".pi" / "runtime.json",
|
|
2647
|
+
)
|
|
2648
|
+
for ignored_file in ignored_files:
|
|
2649
|
+
ignored_file.parent.mkdir(parents=True, exist_ok=True)
|
|
2650
|
+
ignored_file.write_text("ignored support\n", encoding="utf-8")
|
|
2596
2651
|
manifest = tmp_path / ".prizmkit" / "manifest.json"
|
|
2597
2652
|
manifest.write_text('{}\n', encoding="utf-8")
|
|
2598
2653
|
subprocess.run(["git", "checkout", "-b", "dev/interrupted"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2599
2654
|
(tmp_path / "interrupted.txt").write_text("preserved\n", encoding="utf-8")
|
|
2655
|
+
(tmp_path / "interrupted [literal]*.txt").write_text("special path\n", encoding="utf-8")
|
|
2600
2656
|
|
|
2601
2657
|
result = branch_ensure_return(tmp_path, "main", "dev/interrupted")
|
|
2602
2658
|
|
|
@@ -2607,6 +2663,168 @@ def test_branch_return_ignores_installed_runtime_paths_without_blocking_wip_comm
|
|
|
2607
2663
|
assert subprocess.run(
|
|
2608
2664
|
["git", "show", "dev/interrupted:interrupted.txt"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2609
2665
|
).stdout == "preserved\n"
|
|
2666
|
+
assert subprocess.run(
|
|
2667
|
+
["git", "show", "dev/interrupted:interrupted [literal]*.txt"],
|
|
2668
|
+
cwd=tmp_path,
|
|
2669
|
+
check=True,
|
|
2670
|
+
stdout=subprocess.PIPE,
|
|
2671
|
+
text=True,
|
|
2672
|
+
).stdout == "special path\n"
|
|
2673
|
+
committed_paths = subprocess.run(
|
|
2674
|
+
["git", "ls-tree", "-r", "--name-only", "dev/interrupted"],
|
|
2675
|
+
cwd=tmp_path,
|
|
2676
|
+
check=True,
|
|
2677
|
+
stdout=subprocess.PIPE,
|
|
2678
|
+
text=True,
|
|
2679
|
+
).stdout.splitlines()
|
|
2680
|
+
assert not any(path.startswith((".claude/", ".codebuddy/", ".agents/", ".pi/")) for path in committed_paths)
|
|
2681
|
+
|
|
2682
|
+
|
|
2683
|
+
def test_clean_branch_return_skips_wip_commit_and_checks_out_base(tmp_path):
|
|
2684
|
+
from prizmkit_runtime.gitops import branch_ensure_return
|
|
2685
|
+
|
|
2686
|
+
_init_merge_repo(tmp_path)
|
|
2687
|
+
base_head = subprocess.run(
|
|
2688
|
+
["git", "rev-parse", "HEAD"],
|
|
2689
|
+
cwd=tmp_path,
|
|
2690
|
+
check=True,
|
|
2691
|
+
stdout=subprocess.PIPE,
|
|
2692
|
+
text=True,
|
|
2693
|
+
).stdout.strip()
|
|
2694
|
+
subprocess.run(["git", "checkout", "-b", "dev/clean-interrupt"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2695
|
+
|
|
2696
|
+
result = branch_ensure_return(tmp_path, "main", "dev/clean-interrupt")
|
|
2697
|
+
|
|
2698
|
+
assert result.ok is True
|
|
2699
|
+
assert not any(command_result.args[:1] == ("commit",) for command_result in result.results)
|
|
2700
|
+
assert subprocess.run(
|
|
2701
|
+
["git", "branch", "--show-current"],
|
|
2702
|
+
cwd=tmp_path,
|
|
2703
|
+
check=True,
|
|
2704
|
+
stdout=subprocess.PIPE,
|
|
2705
|
+
text=True,
|
|
2706
|
+
).stdout.strip() == "main"
|
|
2707
|
+
assert subprocess.run(
|
|
2708
|
+
["git", "rev-parse", "dev/clean-interrupt"],
|
|
2709
|
+
cwd=tmp_path,
|
|
2710
|
+
check=True,
|
|
2711
|
+
stdout=subprocess.PIPE,
|
|
2712
|
+
text=True,
|
|
2713
|
+
).stdout.strip() == base_head
|
|
2714
|
+
|
|
2715
|
+
|
|
2716
|
+
def test_branch_return_commits_staged_unstaged_deleted_renamed_and_special_paths(tmp_path):
|
|
2717
|
+
from prizmkit_runtime.gitops import branch_ensure_return
|
|
2718
|
+
|
|
2719
|
+
_init_merge_repo(tmp_path)
|
|
2720
|
+
tracked_paths = {
|
|
2721
|
+
"mixed.txt": "base mixed\n",
|
|
2722
|
+
"delete me.txt": "delete me\n",
|
|
2723
|
+
"rename source.txt": "rename me\n",
|
|
2724
|
+
}
|
|
2725
|
+
for relative, content in tracked_paths.items():
|
|
2726
|
+
(tmp_path / relative).write_text(content, encoding="utf-8")
|
|
2727
|
+
subprocess.run(["git", "add", "--", *tracked_paths], cwd=tmp_path, check=True)
|
|
2728
|
+
subprocess.run(["git", "commit", "-m", "wip matrix base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2729
|
+
subprocess.run(["git", "checkout", "-b", "dev/wip-matrix"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2730
|
+
|
|
2731
|
+
(tmp_path / "mixed.txt").write_text("staged version\n", encoding="utf-8")
|
|
2732
|
+
subprocess.run(["git", "add", "mixed.txt"], cwd=tmp_path, check=True)
|
|
2733
|
+
(tmp_path / "mixed.txt").write_text("final unstaged version\n", encoding="utf-8")
|
|
2734
|
+
(tmp_path / "delete me.txt").unlink()
|
|
2735
|
+
subprocess.run(["git", "mv", "rename source.txt", "renamed [literal]*.txt"], cwd=tmp_path, check=True)
|
|
2736
|
+
(tmp_path / "new [literal]*.txt").write_text("new special\n", encoding="utf-8")
|
|
2737
|
+
|
|
2738
|
+
result = branch_ensure_return(tmp_path, "main", "dev/wip-matrix")
|
|
2739
|
+
|
|
2740
|
+
assert result.ok is True
|
|
2741
|
+
assert subprocess.run(
|
|
2742
|
+
["git", "branch", "--show-current"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2743
|
+
).stdout.strip() == "main"
|
|
2744
|
+
assert subprocess.run(
|
|
2745
|
+
["git", "show", "dev/wip-matrix:mixed.txt"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2746
|
+
).stdout == "final unstaged version\n"
|
|
2747
|
+
assert subprocess.run(
|
|
2748
|
+
["git", "show", "dev/wip-matrix:renamed [literal]*.txt"],
|
|
2749
|
+
cwd=tmp_path,
|
|
2750
|
+
check=True,
|
|
2751
|
+
stdout=subprocess.PIPE,
|
|
2752
|
+
text=True,
|
|
2753
|
+
).stdout == "rename me\n"
|
|
2754
|
+
assert subprocess.run(
|
|
2755
|
+
["git", "show", "dev/wip-matrix:new [literal]*.txt"],
|
|
2756
|
+
cwd=tmp_path,
|
|
2757
|
+
check=True,
|
|
2758
|
+
stdout=subprocess.PIPE,
|
|
2759
|
+
text=True,
|
|
2760
|
+
).stdout == "new special\n"
|
|
2761
|
+
task_tree = subprocess.run(
|
|
2762
|
+
["git", "ls-tree", "-r", "--name-only", "dev/wip-matrix"],
|
|
2763
|
+
cwd=tmp_path,
|
|
2764
|
+
check=True,
|
|
2765
|
+
stdout=subprocess.PIPE,
|
|
2766
|
+
text=True,
|
|
2767
|
+
).stdout.splitlines()
|
|
2768
|
+
assert "delete me.txt" not in task_tree
|
|
2769
|
+
assert "rename source.txt" not in task_tree
|
|
2770
|
+
|
|
2771
|
+
|
|
2772
|
+
def test_branch_return_rejects_unmerged_paths_without_commit_or_checkout(tmp_path):
|
|
2773
|
+
from prizmkit_runtime.gitops import branch_ensure_return
|
|
2774
|
+
|
|
2775
|
+
_init_merge_repo(tmp_path)
|
|
2776
|
+
subprocess.run(["git", "checkout", "-b", "dev/conflicted"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2777
|
+
(tmp_path / "app.txt").write_text("task side\n", encoding="utf-8")
|
|
2778
|
+
subprocess.run(["git", "commit", "-am", "task side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2779
|
+
subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2780
|
+
(tmp_path / "app.txt").write_text("base side\n", encoding="utf-8")
|
|
2781
|
+
subprocess.run(["git", "commit", "-am", "base side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2782
|
+
subprocess.run(["git", "checkout", "dev/conflicted"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2783
|
+
merge = subprocess.run(
|
|
2784
|
+
["git", "merge", "main"],
|
|
2785
|
+
cwd=tmp_path,
|
|
2786
|
+
check=False,
|
|
2787
|
+
stdout=subprocess.PIPE,
|
|
2788
|
+
stderr=subprocess.PIPE,
|
|
2789
|
+
text=True,
|
|
2790
|
+
)
|
|
2791
|
+
assert merge.returncode != 0
|
|
2792
|
+
head_before = subprocess.run(
|
|
2793
|
+
["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2794
|
+
).stdout.strip()
|
|
2795
|
+
|
|
2796
|
+
result = branch_ensure_return(tmp_path, "main", "dev/conflicted")
|
|
2797
|
+
|
|
2798
|
+
assert result.ok is False
|
|
2799
|
+
assert "unresolved Git paths" in result.results[-1].stderr
|
|
2800
|
+
assert subprocess.run(
|
|
2801
|
+
["git", "branch", "--show-current"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2802
|
+
).stdout.strip() == "dev/conflicted"
|
|
2803
|
+
assert subprocess.run(
|
|
2804
|
+
["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2805
|
+
).stdout.strip() == head_before
|
|
2806
|
+
assert subprocess.run(
|
|
2807
|
+
["git", "ls-files", "-u"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
|
|
2808
|
+
).stdout.strip()
|
|
2809
|
+
|
|
2810
|
+
|
|
2811
|
+
def test_branch_save_wip_rejects_clean_recorded_branch_mismatch(tmp_path):
|
|
2812
|
+
from prizmkit_runtime.gitops import branch_save_wip
|
|
2813
|
+
|
|
2814
|
+
_init_merge_repo(tmp_path)
|
|
2815
|
+
subprocess.run(["git", "checkout", "-b", "dev/unrelated"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2816
|
+
|
|
2817
|
+
result = branch_save_wip(tmp_path, "dev/recorded-task")
|
|
2818
|
+
|
|
2819
|
+
assert result.ok is False
|
|
2820
|
+
assert result.results[-1].stderr == "Active branch dev/unrelated does not match task branch dev/recorded-task"
|
|
2821
|
+
assert subprocess.run(
|
|
2822
|
+
["git", "branch", "--show-current"],
|
|
2823
|
+
cwd=tmp_path,
|
|
2824
|
+
check=True,
|
|
2825
|
+
stdout=subprocess.PIPE,
|
|
2826
|
+
text=True,
|
|
2827
|
+
).stdout.strip() == "dev/unrelated"
|
|
2610
2828
|
|
|
2611
2829
|
|
|
2612
2830
|
def test_branch_return_commit_failure_does_not_checkout_base(monkeypatch, tmp_path):
|
|
@@ -3104,3 +3322,142 @@ def test_daemon_status_cli_preserves_pure_json_stdout(tmp_path):
|
|
|
3104
3322
|
assert "Python feature daemon" not in result.stdout
|
|
3105
3323
|
assert "Transition command contract" not in result.stdout
|
|
3106
3324
|
|
|
3325
|
+
|
|
3326
|
+
def test_active_checkout_replacement_is_guarded_and_never_writes_reset_tombstone(tmp_path):
|
|
3327
|
+
from prizmkit_runtime.task_checkout import (
|
|
3328
|
+
TaskCheckout,
|
|
3329
|
+
TaskCheckoutError,
|
|
3330
|
+
load_task_checkout,
|
|
3331
|
+
persist_task_checkout,
|
|
3332
|
+
replace_active_task_checkout_locked,
|
|
3333
|
+
)
|
|
3334
|
+
|
|
3335
|
+
state_dir = tmp_path / "state"
|
|
3336
|
+
expected = TaskCheckout.active("feature", "F-001", "dev/F-001-old", "main")
|
|
3337
|
+
replacement = TaskCheckout.active("feature", "F-001", "dev/F-001-resume", "main")
|
|
3338
|
+
persist_task_checkout(state_dir, expected)
|
|
3339
|
+
|
|
3340
|
+
path = replace_active_task_checkout_locked(state_dir, expected, replacement)
|
|
3341
|
+
|
|
3342
|
+
assert path == state_dir / "F-001" / "checkout.json"
|
|
3343
|
+
loaded = load_task_checkout(state_dir, "feature", "F-001")
|
|
3344
|
+
assert loaded == replacement
|
|
3345
|
+
assert loaded is not None and loaded.is_reset is False
|
|
3346
|
+
with pytest.raises(TaskCheckoutError, match="changed before recovery replacement"):
|
|
3347
|
+
replace_active_task_checkout_locked(state_dir, expected, replacement)
|
|
3348
|
+
|
|
3349
|
+
|
|
3350
|
+
def test_checkpoint_rewind_preserves_planning_and_resets_implementation_downstream():
|
|
3351
|
+
from prizmkit_runtime.checkpoint_state import (
|
|
3352
|
+
rewind_checkpoint_for_implementation,
|
|
3353
|
+
validate_checkpoint_data,
|
|
3354
|
+
)
|
|
3355
|
+
|
|
3356
|
+
skills = [
|
|
3357
|
+
"prizmkit-plan",
|
|
3358
|
+
"prizmkit-implement",
|
|
3359
|
+
"prizmkit-code-review",
|
|
3360
|
+
"prizmkit-test",
|
|
3361
|
+
"prizmkit-retrospective",
|
|
3362
|
+
"prizmkit-committer",
|
|
3363
|
+
"completion-summary",
|
|
3364
|
+
]
|
|
3365
|
+
steps = []
|
|
3366
|
+
previous = None
|
|
3367
|
+
for index, skill in enumerate(skills, start=1):
|
|
3368
|
+
status = "pending"
|
|
3369
|
+
stage_result = None
|
|
3370
|
+
if skill == "prizmkit-plan":
|
|
3371
|
+
status, stage_result = "completed", "PLAN_READY"
|
|
3372
|
+
elif skill == "prizmkit-implement":
|
|
3373
|
+
status, stage_result = "completed", "IMPLEMENTED"
|
|
3374
|
+
elif skill == "prizmkit-code-review":
|
|
3375
|
+
status, stage_result = "failed", "REVIEW_NEEDS_FIXES"
|
|
3376
|
+
step = {
|
|
3377
|
+
"id": f"S{index:02d}",
|
|
3378
|
+
"skill": skill,
|
|
3379
|
+
"name": skill,
|
|
3380
|
+
"status": status,
|
|
3381
|
+
"depends_on": previous,
|
|
3382
|
+
"required_artifacts": [],
|
|
3383
|
+
}
|
|
3384
|
+
if stage_result:
|
|
3385
|
+
step["stage_result"] = stage_result
|
|
3386
|
+
steps.append(step)
|
|
3387
|
+
previous = step["id"]
|
|
3388
|
+
payload = {
|
|
3389
|
+
"version": 1,
|
|
3390
|
+
"workflow_type": "feature-pipeline",
|
|
3391
|
+
"steps": steps,
|
|
3392
|
+
"feature_state": {
|
|
3393
|
+
"schema_version": 1,
|
|
3394
|
+
"artifact_dir": ".prizmkit/specs/example",
|
|
3395
|
+
"orchestrator": "prizmkit-l4",
|
|
3396
|
+
"stage": "code-review",
|
|
3397
|
+
"current_stage": "prizmkit-code-review",
|
|
3398
|
+
"status": "failed",
|
|
3399
|
+
"stage_result": "REVIEW_NEEDS_FIXES",
|
|
3400
|
+
"repair_scope": "production",
|
|
3401
|
+
"repair_round": 2,
|
|
3402
|
+
"next_stage": "prizmkit-implement",
|
|
3403
|
+
"resume_from": "prizmkit-implement",
|
|
3404
|
+
"blocked_reason": "review_needs_fixes",
|
|
3405
|
+
"terminal_status": "WORKFLOW_BLOCKED",
|
|
3406
|
+
"completed_stages": ["plan", "implement"],
|
|
3407
|
+
},
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
rewound = rewind_checkpoint_for_implementation(payload)
|
|
3411
|
+
|
|
3412
|
+
assert rewound is not payload
|
|
3413
|
+
assert rewound["steps"][0] == payload["steps"][0]
|
|
3414
|
+
assert all(step["status"] == "pending" for step in rewound["steps"][1:])
|
|
3415
|
+
assert all("stage_result" not in step for step in rewound["steps"][1:])
|
|
3416
|
+
semantic = rewound["feature_state"]
|
|
3417
|
+
assert semantic["stage"] == "implement"
|
|
3418
|
+
assert semantic["current_stage"] == "prizmkit-implement"
|
|
3419
|
+
assert semantic["status"] == "pending"
|
|
3420
|
+
assert semantic["repair_round"] == 0
|
|
3421
|
+
assert semantic["completed_stages"] == ["plan"]
|
|
3422
|
+
assert semantic["blocked_reason"] is None
|
|
3423
|
+
assert semantic["terminal_status"] is None
|
|
3424
|
+
assert "stage_result" not in semantic
|
|
3425
|
+
validated = validate_checkpoint_data(rewound)
|
|
3426
|
+
assert validated.valid is True
|
|
3427
|
+
assert validated.current_step is not None
|
|
3428
|
+
assert validated.current_step.skill == "prizmkit-implement"
|
|
3429
|
+
|
|
3430
|
+
|
|
3431
|
+
def test_git_recovery_helpers_create_branch_without_checkout_and_report_registration(tmp_path):
|
|
3432
|
+
from prizmkit_runtime.gitops import (
|
|
3433
|
+
create_branch_from_base,
|
|
3434
|
+
local_branch_exists,
|
|
3435
|
+
registered_worktrees,
|
|
3436
|
+
)
|
|
3437
|
+
|
|
3438
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3439
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
3440
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
3441
|
+
(tmp_path / "base.txt").write_text("base\n", encoding="utf-8")
|
|
3442
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
3443
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3444
|
+
|
|
3445
|
+
assert local_branch_exists(tmp_path, "main") is True
|
|
3446
|
+
assert local_branch_exists(tmp_path, "missing") is False
|
|
3447
|
+
result = create_branch_from_base(tmp_path, "dev/F-001-resume", "main")
|
|
3448
|
+
|
|
3449
|
+
assert result.return_code == 0
|
|
3450
|
+
assert local_branch_exists(tmp_path, "dev/F-001-resume") is True
|
|
3451
|
+
assert subprocess.run(
|
|
3452
|
+
["git", "branch", "--show-current"],
|
|
3453
|
+
cwd=tmp_path,
|
|
3454
|
+
check=True,
|
|
3455
|
+
stdout=subprocess.PIPE,
|
|
3456
|
+
text=True,
|
|
3457
|
+
).stdout.strip() == "main"
|
|
3458
|
+
registrations = registered_worktrees(tmp_path)
|
|
3459
|
+
assert any(
|
|
3460
|
+
registration.path == tmp_path.resolve() and registration.branch == "main"
|
|
3461
|
+
for registration in registrations
|
|
3462
|
+
)
|
|
3463
|
+
|
|
@@ -51,7 +51,7 @@ Read `${SKILL_DIR}/references/configuration.md` before constructing prerequisite
|
|
|
51
51
|
| Check status | Daemon status and item-level status commands |
|
|
52
52
|
| Show logs | Recent or follow-log command |
|
|
53
53
|
| Stop | Daemon stop command plus optional status command |
|
|
54
|
-
| Retry
|
|
54
|
+
| Retry failed bug work | Preserve-runtime, standard, or clean reset plus a separate run command with safety disclosure |
|
|
55
55
|
|
|
56
56
|
Only the start intent uses the execution-mode and configuration rounds.
|
|
57
57
|
|
|
@@ -151,27 +151,39 @@ Optionally include the daemon status command for the user to run afterward. Do n
|
|
|
151
151
|
|
|
152
152
|
## Retry Command
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
Reset changes state only; it never starts execution and rejects `--run`. Always return reset and run as separate commands.
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
A standard reset can discard uncommitted changes when the target task branch is currently checked out and records a fresh-checkout boundary. Clean reset additionally deletes the task branch and clears task history/artifacts. Preserve-runtime failed recovery instead scans the whole selected list, restores every failed root and its causal `auto_skipped` descendants, and retains valid checkout identity, sessions, checkpoints, artifacts, branches, worktrees, and WIP. If a recorded branch is missing, it creates a replacement from the recorded base, retains old artifacts for reference, and rewinds implementation-and-later checkpoint stages. Never hide these effects behind the word “retry.”
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- **
|
|
158
|
+
Use `AskUserQuestion` before constructing retry commands:
|
|
159
|
+
|
|
160
|
+
- **Preserve failed runtime (Recommended for continuing failed work)** — recover all failed chains in the selected list without cleanup, then run the list; disclose that this is whole-list recovery rather than a single-ID reset.
|
|
161
|
+
- **Standard fresh retry** — reset one bug, then run that bug separately; warn that the active task branch can lose uncommitted changes.
|
|
162
|
+
- **Clean retry** — reset one bug, delete its task branch/history/artifacts, then run it separately.
|
|
163
|
+
- **Cancel** — return no reset or run command.
|
|
164
|
+
|
|
165
|
+
Preserve-runtime template:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset bugfix --failed --preserve-runtime .prizmkit/plans/bug-fix-list.json
|
|
169
|
+
python3 ./.prizmkit/dev-pipeline/cli.py bugfix run .prizmkit/plans/bug-fix-list.json
|
|
170
|
+
```
|
|
161
171
|
|
|
162
172
|
Standard template:
|
|
163
173
|
|
|
164
174
|
```bash
|
|
165
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset bugfix B-001
|
|
175
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset bugfix B-001 .prizmkit/plans/bug-fix-list.json
|
|
176
|
+
python3 ./.prizmkit/dev-pipeline/cli.py bugfix run B-001 .prizmkit/plans/bug-fix-list.json
|
|
166
177
|
```
|
|
167
178
|
|
|
168
179
|
Clean template:
|
|
169
180
|
|
|
170
181
|
```bash
|
|
171
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset bugfix B-001 --clean
|
|
182
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset bugfix B-001 --clean .prizmkit/plans/bug-fix-list.json
|
|
183
|
+
python3 ./.prizmkit/dev-pipeline/cli.py bugfix run B-001 .prizmkit/plans/bug-fix-list.json
|
|
172
184
|
```
|
|
173
185
|
|
|
174
|
-
For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying
|
|
186
|
+
`--preserve-runtime` requires `--failed` and cannot be combined with `--clean`, an ID, a range, or another reset filter. Retry-budget flags such as `--max-retries` and `--max-infra-retries` belong on the run command, never reset. For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying either command.
|
|
175
187
|
|
|
176
188
|
## Output Contract
|
|
177
189
|
|
|
@@ -54,7 +54,7 @@ Classify the request before asking configuration questions:
|
|
|
54
54
|
| Check status | Daemon status and item-level status commands |
|
|
55
55
|
| Show logs | Recent or follow-log command |
|
|
56
56
|
| Stop | Daemon stop command plus optional status command |
|
|
57
|
-
| Retry
|
|
57
|
+
| Retry failed feature work | Preserve-runtime, standard, or clean reset plus a separate run command with safety disclosure |
|
|
58
58
|
|
|
59
59
|
Only the start intent uses the execution-mode and configuration rounds.
|
|
60
60
|
|
|
@@ -169,27 +169,39 @@ Optionally include the daemon status command for the user to run afterward. Do n
|
|
|
169
169
|
|
|
170
170
|
## Retry Command
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
Reset changes state only; it never starts execution and rejects `--run`. Always return reset and run as separate commands.
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
A standard reset can discard uncommitted changes when the target task branch is currently checked out and records a fresh-checkout boundary. Clean reset additionally deletes the task branch and clears task history/artifacts. Preserve-runtime failed recovery instead scans the whole selected list, restores every failed root and its causal `auto_skipped` descendants, and retains valid checkout identity, sessions, checkpoints, artifacts, branches, worktrees, and WIP. If a recorded branch is missing, it creates a replacement from the recorded base, retains old artifacts for reference, and rewinds implementation-and-later checkpoint stages. Never hide these effects behind the word “retry.”
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- **
|
|
176
|
+
Use `AskUserQuestion` before constructing retry commands:
|
|
177
|
+
|
|
178
|
+
- **Preserve failed runtime (Recommended for continuing failed work)** — recover all failed chains in the selected list without cleanup, then run the list; disclose that this is whole-list recovery rather than a single-ID reset.
|
|
179
|
+
- **Standard fresh retry** — reset one feature, then run that feature separately; warn that the active task branch can lose uncommitted changes.
|
|
180
|
+
- **Clean retry** — reset one feature, delete its task branch/history/artifacts, then run it separately.
|
|
181
|
+
- **Cancel** — return no reset or run command.
|
|
182
|
+
|
|
183
|
+
Preserve-runtime template:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset feature --failed --preserve-runtime .prizmkit/plans/feature-list.json
|
|
187
|
+
python3 ./.prizmkit/dev-pipeline/cli.py feature run .prizmkit/plans/feature-list.json
|
|
188
|
+
```
|
|
179
189
|
|
|
180
190
|
Standard template:
|
|
181
191
|
|
|
182
192
|
```bash
|
|
183
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset feature F-001
|
|
193
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset feature F-001 .prizmkit/plans/feature-list.json
|
|
194
|
+
python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001 .prizmkit/plans/feature-list.json
|
|
184
195
|
```
|
|
185
196
|
|
|
186
197
|
Clean template:
|
|
187
198
|
|
|
188
199
|
```bash
|
|
189
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset feature F-001 --clean
|
|
200
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset feature F-001 --clean .prizmkit/plans/feature-list.json
|
|
201
|
+
python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001 .prizmkit/plans/feature-list.json
|
|
190
202
|
```
|
|
191
203
|
|
|
192
|
-
For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying
|
|
204
|
+
`--preserve-runtime` requires `--failed` and cannot be combined with `--clean`, an ID, a range, or another reset filter. Retry-budget flags such as `--max-retries` and `--max-infra-retries` belong on the run command, never reset. For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying either command.
|
|
193
205
|
|
|
194
206
|
## Output Contract
|
|
195
207
|
|
|
@@ -52,7 +52,7 @@ Read `${SKILL_DIR}/references/configuration.md` before constructing prerequisite
|
|
|
52
52
|
| Check status | Daemon status and item-level status commands |
|
|
53
53
|
| Show logs | Recent or follow-log command |
|
|
54
54
|
| Stop | Daemon stop command plus optional status command |
|
|
55
|
-
| Retry
|
|
55
|
+
| Retry failed refactor work | Preserve-runtime, standard, or clean reset plus a separate run command with safety disclosure |
|
|
56
56
|
|
|
57
57
|
Only the start intent uses the execution-mode and configuration rounds.
|
|
58
58
|
|
|
@@ -164,27 +164,39 @@ Optionally include the daemon status command for the user to run afterward. Do n
|
|
|
164
164
|
|
|
165
165
|
## Retry Command
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Reset changes state only; it never starts execution and rejects `--run`. Always return reset and run as separate commands.
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
A standard reset can discard uncommitted changes when the target task branch is currently checked out and records a fresh-checkout boundary. Clean reset additionally deletes the task branch and clears task history/artifacts. Preserve-runtime failed recovery instead scans the whole selected list, restores every failed root and its causal `auto_skipped` descendants, and retains valid checkout identity, sessions, checkpoints, artifacts, branches, worktrees, and WIP. If a recorded branch is missing, it creates a replacement from the recorded base, retains old artifacts for reference, and rewinds implementation-and-later checkpoint stages. Never hide these effects behind the word “retry.”
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- **
|
|
171
|
+
Use `AskUserQuestion` before constructing retry commands:
|
|
172
|
+
|
|
173
|
+
- **Preserve failed runtime (Recommended for continuing failed work)** — recover all failed chains in the selected list without cleanup, then run the list; disclose that this is whole-list recovery rather than a single-ID reset.
|
|
174
|
+
- **Standard fresh retry** — reset one refactor, then run that refactor separately; warn that the active task branch can lose uncommitted changes.
|
|
175
|
+
- **Clean retry** — reset one refactor, delete its task branch/history/artifacts, then run it separately.
|
|
176
|
+
- **Cancel** — return no reset or run command.
|
|
177
|
+
|
|
178
|
+
Preserve-runtime template:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset refactor --failed --preserve-runtime .prizmkit/plans/refactor-list.json
|
|
182
|
+
python3 ./.prizmkit/dev-pipeline/cli.py refactor run .prizmkit/plans/refactor-list.json
|
|
183
|
+
```
|
|
174
184
|
|
|
175
185
|
Standard template:
|
|
176
186
|
|
|
177
187
|
```bash
|
|
178
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset refactor R-001
|
|
188
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset refactor R-001 .prizmkit/plans/refactor-list.json
|
|
189
|
+
python3 ./.prizmkit/dev-pipeline/cli.py refactor run R-001 .prizmkit/plans/refactor-list.json
|
|
179
190
|
```
|
|
180
191
|
|
|
181
192
|
Clean template:
|
|
182
193
|
|
|
183
194
|
```bash
|
|
184
|
-
python3 ./.prizmkit/dev-pipeline/cli.py reset refactor R-001 --clean
|
|
195
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset refactor R-001 --clean .prizmkit/plans/refactor-list.json
|
|
196
|
+
python3 ./.prizmkit/dev-pipeline/cli.py refactor run R-001 .prizmkit/plans/refactor-list.json
|
|
185
197
|
```
|
|
186
198
|
|
|
187
|
-
For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying
|
|
199
|
+
`--preserve-runtime` requires `--failed` and cannot be combined with `--clean`, an ID, a range, or another reset filter. Retry-budget flags such as `--max-retries` and `--max-infra-retries` belong on the run command, never reset. For clean retry, require explicit acknowledgement of branch deletion and possible uncommitted-change loss before displaying either command.
|
|
188
200
|
|
|
189
201
|
## Output Contract
|
|
190
202
|
|
|
@@ -32,6 +32,44 @@ This project uses PrizmKit with the Prizm documentation system for AI-optimized
|
|
|
32
32
|
### Available Commands
|
|
33
33
|
Run `/prizmkit` to see all available PrizmKit commands.
|
|
34
34
|
|
|
35
|
+
### Python Runtime CLI Quick Reference
|
|
36
|
+
|
|
37
|
+
Placeholders: `<family> = feature | bugfix | refactor`; `<task-id> = F-NNN | B-NNN | R-NNN`; `<list>` is optional and defaults by family:
|
|
38
|
+
|
|
39
|
+
| `<family>` | Default `<list>` |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `feature` | `.prizmkit/plans/feature-list.json` |
|
|
42
|
+
| `bugfix` | `.prizmkit/plans/bug-fix-list.json` |
|
|
43
|
+
| `refactor` | `.prizmkit/plans/refactor-list.json` |
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Run/resume the whole list or one task
|
|
47
|
+
python3 ./.prizmkit/dev-pipeline/cli.py <family> run [<list>] [--max-retries N] [--max-infra-retries N]
|
|
48
|
+
python3 ./.prizmkit/dev-pipeline/cli.py <family> run <task-id> [<list>] [--max-retries N] [--max-infra-retries N]
|
|
49
|
+
|
|
50
|
+
# Inspect or recover task state
|
|
51
|
+
python3 ./.prizmkit/dev-pipeline/cli.py <family> status [<list>]
|
|
52
|
+
python3 ./.prizmkit/dev-pipeline/cli.py <family> unskip [<task-id>] [<list>]
|
|
53
|
+
python3 ./.prizmkit/dev-pipeline/cli.py recovery detect
|
|
54
|
+
|
|
55
|
+
# Reset state (never starts execution)
|
|
56
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset <family> <task-id> [<list>]
|
|
57
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset <family> --failed --preserve-runtime [<list>]
|
|
58
|
+
python3 ./.prizmkit/dev-pipeline/cli.py reset <family> <task-id> --clean [<list>]
|
|
59
|
+
|
|
60
|
+
# Background operation
|
|
61
|
+
python3 ./.prizmkit/dev-pipeline/cli.py daemon <family> start [<list>]
|
|
62
|
+
python3 ./.prizmkit/dev-pipeline/cli.py daemon <family> status
|
|
63
|
+
python3 ./.prizmkit/dev-pipeline/cli.py daemon <family> logs [--lines N|--follow]
|
|
64
|
+
python3 ./.prizmkit/dev-pipeline/cli.py daemon <family> stop
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Runtime boundaries:
|
|
68
|
+
- Reset and run are separate operations; reset rejects `--run`. Retry ceilings belong to `run`, not reset.
|
|
69
|
+
- `--preserve-runtime` requires `--failed`, scans the whole selected list, and cannot be combined with `--clean`, an ID/range, or another reset filter.
|
|
70
|
+
- Preserve-runtime recovery keeps valid checkout identity, sessions, checkpoints, artifacts, branches, worktrees, and WIP. If the recorded branch is missing, it creates a replacement from the recorded base and rewinds implementation-and-later checkpoint stages while retaining old artifacts for reference. Clean reset is destructive.
|
|
71
|
+
- In zsh/Bash multiline commands, `\` must be the final character on the line with no trailing space.
|
|
72
|
+
|
|
35
73
|
### Lightweight Planning Path
|
|
36
74
|
Not every change needs the full formal-requirement lifecycle. Use the lightweight path for:
|
|
37
75
|
- Bug fixes with clear root cause, config tweaks, typo fixes, simple refactors
|