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.
Files changed (44) 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/parse-stream-progress.py +10 -12
  17. package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
  18. package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
  19. package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
  20. package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
  21. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  22. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  23. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  24. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
  25. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
  26. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
  27. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
  28. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  29. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
  30. package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  33. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  34. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  35. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  36. package/bundled/skills/recovery-workflow/SKILL.md +7 -5
  37. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  38. package/bundled/skills/recovery-workflow/references/detection.md +3 -3
  39. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
  40. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  41. package/bundled/templates/project-memory-template.md +19 -11
  42. package/package.json +1 -1
  43. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  44. 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
- "recoveryHelpContract": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
219
- "resetHelpContracts": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
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 test_python_entrypoint_contracts_match_snapshot_contracts(tmp_path):
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 test_pipeline_aggregate_replaces_same_item_status_in_first_seen_order(monkeypatch, tmp_path, kind, item_key):
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 == 3
747
- assert result.completed is True
748
- assert result.stopped_reason == "pipeline_complete"
749
- assert result.last_status == "completed"
750
- assert result.details == ("F-049:completed", "F-050:completed")
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 test_pipeline_aggregate_repeated_terminal_update_remains_unique(monkeypatch, tmp_path, kind, item_key):
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: "F-001"}, text=""),
797
- SimpleNamespace(ok=True, stdout="", data={item_key: "F-001"}, text=""),
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
- monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "completed")
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 result.processed == 2
808
- assert result.details == ("F-001:completed",)
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", True),
885
+ ("feature", "skipped", False),
845
886
  ("feature", "auto_skipped", True),
846
887
  ("feature", "split", True),
847
888
  ("bugfix", "completed", True),
848
- ("bugfix", "skipped", True),
889
+ ("bugfix", "skipped", False),
849
890
  ("bugfix", "auto_skipped", True),
850
891
  ("bugfix", "split", False),
851
892
  ("refactor", "completed", True),
852
- ("refactor", "skipped", True),
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
- "pipeline_complete" if expected_completed else "pipeline_complete_with_non_terminal_history"
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), "feature", "status"],
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