prizmkit 1.1.128 → 1.1.130

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 (41) 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 +30 -28
  7. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +8 -3
  8. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
  9. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +11 -4
  10. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
  11. package/bundled/dev-pipeline/scripts/prompt_framework.py +4 -6
  12. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  13. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -19
  14. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  15. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
  16. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +10 -2
  17. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  18. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
  19. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
  20. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +11 -3
  25. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -3
  26. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +11 -3
  30. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
  32. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
  33. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +192 -5
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +215 -9
  36. package/bundled/skills/_metadata.json +1 -1
  37. package/bundled/skills/feature-planner/SKILL.md +2 -2
  38. package/bundled/skills/prizmkit/SKILL.md +1 -1
  39. package/bundled/skills/prizmkit-code-review/SKILL.md +10 -1
  40. package/package.json +1 -1
  41. package/src/manifest.js +8 -3
@@ -1,6 +1,7 @@
1
1
  """Tests for generate-bootstrap-prompt.py core functions."""
2
2
 
3
3
  import json
4
+ import importlib.util
4
5
  import os
5
6
  import re
6
7
  import time
@@ -25,6 +26,7 @@ from generate_bootstrap_prompt import (
25
26
  generate_checkpoint_definition,
26
27
  merge_checkpoint_state,
27
28
  load_active_agent_prompts,
29
+ validate_rendered,
28
30
  main as generate_bootstrap_main,
29
31
  )
30
32
  from continuation import append_continuation_handoff, checkpoint_cursor
@@ -44,6 +46,69 @@ FORBIDDEN_TEST_CMD_PLACEHOLDER = "(" + "$TEST_CMD" + ")"
44
46
  FORBIDDEN_THREE_STRIKE = "3-" + "strike"
45
47
  FORBIDDEN_TWENTY_STEP = "20-" + "step window"
46
48
 
49
+ REVIEW_PROMPT_REQUIRED = (
50
+ "The current Main Agent is the only Code Review executor",
51
+ "Do not invoke another review skill or review workflow",
52
+ "finder, verifier, audit, compatibility review, verification, or gap sweep",
53
+ "`review-report.md` is the only persisted review artifact",
54
+ )
55
+
56
+ REVIEW_PROMPT_FORBIDDEN = (
57
+ "Orchestrator + Reviewer",
58
+ "Reviewer reviews",
59
+ "Reviewer subagents",
60
+ "Reviewer appends Review Notes",
61
+ "Implementation Log/Review Notes",
62
+ "internal Reviewer Agent",
63
+ "spawning Reviewer",
64
+ "delayed teammate/reviewer findings",
65
+ "Gate checks enforce Implementation Log and Review Notes",
66
+ )
67
+
68
+
69
+ def assert_unambiguous_main_agent_review(prompt):
70
+ for required in REVIEW_PROMPT_REQUIRED:
71
+ assert required in prompt
72
+ for forbidden in REVIEW_PROMPT_FORBIDDEN:
73
+ assert forbidden not in prompt
74
+
75
+
76
+ PROMPT_RENDERING_FORBIDDEN = (
77
+ "If implementation is delegated to a Dev subagent",
78
+ "Document in Implementation Log or review notes",
79
+ "{{INIT_SCRIPT_PATH}}",
80
+ )
81
+
82
+
83
+ def assert_complete_prompt_rendering(prompt):
84
+ assert_unambiguous_main_agent_review(prompt)
85
+ for forbidden in PROMPT_RENDERING_FORBIDDEN:
86
+ assert forbidden not in prompt
87
+ assert re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", prompt) == []
88
+
89
+
90
+ def _load_recovery_prompt_module():
91
+ path = Path("dev-pipeline/scripts/generate-recovery-prompt.py")
92
+ spec = importlib.util.spec_from_file_location("generate_recovery_prompt", path)
93
+ module = importlib.util.module_from_spec(spec)
94
+ spec.loader.exec_module(module)
95
+ return module
96
+
97
+
98
+ def test_rendered_prompt_validation_rejects_unresolved_control_placeholders():
99
+ content = (
100
+ "## Your Mission\n"
101
+ "## Execution\n"
102
+ "Run {{INIT_SCRIPT_PATH}}\n"
103
+ "## Failure Capture\n"
104
+ )
105
+
106
+ is_valid, warnings, errors = validate_rendered(content)
107
+
108
+ assert is_valid is False
109
+ assert warnings == []
110
+ assert errors == ["Unreplaced placeholders: {{INIT_SCRIPT_PATH}}"]
111
+
47
112
 
