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.
@@ -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
- def test_stop_on_failure_does_not_stop_on_retryable_pending(monkeypatch, tmp_path):
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("feature", paths)
639
+ family = family_for(kind, paths)
636
640
  invocation = parse_invocation(family, "run", ())
637
- invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
641
+ statuses = iter(("completed", "pending", "completed"))
638
642
  queue = [
639
- SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
640
- SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-002"}), data={"feature_id": "F-002"}, text=""),
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
- def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
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
- monkeypatch.setattr(runners, "_process_item", fake_process)
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 == ["F-001", "F-002"]
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 == "completed"
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 test_preflight_ready_commit_excludes_hidden_support_assets_and_state(tmp_path):
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
- def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_visible_changes(monkeypatch, tmp_path):
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", "dev/F-001-ready")
1744
- family = family_for("feature", paths)
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": paths.feature_plan})
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, "F-001", paths, initial_metadata={})
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 F-001",
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 test_headless_review_guidance_uses_workspace_equivalence_protocol():
815
- guidance_files = [
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 guidance_files:
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 = guidance_files[-1].read_text(encoding="utf-8")
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
- for path in guidance_files[:-1]:
838
- text = path.read_text(encoding="utf-8")
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 test_preflight_ready_commit_excludes_already_staged_hidden_support_assets(tmp_path):
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 "A .claude/agent.md" in _short_status(repo)
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):
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.122",
2
+ "version": "1.1.124",
3
3
  "skills": {
4
4
  "prizmkit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",