prizmkit 1.1.116 → 1.1.118

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.116",
3
- "bundledAt": "2026-07-09T12:20:56.645Z",
4
- "bundledFrom": "5def1a0"
2
+ "frameworkVersion": "1.1.118",
3
+ "bundledAt": "2026-07-09T14:31:05.918Z",
4
+ "bundledFrom": "932cf65"
5
5
  }
@@ -39,6 +39,9 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
39
39
  ":(top,exclude,glob).prizmkit/dev-pipeline/prizmkit_runtime/**",
40
40
  )
41
41
 
42
+ PIPELINE_FALLBACK_GIT_NAME = "PrizmKit Pipeline"
43
+ PIPELINE_FALLBACK_GIT_EMAIL = "prizmkit-pipeline@example.invalid"
44
+
42
45
  WORKTREE_PLATFORM_SUPPORT_DIRS = (".claude", ".codebuddy", ".codex", ".agents")
43
46
  WORKTREE_ROOT_SUPPORT_FILES = ("skills-lock.json", "AGENTS.md")
44
47
  WORKTREE_PRIZMKIT_SUPPORT_ENTRIES = ("prizm-docs", "config.json", "manifest.json")
@@ -640,17 +643,60 @@ def branch_ensure_return(project_root: Path, base_branch: str, working_branch: s
640
643
  context = BranchContext(base_branch=base_branch, working_branch=working_branch or active, project_root=project_root)
641
644
  return run_git_plan(project_root, branch_ensure_return_plan(context))
642
645
 
646
+
647
+ def _git_config_has_value(project_root: Path, key: str) -> bool:
648
+ completed = subprocess.run(
649
+ ["git", "-C", str(project_root), "config", "--get", key],
650
+ stdout=subprocess.PIPE,
651
+ stderr=subprocess.PIPE,
652
+ text=True,
653
+ check=False,
654
+ )
655
+ return completed.returncode == 0 and bool(completed.stdout.strip())
656
+
657
+
658
+ def _identity_value_available(project_root: Path, config_key: str, author_env: str, committer_env: str) -> bool:
659
+ return _git_config_has_value(project_root, config_key) or bool(
660
+ os.environ.get(author_env) and os.environ.get(committer_env)
661
+ )
662
+
663
+
664
+ def _commit_identity_global_args(project_root: Path) -> tuple[str, ...]:
665
+ global_args: list[str] = []
666
+ if not _identity_value_available(project_root, "user.name", "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME"):
667
+ global_args.extend(("-c", f"user.name={PIPELINE_FALLBACK_GIT_NAME}"))
668
+ if not _identity_value_available(project_root, "user.email", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL"):
669
+ global_args.extend(("-c", f"user.email={PIPELINE_FALLBACK_GIT_EMAIL}"))
670
+ return tuple(global_args)
671
+
672
+
673
+ def _commit_args_without_signing(args: tuple[str, ...]) -> tuple[str, ...]:
674
+ explicit_signing = any(
675
+ arg == "-S" or arg.startswith("-S") or arg == "--gpg-sign" or arg.startswith("--gpg-sign=")
676
+ for arg in args[1:]
677
+ )
678
+ if explicit_signing or "--no-gpg-sign" in args:
679
+ return args
680
+ return ("commit", "--no-gpg-sign", *args[1:])
681
+
682
+
643
683
  def run_git_command(project_root: Path, args: Sequence[str]) -> GitCommandResult:
644
684
  """Run one git command in a project root and capture output."""
685
+ command_args = tuple(map(str, args))
686
+ global_args: tuple[str, ...] = ()
687
+ executable_args = command_args
688
+ if command_args and command_args[0] == "commit":
689
+ global_args = _commit_identity_global_args(project_root)
690
+ executable_args = _commit_args_without_signing(command_args)
645
691
  completed = subprocess.run(
646
- ["git", "-C", str(project_root), *map(str, args)],
692
+ ["git", *global_args, "-C", str(project_root), *executable_args],
647
693
  stdout=subprocess.PIPE,
648
694
  stderr=subprocess.PIPE,
649
695
  text=True,
650
696
  check=False,
651
697
  )
652
698
  return GitCommandResult(
653
- args=tuple(map(str, args)),
699
+ args=command_args,
654
700
  return_code=completed.returncode,
655
701
  stdout=completed.stdout,
656
702
  stderr=completed.stderr,
@@ -1570,14 +1570,29 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
1570
1570
  assert ensure_calls == [("main", "dev/F-001-interrupt")]
1571
1571
 
1572
1572
 
1573
- def _init_bookkeeping_repo(project_root):
1573
+ def _init_bookkeeping_repo(project_root, *, configure_identity=True):
1574
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)
1575
+ if configure_identity:
1576
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=project_root, check=True)
1577
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=project_root, check=True)
1577
1578
  (project_root / "staged.txt").write_text("base staged\n", encoding="utf-8")