48
113
  def scoped_report_text(verdict="PASS", boundary_missing=0,
49
114
  happy_path_only=0, completion_gate="yes",
@@ -884,10 +949,38 @@ class TestDirectOrchestratorImplementationPrompts:
884
949
  assert FORBIDDEN_TWENTY_STEP not in rendered
885
950
  assert "Main-Agent review loop" in rendered
886
951
  assert "up to ten completed rounds" in rendered
887
- assert "code-review-specific subagent" in rendered
952
+ assert "The current Main Agent is the only Code Review executor" in rendered
953
+ assert "Do not invoke another review skill or review workflow" in rendered
888
954
  assert "low=0" not in rendered
889
955
  assert "Reviewer 3" not in rendered
890
956
 
957
+ def test_all_rendered_pipeline_prompts_have_one_main_agent_review_model(self, tmp_path):
958
+ recovery = _load_recovery_prompt_module()
959
+ prompts = [
960
+ _render_feature_prompt(tmp_path / "feature-lite", mode="lite"),
961
+ _render_feature_prompt(tmp_path / "feature-standard", mode="standard"),
962
+ _render_feature_prompt(tmp_path / "feature-full", mode="full"),
963
+ _render_feature_prompt(
964
+ tmp_path / "feature-tier3",
965
+ mode="full",
966
+ template="bootstrap-tier3.md",
967
+ ),
968
+ _render_bugfix_prompt(tmp_path / "bugfix"),
969
+ _render_refactor_prompt(tmp_path / "refactor"),
970
+ recovery.build_bugfix_prompt(
971
+ {
972
+ "workflow_type": "bug-fix-workflow",
973
+ "phase": 5,
974
+ "phase_name": "Review",
975
+ "workflow_data": {"bug_id": "B-001"},
976
+ },
977
+ str(tmp_path),
978
+ ),
979
+ ]
980
+
981
+ for prompt in prompts:
982
+ assert_complete_prompt_rendering(prompt)
983
+
891
984
  def test_retained_tier3_fallback_template_uses_direct_orchestrator_implementation(self):
892
985
  rendered = Path("dev-pipeline/templates/bootstrap-tier3.md").read_text(encoding="utf-8")
893
986
 
@@ -1217,7 +1310,7 @@ class TestFeaturePromptUnifiedFullGuidance:
1217
1310
  assert "### Scoped Feature Test Gate — PrizmKit Test" in prompt
1218
1311
  assert "## Test Failure Recovery Protocol" in prompt
1219
1312
  assert "Boundary Matrix" in prompt
1220
- assert "### Browser Verification — MANDATORY" in prompt
1313
+ assert "### Browser Verification — MANDATORY" not in prompt
1221
1314
  assert "### TERMINAL STOP — Commit Completed" in prompt
1222
1315
  assert "# Dev-Pipeline Session Bootstrap — Tier 1" not in prompt
1223
1316
  assert "# Dev-Pipeline Session Bootstrap — Tier 2" not in prompt
@@ -1327,7 +1420,99 @@ class TestBrowserBlockingPromptPolicy:
1327
1420
  assert "Failures do NOT block the commit" not in prompt
1328
1421
 
1329
1422
 
1330
- class TestManualHybridBugfixPromptPolicy:
1423
+ class TestBrowserOptInPromptPolicy:
1424
+ @staticmethod
1425
+ def _rendered_outputs(tmp_path, browser_interaction=None, env_value=None):
1426
+ previous_env = os.environ.get("BROWSER_VERIFY")
1427
+ if env_value is None:
1428
+ os.environ.pop("BROWSER_VERIFY", None)
1429
+ else:
1430
+ os.environ["BROWSER_VERIFY"] = env_value
1431
+ try:
1432
+ feature_prompt = _render_feature_prompt(
1433
+ tmp_path / "feature", browser_interaction=browser_interaction,
1434
+ )
1435
+ bugfix_prompt = _render_bugfix_prompt(
1436
+ tmp_path / "bugfix",
1437
+ bug_overrides=(
1438
+ {"browser_interaction": browser_interaction}
1439
+ if browser_interaction is not None else None
1440
+ ),
1441
+ )
1442
+ refactor_prompt = _render_refactor_prompt(
1443
+ tmp_path / "refactor",
1444
+ refactor_overrides=(
1445
+ {"browser_interaction": browser_interaction}
1446
+ if browser_interaction is not None else None
1447
+ ),
1448
+ )
1449
+ return feature_prompt, bugfix_prompt, refactor_prompt
1450
+ finally:
1451
+ if previous_env is None:
1452
+ os.environ.pop("BROWSER_VERIFY", None)
1453
+ else:
1454
+ os.environ["BROWSER_VERIFY"] = previous_env
1455
+
1456
+ @staticmethod
1457
+ def _checkpoint_skills(project_root, family):
1458
+ if family == "feature":
1459
+ pattern = "**/workflow-checkpoint.json"
1460
+ elif family == "bugfix":
1461
+ pattern = "**/bugfix/B-001/workflow-checkpoint.json"
1462
+ else:
1463
+ pattern = "**/refactor/R-001/workflow-checkpoint.json"
1464
+ path = next(project_root.glob(pattern))
1465
+ return [step["skill"] for step in json.loads(path.read_text(encoding="utf-8"))["steps"]]
1466
+
1467
+ def test_omitted_browser_interaction_disables_browser_for_all_families(self, tmp_path):
1468
+ prompts = self._rendered_outputs(tmp_path / "omitted")
1469
+
1470
+ for prompt in prompts:
1471
+ assert "Browser Verification" not in prompt
1472
+ assert "playwright-cli" not in prompt
1473
+ assert "opencli" not in prompt
1474
+ assert "development-server" not in prompt
1475
+ assert "browser-dev-server.pid" not in prompt
1476
+
1477
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "feature", "feature")
1478
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "bugfix", "bugfix")
1479
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "refactor", "refactor")
1480
+
1481
+ def test_explicit_browser_interaction_preserves_configuration_for_all_families(self, tmp_path):
1482
+ browser_interaction = {
1483
+ "tool": "playwright-cli",
1484
+ "verify_steps": ["Open the page", "Submit the form"],
1485
+ }
1486
+ prompts = self._rendered_outputs(tmp_path / "explicit", browser_interaction)
1487
+
1488
+ for prompt in prompts:
1489
+ assert "Browser Verification" in prompt
1490
+ assert "Browser Verification (playwright-cli)" in prompt
1491
+ assert "playwright-cli" in prompt
1492
+ assert "opencli" not in prompt
1493
+ assert "Goal 1: Open the page" in prompt
1494
+ assert "Goal 2: Submit the form" in prompt
1495
+
1496
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "feature", "feature")
1497
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "bugfix", "bugfix")
1498
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "refactor", "refactor")
1499
+
1500
+ def test_global_disable_suppresses_explicit_browser_for_all_families(self, tmp_path):
1501
+ prompts = self._rendered_outputs(
1502
+ tmp_path / "global-disable",
1503
+ {"tool": "opencli", "verify_steps": ["Open the page"]},
1504
+ env_value="false",
1505
+ )
1506
+
1507
+ for prompt in prompts:
1508
+ assert "Browser Verification" not in prompt
1509
+ assert "opencli" not in prompt
1510
+ assert "browser-dev-server.pid" not in prompt
1511
+
1512
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "feature", "feature")
1513
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "bugfix", "bugfix")
1514
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "refactor", "refactor")
1515
+
1331
1516
  def test_fresh_manual_bugfix_prompt_stops_before_commit_phase(self, tmp_path):
