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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +88 -58
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +4 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +53 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +33 -17
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +28 -26
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -19
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +10 -2
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
- package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +11 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -3
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +11 -3
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +98 -3
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
- package/bundled/dev-pipeline/tests/test_unified_cli.py +215 -9
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/feature-planner/SKILL.md +2 -2
- package/bundled/skills/prizmkit/SKILL.md +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +10 -1
- package/package.json +1 -1
- 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
|
-
|
|
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 —
|
|
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
|
|
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.
|
|
2135
|
-
assert
|
|
2136
|
-
assert
|
|
2137
|
-
assert
|
|
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 "
|
|
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"] ==
|
|
951
|
-
assert data["active_subagent_count"] ==
|
|
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"]) ==
|
|
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[
|
|
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
|
-
|
|
1833
|
-
|
|
1834
|
-
assert
|
|
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[
|
|
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
|
|
|
@@ -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** (
|
|
365
|
-
- `critical` → **full** (strongest
|
|
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:
|
|
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
|
|
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
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|