prizmkit 1.1.115 → 1.1.117
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/dev-pipeline/prizmkit_runtime/gitops.py +2 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +14 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +201 -6
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -31,6 +31,8 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
|
|
|
31
31
|
":(top,exclude,glob).prizmkit/prizm-docs/**",
|
|
32
32
|
":(top,exclude).prizmkit/config.json",
|
|
33
33
|
":(top,exclude).prizmkit/manifest.json",
|
|
34
|
+
":(top,exclude).prizmkit/state",
|
|
35
|
+
":(top,exclude,glob).prizmkit/state/**",
|
|
34
36
|
":(top,exclude).prizmkit/dev-pipeline/scripts",
|
|
35
37
|
":(top,exclude,glob).prizmkit/dev-pipeline/scripts/**",
|
|
36
38
|
":(top,exclude).prizmkit/dev-pipeline/prizmkit_runtime",
|
|
@@ -42,6 +42,10 @@ def _repo_path(project_root: Path, path: Path) -> str:
|
|
|
42
42
|
return path.as_posix()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _visible_status_result(project_root: Path) -> GitCommandResult:
|
|
46
|
+
return run_git_command(project_root, ("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
47
|
+
|
|
48
|
+
|
|
45
49
|
def _changed_paths_in(project_root: Path, paths: Sequence[str]) -> tuple[str, ...]:
|
|
46
50
|
result = run_git_command(project_root, ("status", "--porcelain", "--", *paths))
|
|
47
51
|
if result.return_code != 0:
|
|
@@ -73,7 +77,10 @@ def commit_bookkeeping_changes(
|
|
|
73
77
|
|
|
74
78
|
def commit_preflight_ready_changes(project_root: Path, item_id: str) -> BookkeepingResult:
|
|
75
79
|
"""Preserve visible pre-existing changes before task branch setup."""
|
|
76
|
-
|
|
80
|
+
visible_status = _visible_status_result(project_root)
|
|
81
|
+
if visible_status.return_code != 0:
|
|
82
|
+
return BookkeepingResult(attempted=True, committed=False, result=visible_status)
|
|
83
|
+
if not visible_status.stdout.strip():
|
|
77
84
|
return BookkeepingResult(attempted=False, committed=False)
|
|
78
85
|
add_result = run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
79
86
|
if add_result.return_code != 0:
|
|
@@ -82,7 +89,12 @@ def commit_preflight_ready_changes(project_root: Path, item_id: str) -> Bookkeep
|
|
|
82
89
|
project_root,
|
|
83
90
|
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES),
|
|
84
91
|
)
|
|
85
|
-
|
|
92
|
+
if result.return_code != 0:
|
|
93
|
+
return BookkeepingResult(attempted=True, committed=False, result=result)
|
|
94
|
+
clean_result = _visible_status_result(project_root)
|
|
95
|
+
if clean_result.return_code != 0 or clean_result.stdout.strip():
|
|
96
|
+
return BookkeepingResult(attempted=True, committed=False, result=clean_result)
|
|
97
|
+
return BookkeepingResult(attempted=True, committed=True, result=result)
|
|
86
98
|
|
|
87
99
|
|
|
88
100
|
def commit_task_branch_changes(
|
|
@@ -1471,15 +1471,25 @@ def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp
|
|
|
1471
1471
|
assert observed.updates[-1][2]["base_branch"] == "main"
|
|
1472
1472
|
|
|
1473
1473
|
|
|
1474
|
-
|
|
1474
|
+
@pytest.mark.parametrize(
|
|
1475
|
+
("kind", "item_id", "branch"),
|
|
1476
|
+
[
|
|
1477
|
+
("feature", "F-001", "dev/F-001-preflight-fail"),
|
|
1478
|
+
("bugfix", "B-001", "bugfix/B-001-preflight-fail"),
|
|
1479
|
+
("refactor", "R-001", "refactor/R-001-preflight-fail"),
|
|
1480
|
+
],
|
|
1481
|
+
)
|
|
1482
|
+
def test_preflight_commit_failure_aborts_before_branch_worktree_or_status_for_all_families(monkeypatch, tmp_path, kind, item_id, branch):
|
|
1475
1483
|
from prizmkit_runtime import runners
|
|
1476
1484
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1477
1485
|
|
|
1478
|
-
monkeypatch.setenv("DEV_BRANCH",
|
|
1479
|
-
|
|
1480
|
-
|
|
1486
|
+
monkeypatch.setenv("DEV_BRANCH", branch)
|
|
1487
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
1488
|
+
paths = _make_paths(tmp_path / kind)
|
|
1489
|
+
family = family_for(kind, paths)
|
|
1481
1490
|
invocation = parse_invocation(family, "run", ())
|
|
1482
|
-
|
|
1491
|
+
plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
|
|
1492
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
|
|
1483
1493
|
events = []
|
|
1484
1494
|
|
|
1485
1495
|
monkeypatch.setattr(
|
|
@@ -1490,10 +1500,11 @@ def test_no_worktree_preflight_commit_failure_aborts_before_branch_or_status(mon
|
|
|
1490
1500
|
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1491
1501
|
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=True, committed=False, result=SimpleNamespace(stderr="commit failed\n", stdout="")))
|
|
1492
1502
|
monkeypatch.setattr(runners, "run_git_plan", lambda *args, **kwargs: events.append("branch_create"))
|
|
1503
|
+
monkeypatch.setattr(runners, "ensure_linked_worktree", lambda *args, **kwargs: events.append("worktree_create"), raising=False)
|
|
1493
1504
|
monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: events.append("start_item"))
|
|
1494
1505
|
|
|
1495
1506
|
with pytest.raises(RuntimeError, match="Preflight dirty workspace preservation failed"):
|
|
1496
|
-
runners._process_item(family, invocation,
|
|
1507
|
+
runners._process_item(family, invocation, item_id, paths, initial_metadata={})
|
|
1497
1508
|
|
|
1498
1509
|
assert events == []
|
|
1499
1510
|
|
|
@@ -1559,6 +1570,190 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
1559
1570
|
assert ensure_calls == [("main", "dev/F-001-interrupt")]
|
|
1560
1571
|
|
|
1561
1572
|
|
|
1573
|
+
def _init_bookkeeping_repo(project_root):
|
|
1574
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=project_root, check=True, stdout=subprocess.DEVNULL)
|
|
1575
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=project_root, check=True)
|
|
1576
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=project_root, check=True)
|
|
1577
|
+
(project_root / "staged.txt").write_text("base staged\n", encoding="utf-8")
|
|
1578
|
+
(project_root / "unstaged.txt").write_text("base unstaged\n", encoding="utf-8")
|
|
1579
|
+
subprocess.run(["git", "add", "staged.txt", "unstaged.txt"], cwd=project_root, check=True)
|
|
1580
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=project_root, check=True, stdout=subprocess.DEVNULL)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _head_commit_count(project_root):
|
|
1584
|
+
return int(
|
|
1585
|
+
subprocess.run(
|
|
1586
|
+
["git", "rev-list", "--count", "HEAD"],
|
|
1587
|
+
cwd=project_root,
|
|
1588
|
+
check=True,
|
|
1589
|
+
stdout=subprocess.PIPE,
|
|
1590
|
+
text=True,
|
|
1591
|
+
).stdout.strip()
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _head_message(project_root):
|
|
1596
|
+
return subprocess.run(
|
|
1597
|
+
["git", "log", "-1", "--format=%s"],
|
|
1598
|
+
cwd=project_root,
|
|
1599
|
+
check=True,
|
|
1600
|
+
stdout=subprocess.PIPE,
|
|
1601
|
+
text=True,
|
|
1602
|
+
).stdout.strip()
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
def _head_files(project_root):
|
|
1606
|
+
return set(
|
|
1607
|
+
subprocess.run(
|
|
1608
|
+
["git", "show", "--name-only", "--format=", "HEAD"],
|
|
1609
|
+
cwd=project_root,
|
|
1610
|
+
check=True,
|
|
1611
|
+
stdout=subprocess.PIPE,
|
|
1612
|
+
text=True,
|
|
1613
|
+
).stdout.splitlines()
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
@pytest.mark.parametrize(
|
|
1618
|
+
("scenario", "expected_files"),
|
|
1619
|
+
[
|
|
1620
|
+
("staged_only", {"staged.txt"}),
|
|
1621
|
+
("unstaged_only", {"unstaged.txt"}),
|
|
1622
|
+
("mixed", {"staged.txt", "unstaged.txt", "new-visible.txt"}),
|
|
1623
|
+
],
|
|
1624
|
+
)
|
|
1625
|
+
def test_preflight_ready_commit_preserves_staged_unstaged_and_mixed_visible_changes(tmp_path, scenario, expected_files):
|
|
1626
|
+
from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
|
|
1627
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1628
|
+
|
|
1629
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1630
|
+
if scenario in {"staged_only", "mixed"}:
|
|
1631
|
+
(tmp_path / "staged.txt").write_text("staged change\n", encoding="utf-8")
|
|
1632
|
+
subprocess.run(["git", "add", "staged.txt"], cwd=tmp_path, check=True)
|
|
1633
|
+
if scenario in {"unstaged_only", "mixed"}:
|
|
1634
|
+
(tmp_path / "unstaged.txt").write_text("unstaged change\n", encoding="utf-8")
|
|
1635
|
+
if scenario == "mixed":
|
|
1636
|
+
(tmp_path / "new-visible.txt").write_text("new visible\n", encoding="utf-8")
|
|
1637
|
+
|
|
1638
|
+
before_count = _head_commit_count(tmp_path)
|
|
1639
|
+
result = commit_preflight_ready_changes(tmp_path, "F-123")
|
|
1640
|
+
visible_status = subprocess.run(
|
|
1641
|
+
["git", "status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES],
|
|
1642
|
+
cwd=tmp_path,
|
|
1643
|
+
check=True,
|
|
1644
|
+
stdout=subprocess.PIPE,
|
|
1645
|
+
text=True,
|
|
1646
|
+
).stdout
|
|
1647
|
+
|
|
1648
|
+
assert result.attempted is True
|
|
1649
|
+
assert result.committed is True
|
|
1650
|
+
assert _head_commit_count(tmp_path) == before_count + 1
|
|
1651
|
+
assert _head_message(tmp_path) == "chore: ready for F-123"
|
|
1652
|
+
assert expected_files.issubset(_head_files(tmp_path))
|
|
1653
|
+
assert visible_status == ""
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def test_preflight_ready_commit_excludes_hidden_support_assets_and_state(tmp_path):
|
|
1657
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1658
|
+
|
|
1659
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1660
|
+
(tmp_path / "unstaged.txt").write_text("visible change\n", encoding="utf-8")
|
|
1661
|
+
hidden_paths = [
|
|
1662
|
+
tmp_path / ".claude" / "settings.json",
|
|
1663
|
+
tmp_path / ".codebuddy" / "skills" / "local.md",
|
|
1664
|
+
tmp_path / ".agents" / "skills" / "local.md",
|
|
1665
|
+
tmp_path / ".codex" / "config.toml",
|
|
1666
|
+
tmp_path / ".prizmkit" / "state" / "features" / "F-001" / "status.json",
|
|
1667
|
+
tmp_path / ".claude" / "worktrees" / "agent" / "scratch.txt",
|
|
1668
|
+
]
|
|
1669
|
+
for path in hidden_paths:
|
|
1670
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1671
|
+
path.write_text("hidden support\n", encoding="utf-8")
|
|
1672
|
+
|
|
1673
|
+
result = commit_preflight_ready_changes(tmp_path, "F-124")
|
|
1674
|
+
committed_files = _head_files(tmp_path)
|
|
1675
|
+
hidden_status = subprocess.run(
|
|
1676
|
+
["git", "status", "--porcelain", "--", ".claude", ".codebuddy", ".agents", ".codex", ".prizmkit/state"],
|
|
1677
|
+
cwd=tmp_path,
|
|
1678
|
+
check=True,
|
|
1679
|
+
stdout=subprocess.PIPE,
|
|
1680
|
+
text=True,
|
|
1681
|
+
).stdout
|
|
1682
|
+
|
|
1683
|
+
assert result.committed is True
|
|
1684
|
+
assert committed_files == {"unstaged.txt"}
|
|
1685
|
+
assert ".claude" in hidden_status or ".prizmkit/state" in hidden_status
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def test_preflight_ready_commit_noops_when_workspace_has_no_visible_dirty_content(tmp_path):
|
|
1689
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1690
|
+
|
|
1691
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1692
|
+
(tmp_path / ".claude" / "worktrees" / "agent").mkdir(parents=True)
|
|
1693
|
+
(tmp_path / ".claude" / "worktrees" / "agent" / "scratch.txt").write_text("hidden only\n", encoding="utf-8")
|
|
1694
|
+
before_count = _head_commit_count(tmp_path)
|
|
1695
|
+
|
|
1696
|
+
result = commit_preflight_ready_changes(tmp_path, "F-125")
|
|
1697
|
+
|
|
1698
|
+
assert result.attempted is False
|
|
1699
|
+
assert result.committed is False
|
|
1700
|
+
assert _head_commit_count(tmp_path) == before_count
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_visible_changes(monkeypatch, tmp_path):
|
|
1704
|
+
from prizmkit_runtime import runner_bookkeeping, runners
|
|
1705
|
+
from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
|
|
1706
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1707
|
+
|
|
1708
|
+
paths = _make_paths(tmp_path)
|
|
1709
|
+
_init_bookkeeping_repo(paths.project_root)
|
|
1710
|
+
(paths.project_root / "staged.txt").write_text("staged before run\n", encoding="utf-8")
|
|
1711
|
+
subprocess.run(["git", "add", "staged.txt"], cwd=paths.project_root, check=True)
|
|
1712
|
+
(paths.project_root / "unstaged.txt").write_text("unstaged before run\n", encoding="utf-8")
|
|
1713
|
+
before_count = _head_commit_count(paths.project_root)
|
|
1714
|
+
branch_events = []
|
|
1715
|
+
|
|
1716
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-ready")
|
|
1717
|
+
family = family_for("feature", paths)
|
|
1718
|
+
invocation = parse_invocation(family, "run", ())
|
|
1719
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1720
|
+
_install_runner_session_fakes(monkeypatch, runners)
|
|
1721
|
+
monkeypatch.setattr(runners, "commit_preflight_ready_changes", runner_bookkeeping.commit_preflight_ready_changes)
|
|
1722
|
+
|
|
1723
|
+
def fake_run_git_plan(project_root, plan):
|
|
1724
|
+
if plan.name == "branch_create":
|
|
1725
|
+
branch_events.append(
|
|
1726
|
+
{
|
|
1727
|
+
"commit_count": _head_commit_count(project_root),
|
|
1728
|
+
"message": _head_message(project_root),
|
|
1729
|
+
"visible_status": subprocess.run(
|
|
1730
|
+
["git", "status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES],
|
|
1731
|
+
cwd=project_root,
|
|
1732
|
+
check=True,
|
|
1733
|
+
stdout=subprocess.PIPE,
|
|
1734
|
+
text=True,
|
|
1735
|
+
).stdout,
|
|
1736
|
+
}
|
|
1737
|
+
)
|
|
1738
|
+
return SimpleNamespace(ok=True, plan=plan, results=())
|
|
1739
|
+
|
|
1740
|
+
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1741
|
+
|
|
1742
|
+
status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
1743
|
+
|
|
1744
|
+
assert status == "completed"
|
|
1745
|
+
assert branch_events == [
|
|
1746
|
+
{
|
|
1747
|
+
"commit_count": before_count + 1,
|
|
1748
|
+
"message": "chore: ready for F-001",
|
|
1749
|
+
"visible_status": "",
|
|
1750
|
+
}
|
|
1751
|
+
]
|
|
1752
|
+
assert _head_commit_count(paths.project_root) == before_count + 1
|
|
1753
|
+
assert {"staged.txt", "unstaged.txt"}.issubset(_head_files(paths.project_root))
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
|
|
1562
1757
|
def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
|
|
1563
1758
|
from prizmkit_runtime.runner_bookkeeping import commit_final_bookkeeping
|
|
1564
1759
|
from prizmkit_runtime.runner_models import RunnerFamily
|