1578
1579
  (project_root / "unstaged.txt").write_text("base unstaged\n", encoding="utf-8")
1579
1580
  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
+ subprocess.run(
1582
+ [
1583
+ "git",
1584
+ "-c",
1585
+ "user.email=test@example.com",
1586
+ "-c",
1587
+ "user.name=Test",
1588
+ "commit",
1589
+ "-m",
1590
+ "base",
1591
+ ],
1592
+ cwd=project_root,
1593
+ check=True,
1594
+ stdout=subprocess.DEVNULL,
1595
+ )
1581
1596
 
1582
1597
 
1583
1598
  def _head_commit_count(project_root):
@@ -1754,6 +1769,69 @@ def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_vis
1754
1769
 
1755
1770
 
1756
1771
 
1772
+ def test_process_item_ready_commit_uses_fallback_identity_before_branch_setup(monkeypatch, tmp_path):
1773
+ from prizmkit_runtime import runner_bookkeeping, runners
1774
+ from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES, PIPELINE_FALLBACK_GIT_EMAIL, PIPELINE_FALLBACK_GIT_NAME
1775
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
1776
+
1777
+ monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "missing-global-gitconfig"))
1778
+ monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
1779
+ for key in ("GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"):
1780
+ monkeypatch.delenv(key, raising=False)
1781
+ paths = _make_paths(tmp_path)
1782
+ _init_bookkeeping_repo(paths.project_root, configure_identity=False)
1783
+ (paths.project_root / "new-visible.txt").write_text("dirty before run\n", encoding="utf-8")
1784
+ before_count = _head_commit_count(paths.project_root)
1785
+ branch_events = []
1786
+
1787
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-ready-no-identity")
1788
+ family = family_for("feature", paths)
1789
+ invocation = parse_invocation(family, "run", ())
1790
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1791
+ _install_runner_session_fakes(monkeypatch, runners)
1792
+ monkeypatch.setattr(runners, "commit_preflight_ready_changes", runner_bookkeeping.commit_preflight_ready_changes)
1793
+
1794
+ def fake_run_git_plan(project_root, plan):
1795
+ if plan.name == "branch_create":
1796
+ author = subprocess.run(
1797
+ ["git", "log", "-1", "--format=%an <%ae>"],
1798
+ cwd=project_root,
1799
+ check=True,
1800
+ stdout=subprocess.PIPE,
1801
+ text=True,
1802
+ ).stdout.strip()
1803
+ branch_events.append(
1804
+ {
1805
+ "commit_count": _head_commit_count(project_root),
1806
+ "message": _head_message(project_root),
1807
+ "author": author,
1808
+ "visible_status": subprocess.run(
1809
+ ["git", "status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES],
1810
+ cwd=project_root,
1811
+ check=True,
1812
+ stdout=subprocess.PIPE,
1813
+ text=True,
1814
+ ).stdout,
1815
+ }
1816
+ )
1817
+ return SimpleNamespace(ok=True, plan=plan, results=())
1818
+
1819
+ monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
1820
+
1821
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1822
+
1823
+ assert status == "completed"
1824
+ assert branch_events == [
1825
+ {
1826
+ "commit_count": before_count + 1,
1827
+ "message": "chore: ready for F-001",
1828
+ "author": f"{PIPELINE_FALLBACK_GIT_NAME} <{PIPELINE_FALLBACK_GIT_EMAIL}>",
1829
+ "visible_status": "",
1830
+ }
1831
+ ]
1832
+ assert "new-visible.txt" in _head_files(paths.project_root)
1833
+
1834
+
1757
1835
  def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
1758
1836
  from prizmkit_runtime.runner_bookkeeping import commit_final_bookkeeping
1759
1837
  from prizmkit_runtime.runner_models import RunnerFamily
@@ -1776,14 +1776,29 @@ def test_git_status_safe_ignores_worktree_support_assets(monkeypatch, tmp_path):
1776
1776
  assert ".prizmkit-worktree" in joined
1777
1777
 
1778
1778
 
1779
- def _init_temp_repo(repo):
1779
+ def _init_temp_repo(repo, *, configure_identity=True):
1780
1780
  repo.mkdir(parents=True, exist_ok=True)
1781
1781
  subprocess.run(["git", "init", "-b", "main"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
1782
- subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo, check=True)
1783
- subprocess.run(["git", "config", "user.name", "Test"], cwd=repo, check=True)
1782
+ if configure_identity:
1783
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo, check=True)
1784
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=repo, check=True)
1784
1785
  (repo / "base.txt").write_text("base\n", encoding="utf-8")