1332
1517
  prompt = _render_bugfix_prompt(
1333
1518
  tmp_path / "fresh-manual-bugfix",
@@ -1446,7 +1631,8 @@ class TestHeadlessPromptCleanupF033:
1446
1631
 
1447
1632
  assert "{{" + "REVIEWER_" + "SUBAGENT_PATH" + "}}" not in replacements
1448
1633
  assert "Main-Agent review loop" in refactor_prompt
1449
- assert "code-review-specific subagent" in refactor_prompt
1634
+ assert "The current Main Agent is the only Code Review executor" in refactor_prompt
1635
+ assert "Do not invoke another review skill or review workflow" in refactor_prompt
1450
1636
  assert "Reviewer 3" not in refactor_prompt
1451
1637
 
1452
1638
  def test_bugfix_refactor_explicit_codex_override(self, tmp_path, monkeypatch):
@@ -1469,7 +1655,8 @@ class TestHeadlessPromptCleanupF033:
1469
1655
 
1470
1656
  assert "{{" + "REVIEWER_" + "SUBAGENT_PATH" + "}}" not in replacements
1471
1657
  assert "Main-Agent review loop" in refactor_prompt
1472
- assert "code-review-specific subagent" in refactor_prompt
1658
+ assert "The current Main Agent is the only Code Review executor" in refactor_prompt
1659
+ assert "Do not invoke another review skill or review workflow" in refactor_prompt
1473
1660
  assert "Reviewer 3" not in refactor_prompt
1474
1661
 
1475
1662
 
@@ -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