prizmkit 1.1.128 → 1.1.129

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.
Files changed (40) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +88 -58
  3. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +4 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +53 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +33 -17
  6. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +28 -26
  7. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -1
  8. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
  9. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
  10. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
  11. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  12. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -19
  13. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  14. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
  15. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +10 -2
  16. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  17. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
  18. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
  19. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  20. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +11 -3
  24. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -3
  25. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +11 -3
  29. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
  31. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
  32. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +98 -3
  33. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
  34. package/bundled/dev-pipeline/tests/test_unified_cli.py +215 -9
  35. package/bundled/skills/_metadata.json +1 -1
  36. package/bundled/skills/feature-planner/SKILL.md +2 -2
  37. package/bundled/skills/prizmkit/SKILL.md +1 -1
  38. package/bundled/skills/prizmkit-code-review/SKILL.md +10 -1
  39. package/package.json +1 -1
  40. package/src/manifest.js +8 -3
@@ -1408,7 +1408,8 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
1408
1408
  lifecycle_order.append("branch_create")
1409
1409
  branch_events.append(("create", plan.commands[1].args[2]))
1410
1410
  if plan.name == "branch_merge":
1411
- branch_events.append(("merge", plan.commands[5].args[2]))
1411
+ merge_command = next(command for command in plan.commands if command.args[:2] == ("merge", "--ff-only"))
1412
+ branch_events.append(("merge", merge_command.args[2]))
1412
1413
  return SimpleNamespace(ok=True, plan=plan, results=())
1413
1414
 
1414
1415
  def fake_start_item(*args, **kwargs):
@@ -1603,6 +1604,79 @@ def test_worktree_recovers_completed_pending_branch_from_base_session_history(mo
1603
1604
  assert merge_calls and cleanup_calls[-1][1] is True
1604
1605
 
1605
1606
 
1607
+ def test_success_final_commit_failure_preserves_checkout_without_merge(monkeypatch, tmp_path):
1608
+ from prizmkit_runtime import runners
1609
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
1610
+ from prizmkit_runtime.task_checkout import load_task_checkout
1611
+
1612
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-finalize-fail")
1613
+ paths = _make_paths(tmp_path)
1614
+ family = family_for("feature", paths)
1615
+ invocation = parse_invocation(family, "run", ())
1616
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1617
+ _install_runner_session_fakes(monkeypatch, runners)
1618
+ merge_calls = []
1619
+
1620
+ monkeypatch.setattr(
1621
+ runners,
1622
+ "commit_final_bookkeeping",
1623
+ lambda *args, **kwargs: SimpleNamespace(ok=False, attempted=True, committed=False),
1624
+ )
1625
+ monkeypatch.setattr(
1626
+ runners,
1627
+ "commit_task_branch_changes",
1628
+ lambda *args, **kwargs: SimpleNamespace(ok=True, attempted=False, committed=False),
1629
+ )
1630
+ monkeypatch.setattr(
1631
+ runners,
1632
+ "run_git_plan",
1633
+ lambda root, plan: merge_calls.append(plan.name) or SimpleNamespace(ok=True, plan=plan, results=()),
1634
+ )
1635
+
1636
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1637
+
1638
+ assert status == "completed"
1639
+ assert "branch_merge" not in merge_calls
1640
+ checkout = load_task_checkout(family.state_dir, family.task_type, "F-001")
1641
+ assert checkout is not None and checkout.is_active
1642
+ session_status = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-1" / "session-status.json"
1643
+ assert json.loads(session_status.read_text(encoding="utf-8"))["status"] == "merge_conflict"
1644
+
1645
+
1646
+ def test_wip_commit_failure_skips_base_return(monkeypatch, tmp_path):
1647
+ from prizmkit_runtime import runners
1648
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
1649
+
1650
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-wip-fail")
1651
+ paths = _make_paths(tmp_path)
1652
+ family = family_for("feature", paths)
1653
+ invocation = parse_invocation(family, "run", ())
1654
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1655
+ _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="failed")
1656
+ returns = []
1657
+
1658
+ monkeypatch.setattr(
1659
+ runners,
1660
+ "commit_wip_changes",
1661
+ lambda *args, **kwargs: SimpleNamespace(ok=False, attempted=True, committed=False),
1662
+ )
1663
+ monkeypatch.setattr(
1664
+ runners,
1665
+ "branch_ensure_return",
1666
+ lambda *args, **kwargs: returns.append(args) or SimpleNamespace(ok=True),
1667
+ )
1668
+ monkeypatch.setattr(
1669
+ runners,
1670
+ "run_git_plan",
1671
+ lambda root, plan: SimpleNamespace(ok=True, plan=plan, results=()),
1672
+ )
1673
+
1674
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1675
+
1676
+ assert status == "failed"
1677
+ assert returns == []
1678
+
1679
+
1606
1680
  def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch, tmp_path):
