prizmkit 1.1.115 → 1.1.116

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.115",
3
- "bundledAt": "2026-07-09T10:20:47.948Z",
4
- "bundledFrom": "5d2c9f0"
2
+ "frameworkVersion": "1.1.116",
3
+ "bundledAt": "2026-07-09T12:20:56.645Z",
4
+ "bundledFrom": "5def1a0"
5
5
  }
@@ -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
- if not git_status_safe(project_root).strip():
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
- return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
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
- def test_no_worktree_preflight_commit_failure_aborts_before_branch_or_status(monkeypatch, tmp_path):
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", "dev/F-001-preflight-fail")
1479
- paths = _make_paths(tmp_path)
1480
- family = family_for("feature", paths)
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
- invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
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, "F-001", paths, initial_metadata={})
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.115",
2
+ "version": "1.1.116",
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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.115",
3
+ "version": "1.1.116",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {