prizmkit 1.1.122 → 1.1.124
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/adapters/claude/command-adapter.js +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +46 -38
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +365 -22
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +118 -109
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +21 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +33 -17
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +109 -63
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +179 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +80 -52
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +348 -154
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +188 -0
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +222 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
|
@@ -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
|
-
processed.append(item_id)
|
|
650
|
-
return "pending" if item_id == "F-001" else "completed"
|
|
653
|
+
result = runners._run_pipeline(family, invocation, paths)
|
|
651
654
|
|
|
652
|
-
|
|
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")
|
|
660
|
+
|
|
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
|
|
@@ -1680,7 +1835,180 @@ def test_preflight_ready_commit_preserves_staged_unstaged_and_mixed_visible_chan
|
|
|
1680
1835
|
assert visible_status == ""
|
|
1681
1836
|
|
|
1682
1837
|
|
|
1683
|
-
def
|
|
1838
|
+
def test_preflight_ready_commit_handles_exact_special_and_renamed_paths(tmp_path):
|
|
1839
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1840
|
+
|
|
1841
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1842
|
+
old_path = tmp_path / "old name -> café.txt"
|
|
1843
|
+
old_path.write_text("old\n", encoding="utf-8")
|
|
1844
|
+
subprocess.run(["git", "add", old_path.name], cwd=tmp_path, check=True)
|
|
1845
|
+
subprocess.run(["git", "commit", "-m", "track special path"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1846
|
+
new_path = tmp_path / 'new "quoted" \\ 名称.txt'
|
|
1847
|
+
old_path.rename(new_path)
|
|
1848
|
+
untracked_path = tmp_path / "untracked -> line\nbreak.txt"
|
|
1849
|
+
untracked_path.write_text("new\n", encoding="utf-8")
|
|
1850
|
+
colon_path = tmp_path / ":(glob)**.txt"
|
|
1851
|
+
colon_path.write_text("literal pathspec\n", encoding="utf-8")
|
|
1852
|
+
|
|
1853
|
+
result = commit_preflight_ready_changes(tmp_path, "R-001")
|
|
1854
|
+
committed_files = set(
|
|
1855
|
+
filter(
|
|
1856
|
+
None,
|
|
1857
|
+
subprocess.run(
|
|
1858
|
+
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "HEAD"],
|
|
1859
|
+
cwd=tmp_path,
|
|
1860
|
+
check=True,
|
|
1861
|
+
stdout=subprocess.PIPE,
|
|
1862
|
+
).stdout.decode().split("\0"),
|
|
1863
|
+
)
|
|
1864
|
+
)
|
|
1865
|
+
|
|
1866
|
+
assert result.committed is True
|
|
1867
|
+
assert old_path.name in committed_files
|
|
1868
|
+
assert new_path.name in committed_files
|
|
1869
|
+
assert untracked_path.name in committed_files
|
|
1870
|
+
assert colon_path.name in committed_files
|
|
1871
|
+
assert _head_message(tmp_path) == "chore: ready for R-001"
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
@pytest.mark.parametrize("staged", [False, True])
|
|
1875
|
+
def test_preflight_ready_commit_rejects_tracked_local_settings_before_checkout(tmp_path, staged):
|
|
1876
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1877
|
+
|
|
1878
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1879
|
+
local_settings = tmp_path / ".claude" / "settings.local.json"
|
|
1880
|
+
local_settings.parent.mkdir(parents=True)
|
|
1881
|
+
local_settings.write_text('{"permissions": []}\n', encoding="utf-8")
|
|
1882
|
+
subprocess.run(["git", "add", "-f", ".claude/settings.local.json"], cwd=tmp_path, check=True)
|
|
1883
|
+
subprocess.run(["git", "commit", "-m", "track local settings"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1884
|
+
local_settings.write_text('{"permissions": ["secret"]}\n', encoding="utf-8")
|
|
1885
|
+
if staged:
|
|
1886
|
+
subprocess.run(["git", "add", ".claude/settings.local.json"], cwd=tmp_path, check=True)
|
|
1887
|
+
|
|
1888
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1889
|
+
|
|
1890
|
+
assert result.attempted is True
|
|
1891
|
+
assert result.committed is False
|
|
1892
|
+
assert result.result is not None
|
|
1893
|
+
assert "Refusing to auto-commit local-only platform settings" in result.result.stderr
|
|
1894
|
+
assert _head_message(tmp_path) == "track local settings"
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def test_preflight_ready_commit_excludes_untracked_python_cache_outside_support_dirs(tmp_path):
|
|
1898
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1899
|
+
|
|
1900
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1901
|
+
(tmp_path / "visible.txt").write_text("visible\n", encoding="utf-8")
|
|
1902
|
+
cache_paths = [
|
|
1903
|
+
tmp_path / "__pycache__" / "top.cpython-312.pyc",
|
|
1904
|
+
tmp_path / "src" / "package" / "__pycache__" / "module.cpython-312.pyc",
|
|
1905
|
+
tmp_path / "src" / "standalone.pyo",
|
|
1906
|
+
]
|
|
1907
|
+
for path in cache_paths:
|
|
1908
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1909
|
+
path.write_text("compiled\n", encoding="utf-8")
|
|
1910
|
+
|
|
1911
|
+
result = commit_preflight_ready_changes(tmp_path, "F-001")
|
|
1912
|
+
committed_files = _head_files(tmp_path)
|
|
1913
|
+
|
|
1914
|
+
assert result.committed is True
|
|
1915
|
+
assert committed_files == {"visible.txt"}
|
|
1916
|
+
assert all(path.is_file() for path in cache_paths)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def test_tracked_python_cache_remains_visible_to_task_commit(tmp_path):
|
|
1920
|
+
from prizmkit_runtime.runner_bookkeeping import commit_task_branch_changes
|
|
1921
|
+
from prizmkit_runtime.runner_models import family_for
|
|
1922
|
+
|
|
1923
|
+
paths = _make_paths(tmp_path)
|
|
1924
|
+
_init_bookkeeping_repo(paths.project_root)
|
|
1925
|
+
tracked_cache = paths.project_root / "src" / "__pycache__" / "module.pyc"
|
|
1926
|
+
tracked_cache.parent.mkdir(parents=True)
|
|
1927
|
+
tracked_cache.write_text("baseline\n", encoding="utf-8")
|
|
1928
|
+
subprocess.run(["git", "add", tracked_cache.relative_to(paths.project_root).as_posix()], cwd=paths.project_root, check=True)
|
|
1929
|
+
subprocess.run(["git", "commit", "-m", "track cache"], cwd=paths.project_root, check=True, stdout=subprocess.DEVNULL)
|
|
1930
|
+
tracked_cache.write_text("task change\n", encoding="utf-8")
|
|
1931
|
+
|
|
1932
|
+
result = commit_task_branch_changes(paths.project_root, family_for("bugfix", paths), "B-001", "completed")
|
|
1933
|
+
|
|
1934
|
+
assert result.committed is True
|
|
1935
|
+
assert tracked_cache.relative_to(paths.project_root).as_posix() in _head_files(paths.project_root)
|
|
1936
|
+
assert subprocess.run(
|
|
1937
|
+
["git", "status", "--short", "--untracked-files=no"],
|
|
1938
|
+
cwd=paths.project_root,
|
|
1939
|
+
check=True,
|
|
1940
|
+
stdout=subprocess.PIPE,
|
|
1941
|
+
text=True,
|
|
1942
|
+
).stdout == ""
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
def test_preflight_ready_commit_includes_tracked_state_but_excludes_untracked_state(tmp_path):
|
|
1946
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1947
|
+
|
|
1948
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1949
|
+
tracked_state = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "tracked.json"
|
|
1950
|
+
tracked_state.parent.mkdir(parents=True)
|
|
1951
|
+
tracked_state.write_text("baseline\n", encoding="utf-8")
|
|
1952
|
+
subprocess.run(["git", "add", "-f", tracked_state.relative_to(tmp_path).as_posix()], cwd=tmp_path, check=True)
|
|
1953
|
+
subprocess.run(["git", "commit", "-m", "track state"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1954
|
+
tracked_state.write_text("changed\n", encoding="utf-8")
|
|
1955
|
+
untracked_state = tracked_state.parent / "untracked.json"
|
|
1956
|
+
untracked_state.write_text("transient\n", encoding="utf-8")
|
|
1957
|
+
|
|
1958
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1959
|
+
|
|
1960
|
+
assert result.committed is True
|
|
1961
|
+
assert tracked_state.relative_to(tmp_path).as_posix() in _head_files(tmp_path)
|
|
1962
|
+
assert untracked_state.relative_to(tmp_path).as_posix() not in _head_files(tmp_path)
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def test_preflight_ready_commit_includes_tracked_framework_support_but_excludes_untracked_support_and_state(tmp_path):
|
|
1966
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1967
|
+
|
|
1968
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1969
|
+
tracked_support_paths = [
|
|
1970
|
+
tmp_path / ".prizmkit" / "manifest.json",
|
|
1971
|
+
tmp_path / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "runners.py",
|
|
1972
|
+
tmp_path / ".claude" / "commands" / "prizmkit.md",
|
|
1973
|
+
]
|
|
1974
|
+
for path in tracked_support_paths:
|
|
1975
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1976
|
+
path.write_text("installed\n", encoding="utf-8")
|
|
1977
|
+
subprocess.run(
|
|
1978
|
+
["git", "add", *(path.relative_to(tmp_path).as_posix() for path in tracked_support_paths)],
|
|
1979
|
+
cwd=tmp_path,
|
|
1980
|
+
check=True,
|
|
1981
|
+
)
|
|
1982
|
+
subprocess.run(["git", "commit", "-m", "track support"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1983
|
+
for path in tracked_support_paths:
|
|
1984
|
+
path.write_text("upgraded\n", encoding="utf-8")
|
|
1985
|
+
|
|
1986
|
+
untracked_support_paths = [
|
|
1987
|
+
tmp_path / ".claude" / "settings.local.json",
|
|
1988
|
+
tmp_path / ".prizmkit" / "dev-pipeline" / "scripts" / "__pycache__" / "runtime.pyc",
|
|
1989
|
+
tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "status.json",
|
|
1990
|
+
]
|
|
1991
|
+
for path in untracked_support_paths:
|
|
1992
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1993
|
+
path.write_text("transient\n", encoding="utf-8")
|
|
1994
|
+
|
|
1995
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1996
|
+
committed_files = _head_files(tmp_path)
|
|
1997
|
+
|
|
1998
|
+
assert result.committed is True
|
|
1999
|
+
assert {path.relative_to(tmp_path).as_posix() for path in tracked_support_paths}.issubset(committed_files)
|
|
2000
|
+
assert not {path.relative_to(tmp_path).as_posix() for path in untracked_support_paths}.intersection(committed_files)
|
|
2001
|
+
assert _head_message(tmp_path) == "chore: ready for B-001"
|
|
2002
|
+
assert subprocess.run(
|
|
2003
|
+
["git", "status", "--short", "--untracked-files=no"],
|
|
2004
|
+
cwd=tmp_path,
|
|
2005
|
+
check=True,
|
|
2006
|
+
stdout=subprocess.PIPE,
|
|
2007
|
+
text=True,
|
|
2008
|
+
).stdout == ""
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def test_preflight_ready_commit_excludes_untracked_support_assets_and_state(tmp_path):
|
|
1684
2012
|
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1685
2013
|
|
|
1686
2014
|
_init_bookkeeping_repo(tmp_path)
|
|
@@ -1727,23 +2055,38 @@ def test_preflight_ready_commit_noops_when_workspace_has_no_visible_dirty_conten
|
|
|
1727
2055
|
assert _head_commit_count(tmp_path) == before_count
|
|
1728
2056
|
|
|
1729
2057
|
|
|
1730
|
-
|
|
2058
|
+
@pytest.mark.parametrize(
|
|
2059
|
+
("kind", "item_id", "branch"),
|
|
2060
|
+
[
|
|
2061
|
+
("feature", "F-001", "dev/F-001-ready"),
|
|
2062
|
+
("bugfix", "B-001", "bugfix/B-001-ready"),
|
|
2063
|
+
("refactor", "R-001", "refactor/R-001-ready"),
|
|
2064
|
+
],
|
|
2065
|
+
)
|
|
2066
|
+
def test_process_item_creates_one_ready_commit_before_branch_setup_for_all_families(
|
|
2067
|
+
monkeypatch,
|
|
2068
|
+
tmp_path,
|
|
2069
|
+
kind,
|
|
2070
|
+
item_id,
|
|
2071
|
+
branch,
|
|
2072
|
+
):
|
|
1731
2073
|
from prizmkit_runtime import runner_bookkeeping, runners
|
|
1732
2074
|
from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
|
|
1733
2075
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1734
2076
|
|
|
1735
|
-
paths = _make_paths(tmp_path)
|
|
2077
|
+
paths = _make_paths(tmp_path / kind)
|
|
1736
2078
|
_init_bookkeeping_repo(paths.project_root)
|
|
1737
2079
|
(paths.project_root / "staged.txt").write_text("staged before run\n", encoding="utf-8")
|
|
1738
2080
|
subprocess.run(["git", "add", "staged.txt"], cwd=paths.project_root, check=True)
|
|
1739
2081
|
(paths.project_root / "unstaged.txt").write_text("unstaged before run\n", encoding="utf-8")
|
|
2082
|
+
(paths.project_root / "untracked.txt").write_text("untracked before run\n", encoding="utf-8")
|
|
1740
2083
|
before_count = _head_commit_count(paths.project_root)
|
|
1741
2084
|
branch_events = []
|
|
1742
2085
|
|
|
1743
|
-
monkeypatch.setenv("DEV_BRANCH",
|
|
1744
|
-
family = family_for(
|
|
2086
|
+
monkeypatch.setenv("DEV_BRANCH", branch)
|
|
2087
|
+
family = family_for(kind, paths)
|
|
1745
2088
|
invocation = parse_invocation(family, "run", ())
|
|
1746
|
-
invocation = invocation.__class__(**{**invocation.__dict__, "list_path":
|
|
2089
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
|
|
1747
2090
|
_install_runner_session_fakes(monkeypatch, runners)
|
|
1748
2091
|
monkeypatch.setattr(runners, "commit_preflight_ready_changes", runner_bookkeeping.commit_preflight_ready_changes)
|
|
1749
2092
|
|
|
@@ -1766,18 +2109,18 @@ def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_vis
|
|
|
1766
2109
|
|
|
1767
2110
|
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1768
2111
|
|
|
1769
|
-
status = runners._process_item(family, invocation,
|
|
2112
|
+
status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
|
|
1770
2113
|
|
|
1771
2114
|
assert status == "completed"
|
|
1772
2115
|
assert branch_events == [
|
|
1773
2116
|
{
|
|
1774
2117
|
"commit_count": before_count + 1,
|
|
1775
|
-
"message": "chore: ready for
|
|
2118
|
+
"message": f"chore: ready for {item_id}",
|
|
1776
2119
|
"visible_status": "",
|
|
1777
2120
|
}
|
|
1778
2121
|
]
|
|
1779
2122
|
assert _head_commit_count(paths.project_root) == before_count + 1
|
|
1780
|
-
assert {"staged.txt", "unstaged.txt"}.issubset(_head_files(paths.project_root))
|
|
2123
|
+
assert {"staged.txt", "unstaged.txt", "untracked.txt"}.issubset(_head_files(paths.project_root))
|
|
1781
2124
|
|
|
1782
2125
|
|
|
1783
2126
|
|
|
@@ -811,14 +811,21 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
|
|
|
811
811
|
assert str(prompt) not in "\n".join(lines[:6])
|
|
812
812
|
|
|
813
813
|
|
|
814
|
-
def
|
|
815
|
-
|
|
814
|
+
def test_headless_review_guidance_uses_workspace_and_execution_protocols():
|
|
815
|
+
headless_guidance_files = [
|
|
816
816
|
REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
|
|
817
817
|
REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
|
|
818
|
-
REPO_ROOT / "core" / "skills" / "prizmkit-skill" / "prizmkit-code-review" / "SKILL.md",
|
|
819
818
|
]
|
|
819
|
+
skill_path = (
|
|
820
|
+
REPO_ROOT
|
|
821
|
+
/ "core"
|
|
822
|
+
/ "skills"
|
|
823
|
+
/ "prizmkit-skill"
|
|
824
|
+
/ "prizmkit-code-review"
|
|
825
|
+
/ "SKILL.md"
|
|
826
|
+
)
|
|
820
827
|
|
|
821
|
-
for path in
|
|
828
|
+
for path in headless_guidance_files:
|
|
822
829
|
text = path.read_text(encoding="utf-8")
|
|
823
830
|
assert "platform-supported" in text, path
|
|
824
831
|
assert "snapshot" in text and "equivalent" in text, path
|
|
@@ -828,15 +835,17 @@ def test_headless_review_guidance_uses_workspace_equivalence_protocol():
|
|
|
828
835
|
assert ".prizmkit/state/worktrees/..." not in text, path
|
|
829
836
|
assert "USE_WORKTREE" not in text, path
|
|
830
837
|
assert "isolation:" not in text, path
|
|
838
|
+
assert "Read existing source files in the modules this plan touches" not in text, path
|
|
831
839
|
|
|
832
|
-
skill_text =
|
|
840
|
+
skill_text = skill_path.read_text(encoding="utf-8")
|
|
833
841
|
assert "reviewer-workspace-protocol.md" in skill_text
|
|
842
|
+
assert "reviewer-execution-protocol.md" in skill_text
|
|
834
843
|
assert "staged, unstaged, untracked, deleted, and renamed" in skill_text
|
|
844
|
+
assert "content-equivalent" in skill_text
|
|
835
845
|
assert "WORKSPACE_MISMATCH" in skill_text
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
assert "Read existing source files in the modules this plan touches" not in text, path
|
|
846
|
+
assert "REVIEW_INFRASTRUCTURE_BLOCKED" in skill_text
|
|
847
|
+
assert "do not substitute Main-Agent self-review" in skill_text
|
|
848
|
+
assert "platform-specific role" in skill_text
|
|
840
849
|
|
|
841
850
|
|
|
842
851
|
def test_progress_parser_adds_required_metadata(tmp_path):
|
|
@@ -2109,7 +2118,7 @@ def test_preflight_ready_commit_skips_hidden_support_asset_only_changes(tmp_path
|
|
|
2109
2118
|
assert (repo / ".claude" / "settings.local.json").is_file()
|
|
2110
2119
|
|
|
2111
2120
|
|
|
2112
|
-
def
|
|
2121
|
+
def test_preflight_ready_commit_includes_explicitly_staged_support_assets(tmp_path):
|
|
2113
2122
|
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
2114
2123
|
|
|
2115
2124
|
repo = tmp_path / "repo"
|
|
@@ -2123,8 +2132,8 @@ def test_preflight_ready_commit_excludes_already_staged_hidden_support_assets(tm
|
|
|
2123
2132
|
|
|
2124
2133
|
assert result.committed
|
|
2125
2134
|
assert _head_commit_message(repo) == "chore: ready for F-005"
|
|
2126
|
-
assert _head_files(repo) == {"visible.txt"}
|
|
2127
|
-
assert
|
|
2135
|
+
assert _head_files(repo) == {".claude/agent.md", "visible.txt"}
|
|
2136
|
+
assert _short_status(repo) == ""
|
|
2128
2137
|
|
|
2129
2138
|
|
|
2130
2139
|
def test_preflight_ready_commit_excludes_ignored_tool_worktree_directories(tmp_path):
|