1607
1681
  from prizmkit_runtime import runners
1608
1682
  from prizmkit_runtime.runner_models import family_for
@@ -1752,7 +1826,7 @@ def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_pa
1752
1826
  assert "Feature: F-001 — Readable output" in captured.err
1753
1827
  assert "Code retry: 1 / 3" in captured.err
1754
1828
  assert "Preflight: no user-visible changes to preserve before feature task F-001" in captured.err
1755
- assert "Pipeline mode: full (Tier 3 — Orchestrator + Reviewer)" in captured.err
1829
+ assert "Pipeline mode: full (Tier 3 — Main-Agent Full Guardrails)" in captured.err
1756
1830
  assert "Agents: 3" in captured.err
1757
1831
  assert "Spawning AI CLI session: F-001-session-1" in captured.err
1758
1832
  assert "Session result: success" in captured.err
@@ -2098,24 +2172,27 @@ def test_preflight_ready_commit_includes_tracked_state_but_excludes_untracked_st
2098
2172
  assert untracked_state.relative_to(tmp_path).as_posix() not in _head_files(tmp_path)
2099
2173
 
2100
2174
 
2101
- def test_preflight_ready_commit_includes_tracked_framework_support_but_excludes_untracked_support_and_state(tmp_path):
2175
+ def test_preflight_ready_commit_excludes_tracked_framework_manifest_but_preserves_other_tracked_support(tmp_path):
2102
2176
  from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
2103
2177
 
2104
2178
  _init_bookkeeping_repo(tmp_path)
2179
+ manifest_path = tmp_path / ".prizmkit" / "manifest.json"
2105
2180
  tracked_support_paths = [
2106
- tmp_path / ".prizmkit" / "manifest.json",
2107
2181
  tmp_path / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "runners.py",
2108
2182
  tmp_path / ".claude" / "commands" / "prizmkit.md",
2109
2183
  ]
2184
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
2185
+ manifest_path.write_text('{"version":"1.1.127"}\n', encoding="utf-8")
2110
2186
  for path in tracked_support_paths:
2111
2187
  path.parent.mkdir(parents=True, exist_ok=True)
2112
2188
  path.write_text("installed\n", encoding="utf-8")
2113
2189
  subprocess.run(
2114
- ["git", "add", *(path.relative_to(tmp_path).as_posix() for path in tracked_support_paths)],
2190
+ ["git", "add", manifest_path.relative_to(tmp_path).as_posix(), *(path.relative_to(tmp_path).as_posix() for path in tracked_support_paths)],
2115
2191
  cwd=tmp_path,
2116
2192
  check=True,
2117
2193
  )