1785
1786
  subprocess.run(["git", "add", "base.txt"], cwd=repo, check=True)
1786
- subprocess.run(["git", "commit", "-m", "base"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
1787
+ subprocess.run(
1788
+ [
1789
+ "git",
1790
+ "-c",
1791
+ "user.email=test@example.com",
1792
+ "-c",
1793
+ "user.name=Test",
1794
+ "commit",
1795
+ "-m",
1796
+ "base",
1797
+ ],
1798
+ cwd=repo,
1799
+ check=True,
1800
+ stdout=subprocess.DEVNULL,
1801
+ )
1787
1802
 
1788
1803
 
1789
1804
  def _head_commit_message(repo):
@@ -1915,6 +1930,34 @@ def test_preflight_ready_commit_excludes_ignored_tool_worktree_directories(tmp_p
1915
1930
  assert (repo / ".cache" / "worktrees" / "agent" / "artifact.txt").is_file()
1916
1931
 
1917
1932
 
1933
+ def test_preflight_ready_commit_preserves_dirty_changes_without_repo_git_identity(tmp_path, monkeypatch):
1934
+ from prizmkit_runtime.gitops import PIPELINE_FALLBACK_GIT_EMAIL, PIPELINE_FALLBACK_GIT_NAME
1935
+ from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
1936
+
1937
+ repo = tmp_path / "repo"
1938
+ monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "missing-global-gitconfig"))
1939
+ monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
1940
+ for key in ("GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"):
1941
+ monkeypatch.delenv(key, raising=False)
1942
+ _init_temp_repo(repo, configure_identity=False)
1943
+ (repo / "visible.txt").write_text("dirty\n", encoding="utf-8")
1944
+
1945
+ result = commit_preflight_ready_changes(repo, "F-007")
1946
+ author = subprocess.run(
1947
+ ["git", "log", "-1", "--format=%an <%ae>"],
1948
+ cwd=repo,
1949
+ check=True,
1950
+ stdout=subprocess.PIPE,
1951
+ text=True,
1952
+ ).stdout.strip()
1953
+
1954
+ assert result.committed
1955
+ assert _head_commit_message(repo) == "chore: ready for F-007"
1956
+ assert _head_files(repo) == {"visible.txt"}
1957
+ assert author == f"{PIPELINE_FALLBACK_GIT_NAME} <{PIPELINE_FALLBACK_GIT_EMAIL}>"
1958
+ assert _short_status(repo) == ""
1959
+
1960
+
1918
1961
  def test_materialize_worktree_support_assets_copies_ignored_assets_without_retired_shell_sources(tmp_path):
1919
1962
  from prizmkit_runtime.gitops import (
1920
1963
  BranchContext,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.116",
2
+ "version": "1.1.118",
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.116",
3
+ "version": "1.1.118",
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": {