prizmkit 1.1.106 → 1.1.108
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/bin/create-prizmkit.js +4 -0
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +81 -14
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +19 -8
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/launch_profiles.py +18 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +22 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +56 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +11 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +298 -63
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +243 -37
- package/bundled/dev-pipeline/scripts/continuation.py +0 -5
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +10 -82
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +7 -64
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +40 -63
- package/bundled/dev-pipeline/scripts/utils.py +123 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +4 -3
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +36 -35
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +53 -80
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +43 -78
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +5 -17
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +6 -18
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -48
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +7 -14
- package/bundled/dev-pipeline/templates/sections/feature-context.md +0 -4
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +0 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +6 -8
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +3 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -16
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -17
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -8
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +15 -16
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +2 -15
- package/bundled/dev-pipeline/tests/conftest.py +0 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +395 -21
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +50 -14
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +154 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +431 -3
- package/bundled/dev-pipeline/tests/test_unified_cli.py +857 -5
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/app-planner/SKILL.md +1 -1
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -128
- package/bundled/skills/bug-planner/SKILL.md +101 -15
- package/bundled/skills/bug-planner/references/severity-rules.md +20 -8
- package/bundled/skills/feature-planner/SKILL.md +87 -8
- package/bundled/skills/feature-planner/assets/planning-guide.md +34 -10
- package/bundled/skills/feature-workflow/SKILL.md +100 -81
- package/bundled/skills/{prizm-kit → prizmkit}/SKILL.md +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/bundled/skills/refactor-planner/SKILL.md +88 -7
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -1
- package/bundled/skills/refactor-planner/references/planning-phases.md +29 -11
- package/bundled/skills/refactor-workflow/SKILL.md +116 -57
- package/bundled/templates/project-memory-template.md +1 -1
- package/package.json +1 -1
- package/src/ai-cli-launch.js +194 -0
- package/src/config.js +26 -14
- package/src/index.js +11 -44
- package/src/platforms.js +10 -3
- package/src/prompts.js +60 -2
- package/src/scaffold.js +26 -14
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +0 -446
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +0 -20
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +0 -107
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
|
@@ -318,6 +318,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
|
|
|
318
318
|
assert env.stale_kill_threshold_seconds == 9
|
|
319
319
|
assert env.max_retries == 9
|
|
320
320
|
assert env.strict_behavior_check is False
|
|
321
|
+
assert not hasattr(env, "max_" + "log_size")
|
|
321
322
|
|
|
322
323
|
|
|
323
324
|
def test_runner_environment_disables_worktree_by_default_and_accepts_truthy_opt_in():
|
|
@@ -414,10 +415,67 @@ def test_stop_on_failure_does_not_stop_on_retryable_pending(monkeypatch, tmp_pat
|
|
|
414
415
|
|
|
415
416
|
result = runners._run_pipeline(family, invocation, paths)
|
|
416
417
|
|
|
418
|
+
assert processed == ["F-001", "F-002"]
|
|
419
|
+
assert result.completed is False
|
|
420
|
+
assert result.stopped_reason == "pipeline_complete_with_non_terminal_history"
|
|
421
|
+
assert result.last_status == "completed"
|
|
422
|
+
assert result.details == ("F-001:pending", "F-002:completed")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_pipeline_complete_rejects_retryable_detail(monkeypatch, tmp_path):
|
|
426
|
+
from prizmkit_runtime import runners
|
|
427
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
428
|
+
|
|
429
|
+
paths = _make_paths(tmp_path)
|
|
430
|
+
family = family_for("feature", paths)
|
|
431
|
+
invocation = parse_invocation(family, "run", ())
|
|
432
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
433
|
+
queue = [
|
|
434
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
|
|
435
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
monkeypatch.setenv("STOP_ON_FAILURE", "0")
|
|
439
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
440
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "retryable")
|
|
441
|
+
|
|
442
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
443
|
+
|
|
444
|
+
assert result.completed is False
|
|
445
|
+
assert result.stopped_reason == "pipeline_complete_with_non_terminal_history"
|
|
446
|
+
assert result.last_status == "retryable"
|
|
447
|
+
assert result.details == ("F-001:retryable",)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_full_list_continues_from_completed_item_to_next_pending(monkeypatch, tmp_path):
|
|
451
|
+
from prizmkit_runtime import runners
|
|
452
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
453
|
+
|
|
454
|
+
paths = _make_paths(tmp_path)
|
|
455
|
+
family = family_for("feature", paths)
|
|
456
|
+
invocation = parse_invocation(family, "run", ())
|
|
457
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
458
|
+
queue = [
|
|
459
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
|
|
460
|
+
SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-002"}), data={"feature_id": "F-002"}, text=""),
|
|
461
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
462
|
+
]
|
|
463
|
+
processed = []
|
|
464
|
+
|
|
465
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
466
|
+
|
|
467
|
+
def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
|
|
468
|
+
processed.append(item_id)
|
|
469
|
+
return "completed"
|
|
470
|
+
|
|
471
|
+
monkeypatch.setattr(runners, "_process_item", fake_process)
|
|
472
|
+
|
|
473
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
474
|
+
|
|
417
475
|
assert processed == ["F-001", "F-002"]
|
|
418
476
|
assert result.completed is True
|
|
419
477
|
assert result.stopped_reason == "pipeline_complete"
|
|
420
|
-
assert result.details == ("F-001:
|
|
478
|
+
assert result.details == ("F-001:completed", "F-002:completed")
|
|
421
479
|
|
|
422
480
|
|
|
423
481
|
def test_stop_on_failure_still_stops_on_terminal_status(monkeypatch, tmp_path):
|
|
@@ -546,6 +604,7 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
546
604
|
|
|
547
605
|
prompt_calls = []
|
|
548
606
|
launch_cwds = []
|
|
607
|
+
launch_configs = []
|
|
549
608
|
updates = []
|
|
550
609
|
cleanup_calls = []
|
|
551
610
|
merge_calls = []
|
|
@@ -563,6 +622,7 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
563
622
|
)
|
|
564
623
|
monkeypatch.setattr(runners, "detect_stream_json_support", lambda command, platform: SimpleNamespace(enabled=False))
|
|
565
624
|
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"}))
|
|
625
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=False, committed=False, ok=True, result=None))
|
|
566
626
|
monkeypatch.setattr(runners, "commit_pre_branch_bookkeeping", lambda *args, **kwargs: None)
|
|
567
627
|
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
568
628
|
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
@@ -594,6 +654,7 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
594
654
|
|
|
595
655
|
def run(self):
|
|
596
656
|
launch_cwds.append(self.config.cwd)
|
|
657
|
+
launch_configs.append(self.config)
|
|
597
658
|
return SimpleNamespace(progress_summary=SimpleNamespace(), fatal_error_code="")
|
|
598
659
|
|
|
599
660
|
def fake_classify(*args, **kwargs):
|
|
@@ -632,6 +693,7 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
632
693
|
return SimpleNamespace(
|
|
633
694
|
prompt_calls=prompt_calls,
|
|
634
695
|
launch_cwds=launch_cwds,
|
|
696
|
+
launch_configs=launch_configs,
|
|
635
697
|
updates=updates,
|
|
636
698
|
cleanup_calls=cleanup_calls,
|
|
637
699
|
merge_calls=merge_calls,
|
|
@@ -639,6 +701,36 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
639
701
|
)
|
|
640
702
|
|
|
641
703
|
|
|
704
|
+
|
|
705
|
+
def test_runner_launch_config_preserves_artifact_paths_live_output_and_split_thresholds(monkeypatch, tmp_path):
|
|
706
|
+
from prizmkit_runtime import runners
|
|
707
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
708
|
+
|
|
709
|
+
monkeypatch.setenv("PRIZMKIT_LIVE_OUTPUT", "0")
|
|
710
|
+
monkeypatch.setenv("HEARTBEAT_INTERVAL", "7")
|
|
711
|
+
monkeypatch.setenv("HEARTBEAT_STALE_THRESHOLD", "11")
|
|
712
|
+
monkeypatch.setenv("STALE_KILL_THRESHOLD", "13")
|
|
713
|
+
paths = _make_paths(tmp_path / "artifact-wiring")
|
|
714
|
+
family = family_for("feature", paths)
|
|
715
|
+
invocation = parse_invocation(family, "run", ())
|
|
716
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
717
|
+
observed = _install_runner_session_fakes(monkeypatch, runners)
|
|
718
|
+
monkeypatch.setattr(runners, "run_git_plan", lambda *args, **kwargs: SimpleNamespace(ok=True, reason="", results=()))
|
|
719
|
+
|
|
720
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
721
|
+
|
|
722
|
+
config = observed.launch_configs[0]
|
|
723
|
+
expected_log = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-1" / "logs" / "session.log"
|
|
724
|
+
assert status == "completed"
|
|
725
|
+
assert config.live_output is False
|
|
726
|
+
assert config.live_progress_interval_seconds == 7
|
|
727
|
+
assert config.heartbeat_stale_threshold_seconds == 11
|
|
728
|
+
assert config.stale_kill_threshold_seconds == 13
|
|
729
|
+
assert config.log_path == expected_log
|
|
730
|
+
assert config.progress_path == expected_log.with_name("progress.json")
|
|
731
|
+
assert config.heartbeat_path == expected_log.with_name("heartbeat.json")
|
|
732
|
+
|
|
733
|
+
|
|
642
734
|
@pytest.mark.parametrize(("kind", "item_id", "branch"), [("feature", "F-001", "dev/F-001-test"), ("bugfix", "B-001", "bugfix/B-001-test")])
|
|
643
735
|
def test_feature_and_bugfix_explicit_worktree_launches_from_worktree_and_renders_worktree_root(monkeypatch, tmp_path, kind, item_id, branch):
|
|
644
736
|
from prizmkit_runtime import runners
|
|
@@ -652,18 +744,26 @@ def test_feature_and_bugfix_explicit_worktree_launches_from_worktree_and_renders
|
|
|
652
744
|
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan if kind == "feature" else paths.bugfix_plan})
|
|
653
745
|
observed = _install_runner_session_fakes(monkeypatch, runners)
|
|
654
746
|
setup_calls = []
|
|
747
|
+
lifecycle_order = []
|
|
655
748
|
|
|
656
749
|
def fake_setup(runtime):
|
|
750
|
+
lifecycle_order.append("worktree_setup")
|
|
657
751
|
setup_calls.append(runtime)
|
|
658
752
|
runtime.worktree_path.mkdir(parents=True, exist_ok=True)
|
|
659
753
|
return SimpleNamespace(ok=True, runtime=runtime, reason="")
|
|
660
754
|
|
|
755
|
+
def fake_preflight(*args, **kwargs):
|
|
756
|
+
lifecycle_order.append("preflight")
|
|
757
|
+
return SimpleNamespace(attempted=False, committed=False, ok=True, result=None)
|
|
758
|
+
|
|
759
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", fake_preflight)
|
|
661
760
|
monkeypatch.setattr(runners, "ensure_linked_worktree", fake_setup, raising=False)
|
|
662
761
|
|
|
663
762
|
status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
|
|
664
763
|
|
|
665
764
|
expected_worktree = paths.state_dir / "worktrees" / branch.replace("/", "-")
|
|
666
765
|
assert status == "completed"
|
|
766
|
+
assert lifecycle_order[:2] == ["preflight", "worktree_setup"]
|
|
667
767
|
assert setup_calls[0].worktree_path == expected_worktree.resolve()
|
|
668
768
|
assert observed.launch_cwds == [expected_worktree.resolve()]
|
|
669
769
|
assert observed.prompt_calls[0]["project_root"] == paths.project_root
|
|
@@ -784,6 +884,7 @@ def test_explicit_worktree_materializes_ignored_support_assets_before_prompt_and
|
|
|
784
884
|
)
|
|
785
885
|
monkeypatch.setattr(runners, "detect_stream_json_support", lambda command, platform: SimpleNamespace(enabled=False))
|
|
786
886
|
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"}))
|
|
887
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=False, committed=False, ok=True, result=None))
|
|
787
888
|
monkeypatch.setattr(runners, "commit_pre_branch_bookkeeping", lambda *args, **kwargs: None)
|
|
788
889
|
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
789
890
|
monkeypatch.setattr(runners, "_session_id", lambda item_id: f"{item_id}-session")
|
|
@@ -863,6 +964,11 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
|
|
|
863
964
|
lifecycle_order.append("start_item")
|
|
864
965
|
return SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"})
|
|
865
966
|
|
|
967
|
+
def fake_preflight(*args, **kwargs):
|
|
968
|
+
lifecycle_order.append("preflight")
|
|
969
|
+
return SimpleNamespace(attempted=False, committed=False, ok=True, result=None)
|
|
970
|
+
|
|
971
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", fake_preflight)
|
|
866
972
|
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
867
973
|
monkeypatch.setattr(runners, "start_item", fake_start_item)
|
|
868
974
|
monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (_ for _ in ()).throw(AssertionError("worktree setup should not run")), raising=False)
|
|
@@ -874,10 +980,178 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
|
|
|
874
980
|
assert observed.prompt_calls[0].get("execution_root") == paths.project_root
|
|
875
981
|
assert "branch_create" in plan_names
|
|
876
982
|
assert "branch_merge" in plan_names
|
|
877
|
-
assert lifecycle_order[:
|
|
983
|
+
assert lifecycle_order[:3] == ["preflight", "branch_create", "start_item"]
|
|
878
984
|
assert branch_events == [("create", "dev/F-001-test"), ("merge", "dev/F-001-test")]
|
|
879
985
|
|
|
880
986
|
|
|
987
|
+
def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_path):
|
|
988
|
+
from prizmkit_runtime.runner_classification import has_branch_completion_evidence
|
|
989
|
+
|
|
990
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
991
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
992
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
993
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
994
|
+
artifact_dir.mkdir(parents=True)
|
|
995
|
+
(artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
|
|
996
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
997
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
998
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
999
|
+
subprocess.run(["git", "checkout", "-b", "dev/F-001"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1000
|
+
(tmp_path / "done.txt").write_text("done", encoding="utf-8")
|
|
1001
|
+
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
1002
|
+
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1003
|
+
|
|
1004
|
+
assert has_branch_completion_evidence(tmp_path, "main", "dev/F-001", artifact_dir) is True
|
|
1005
|
+
|
|
1006
|
+
subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1007
|
+
(tmp_path / "main.txt").write_text("main", encoding="utf-8")
|
|
1008
|
+
subprocess.run(["git", "add", "main.txt"], cwd=tmp_path, check=True)
|
|
1009
|
+
subprocess.run(["git", "commit", "-m", "advance-main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1010
|
+
subprocess.run(["git", "branch", "stale/F-001", "HEAD~1"], cwd=tmp_path, check=True)
|
|
1011
|
+
|
|
1012
|
+
assert has_branch_completion_evidence(tmp_path, "main", "stale/F-001", artifact_dir) is False
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def test_no_worktree_late_runtime_error_success_updates_and_merges(monkeypatch, tmp_path):
|
|
1016
|
+
from prizmkit_runtime import runners
|
|
1017
|
+
from prizmkit_runtime.runner_models import SessionClassification, family_for, parse_invocation
|
|
1018
|
+
|
|
1019
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-late-error")
|
|
1020
|
+
paths = _make_paths(tmp_path)
|
|
1021
|
+
family = family_for("feature", paths)
|
|
1022
|
+
invocation = parse_invocation(family, "run", ())
|
|
1023
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1024
|
+
observed = _install_runner_session_fakes(monkeypatch, runners, statuses=("infra_error",))
|
|
1025
|
+
events = []
|
|
1026
|
+
|
|
1027
|
+
def fake_classify(*args, **kwargs):
|
|
1028
|
+
return SessionClassification("success", reason="completed_checkpoint_with_late_runtime_error")
|
|
1029
|
+
|
|
1030
|
+
def fake_run_git_plan(project_root, plan):
|
|
1031
|
+
events.append(plan.name)
|
|
1032
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
1033
|
+
|
|
1034
|
+
monkeypatch.setattr(runners, "classify_session", fake_classify)
|
|
1035
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1036
|
+
|
|
1037
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
1038
|
+
|
|
1039
|
+
assert status == "completed"
|
|
1040
|
+
assert observed.updates[-1][0] == "success"
|
|
1041
|
+
assert "branch_merge" in events
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def test_no_worktree_recovers_completed_pending_branch_before_new_session(monkeypatch, tmp_path):
|
|
1045
|
+
from prizmkit_runtime import runners
|
|
1046
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1047
|
+
|
|
1048
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-recover")
|
|
1049
|
+
paths = _make_paths(tmp_path)
|
|
1050
|
+
family = family_for("feature", paths)
|
|
1051
|
+
invocation = parse_invocation(family, "run", ())
|
|
1052
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1053
|
+
session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
|
|
1054
|
+
session_dir.mkdir(parents=True)
|
|
1055
|
+
artifact_dir = paths.project_root / ".prizmkit" / "specs" / "001-low"
|
|
1056
|
+
artifact_dir.mkdir(parents=True)
|
|
1057
|
+
(artifact_dir / "workflow-checkpoint.json").write_text(
|
|
1058
|
+
json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}),
|
|
1059
|
+
encoding="utf-8",
|
|
1060
|
+
)
|
|
1061
|
+
updates = []
|
|
1062
|
+
events = []
|
|
1063
|
+
|
|
1064
|
+
monkeypatch.setattr(
|
|
1065
|
+
runners,
|
|
1066
|
+
"load_runtime_config",
|
|
1067
|
+
lambda paths: SimpleNamespace(ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"), model="", effort=None),
|
|
1068
|
+
)
|
|
1069
|
+
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1070
|
+
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not restart completed work")))
|
|
1071
|
+
monkeypatch.setattr(runners, "generate_prompt", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not launch prompt")))
|
|
1072
|
+
monkeypatch.setattr(runners, "has_branch_completion_evidence", lambda *args, **kwargs: True)
|
|
1073
|
+
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
1074
|
+
monkeypatch.setattr(runners, "commit_task_branch_changes", lambda *args, **kwargs: None)
|
|
1075
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=False, committed=False, ok=True, result=None))
|
|
1076
|
+
|
|
1077
|
+
def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
|
|
1078
|
+
updates.append((session_status, session_id, metadata))
|
|
1079
|
+
return SimpleNamespace(ok=True, text="", data={"new_status": "completed"})
|
|
1080
|
+
|
|
1081
|
+
def fake_run_git_plan(project_root, plan):
|
|
1082
|
+
events.append(plan.name)
|
|
1083
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
1084
|
+
|
|
1085
|
+
monkeypatch.setattr(runners, "update_item", fake_update)
|
|
1086
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1087
|
+
|
|
1088
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={"title": "Low"})
|
|
1089
|
+
|
|
1090
|
+
assert status == "completed"
|
|
1091
|
+
assert updates == [("success", "F-001-session-old", {"active_dev_branch": "dev/F-001-recover", "base_branch": "main"})]
|
|
1092
|
+
assert "branch_merge" in events
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def test_worktree_recovers_completed_pending_branch_from_base_session_history(monkeypatch, tmp_path):
|
|
1096
|
+
from prizmkit_runtime import runners
|
|
1097
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1098
|
+
|
|
1099
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
1100
|
+
paths = _make_paths(tmp_path)
|
|
1101
|
+
family = family_for("feature", paths)
|
|
1102
|
+
invocation = parse_invocation(family, "run", ())
|
|
1103
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1104
|
+
base_session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
|
|
1105
|
+
base_session_dir.mkdir(parents=True)
|
|
1106
|
+
(base_session_dir / "session-status.json").write_text(
|
|
1107
|
+
json.dumps({"base_branch": "main", "active_dev_branch": "dev/F-001-worktree"}),
|
|
1108
|
+
encoding="utf-8",
|
|
1109
|
+
)
|
|
1110
|
+
updates = []
|
|
1111
|
+
cleanup_calls = []
|
|
1112
|
+
merge_calls = []
|
|
1113
|
+
|
|
1114
|
+
monkeypatch.setattr(
|
|
1115
|
+
runners,
|
|
1116
|
+
"load_runtime_config",
|
|
1117
|
+
lambda paths: SimpleNamespace(ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"), model="", effort=None),
|
|
1118
|
+
)
|
|
1119
|
+
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1120
|
+
monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""), raising=False)
|
|
1121
|
+
monkeypatch.setattr(runners, "materialize_worktree_support_assets", lambda runtime: SimpleNamespace(ok=True, reason=""), raising=False)
|
|
1122
|
+
monkeypatch.setattr(runners, "has_completed_artifact_path", lambda artifact: True)
|
|
1123
|
+
monkeypatch.setattr(runners, "has_branch_completion_evidence", lambda *args, **kwargs: True)
|
|
1124
|
+
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not restart completed work")))
|
|
1125
|
+
monkeypatch.setattr(runners, "generate_prompt", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not launch prompt")))
|
|
1126
|
+
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
1127
|
+
monkeypatch.setattr(runners, "commit_task_branch_changes", lambda *args, **kwargs: None)
|
|
1128
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=False, committed=False, ok=True, result=None))
|
|
1129
|
+
|
|
1130
|
+
def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
|
|
1131
|
+
updates.append((session_status, session_id, project_root, metadata))
|
|
1132
|
+
return SimpleNamespace(ok=True, text="", data={"new_status": "completed"})
|
|
1133
|
+
|
|
1134
|
+
def fake_merge(runtime, *, auto_push=False):
|
|
1135
|
+
merge_calls.append((runtime, auto_push))
|
|
1136
|
+
return SimpleNamespace(ok=True, reason="", runtime=runtime, results=())
|
|
1137
|
+
|
|
1138
|
+
def fake_cleanup(runtime, *, delete_branch=True):
|
|
1139
|
+
cleanup_calls.append((runtime, delete_branch))
|
|
1140
|
+
return SimpleNamespace(ok=True, reason="")
|
|
1141
|
+
|
|
1142
|
+
monkeypatch.setattr(runners, "update_item", fake_update)
|
|
1143
|
+
monkeypatch.setattr(runners, "merge_linked_worktree", fake_merge, raising=False)
|
|
1144
|
+
monkeypatch.setattr(runners, "guarded_worktree_remove", fake_cleanup, raising=False)
|
|
1145
|
+
|
|
1146
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={"title": "Low"})
|
|
1147
|
+
|
|
1148
|
+
assert status == "completed"
|
|
1149
|
+
assert updates[0][0] == "success"
|
|
1150
|
+
assert updates[0][1] == "F-001-session-old"
|
|
1151
|
+
assert updates[0][2] != paths.project_root
|
|
1152
|
+
assert merge_calls and cleanup_calls[-1][1] is True
|
|
1153
|
+
|
|
1154
|
+
|
|
881
1155
|
def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch, tmp_path):
|
|
882
1156
|
from prizmkit_runtime import runners
|
|
883
1157
|
from prizmkit_runtime.runner_models import family_for
|
|
@@ -963,6 +1237,33 @@ def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp
|
|
|
963
1237
|
assert observed.updates[-1][2]["base_branch"] == "main"
|
|
964
1238
|
|
|
965
1239
|
|
|
1240
|
+
def test_no_worktree_preflight_commit_failure_aborts_before_branch_or_status(monkeypatch, tmp_path):
|
|
1241
|
+
from prizmkit_runtime import runners
|
|
1242
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1243
|
+
|
|
1244
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-preflight-fail")
|
|
1245
|
+
paths = _make_paths(tmp_path)
|
|
1246
|
+
family = family_for("feature", paths)
|
|
1247
|
+
invocation = parse_invocation(family, "run", ())
|
|
1248
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1249
|
+
events = []
|
|
1250
|
+
|
|
1251
|
+
monkeypatch.setattr(
|
|
1252
|
+
runners,
|
|
1253
|
+
"load_runtime_config",
|
|
1254
|
+
lambda paths: SimpleNamespace(ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"), model="", effort=None),
|
|
1255
|
+
)
|
|
1256
|
+
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1257
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=True, committed=False, result=SimpleNamespace(stderr="commit failed\n", stdout="")))
|
|
1258
|
+
monkeypatch.setattr(runners, "run_git_plan", lambda *args, **kwargs: events.append("branch_create"))
|
|
1259
|
+
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: events.append("start_item"))
|
|
1260
|
+
|
|
1261
|
+
with pytest.raises(RuntimeError, match="Preflight dirty workspace preservation failed"):
|
|
1262
|
+
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
1263
|
+
|
|
1264
|
+
assert events == []
|
|
1265
|
+
|
|
1266
|
+
|
|
966
1267
|
def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_path, capsys):
|
|
967
1268
|
from prizmkit_runtime import runners
|
|
968
1269
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
@@ -988,7 +1289,7 @@ def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_pa
|
|
|
988
1289
|
assert "────────────────────────────────────────────────────" in captured.err
|
|
989
1290
|
assert "Feature: F-001 — Readable output" in captured.err
|
|
990
1291
|
assert "Code retry: 1 / 3" in captured.err
|
|
991
|
-
assert "
|
|
1292
|
+
assert "Preflight: no user-visible changes to preserve before feature task F-001" in captured.err
|
|
992
1293
|
assert "Pipeline mode: lite (Tier 1 — Single Agent)" in captured.err
|
|
993
1294
|
assert "Agents: 1 (critic: disabled)" in captured.err
|
|
994
1295
|
assert "Spawning AI CLI session: F-001-session-1" in captured.err
|
|
@@ -1108,6 +1409,133 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
|
|
|
1108
1409
|
assert observed.cleanup_calls[0][1] is True
|
|
1109
1410
|
|
|
1110
1411
|
|
|
1412
|
+
def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_commit(tmp_path):
|
|
1413
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1414
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
1415
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1416
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1417
|
+
|
|
1418
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1419
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1420
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1421
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1422
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1423
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1424
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1425
|
+
(tmp_path / "done.txt").write_text("done", encoding="utf-8")
|
|
1426
|
+
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
1427
|
+
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1428
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1429
|
+
artifact_dir.mkdir(parents=True)
|
|
1430
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
1431
|
+
checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}), encoding="utf-8")
|
|
1432
|
+
log = tmp_path / "session.log"
|
|
1433
|
+
log.write_text("model quota exceeded; API error after commit", encoding="utf-8")
|
|
1434
|
+
raw = AISessionResult(
|
|
1435
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1436
|
+
exit_code=1,
|
|
1437
|
+
timed_out=False,
|
|
1438
|
+
termination_reason=None,
|
|
1439
|
+
stale_marker=None,
|
|
1440
|
+
fatal_error_code="api_error",
|
|
1441
|
+
progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
|
|
1442
|
+
session_log=log,
|
|
1443
|
+
backup_log=tmp_path / "backup.log",
|
|
1444
|
+
log_recovery=None,
|
|
1445
|
+
started_epoch=0,
|
|
1446
|
+
ended_epoch=1,
|
|
1447
|
+
)
|
|
1448
|
+
prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", checkpoint_path=checkpoint, artifact_path=artifact_dir)
|
|
1449
|
+
|
|
1450
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
|
|
1451
|
+
|
|
1452
|
+
assert classification.session_status == "success"
|
|
1453
|
+
assert classification.reason == "completed_checkpoint_with_late_runtime_error"
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tmp_path):
|
|
1457
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1458
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
1459
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1460
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1461
|
+
|
|
1462
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1463
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1464
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1465
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1466
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1467
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1468
|
+
subprocess.run(["git", "checkout", "-b", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1469
|
+
(tmp_path / "side.txt").write_text("side", encoding="utf-8")
|
|
1470
|
+
subprocess.run(["git", "add", "side.txt"], cwd=tmp_path, check=True)
|
|
1471
|
+
subprocess.run(["git", "commit", "-m", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1472
|
+
side_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1473
|
+
subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1474
|
+
(tmp_path / "main.txt").write_text("main", encoding="utf-8")
|
|
1475
|
+
subprocess.run(["git", "add", "main.txt"], cwd=tmp_path, check=True)
|
|
1476
|
+
subprocess.run(["git", "commit", "-m", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1477
|
+
subprocess.run(["git", "checkout", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1478
|
+
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1479
|
+
artifact_dir.mkdir(parents=True)
|
|
1480
|
+
(artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
|
|
1481
|
+
log = tmp_path / "session.log"
|
|
1482
|
+
log.write_text("quota exceeded", encoding="utf-8")
|
|
1483
|
+
raw = AISessionResult(
|
|
1484
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1485
|
+
exit_code=1,
|
|
1486
|
+
timed_out=False,
|
|
1487
|
+
termination_reason=None,
|
|
1488
|
+
stale_marker=None,
|
|
1489
|
+
fatal_error_code="api_error",
|
|
1490
|
+
progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
|
|
1491
|
+
session_log=log,
|
|
1492
|
+
backup_log=tmp_path / "backup.log",
|
|
1493
|
+
log_recovery=None,
|
|
1494
|
+
started_epoch=0,
|
|
1495
|
+
ended_epoch=1,
|
|
1496
|
+
)
|
|
1497
|
+
prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", artifact_path=artifact_dir)
|
|
1498
|
+
|
|
1499
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
|
|
1500
|
+
|
|
1501
|
+
assert classification.session_status == "infra_error"
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
|
|
1505
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
1506
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
1507
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
1508
|
+
|
|
1509
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1510
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1511
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1512
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1513
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1514
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1515
|
+
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1516
|
+
log = tmp_path / "session.log"
|
|
1517
|
+
log.write_text("authentication failed", encoding="utf-8")
|
|
1518
|
+
raw = AISessionResult(
|
|
1519
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
1520
|
+
exit_code=1,
|
|
1521
|
+
timed_out=False,
|
|
1522
|
+
termination_reason=None,
|
|
1523
|
+
stale_marker=None,
|
|
1524
|
+
fatal_error_code="authentication_error",
|
|
1525
|
+
progress_summary=ProgressSummary(terminal_result_text="authentication failed"),
|
|
1526
|
+
session_log=log,
|
|
1527
|
+
backup_log=tmp_path / "backup.log",
|
|
1528
|
+
log_recovery=None,
|
|
1529
|
+
started_epoch=0,
|
|
1530
|
+
ended_epoch=1,
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
|
|
1534
|
+
|
|
1535
|
+
assert classification.session_status == "infra_error"
|
|
1536
|
+
assert classification.reason == "ai_runtime_or_provider_error"
|
|
1537
|
+
|
|
1538
|
+
|
|
1111
1539
|
def test_classification_accepts_terminal_success_with_completed_checkpoint_without_git_delta(tmp_path):
|
|
1112
1540
|
from prizmkit_runtime.runner_classification import classify_session
|
|
1113
1541
|
from prizmkit_runtime.runner_models import PromptGenerationResult
|