2118
2194
  subprocess.run(["git", "commit", "-m", "track support"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2195
+ manifest_path.write_text('{"version":"1.1.128"}\n', encoding="utf-8")
2119
2196
  for path in tracked_support_paths:
2120
2197
  path.write_text("upgraded\n", encoding="utf-8")
2121
2198
 
@@ -2129,19 +2206,23 @@ def test_preflight_ready_commit_includes_tracked_framework_support_but_excludes_
2129
2206
  path.write_text("transient\n", encoding="utf-8")
2130
2207
 
2131
2208
  result = commit_preflight_ready_changes(tmp_path, "B-001")
2132
- committed_files = _head_files(tmp_path)
2133
2209
 
2134
- assert result.committed is True
2135
- assert {path.relative_to(tmp_path).as_posix() for path in tracked_support_paths}.issubset(committed_files)
2136
- assert not {path.relative_to(tmp_path).as_posix() for path in untracked_support_paths}.intersection(committed_files)
2137
- assert _head_message(tmp_path) == "chore: ready for B-001"
2210
+ assert result.attempted is True
2211
+ assert result.committed is False
2212
+ assert result.result is not None
2213
+ assert "installer-owned .prizmkit/manifest.json" in result.result.stderr
2214
+ assert _head_message(tmp_path) == "track support"
2138
2215
  assert subprocess.run(
2139
2216
  ["git", "status", "--short", "--untracked-files=no"],
2140
2217
  cwd=tmp_path,
2141
2218
  check=True,
2142
2219
  stdout=subprocess.PIPE,
2143
2220
  text=True,
2144
- ).stdout == ""
2221
+ ).stdout.splitlines() == [
2222
+ " M .claude/commands/prizmkit.md",
2223
+ " M .prizmkit/dev-pipeline/prizmkit_runtime/runners.py",
2224
+ " M .prizmkit/manifest.json",
2225
+ ]
2145
2226
 
2146
2227
 
2147
2228
  def test_preflight_ready_commit_excludes_untracked_support_assets_and_state(tmp_path):
@@ -2176,6 +2257,53 @@ def test_preflight_ready_commit_excludes_untracked_support_assets_and_state(tmp_
2176
2257
  assert ".claude" in hidden_status or ".prizmkit/state" in hidden_status
2177
2258
 
2178
2259
 
2260
+ def test_preflight_ready_commit_rejects_unmerged_index(tmp_path):
2261
+ from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
2262
+
2263
+ _init_bookkeeping_repo(tmp_path)
2264
+ subprocess.run(["git", "checkout", "-b", "other"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2265
+ (tmp_path / "staged.txt").write_text("other\n", encoding="utf-8")
2266
+ subprocess.run(["git", "commit", "-am", "other"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2267
+ subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2268
+ (tmp_path / "staged.txt").write_text("main\n", encoding="utf-8")
2269
+ subprocess.run(["git", "commit", "-am", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2270
+ subprocess.run(["git", "merge", "other"], cwd=tmp_path, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2271
+ before_count = _head_commit_count(tmp_path)
2272
+
2273
+ result = commit_preflight_ready_changes(tmp_path, "F-126")
2274
+
2275
+ assert result.attempted is True
2276
+ assert result.committed is False
2277
+ assert result.result is not None
2278
+ assert "unmerged Git paths" in result.result.stderr
2279
+ assert _head_commit_count(tmp_path) == before_count
2280
+
2281
+
2282
+ def test_preflight_ready_commit_rejects_conflict_markers_and_malformed_manifest(tmp_path):
2283
+ from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
2284
+
2285
+ _init_bookkeeping_repo(tmp_path)
2286
+ (tmp_path / "unstaged.txt").write_text("<<<<<<< Updated upstream\ncurrent\n=======\nstashed\n>>>>>>> Stashed changes\n", encoding="utf-8")
2287
+
2288
+ marker_result = commit_preflight_ready_changes(tmp_path, "F-127")
2289
+
2290
+ assert marker_result.committed is False
2291
+ assert marker_result.result is not None
2292
+ assert "unresolved conflict markers" in marker_result.result.stderr
2293
+
2294
+ (tmp_path / "unstaged.txt").write_text("safe\n", encoding="utf-8")
2295
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
2296
+ manifest.parent.mkdir(parents=True, exist_ok=True)
2297
+ manifest.write_text("<<<<<<< Updated upstream\n{}\n", encoding="utf-8")
2298
+
2299
+ manifest_result = commit_preflight_ready_changes(tmp_path, "F-128")
2300
+
2301
+ assert manifest_result.committed is False
2302
+ assert manifest_result.result is not None
2303
+ assert ".prizmkit/manifest.json" in manifest_result.result.stderr
2304
+ assert "valid JSON" in manifest_result.result.stderr
2305
+
2306
+
2179
2307
  def test_preflight_ready_commit_noops_when_workspace_has_no_visible_dirty_content(tmp_path):
2180
2308
  from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
2181
2309
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import shutil
6
7
  import subprocess
7
8
  import sys
@@ -830,7 +831,8 @@ def test_headless_review_guidance_uses_main_agent_review_loop():
830
831
  assert "Main-Agent review loop" in text, path
831
832
  assert "up to ten completed rounds" in text, path
832
833
  assert "text final-verdict" in text, path
833
- assert "code-review-specific subagent" in text, path
834
+ assert "The current Main Agent is the only Code Review executor" in text, path
835
+ assert "Do not invoke another review skill or review workflow" in text, path
834
836
  assert "low=0" not in text and "Reviewer 3" not in text, path
835
837
  assert "REVIEW_BLOCKED" not in text, path
836
838
  assert "forced-worktree changed-content fallback" not in text, path
@@ -947,10 +949,11 @@ def test_progress_parser_normalizes_codebuddy_agent_tools_with_deduplication():
947
949
 
948
950
  assert data["cb_session_id"] == "cb-session"
949
951
  assert data["subagent_provider"] == "codebuddy"
950
- assert data["subagent_spawn_count"] == 2
951
- assert data["active_subagent_count"] == 2
952
+ assert data["subagent_spawn_count"] == 1
953
+ assert data["active_subagent_count"] == 1
952
954
  assert {item["status"] for item in data["subagent_states"]} == {"running"}
953
- assert len(data["subagent_statistics"]["records"]) == 2
955
+ assert len(data["subagent_statistics"]["records"]) == 1
956
+ assert data["subagent_statistics"]["records"][0]["metadata"]["tool"] == "Agent"
954
957
 
955
958
 
956
959
  def test_progress_parser_normalizes_codex_collab_metadata_with_missing_fields():
@@ -1827,14 +1830,18 @@ def test_git_branch_worktree_plans_preserve_shell_invariants(tmp_path):
1827
1830
 
1828
1831
  assert create.name == "branch_create"
1829
1832
  assert any(command.args[:2] == ("checkout", "-b") for command in create.commands)
1830
- assert any(command.args[:2] == ("stash", "push") for command in merge.commands)
1833
+ assert not any(command.args and command.args[0] == "stash" for command in merge.commands)
1834
+ assert any(command.args[:2] == ("status", "--porcelain") for command in merge.commands)
1831
1835
  assert any(command.args[:2] == ("merge", "--ff-only") for command in merge.commands)
1832
- assert any(command.args == ("push",) for command in merge.commands)
1833
- assert save_wip.never_fails is True
1834
- assert ensure.never_fails is True
1836
+ push_index = next(index for index, command in enumerate(merge.commands) if command.args == ("push",))
1837
+ merge_index = next(index for index, command in enumerate(merge.commands) if command.args[:2] == ("merge", "--ff-only"))
1838
+ assert merge_index < push_index
1839
+ assert save_wip.never_fails is False
1840
+ assert ensure.never_fails is False
1835
1841
  assert any(command.args[:2] == ("rebase", "--abort") for command in ensure.commands)
1836
1842
  assert any(command.args[:2] == ("add", "-A") for command in ensure.commands)
1837
- assert not any(command.args[:2] == ("stash", "pop") for command in ensure.commands)
1843
+ assert not any(command.args and command.args[0] == "stash" for command in ensure.commands)
1844
+ assert ensure.never_fails is False
1838
1845
  assert any(".*/worktrees/**" in " ".join(command.args) for command in save_wip.commands)
1839
1846
  assert worktree.commands[0].args[:2] == ("worktree", "add")
1840
1847
 
@@ -1862,6 +1869,205 @@ def test_git_branch_worktree_plans_preserve_shell_invariants(tmp_path):
1862
1869
  assert skipped_wip.results == ()
1863
1870
 
1864
1871
 
1872
+ def test_runtime_source_constructs_no_git_stash_commands():
1873
+ runtime = Path("dev-pipeline/prizmkit_runtime")
1874
+ matches = []
1875
+ for path in runtime.glob("*.py"):
1876
+ text = path.read_text(encoding="utf-8")
1877
+ if re.search(r'["\']stash["\']', text):
1878
+ matches.append(path.as_posix())
1879
+
1880
+ assert matches == []
1881
+
1882
+
1883
+ def _init_merge_repo(project_root):
1884
+ subprocess.run(["git", "init", "-b", "main"], cwd=project_root, check=True, stdout=subprocess.DEVNULL)
1885
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=project_root, check=True)
1886
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=project_root, check=True)
1887
+ manifest = project_root / ".prizmkit" / "manifest.json"
1888
+ manifest.parent.mkdir(parents=True)
1889
+ manifest.write_text('{"version":"current"}\n', encoding="utf-8")
1890
+ (project_root / "app.txt").write_text("base\n", encoding="utf-8")
1891
+ subprocess.run(["git", "add", "."], cwd=project_root, check=True)
1892
+ subprocess.run(["git", "commit", "-m", "base"], cwd=project_root, check=True, stdout=subprocess.DEVNULL)
1893
+
1894
+
1895
+ def _stash_oids(project_root):
1896
+ output = subprocess.run(
1897
+ ["git", "stash", "list", "--format=%H"],
1898
+ cwd=project_root,
1899
+ check=True,
1900
+ stdout=subprocess.PIPE,
1901
+ text=True,
1902
+ ).stdout
1903
+ return tuple(line for line in output.splitlines() if line)
1904
+
1905
+
1906
+ def test_branch_merge_preserves_preexisting_stash_when_workspace_is_clean(tmp_path):
1907
+ from prizmkit_runtime.gitops import BranchContext, branch_merge_plan, run_git_plan
1908
+
1909
+ _init_merge_repo(tmp_path)
1910
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
1911
+ manifest.write_text('{"version":"stale"}\n', encoding="utf-8")
1912
+ subprocess.run(["git", "stash", "push", "-m", "stale-install", "--", ".prizmkit/manifest.json"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1913
+ stale_stashes = _stash_oids(tmp_path)
1914
+ subprocess.run(["git", "checkout", "-b", "dev/example"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1915
+ (tmp_path / "feature.txt").write_text("done\n", encoding="utf-8")
1916
+ subprocess.run(["git", "add", "feature.txt"], cwd=tmp_path, check=True)
1917
+ subprocess.run(["git", "commit", "-m", "feature"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1918
+
1919
+ result = run_git_plan(
1920
+ tmp_path,
1921
+ branch_merge_plan(BranchContext("main", "dev/example", tmp_path)),
1922
+ )
1923
+
1924
+ assert result.ok is True
1925
+ assert _stash_oids(tmp_path) == stale_stashes
1926
+ assert json.loads(manifest.read_text(encoding="utf-8"))["version"] == "current"
1927
+ assert (tmp_path / "feature.txt").read_text(encoding="utf-8") == "done\n"
1928
+
1929
+
1930
+ def test_branch_merge_rejects_dirty_task_checkout_without_touching_stashes(tmp_path):
1931
+ from prizmkit_runtime.gitops import BranchContext, branch_merge_plan, run_git_plan
1932
+
1933
+ _init_merge_repo(tmp_path)
1934
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
1935
+ manifest.write_text('{"version":"stale"}\n', encoding="utf-8")
1936
+ subprocess.run(["git", "stash", "push", "-m", "user-stash", "--", ".prizmkit/manifest.json"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1937
+ stale_stashes = _stash_oids(tmp_path)
1938
+ subprocess.run(["git", "checkout", "-b", "dev/example"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1939
+ (tmp_path / "feature.txt").write_text("done\n", encoding="utf-8")
1940
+ subprocess.run(["git", "add", "feature.txt"], cwd=tmp_path, check=True)
1941
+ subprocess.run(["git", "commit", "-m", "feature"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1942
+ (tmp_path / "app.txt").write_text("local visible change\n", encoding="utf-8")
1943
+
1944
+ result = run_git_plan(
1945
+ tmp_path,
1946
+ branch_merge_plan(BranchContext("main", "dev/example", tmp_path)),
1947
+ )
1948
+
1949
+ assert result.ok is False
1950
+ assert _stash_oids(tmp_path) == stale_stashes
1951
+ assert (tmp_path / "app.txt").read_text(encoding="utf-8") == "local visible change\n"
1952
+ assert json.loads(manifest.read_text(encoding="utf-8"))["version"] == "current"
1953
+ assert subprocess.run(
1954
+ ["git", "branch", "--show-current"],
1955
+ cwd=tmp_path,
1956
+ check=True,
1957
+ stdout=subprocess.PIPE,
1958
+ text=True,
1959
+ ).stdout.strip() == "dev/example"
1960
+
1961
+
1962
+ def test_branch_merge_rebases_committed_task_without_stash_restore_conflict(tmp_path):
1963
+ from prizmkit_runtime.gitops import BranchContext, branch_merge_plan, run_git_plan
1964
+
1965
+ _init_merge_repo(tmp_path)
1966
+ subprocess.run(["git", "checkout", "-b", "dev/example"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1967
+ (tmp_path / "feature.txt").write_text("done\n", encoding="utf-8")
1968
+ subprocess.run(["git", "add", "feature.txt"], cwd=tmp_path, check=True)
1969
+ subprocess.run(["git", "commit", "-m", "feature"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1970
+ subprocess.run(["git", "checkout", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1971
+ (tmp_path / "app.txt").write_text("main changed\n", encoding="utf-8")
1972
+ subprocess.run(["git", "commit", "-am", "advance main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1973
+ subprocess.run(["git", "checkout", "dev/example"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1974
+
1975
+ result = run_git_plan(
1976
+ tmp_path,
1977
+ branch_merge_plan(BranchContext("main", "dev/example", tmp_path)),
1978
+ )
1979
+
1980
+ assert result.ok is True
1981
+ assert _stash_oids(tmp_path) == ()
1982
+ assert (tmp_path / "app.txt").read_text(encoding="utf-8") == "main changed\n"
1983
+ assert subprocess.run(
1984
+ ["git", "branch", "--show-current"],
1985
+ cwd=tmp_path,
1986
+ check=True,
1987
+ stdout=subprocess.PIPE,
1988
+ text=True,
1989
+ ).stdout.strip() == "main"
1990
+
1991
+
1992
+ def test_branch_return_commits_wip_without_touching_user_stashes(tmp_path):
1993
+ from prizmkit_runtime.gitops import branch_ensure_return
1994
+
1995
+ _init_merge_repo(tmp_path)
1996
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
1997
+ manifest.write_text('{"version":"stale"}\n', encoding="utf-8")
1998
+ subprocess.run(["git", "stash", "push", "-m", "user-stash", "--", ".prizmkit/manifest.json"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1999
+ user_stashes = _stash_oids(tmp_path)
2000
+ subprocess.run(["git", "checkout", "-b", "dev/interrupted"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2001
+ (tmp_path / "app.txt").write_text("interrupted work\n", encoding="utf-8")
2002
+
2003
+ result = branch_ensure_return(tmp_path, "main", "dev/interrupted")
2004
+
2005
+ assert result.ok is True
2006
+ assert _stash_oids(tmp_path) == user_stashes
2007
+ assert subprocess.run(
2008
+ ["git", "branch", "--show-current"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
2009
+ ).stdout.strip() == "main"
2010
+ assert subprocess.run(
2011
+ ["git", "show", "dev/interrupted:app.txt"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True,
2012
+ ).stdout == "interrupted work\n"
2013
+
2014
+
2015
+ def test_branch_return_commit_failure_does_not_checkout_base(monkeypatch, tmp_path):
2016
+ from prizmkit_runtime import gitops
2017
+
2018
+ calls = []
2019
+
2020
+ def fake_run(_root, args):
2021
+ args = tuple(args)
2022
+ calls.append(args)
2023
+ if args[:3] == ("rev-parse", "--abbrev-ref", "HEAD"):
2024
+ return gitops.GitCommandResult(args, 0, "dev/interrupted\n", "")
2025
+ if args and args[0] == "commit":
2026
+ return gitops.GitCommandResult(args, 1, "", "commit failed")
2027
+ if args[:2] == ("status", "--porcelain"):
2028
+ return gitops.GitCommandResult(args, 0, " M app.txt\n", "")
2029
+ return gitops.GitCommandResult(args, 0, "", "")
2030
+
2031
+ monkeypatch.setattr(gitops, "run_git_command", fake_run)
2032
+
2033
+ result = gitops.branch_ensure_return(tmp_path, "main", "dev/interrupted")
2034
+
2035
+ assert result.ok is False
2036
+ assert not any(args[:2] == ("checkout", "main") for args in calls)
2037
+ assert not any(args and args[0] == "stash" for args in calls)
2038
+
2039
+
2040
+ def test_non_current_reset_preserves_dirty_workspace_and_user_stashes(tmp_path):
2041
+ from prizmkit_runtime.gitops import reset_cleanup_branch
2042
+
2043
+ _init_merge_repo(tmp_path)
2044
+ subprocess.run(["git", "branch", "dev/other"], cwd=tmp_path, check=True)
2045
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
2046
+ manifest.write_text('{"version":"stale"}\n', encoding="utf-8")
2047
+ subprocess.run(["git", "stash", "push", "-m", "user-stash", "--", ".prizmkit/manifest.json"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2048
+ user_stashes = _stash_oids(tmp_path)
2049
+ (tmp_path / "app.txt").write_text("unstaged\n", encoding="utf-8")
2050
+ (tmp_path / "staged.txt").write_text("staged\n", encoding="utf-8")
2051
+ subprocess.run(["git", "add", "staged.txt"], cwd=tmp_path, check=True)
2052
+ (tmp_path / "untracked.txt").write_text("untracked\n", encoding="utf-8")
2053
+ before = subprocess.run(["git", "status", "--porcelain"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout
2054
+
2055
+ reset_result = reset_cleanup_branch(tmp_path, "dev/other", return_branch="main", delete=False)
2056
+
2057
+ assert reset_result.branch_preserved is True
2058
+ assert reset_result.checkout_discarded is False
2059
+ assert subprocess.run(["git", "branch", "--show-current"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip() == "main"
2060
+ assert subprocess.run(["git", "status", "--porcelain"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout == before
2061
+ assert _stash_oids(tmp_path) == user_stashes
2062
+ assert subprocess.run(["git", "show-ref", "--verify", "refs/heads/dev/other"], cwd=tmp_path, check=False).returncode == 0
2063
+
2064
+ clean_result = reset_cleanup_branch(tmp_path, "dev/other", return_branch="main", delete=True)
2065
+
2066
+ assert clean_result.deleted is True
2067
+ assert subprocess.run(["git", "status", "--porcelain"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout == before
2068
+ assert _stash_oids(tmp_path) == user_stashes
2069
+
2070
+
1865
2071
  def test_worktree_runtime_context_uses_safe_deterministic_path(tmp_path):
1866
2072
  from prizmkit_runtime.gitops import BranchContext, WorktreePolicy, worktree_runtime_context
1867
2073
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.128",
2
+ "version": "1.1.129",
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.",
@@ -361,8 +361,8 @@ Key requirements:
361
361
  - descriptions: minimum 15 words (error), recommended minimum 30/50/80/100+ for low/medium/high/critical (warning). No upper limit — more detail prevents AI guessing
362
362
  - `estimated_complexity` determines pipeline execution tier:
363
363
  - `low` / `medium` → **lite** (single agent, no subagents)
364
- - `high` → **standard** (orchestrator + reviewer guardrails, 3-agent metadata)
365
- - `critical` → **full** (strongest orchestrator/reviewer guardrails). Use for: architectural changes touching 10+ files, cross-module refactoring with API surface changes, or safety/security/data-loss risk
364
+ - `high` → **standard** (Main-Agent review guardrails, 3-agent compatibility metadata)
365
+ - `critical` → **full** (strongest Main-Agent review guardrails). Use for: architectural changes touching 10+ files, cross-module refactoring with API surface changes, or safety/security/data-loss risk
366
366
 
367
367
  **IMPORTANT: Do NOT hand-write the final JSON file.** Instead:
368
368
  1. Write a draft JSON to a temporary path (e.g., `.prizmkit/plans/feature-list.draft.json`)
@@ -110,7 +110,7 @@ PrizmKit supports any development scenario through the same skill chain. `/prizm
110
110
  | `/prizmkit-plan` | First implementation planning step: run ahead of `/prizmkit-implement` to turn natural language into `spec.md` + `plan.md` with executable tasks. | "specify", "plan", "new task", "architect", "break it down" |
111
111
  | `/prizmkit-implement` | Implementation step: use after `/prizmkit-plan` to execute `plan.md` tasks with TDD where applicable, task order, and checkpoints. | "implement", "build", "code it", "start coding" |
112
112
  | `/prizmkit-test` | Risk-triggered quality gate: run after implementation when behavior, interfaces, data, security, or deploy risk requires tests and boundary coverage. | "test", "run tests", "verify", "quality check", "boundary tests" |
113
- | `/prizmkit-code-review` | Full path quality gate: run after implementation; requires the dedicated reviewer agent in the active checkout and stops if that launch mode is unavailable. | "review", "check code", "code review", "is it ready to commit" |
113
+ | `/prizmkit-code-review` | Full path quality gate: the current Main Agent reviews and repairs the complete change in the active checkout without direct or indirect review delegation. | "review", "check code", "code review", "is it ready to commit" |
114
114
  | `/prizmkit-retrospective` | Docs maintenance step: run after review/implementation when structure, interfaces, dependencies, behavior, or durable knowledge changed. | "retrospective", "retro", "update docs", "sync docs", "wrap up" |
115
115
  | `/prizmkit-committer` | Final lifecycle commit step: use after required gates are satisfied; safely stages and creates a Conventional Commit without changing changelog by default. | "commit", "submit", "finish", "done" |
116
116
  | `/prizmkit-prizm-docs` | Documentation system entry point: use for init/status/rebuild/validate/migrate or out-of-band repair, not normal development sync. | "initialize docs", "check docs", "rebuild docs", "validate docs", "docs drifted" |
@@ -5,7 +5,16 @@ description: "Review the complete current change in a bounded Main-Agent loop. A
5
5
 
6
6
  # PrizmKit Code Review
7
7
 
8
- Use one Main-Agent review loop. The Main Agent performs the complete review, filters candidate findings, repairs accepted findings, verifies the repairs, and decides the final result without delegating code review.
8
+ Use one Main-Agent review loop. The current Main Agent is the only Code Review executor: it performs the complete review, filters candidate findings, repairs accepted findings, verifies the repairs, and decides the final result.
9
+
10
+ ## Execution Boundary
11
+
12
+ - Do not delegate any part of Code Review directly or indirectly.
13
+ - Do not invoke another review skill or review workflow from inside this skill.
14
+ - Do not launch review work through a general-purpose execution unit or relabel it as a finder, verifier, audit, compatibility review, verification, or gap sweep.
15
+ - These restrictions apply for the entire review phase, including candidate discovery, evidence checks, repair verification, and final gap analysis.
16
+ - The Main Agent may directly use non-delegating read, search, edit, and test capabilities in the active workspace.
17
+ - `{artifact_dir}/review-report.md` is the only persisted review artifact. Do not append separate Review Notes to `context-snapshot.md`.
9
18
 
10
19
  ## When to Use
11
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.128",
3
+ "version": "1.1.129",
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": {
package/src/manifest.js CHANGED
@@ -19,6 +19,7 @@ const MANIFEST_DIR = '.prizmkit';
19
19
  * Read the manifest from a project directory.
20
20
  * @param {string} projectRoot
21
21
  * @returns {Promise<Object|null>} parsed manifest or null if not found
22
+ * @throws {Error} when a present manifest cannot be read or parsed
22
23
  */
23
24
  export async function readManifest(projectRoot) {
24
25
  const manifestPath = path.join(projectRoot, MANIFEST_DIR, MANIFEST_FILE);
@@ -26,9 +27,13 @@ export async function readManifest(projectRoot) {
26
27
  return null;
27
28
  }
28
29
  try {
29
- return await fs.readJSON(manifestPath);
30
- } catch {
31
- return null;
30
+ const manifest = await fs.readJSON(manifestPath);
31
+ if (manifest === null || Array.isArray(manifest) || typeof manifest !== 'object') {
32
+ throw new TypeError('manifest root must be a JSON object');
33
+ }
34
+ return manifest;
35
+ } catch (error) {
36
+ throw new Error(`Failed to read PrizmKit manifest at ${manifestPath}: ${error.message}`, { cause: error });
32
37
  }
33
38
  }
34
39