prizmkit 1.1.122 → 1.1.123
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
CHANGED
|
@@ -107,31 +107,39 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
107
107
|
from .runner_models import PipelineRunResult
|
|
108
108
|
|
|
109
109
|
processed = 0
|
|
110
|
-
|
|
110
|
+
item_statuses: dict[str, str] = {}
|
|
111
111
|
if invocation.item_id:
|
|
112
112
|
try:
|
|
113
113
|
status = _process_item(family, invocation, invocation.item_id, paths, initial_metadata={})
|
|
114
114
|
except KeyboardInterrupt:
|
|
115
|
-
return PipelineRunResult(False, 1, "interrupted", "interrupted"
|
|
116
|
-
return PipelineRunResult(status == "completed", 1, "single_item_done", status
|
|
115
|
+
return PipelineRunResult(False, 1, "interrupted", "interrupted")
|
|
116
|
+
return PipelineRunResult(status == "completed", 1, "single_item_done", status)
|
|
117
117
|
|
|
118
118
|
while True:
|
|
119
119
|
next_result = get_next(family, invocation, paths.project_root)
|
|
120
|
+
details = _render_item_statuses(item_statuses)
|
|
120
121
|
if not next_result.ok:
|
|
121
122
|
return PipelineRunResult(False, processed, "get_next_failed", details=(next_result.text,))
|
|
122
123
|
marker = next_result.stdout.strip()
|
|
123
124
|
if marker == "PIPELINE_COMPLETE":
|
|
124
|
-
|
|
125
|
+
blocking_statuses = _blocking_statuses(family, item_statuses)
|
|
126
|
+
if blocking_statuses:
|
|
125
127
|
return PipelineRunResult(
|
|
126
128
|
False,
|
|
127
129
|
processed,
|
|
128
130
|
"pipeline_complete_with_non_terminal_history",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
blocking_statuses[-1],
|
|
132
|
+
details,
|
|
131
133
|
)
|
|
132
|
-
return PipelineRunResult(
|
|
134
|
+
return PipelineRunResult(
|
|
135
|
+
True,
|
|
136
|
+
processed,
|
|
137
|
+
"pipeline_complete",
|
|
138
|
+
_last_item_status(item_statuses),
|
|
139
|
+
details,
|
|
140
|
+
)
|
|
133
141
|
if marker == "PIPELINE_BLOCKED":
|
|
134
|
-
return PipelineRunResult(False, processed, "pipeline_blocked", details=
|
|
142
|
+
return PipelineRunResult(False, processed, "pipeline_blocked", details=details)
|
|
135
143
|
if not isinstance(next_result.data, dict):
|
|
136
144
|
return PipelineRunResult(False, processed, "invalid_get_next_output", details=(marker[:500],))
|
|
137
145
|
item_id = str(next_result.data.get(f"{family.kind}_id") or next_result.data.get("feature_id") or next_result.data.get("bug_id") or next_result.data.get("refactor_id") or "")
|
|
@@ -141,50 +149,50 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
141
149
|
final_status = _process_item(family, invocation, item_id, paths, initial_metadata=next_result.data)
|
|
142
150
|
except KeyboardInterrupt:
|
|
143
151
|
processed += 1
|
|
144
|
-
|
|
145
|
-
return PipelineRunResult(
|
|
152
|
+
item_statuses[item_id] = "interrupted"
|
|
153
|
+
return PipelineRunResult(
|
|
154
|
+
False,
|
|
155
|
+
processed,
|
|
156
|
+
"interrupted",
|
|
157
|
+
"interrupted",
|
|
158
|
+
_render_item_statuses(item_statuses),
|
|
159
|
+
)
|
|
146
160
|
processed += 1
|
|
147
|
-
|
|
148
|
-
if final_status
|
|
149
|
-
|
|
161
|
+
item_statuses[item_id] = final_status
|
|
162
|
+
if final_status in _completion_compatible_statuses(family):
|
|
163
|
+
if final_status == "completed":
|
|
164
|
+
_emit_info(f"Pausing 5s before next {family.kind}...")
|
|
150
165
|
continue
|
|
151
166
|
if final_status == "pending":
|
|
152
167
|
continue
|
|
168
|
+
details = _render_item_statuses(item_statuses)
|
|
153
169
|
if RunnerEnvironment.from_env().stop_on_failure:
|
|
154
|
-
return PipelineRunResult(False, processed, "stop_on_failure", final_status,
|
|
170
|
+
return PipelineRunResult(False, processed, "stop_on_failure", final_status, details)
|
|
155
171
|
if final_status in {"failed", "needs_info"}:
|
|
156
|
-
return PipelineRunResult(False, processed, "item_failed", final_status,
|
|
172
|
+
return PipelineRunResult(False, processed, "item_failed", final_status, details)
|
|
173
|
+
|
|
157
174
|
|
|
175
|
+
_COMPLETION_COMPATIBLE_STATUSES = {"completed", "skipped", "auto_skipped"}
|
|
158
176
|
|
|
159
|
-
_NON_TERMINAL_DETAIL_STATUSES = {
|
|
160
|
-
"pending",
|
|
161
|
-
"retryable",
|
|
162
|
-
"in_progress",
|
|
163
|
-
"infra_error",
|
|
164
|
-
"context_overflow",
|
|
165
|
-
"stalled_context_continuation",
|
|
166
|
-
"crashed",
|
|
167
|
-
"timed_out",
|
|
168
|
-
"failed",
|
|
169
|
-
"needs_info",
|
|
170
|
-
"merge_conflict",
|
|
171
|
-
"interrupted",
|
|
172
|
-
"unknown",
|
|
173
|
-
}
|
|
174
177
|
|
|
178
|
+
def _completion_compatible_statuses(family: RunnerFamily) -> set[str]:
|
|
179
|
+
statuses = set(_COMPLETION_COMPATIBLE_STATUSES)
|
|
180
|
+
if family.kind == "feature":
|
|
181
|
+
statuses.add("split")
|
|
182
|
+
return statuses
|
|
175
183
|
|
|
176
|
-
def _detail_status(detail: str) -> str:
|
|
177
|
-
return detail.rsplit(":", 1)[-1] if ":" in detail else detail
|
|
178
184
|
|
|
185
|
+
def _render_item_statuses(item_statuses: Mapping[str, str]) -> tuple[str, ...]:
|
|
186
|
+
return tuple(f"{item_id}:{status}" for item_id, status in item_statuses.items())
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return _detail_status(details[-1])
|
|
188
|
+
|
|
189
|
+
def _last_item_status(item_statuses: Mapping[str, str]) -> str:
|
|
190
|
+
return next(reversed(item_statuses.values()), "")
|
|
184
191
|
|
|
185
192
|
|
|
186
|
-
def
|
|
187
|
-
|
|
193
|
+
def _blocking_statuses(family: RunnerFamily, item_statuses: Mapping[str, str]) -> tuple[str, ...]:
|
|
194
|
+
compatible = _completion_compatible_statuses(family)
|
|
195
|
+
return tuple(status for status in item_statuses.values() if status not in compatible)
|
|
188
196
|
|
|
189
197
|
|
|
190
198
|
_SEPARATOR = "─" * 52
|
|
@@ -627,39 +627,194 @@ def test_status_bridge_passes_max_retries_from_invocation_or_environment(monkeyp
|
|
|
627
627
|
assert second[second.index("--max-retries") + 1] == "6"
|
|
628
628
|
|
|
629
629
|
|
|
630
|
-
|
|
630
|
+
@pytest.mark.parametrize(
|
|
631
|
+
("kind", "item_key"),
|
|
632
|
+
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
633
|
+
)
|
|
634
|
+
def test_pipeline_aggregate_replaces_same_item_status_in_first_seen_order(monkeypatch, tmp_path, kind, item_key):
|
|
631
635
|
from prizmkit_runtime import runners
|
|
632
636
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
633
637
|
|
|
634
638
|
paths = _make_paths(tmp_path)
|
|
635
|
-
family = family_for(
|
|
639
|
+
family = family_for(kind, paths)
|
|
636
640
|
invocation = parse_invocation(family, "run", ())
|
|
637
|
-
|
|
641
|
+
statuses = iter(("completed", "pending", "completed"))
|
|
638
642
|
queue = [
|
|
639
|
-
SimpleNamespace(ok=True, stdout=
|
|
640
|
-
SimpleNamespace(ok=True, stdout=
|
|
643
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-049"}, text=""),
|
|
644
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-050"}, text=""),
|
|
645
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-050"}, text=""),
|
|
641
646
|
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
642
647
|
]
|
|
643
|
-
processed = []
|
|
644
648
|
|
|
645
|
-
monkeypatch.setenv("STOP_ON_FAILURE", "1")
|
|
646
649
|
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
650
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: next(statuses))
|
|
651
|
+
monkeypatch.setattr(runners, "_emit_info", lambda *args, **kwargs: None)
|
|
647
652
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
653
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
654
|
+
|
|
655
|
+
assert result.processed == 3
|
|
656
|
+
assert result.completed is True
|
|
657
|
+
assert result.stopped_reason == "pipeline_complete"
|
|
658
|
+
assert result.last_status == "completed"
|
|
659
|
+
assert result.details == ("F-049:completed", "F-050:completed")
|
|
651
660
|
|
|
652
|
-
|
|
661
|
+
|
|
662
|
+
@pytest.mark.parametrize(
|
|
663
|
+
("kind", "item_key"),
|
|
664
|
+
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
665
|
+
)
|
|
666
|
+
def test_pipeline_aggregate_keeps_distinct_unresolved_item_blocking(monkeypatch, tmp_path, kind, item_key):
|
|
667
|
+
from prizmkit_runtime import runners
|
|
668
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
669
|
+
|
|
670
|
+
paths = _make_paths(tmp_path)
|
|
671
|
+
family = family_for(kind, paths)
|
|
672
|
+
invocation = parse_invocation(family, "run", ())
|
|
673
|
+
statuses = iter(("pending", "completed"))
|
|
674
|
+
queue = [
|
|
675
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-001"}, text=""),
|
|
676
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-002"}, text=""),
|
|
677
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
681
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: next(statuses))
|
|
682
|
+
monkeypatch.setattr(runners, "_emit_info", lambda *args, **kwargs: None)
|
|
653
683
|
|
|
654
684
|
result = runners._run_pipeline(family, invocation, paths)
|
|
655
685
|
|
|
656
|
-
assert processed ==
|
|
686
|
+
assert result.processed == 2
|
|
657
687
|
assert result.completed is False
|
|
658
688
|
assert result.stopped_reason == "pipeline_complete_with_non_terminal_history"
|
|
659
|
-
assert result.last_status == "
|
|
689
|
+
assert result.last_status == "pending"
|
|
660
690
|
assert result.details == ("F-001:pending", "F-002:completed")
|
|
661
691
|
|
|
662
692
|
|
|
693
|
+
@pytest.mark.parametrize(
|
|
694
|
+
("kind", "item_key"),
|
|
695
|
+
(("feature", "feature_id"), ("bugfix", "bug_id"), ("refactor", "refactor_id")),
|
|
696
|
+
)
|
|
697
|
+
def test_pipeline_aggregate_repeated_terminal_update_remains_unique(monkeypatch, tmp_path, kind, item_key):
|
|
698
|
+
from prizmkit_runtime import runners
|
|
699
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
700
|
+
|
|
701
|
+
paths = _make_paths(tmp_path)
|
|
702
|
+
family = family_for(kind, paths)
|
|
703
|
+
invocation = parse_invocation(family, "run", ())
|
|
704
|
+
queue = [
|
|
705
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-001"}, text=""),
|
|
706
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "F-001"}, text=""),
|
|
707
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
711
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "completed")
|
|
712
|
+
monkeypatch.setattr(runners, "_emit_info", lambda *args, **kwargs: None)
|
|
713
|
+
|
|
714
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
715
|
+
|
|
716
|
+
assert result.processed == 2
|
|
717
|
+
assert result.details == ("F-001:completed",)
|
|
718
|
+
assert result.last_status == "completed"
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
722
|
+
def test_pipeline_aggregate_empty_authoritative_completion(monkeypatch, tmp_path, kind):
|
|
723
|
+
from prizmkit_runtime import runners
|
|
724
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
725
|
+
|
|
726
|
+
paths = _make_paths(tmp_path)
|
|
727
|
+
family = family_for(kind, paths)
|
|
728
|
+
invocation = parse_invocation(family, "run", ())
|
|
729
|
+
monkeypatch.setattr(
|
|
730
|
+
runners,
|
|
731
|
+
"get_next",
|
|
732
|
+
lambda *args, **kwargs: SimpleNamespace(
|
|
733
|
+
ok=True,
|
|
734
|
+
stdout="PIPELINE_COMPLETE\n",
|
|
735
|
+
data=None,
|
|
736
|
+
text="PIPELINE_COMPLETE",
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
741
|
+
|
|
742
|
+
assert result.processed == 0
|
|
743
|
+
assert result.completed is True
|
|
744
|
+
assert result.stopped_reason == "pipeline_complete"
|
|
745
|
+
assert result.last_status == ""
|
|
746
|
+
assert result.details == ()
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@pytest.mark.parametrize(
|
|
750
|
+
("kind", "status", "expected_completed"),
|
|
751
|
+
(
|
|
752
|
+
("feature", "completed", True),
|
|
753
|
+
("feature", "skipped", True),
|
|
754
|
+
("feature", "auto_skipped", True),
|
|
755
|
+
("feature", "split", True),
|
|
756
|
+
("bugfix", "completed", True),
|
|
757
|
+
("bugfix", "skipped", True),
|
|
758
|
+
("bugfix", "auto_skipped", True),
|
|
759
|
+
("bugfix", "split", False),
|
|
760
|
+
("refactor", "completed", True),
|
|
761
|
+
("refactor", "skipped", True),
|
|
762
|
+
("refactor", "auto_skipped", True),
|
|
763
|
+
("refactor", "split", False),
|
|
764
|
+
),
|
|
765
|
+
)
|
|
766
|
+
def test_pipeline_aggregate_completion_compatible_statuses(monkeypatch, tmp_path, kind, status, expected_completed):
|
|
767
|
+
from prizmkit_runtime import runners
|
|
768
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
769
|
+
|
|
770
|
+
paths = _make_paths(tmp_path)
|
|
771
|
+
family = family_for(kind, paths)
|
|
772
|
+
invocation = parse_invocation(family, "run", ())
|
|
773
|
+
item_key = {"feature": "feature_id", "bugfix": "bug_id", "refactor": "refactor_id"}[kind]
|
|
774
|
+
queue = [
|
|
775
|
+
SimpleNamespace(ok=True, stdout="", data={item_key: "X-001"}, text=""),
|
|
776
|
+
SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
|
|
777
|
+
]
|
|
778
|
+
|
|
779
|
+
monkeypatch.setenv("STOP_ON_FAILURE", "0")
|
|
780
|
+
monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
|
|
781
|
+
monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: status)
|
|
782
|
+
|
|
783
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
784
|
+
|
|
785
|
+
assert result.completed is expected_completed
|
|
786
|
+
assert result.stopped_reason == (
|
|
787
|
+
"pipeline_complete" if expected_completed else "pipeline_complete_with_non_terminal_history"
|
|
788
|
+
)
|
|
789
|
+
assert result.last_status == status
|
|
790
|
+
assert result.details == (f"X-001:{status}",)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
@pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
|
|
794
|
+
def test_run_pipeline_command_omits_empty_last_status_detail(monkeypatch, tmp_path, kind):
|
|
795
|
+
from prizmkit_runtime import runners
|
|
796
|
+
from prizmkit_runtime.runner_models import PipelineRunResult
|
|
797
|
+
|
|
798
|
+
paths = _make_paths(tmp_path)
|
|
799
|
+
plan_path = {
|
|
800
|
+
"feature": paths.feature_plan,
|
|
801
|
+
"bugfix": paths.bugfix_plan,
|
|
802
|
+
"refactor": paths.refactor_plan,
|
|
803
|
+
}[kind]
|
|
804
|
+
plan_path.write_text("{}", encoding="utf-8")
|
|
805
|
+
monkeypatch.setattr(
|
|
806
|
+
runners,
|
|
807
|
+
"_run_pipeline",
|
|
808
|
+
lambda *args, **kwargs: PipelineRunResult(True, 0, "pipeline_complete"),
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
result = runners.run_pipeline_command(kind, "run", (), paths)
|
|
812
|
+
|
|
813
|
+
assert result.exit_code == 0
|
|
814
|
+
assert result.message == "processed=0 completed=True stopped_reason=pipeline_complete"
|
|
815
|
+
assert result.details == ()
|
|
816
|
+
|
|
817
|
+
|
|
663
818
|
def test_pipeline_complete_rejects_retryable_detail(monkeypatch, tmp_path):
|
|
664
819
|
from prizmkit_runtime import runners
|
|
665
820
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|