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
|
@@ -215,9 +215,8 @@ def test_python_parity_suite_declares_coverage_for_all_legacy_unix_snapshot_scen
|
|
|
215
215
|
"entrypointContracts": {
|
|
216
216
|
"runnerHelpContracts": ["test_cli_help_exposes_python_runtime_command_groups"],
|
|
217
217
|
"runnerMissingListErrors": ["test_python_runner_missing_list_errors_match_snapshot_contract"],
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"daemonStatusContracts": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
|
|
218
|
+
"resetHelpContracts": ["test_reset_and_daemon_entrypoint_contracts_match_snapshot_contracts"],
|
|
219
|
+
"daemonStatusContracts": ["test_reset_and_daemon_entrypoint_contracts_match_snapshot_contracts"],
|
|
221
220
|
},
|
|
222
221
|
"envResolution": {
|
|
223
222
|
"verboseAndStreamJson": ["test_python_env_resolution_scenarios_match_snapshot_contracts"],
|
|
@@ -283,8 +282,7 @@ def test_python_runner_missing_list_errors_match_snapshot_contract(tmp_path):
|
|
|
283
282
|
assert "Plan list not found" in result.render()
|
|
284
283
|
|
|
285
284
|
|
|
286
|
-
def
|
|
287
|
-
from prizmkit_runtime.commands import handle_runtime_command
|
|
285
|
+
def test_reset_and_daemon_entrypoint_contracts_match_snapshot_contracts(tmp_path):
|
|
288
286
|
from prizmkit_runtime.daemon import run_daemon_command
|
|
289
287
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
290
288
|
from prizmkit_runtime.reset import run_reset_command
|
|
@@ -292,11 +290,6 @@ def test_python_entrypoint_contracts_match_snapshot_contracts(tmp_path):
|
|
|
292
290
|
snapshot = _load_unix_runtime_snapshot()["entrypointContracts"]
|
|
293
291
|
paths = _make_paths(tmp_path / "entrypoints")
|
|
294
292
|
|
|
295
|
-
recovery = handle_runtime_command("recovery", "describe", paths=paths)
|
|
296
|
-
assert recovery.exit_code == snapshot["recoveryHelpContract"]["exitCode"]
|
|
297
|
-
assert "Python runtime command contract" in recovery.render()
|
|
298
|
-
assert "recovery describe" in recovery.render()
|
|
299
|
-
|
|
300
293
|
for kind in ("feature", "bugfix", "refactor"):
|
|
301
294
|
reset = run_reset_command(kind, ("--help",), paths)
|
|
302
295
|
assert reset.exit_code == snapshot["resetHelpContracts"][kind]["exitCode"]
|
|
@@ -722,7 +715,7 @@ def test_status_bridge_passes_max_retries_from_invocation_or_environment(monkeyp
|
|
|
722
715
|
("kind", "item_key"),
|
|
723
716
|
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
724
717
|
)
|
|
725
|
-
def
|
|
718
|
+
def test_pipeline_aggregate_reselection_keeps_first_seen_details_and_stops(monkeypatch, tmp_path, kind, item_key):
|
|
726
719
|
from prizmkit_runtime import runners
|
|
727
720
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
728
721
|
|
|
@@ -743,11 +736,11 @@ def test_pipeline_aggregate_replaces_same_item_status_in_first_seen_order(monkey
|
|
|
743
736
|
|
|
744
737
|
result = runners._run_pipeline(family, invocation, paths)
|
|
745
738
|
|
|
746
|
-
assert result.processed ==
|
|
747
|
-
assert result.completed is
|
|
748
|
-
assert result.stopped_reason == "
|
|
749
|
-
assert result.last_status == "
|
|
750
|
-
assert result.details == ("F-049:completed", "F-050:
|
|
739
|
+
assert result.processed == 2
|
|
740
|
+
assert result.completed is False
|
|
741
|
+
assert result.stopped_reason == "reselected_item"
|
|
742
|
+
assert result.last_status == "pending"
|
|
743
|
+
assert result.details == ("F-049:completed", "F-050:pending")
|
|
751
744
|
|
|
752
745
|
|
|
753
746
|
@pytest.mark.parametrize(
|
|
@@ -785,7 +778,46 @@ def test_pipeline_aggregate_keeps_distinct_unresolved_item_blocking(monkeypatch,
|
|
|
785
778
|
("kind", "item_key"),
|
|
786
779
|
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
787
780
|
)
|
|
788
|
-
def
|
|
781
|
+
def test_full_list_stops_after_newly_skipped_item(monkeypatch, tmp_path, kind, item_key):
|
|
782
|
+
from prizmkit_runtime import runners
|
|
783
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
784
|
+
|
|
785
|
+
paths = _make_paths(tmp_path)
|
|
786
|
+
family = family_for(kind, paths)
|
|
787
|
+
invocation = parse_invocation(family, "run", ())
|
|
788
|
+
selector_calls = 0
|
|
789
|
+
processor_calls = []
|
|
790
|
+
|
|
791
|
+
def fake_get_next(*args, **kwargs):
|
|
792
|
+
nonlocal selector_calls
|
|
793
|
+
selector_calls += 1
|
|
794
|
+
if selector_calls > 1:
|
|
795
|
+
raise AssertionError("selector called after terminal skipped item")
|
|
796
|
+
return SimpleNamespace(ok=True, stdout="", data={item_key: "X-001"}, text="")
|
|
797
|
+
|
|
798
|
+
def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
|
|
799
|
+
processor_calls.append(item_id)
|
|
800
|
+
return "skipped"
|
|
801
|
+
|
|
802
|
+
monkeypatch.setattr(runners, "get_next", fake_get_next)
|
|
803
|
+
monkeypatch.setattr(runners, "_process_item", fake_process)
|
|
804
|
+
|
|
805
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
806
|
+
|
|
807
|
+
assert selector_calls == 1
|
|
808
|
+
assert processor_calls == ["X-001"]
|
|
809
|
+
assert result.completed is False
|
|
810
|
+
assert result.processed == 1
|
|
811
|
+
assert result.stopped_reason == "item_skipped"
|
|
812
|
+
assert result.last_status == "skipped"
|
|
813
|
+
assert result.details == ("X-001:skipped",)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@pytest.mark.parametrize(
|
|
817
|
+
("kind", "item_key"),
|
|
818
|
+
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
819
|
+
)
|
|
820
|
+
def test_full_list_rejects_reselected_processed_item_before_second_launch(monkeypatch, tmp_path, kind, item_key):
|
|
789
821
|
from prizmkit_runtime import runners
|
|
790
822
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
791
823
|
|
|
@@ -793,20 +825,29 @@ def test_pipeline_aggregate_repeated_terminal_update_remains_unique(monkeypatch,
|
|
|
793
825
|
family = family_for(kind, paths)
|
|
794
826
|
invocation = parse_invocation(family, "run", ())
|
|
795
827
|
queue = [
|
|
796
|
-
SimpleNamespace(ok=True, stdout="", data={item_key: "
|
|
797
|
-
SimpleNamespace(ok=True, stdout="", data={item_key: "
|
|
828
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "X-001"}, text=""),
|
|
829
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "X-001"}, text=""),
|
|
798
830
|
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
799
831
|
]
|
|
832
|
+
processor_calls = []
|
|
800
833
|
|
|
801
834
|
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
802
|
-
|
|
835
|
+
|
|
836
|
+
def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
|
|
837
|
+
processor_calls.append(item_id)
|
|
838
|
+
return "completed"
|
|
839
|
+
|
|
840
|
+
monkeypatch.setattr(runners, "_process_item", fake_process)
|
|
803
841
|
monkeypatch.setattr(runners, "_emit_info", lambda *args, **kwargs: None)
|
|
804
842
|
|
|
805
843
|
result = runners._run_pipeline(family, invocation, paths)
|
|
806
844
|
|
|
807
|
-
assert
|
|
808
|
-
assert result.
|
|
845
|
+
assert processor_calls == ["X-001"]
|
|
846
|
+
assert result.completed is False
|
|
847
|
+
assert result.processed == 1
|
|
848
|
+
assert result.stopped_reason == "reselected_item"
|
|
809
849
|
assert result.last_status == "completed"
|
|
850
|
+
assert result.details == ("X-001:completed",)
|
|
810
851
|
|
|
811
852
|
|
|
812
853
|
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
@@ -841,15 +882,15 @@ def test_pipeline_aggregate_empty_authoritative_completion(monkeypatch, tmp_path
|
|
|
841
882
|
("kind", "status", "expected_completed"),
|
|
842
883
|
(
|
|
843
884
|
("feature", "completed", True),
|
|
844
|
-
("feature", "skipped",
|
|
885
|
+
("feature", "skipped", False),
|
|
845
886
|
("feature", "auto_skipped", True),
|
|
846
887
|
("feature", "split", True),
|
|
847
888
|
("bugfix", "completed", True),
|
|
848
|
-
("bugfix", "skipped",
|
|
889
|
+
("bugfix", "skipped", False),
|
|
849
890
|
("bugfix", "auto_skipped", True),
|
|
850
891
|
("bugfix", "split", False),
|
|
851
892
|
("refactor", "completed", True),
|
|
852
|
-
("refactor", "skipped",
|
|
893
|
+
("refactor", "skipped", False),
|
|
853
894
|
("refactor", "auto_skipped", True),
|
|
854
895
|
("refactor", "split", False),
|
|
855
896
|
),
|
|
@@ -875,7 +916,9 @@ def test_pipeline_aggregate_completion_compatible_statuses(monkeypatch, tmp_path
|
|
|
875
916
|
|
|
876
917
|
assert result.completed is expected_completed
|
|
877
918
|
assert result.stopped_reason == (
|
|
878
|
-
"
|
|
919
|
+
"item_skipped"
|
|
920
|
+
if status == "skipped"
|
|
921
|
+
else ("pipeline_complete" if expected_completed else "pipeline_complete_with_non_terminal_history")
|
|
879
922
|
)
|
|
880
923
|
assert result.last_status == status
|
|
881
924
|
assert result.details == (f"X-001:{status}",)
|
|
@@ -906,6 +949,37 @@ def test_run_pipeline_command_omits_empty_last_status_detail(monkeypatch, tmp_pa
|
|
|
906
949
|
assert result.details == ()
|
|
907
950
|
|
|
908
951
|
|
|
952
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
953
|
+
def test_run_pipeline_command_returns_nonzero_for_terminal_skipped_item(monkeypatch, tmp_path, kind):
|
|
954
|
+
from prizmkit_runtime import runners
|
|
955
|
+
from prizmkit_runtime.runner_models import PipelineRunResult
|
|
956
|
+
|
|
957
|
+
paths = _make_paths(tmp_path)
|
|
958
|
+
plan_path = {
|
|
959
|
+
"feature": paths.feature_plan,
|
|
960
|
+
"bugfix": paths.bugfix_plan,
|
|
961
|
+
"refactor": paths.refactor_plan,
|
|
962
|
+
}[kind]
|
|
963
|
+
plan_path.write_text("{}", encoding="utf-8")
|
|
964
|
+
monkeypatch.setattr(
|
|
965
|
+
runners,
|
|
966
|
+
"_run_pipeline",
|
|
967
|
+
lambda *args, **kwargs: PipelineRunResult(
|
|
968
|
+
False,
|
|
969
|
+
1,
|
|
970
|
+
"item_skipped",
|
|
971
|
+
"skipped",
|
|
972
|
+
("X-001:skipped",),
|
|
973
|
+
),
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
result = runners.run_pipeline_command(kind, "run", (), paths)
|
|
977
|
+
|
|
978
|
+
assert result.exit_code == 1
|
|
979
|
+
assert result.message == "processed=1 completed=False stopped_reason=item_skipped"
|
|
980
|
+
assert result.details == ("X-001:skipped", "last_status: skipped")
|
|
981
|
+
|
|
982
|
+
|
|
909
983
|
def test_pipeline_complete_rejects_retryable_detail(monkeypatch, tmp_path):
|
|
910
984
|
from prizmkit_runtime import runners
|
|
911
985
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
@@ -1898,6 +1972,7 @@ def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_pa
|
|
|
1898
1972
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1899
1973
|
|
|
1900
1974
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-output")
|
|
1975
|
+
monkeypatch.delenv("MAX_RETRIES", raising=False)
|
|
1901
1976
|
paths = _make_paths(tmp_path)
|
|
1902
1977
|
family = family_for("feature", paths)
|
|
1903
1978
|
invocation = parse_invocation(family, "run", ())
|
|
@@ -1976,6 +2051,8 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
1976
2051
|
from prizmkit_runtime.task_checkout import load_task_checkout
|
|
1977
2052
|
|
|
1978
2053
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
|
|
2054
|
+
monkeypatch.delenv("MAX_RETRIES", raising=False)
|
|
2055
|
+
monkeypatch.delenv("MAX_INFRA_RETRIES", raising=False)
|
|
1979
2056
|
paths = _make_paths(tmp_path)
|
|
1980
2057
|
_init_bookkeeping_repo(paths.project_root)
|
|
1981
2058
|
family = family_for("feature", paths)
|
|
@@ -3390,37 +3467,11 @@ def test_classification_stalls_after_repeated_no_progress_context_overflow(tmp_p
|
|
|
3390
3467
|
assert classification.no_progress_count == 2
|
|
3391
3468
|
|
|
3392
3469
|
|
|
3393
|
-
def test_recovery_prompt_keeps_existing_branch_instead_of_recovery_suffix():
|
|
3394
|
-
text = (PIPELINE_ROOT / "scripts" / "generate-recovery-prompt.py").read_text(encoding="utf-8")
|
|
3395
|
-
|
|
3396
|
-
assert "fix/{bug_id}-recovery" not in text
|
|
3397
|
-
assert "do not create an additional" in text
|
|
3398
|
-
assert "last-resort fallback" in text
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
def test_recovery_detector_resolver_prefers_installed_platform_asset(tmp_path):
|
|
3402
|
-
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
3403
|
-
from prizmkit_runtime.runner_recovery import RecoveryDetectorResolver
|
|
3404
|
-
|
|
3405
|
-
project = tmp_path / "project"
|
|
3406
|
-
agents = project / ".agents" / "skills" / "recovery-workflow" / "scripts"
|
|
3407
|
-
claude = project / ".claude" / "command-assets" / "recovery-workflow" / "scripts"
|
|
3408
|
-
agents.mkdir(parents=True)
|
|
3409
|
-
claude.mkdir(parents=True)
|
|
3410
|
-
agent_detector = agents / "detect-recovery-state.py"
|
|
3411
|
-
claude_detector = claude / "detect-recovery-state.py"
|
|
3412
|
-
agent_detector.write_text("# agent", encoding="utf-8")
|
|
3413
|
-
claude_detector.write_text("# claude", encoding="utf-8")
|
|
3414
|
-
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
|
|
3415
|
-
|
|
3416
|
-
assert RecoveryDetectorResolver(paths).resolve() == agent_detector
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
3470
|
def test_cli_family_status_uses_python_dispatch_not_transition_description(tmp_path):
|
|
3420
3471
|
paths = _make_paths(tmp_path)
|
|
3421
3472
|
_write_feature_list(paths)
|
|
3422
3473
|
result = subprocess.run(
|
|
3423
|
-
[PYTHON_310_PLUS, str(CLI_PATH), "--project-root", str(paths.project_root), "
|
|
3474
|
+
[PYTHON_310_PLUS, str(CLI_PATH), "--project-root", str(paths.project_root), "status", "feature"],
|
|
3424
3475
|
cwd=REPO_ROOT,
|
|
3425
3476
|
stdout=subprocess.PIPE,
|
|
3426
3477
|
stderr=subprocess.PIPE,
|
|
@@ -3480,7 +3531,7 @@ def test_python_reset_warns_before_mutation_and_matches_snapshot_cleanup(tmp_pat
|
|
|
3480
3531
|
for kind, item_id in [("feature", "F-001"), ("bugfix", "B-001"), ("refactor", "R-001")]:
|
|
3481
3532
|
paths = _make_paths(tmp_path / kind)
|
|
3482
3533
|
_plan, _state_dir, status_file = _write_family_case(paths, kind, item_id)
|
|
3483
|
-
result = run_reset_command(kind, (item_id,), paths)
|
|
3534
|
+
result = run_reset_command(kind, (item_id, "--fresh-checkout"), paths)
|
|
3484
3535
|
output = result.render()
|
|
3485
3536
|
reset_marker = f"Resetting {item_id} status"
|
|
3486
3537
|
|
|
@@ -3522,7 +3573,7 @@ def test_python_reset_cleans_authoritative_custom_branch_before_reset_marker(mon
|
|
|
3522
3573
|
or SimpleNamespace(branch=branch, cleaned=True, deleted=False, returned_to=return_branch, warnings=()),
|
|
3523
3574
|
)
|
|
3524
3575
|
|
|
3525
|
-
result = run_reset_command("feature", ("F-001",), paths)
|
|
3576
|
+
result = run_reset_command("feature", ("F-001", "--fresh-checkout"), paths)
|
|
3526
3577
|
|
|
3527
3578
|
assert result.exit_code == 0
|
|
3528
3579
|
assert cleanup_calls == [(custom_branch, False)]
|
|
@@ -3554,7 +3605,7 @@ def test_python_reset_failure_keeps_authoritative_checkout_active(monkeypatch, t
|
|
|
3554
3605
|
),
|
|
3555
3606
|
)
|
|
3556
3607
|
|
|
3557
|
-
result = run_reset_command("feature", ("F-001",), paths)
|
|
3608
|
+
result = run_reset_command("feature", ("F-001", "--fresh-checkout"), paths)
|
|
3558
3609
|
|
|
3559
3610
|
assert result.exit_code == 1
|
|
3560
3611
|
assert load_task_checkout(family.state_dir, family.task_type, "F-001") == checkout
|
|
@@ -3674,7 +3725,7 @@ def test_python_reset_stalled_filter_selects_stalled_context_items(tmp_path):
|
|
|
3674
3725
|
data[{"feature": "features", "bugfix": "bugs", "refactor": "refactors"}[kind]][0]["status"] = "stalled_context_continuation"
|
|
3675
3726
|
plan.write_text(json.dumps(data), encoding="utf-8")
|
|
3676
3727
|
|
|
3677
|
-
result = run_reset_command(kind, ("--stalled",), paths)
|
|
3728
|
+
result = run_reset_command(kind, ("--stalled", "--fresh-checkout"), paths)
|
|
3678
3729
|
|
|
3679
3730
|
assert result.exit_code == 0
|
|
3680
3731
|
assert f"Reset: {item_id}" in result.render()
|
|
@@ -3707,6 +3758,7 @@ def test_python_daemon_progress_uses_list_status_without_runtime_files(tmp_path)
|
|
|
3707
3758
|
{"id": "B-001", "title": "Done", "status": "completed"},
|
|
3708
3759
|
{"id": "B-002", "title": "Failed", "status": "failed"},
|
|
3709
3760
|
{"id": "B-003", "title": "Needs Info", "status": "needs_info"},
|
|
3761
|
+
{"id": "B-004", "title": "Blocked", "status": "auto_skipped"},
|
|
3710
3762
|
]}),
|
|
3711
3763
|
encoding="utf-8",
|
|
3712
3764
|
)
|
|
@@ -3724,12 +3776,13 @@ def test_python_daemon_progress_uses_list_status_without_runtime_files(tmp_path)
|
|
|
3724
3776
|
|
|
3725
3777
|
summary = _progress_summary(family_for(kind, paths), plan)
|
|
3726
3778
|
|
|
3727
|
-
assert summary["total"] == 3
|
|
3779
|
+
assert summary["total"] == (4 if kind == "bugfix" else 3)
|
|
3728
3780
|
assert summary["completed"] == 1
|
|
3729
3781
|
assert summary["failed"] == 1
|
|
3730
|
-
assert summary["percent"] == 33.3
|
|
3782
|
+
assert summary["percent"] == (25.0 if kind == "bugfix" else 33.3)
|
|
3731
3783
|
if kind == "bugfix":
|
|
3732
3784
|
assert summary["needs_info"] == 1
|
|
3785
|
+
assert summary["auto_skipped"] == 1
|
|
3733
3786
|
else:
|
|
3734
3787
|
assert summary["auto_skipped"] == 1
|
|
3735
3788
|
|
|
@@ -4396,38 +4449,10 @@ def test_semantic_blocked_classification_is_terminal_and_retry_neutral(tmp_path)
|
|
|
4396
4449
|
assert classification.checkpoint_state["repair_round"] == 3
|
|
4397
4450
|
|
|
4398
4451
|
|
|
4399
|
-
def test_recovery_outcome_preserves_dirty_partial_work_and_uses_semantic_checkpoint(tmp_path):
|
|
4400
|
-
from prizmkit_runtime.runner_recovery import _recovery_outcome
|
|
4401
|
-
|
|
4402
|
-
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
4403
|
-
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
4404
|
-
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
4405
|
-
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
4406
|
-
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
4407
|
-
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
4408
|
-
(tmp_path / "partial.txt").write_text("preserve me", encoding="utf-8")
|
|
4409
|
-
artifact = tmp_path / ".prizmkit" / "specs" / "done"
|
|
4410
|
-
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
|
|
4411
|
-
|
|
4412
|
-
outcome, semantic = _recovery_outcome(tmp_path, 1, checkpoint_path=checkpoint)
|
|
4413
|
-
|
|
4414
|
-
assert outcome == "success"
|
|
4415
|
-
assert semantic["terminal_status"] == "WORKFLOW_COMPLETED"
|
|
4416
|
-
assert (tmp_path / "partial.txt").read_text(encoding="utf-8") == "preserve me"
|
|
4417
|
-
assert subprocess.run(
|
|
4418
|
-
["git", "status", "--porcelain", "--", "partial.txt"],
|
|
4419
|
-
cwd=tmp_path,
|
|
4420
|
-
check=True,
|
|
4421
|
-
stdout=subprocess.PIPE,
|
|
4422
|
-
text=True,
|
|
4423
|
-
).stdout.startswith("??")
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
4452
|
def test_runtime_semantic_paths_do_not_invoke_composite_workflow():
|
|
4427
4453
|
sources = [
|
|
4428
4454
|
PIPELINE_ROOT / "prizmkit_runtime" / "checkpoint_state.py",
|
|
4429
4455
|
PIPELINE_ROOT / "prizmkit_runtime" / "runner_classification.py",
|
|
4430
|
-
PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py",
|
|
4431
4456
|
PIPELINE_ROOT / "prizmkit_runtime" / "runners.py",
|
|
4432
4457
|
]
|
|
4433
4458
|
|
|
@@ -4494,110 +4519,10 @@ def test_family_status_persists_identical_semantic_blocked_snapshot_without_retr
|
|
|
4494
4519
|
assert status[key] == value
|
|
4495
4520
|
|
|
4496
4521
|
|
|
4497
|
-
|
|
4498
|
-
def test_recovery_detector_returns_no_candidate_for_empty_project(tmp_path):
|
|
4499
|
-
import importlib.util
|
|
4500
|
-
|
|
4501
|
-
detector_path = (
|
|
4502
|
-
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
4503
|
-
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
4504
|
-
)
|
|
4505
|
-
spec = importlib.util.spec_from_file_location("recovery_detector_empty", detector_path)
|
|
4506
|
-
module = importlib.util.module_from_spec(spec)
|
|
4507
|
-
spec.loader.exec_module(module)
|
|
4508
|
-
|
|
4509
|
-
report = module.build_report(tmp_path)
|
|
4510
|
-
|
|
4511
|
-
assert report["detected"] is False
|
|
4512
|
-
assert report["candidates"] == []
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
def test_recovery_detector_returns_active_checkout_command(tmp_path, monkeypatch):
|
|
4516
|
-
import importlib.util
|
|
4517
|
-
|
|
4518
|
-
detector_path = (
|
|
4519
|
-
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
4520
|
-
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
4521
|
-
)
|
|
4522
|
-
spec = importlib.util.spec_from_file_location("recovery_detector_active", detector_path)
|
|
4523
|
-
module = importlib.util.module_from_spec(spec)
|
|
4524
|
-
spec.loader.exec_module(module)
|
|
4525
|
-
|
|
4526
|
-
checkout_path = tmp_path / ".prizmkit" / "state" / "features" / "F-001" / "checkout.json"
|
|
4527
|
-
checkout_path.parent.mkdir(parents=True)
|
|
4528
|
-
checkout_path.write_text(
|
|
4529
|
-
json.dumps({
|
|
4530
|
-
"task_type": "feature",
|
|
4531
|
-
"task_id": "F-001",
|
|
4532
|
-
"state": "active",
|
|
4533
|
-
"active_dev_branch": "dev/F-001-resume",
|
|
4534
|
-
"base_branch": "main",
|
|
4535
|
-
"checkout_mode": "branch",
|
|
4536
|
-
}),
|
|
4537
|
-
encoding="utf-8",
|
|
4538
|
-
)
|
|
4539
|
-
monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
|
|
4540
|
-
|
|
4541
|
-
report = module.build_report(tmp_path)
|
|
4542
|
-
|
|
4543
|
-
assert report["detected"] is True
|
|
4544
|
-
assert report["selection_required"] is False
|
|
4545
|
-
assert report["candidates"][0]["continue_command"] == (
|
|
4546
|
-
"python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001"
|
|
4547
|
-
)
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
def test_recovery_detector_ignores_completed_checkout(tmp_path, monkeypatch):
|
|
4551
|
-
import importlib.util
|
|
4552
|
-
|
|
4553
|
-
detector_path = (
|
|
4554
|
-
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
4555
|
-
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
4556
|
-
)
|
|
4557
|
-
spec = importlib.util.spec_from_file_location("recovery_detector_completed", detector_path)
|
|
4558
|
-
module = importlib.util.module_from_spec(spec)
|
|
4559
|
-
spec.loader.exec_module(module)
|
|
4560
|
-
|
|
4561
|
-
checkout_path = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "checkout.json"
|
|
4562
|
-
checkout_path.parent.mkdir(parents=True)
|
|
4563
|
-
checkout_path.write_text(
|
|
4564
|
-
json.dumps({
|
|
4565
|
-
"task_type": "bugfix",
|
|
4566
|
-
"task_id": "B-001",
|
|
4567
|
-
"state": "completed",
|
|
4568
|
-
"active_dev_branch": "bugfix/B-001-done",
|
|
4569
|
-
"base_branch": "main",
|
|
4570
|
-
}),
|
|
4571
|
-
encoding="utf-8",
|
|
4572
|
-
)
|
|
4573
|
-
monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
|
|
4574
|
-
|
|
4575
|
-
report = module.build_report(tmp_path)
|
|
4576
|
-
|
|
4577
|
-
assert report["detected"] is False
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
def test_l4_runtime_and_recovery_sources_do_not_depend_on_direct_l1_composite():
|
|
4522
|
+
def test_runtime_sources_do_not_depend_on_direct_lifecycle_composite():
|
|
4581
4523
|
runtime_root = REPO_ROOT / "dev-pipeline"
|
|
4582
4524
|
source_files = list(runtime_root.rglob("*.py"))
|
|
4583
4525
|
for source in source_files:
|
|
4584
4526
|
if "__pycache__" in source.parts or "tests" in source.parts or not source.is_file():
|
|
4585
4527
|
continue
|
|
4586
4528
|
assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
def test_recovery_detector_has_no_direct_lifecycle_fallback(tmp_path):
|
|
4590
|
-
import importlib.util
|
|
4591
|
-
|
|
4592
|
-
detector_path = (
|
|
4593
|
-
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
4594
|
-
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
4595
|
-
)
|
|
4596
|
-
spec = importlib.util.spec_from_file_location("recovery_detector_contract", detector_path)
|
|
4597
|
-
module = importlib.util.module_from_spec(spec)
|
|
4598
|
-
spec.loader.exec_module(module)
|
|
4599
|
-
|
|
4600
|
-
report = module.build_report(tmp_path)
|
|
4601
|
-
|
|
4602
|
-
assert "continue_command" not in report
|
|
4603
|
-
assert report["detected"] is False
|