prizmkit 1.1.120 → 1.1.121

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 (45) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +23 -25
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +43 -29
  6. package/bundled/dev-pipeline/scripts/continuation.py +51 -6
  7. package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
  8. package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
  9. package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
  10. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
  11. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
  12. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  13. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  14. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
  15. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
  16. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  17. package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
  18. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +10 -3
  19. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +9 -2
  20. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +9 -1
  21. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +222 -18
  22. package/bundled/dev-pipeline/tests/test_unified_cli.py +14 -10
  23. package/bundled/skills/_metadata.json +4 -4
  24. package/bundled/skills/prizmkit-code-review/SKILL.md +49 -48
  25. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
  26. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +46 -32
  27. package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
  28. package/bundled/skills/prizmkit-test/SKILL.md +159 -155
  29. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
  30. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
  31. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
  32. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
  33. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
  34. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
  35. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +194 -0
  36. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
  37. package/bundled/skills/prizmkit-test/references/examples.md +65 -108
  38. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
  39. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
  40. package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
  41. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
  42. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +723 -0
  43. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1426 -0
  44. package/package.json +1 -1
  45. package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +0 -347
@@ -904,7 +904,20 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
904
904
 
905
905
  def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
906
906
  updates.append((session_status, session_id, metadata))
907
- return SimpleNamespace(ok=True, text="", data={"new_status": update_status})
907
+ current_update_status = update_status
908
+ if session_status in {"context_overflow", "infra_error", "crashed", "timed_out", "failed"} and len(status_queue) > 0:
909
+ current_update_status = "in_progress"
910
+ return SimpleNamespace(
911
+ ok=True,
912
+ text="",
913
+ data={
914
+ "new_status": current_update_status,
915
+ "retry_count": sum(status in {"crashed", "timed_out", "failed"} for status, *_ in updates),
916
+ "infra_error_count": sum(status == "infra_error" for status, *_ in updates),
917
+ "continuation_count": len(updates),
918
+ "context_overflow_count": sum(status == "context_overflow" for status, *_ in updates),
919
+ },
920
+ )
908
921
 
909
922
  def fake_cleanup(runtime, *, delete_branch=True):
910
923
  cleanup_calls.append((runtime, delete_branch))
@@ -1096,8 +1109,8 @@ def test_explicit_worktree_materializes_ignored_support_assets_before_prompt_and
1096
1109
  (project / ".prizmkit" / "prizm-docs" / "root.prizm").write_text("PRIZM_VERSION: 1\n", encoding="utf-8")
1097
1110
  (project / ".prizmkit" / "dev-pipeline" / "scripts").mkdir(parents=True)
1098
1111
  (project / ".prizmkit" / "dev-pipeline" / "scripts" / "init-change-artifact.py").write_text("# init", encoding="utf-8")
1099
- (project / ".prizmkit" / "config.json").write_text("{}", encoding="utf-8")
1100
- (project / ".prizmkit" / "manifest.json").write_text("{}", encoding="utf-8")
1112
+ (project / ".prizmkit" / "config.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1113
+ (project / ".prizmkit" / "manifest.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1101
1114
 
1102
1115
  paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
1103
1116
  family = family_for("feature", paths)
@@ -1225,7 +1238,7 @@ def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_p
1225
1238
  subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1226
1239
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1227
1240
  artifact_dir.mkdir(parents=True)
1228
- (artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
1241
+ (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1229
1242
  (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1230
1243
  subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
1231
1244
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
@@ -1405,7 +1418,7 @@ def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch,
1405
1418
  ("refactor", "R-001", "refactor/R-001-20260707010203"),
1406
1419
  ],
1407
1420
  )
1408
- def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families(monkeypatch, tmp_path, kind, item_id, branch):
1421
+ def test_no_worktree_exhausted_failure_stays_on_task_branch_without_merge_for_all_families(monkeypatch, tmp_path, kind, item_id, branch):
1409
1422
  from prizmkit_runtime import runners
1410
1423
  from prizmkit_runtime.runner_models import family_for, parse_invocation
1411
1424
 
@@ -1415,7 +1428,7 @@ def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families
1415
1428
  invocation = parse_invocation(family, "run", ())
1416
1429
  plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
1417
1430
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
1418
- _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="pending")
1431
+ _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="failed")
1419
1432
  events = []
1420
1433
  final_commits = []
1421
1434
  wip_commits = []
@@ -1431,12 +1444,12 @@ def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families
1431
1444
 
1432
1445
  status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
1433
1446
 
1434
- assert status == "pending"
1447
+ assert status == "failed"
1435
1448
  assert events[0] == "branch_create"
1436
1449
  assert "branch_merge" not in events
1437
1450
  assert events[-1] == ("return", "main", branch)
1438
- assert final_commits == [(paths.project_root, item_id, "pending")]
1439
- assert wip_commits == [(paths.project_root, item_id, "pending")]
1451
+ assert final_commits == [(paths.project_root, item_id, "failed")]
1452
+ assert wip_commits == [(paths.project_root, item_id, "failed")]
1440
1453
 
1441
1454
 
1442
1455
  def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp_path):
@@ -1845,7 +1858,7 @@ def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
1845
1858
  item_state.mkdir(parents=True)
1846
1859
  plan = plans / "feature-list.json"
1847
1860
  plan.write_text('{"features": []}', encoding="utf-8")
1848
- (item_state / "status.json").write_text("{}", encoding="utf-8")
1861
+ (item_state / "status.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1849
1862
  source = tmp_path / "src.txt"
1850
1863
  source.write_text("base", encoding="utf-8")
1851
1864
  subprocess.run(["git", "add", ".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json", "src.txt"], cwd=tmp_path, check=True)
@@ -1899,6 +1912,8 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
1899
1912
  invocation = parse_invocation(family, "run", ())
1900
1913
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1901
1914
  observed = _install_runner_session_fakes(monkeypatch, runners, statuses=("context_overflow", "success"))
1915
+ sleep_calls = []
1916
+ monkeypatch.setattr(runners.time, "sleep", lambda seconds: sleep_calls.append(seconds))
1902
1917
  setup_calls = []
1903
1918
  monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (setup_calls.append(runtime) or SimpleNamespace(ok=True, runtime=runtime, reason="")), raising=False)
1904
1919
 
@@ -1911,6 +1926,7 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
1911
1926
  assert observed.prompt_calls[0].get("continuation") is None
1912
1927
  assert observed.prompt_calls[1]["continuation"]["previous_session_id"] == "F-001-session-1"
1913
1928
  assert observed.updates[0][0] == "context_overflow"
1929
+ assert sleep_calls == [5]
1914
1930
  assert len(observed.cleanup_calls) == 1
1915
1931
  assert observed.cleanup_calls[0][1] is True
1916
1932
 
@@ -1959,7 +1975,7 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
1959
1975
  assert classification.reason == "completed_checkpoint_with_late_runtime_error"
1960
1976
 
1961
1977
 
1962
- def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tmp_path):
1978
+ def test_classification_accepts_late_infra_with_completion_artifact_without_new_commit(tmp_path):
1963
1979
  from prizmkit_runtime.runner_classification import classify_session
1964
1980
  from prizmkit_runtime.runner_models import PromptGenerationResult
1965
1981
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -1983,7 +1999,7 @@ def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tm
1983
1999
  subprocess.run(["git", "checkout", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1984
2000
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1985
2001
  artifact_dir.mkdir(parents=True)
1986
- (artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
2002
+ (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1987
2003
  log = tmp_path / "session.log"
1988
2004
  log.write_text("quota exceeded", encoding="utf-8")
1989
2005
  raw = AISessionResult(
@@ -2004,7 +2020,8 @@ def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tm
2004
2020
 
2005
2021
  classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
2006
2022
 
2007
- assert classification.session_status == "infra_error"
2023
+ assert classification.session_status == "success"
2024
+ assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2008
2025
 
2009
2026
 
2010
2027
  def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
@@ -2060,7 +2077,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2060
2077
  artifact_dir.mkdir(parents=True)
2061
2078
  checkpoint = artifact_dir / "workflow-checkpoint.json"
2062
2079
  checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2063
- (artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
2080
+ (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
2064
2081
  log = tmp_path / "session.log"
2065
2082
  log.write_text("terminal success", encoding="utf-8")
2066
2083
  raw = AISessionResult(
@@ -2122,8 +2139,8 @@ def test_classification_preserves_success_before_context_overflow(tmp_path):
2122
2139
  )
2123
2140
 
2124
2141
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
2125
- assert classification.session_status == "success"
2126
- assert classification.reason == "commit_since_base"
2142
+ assert classification.session_status == "context_overflow"
2143
+ assert classification.reason == "context_overflow"
2127
2144
 
2128
2145
 
2129
2146
  def test_classification_crash_with_commit_since_base_does_not_become_success(tmp_path):
@@ -2256,7 +2273,7 @@ def _write_family_case(paths, kind, item_id=""):
2256
2273
  state_dir = paths.refactor_state_dir
2257
2274
  plan = paths.refactor_plan
2258
2275
  state_dir.mkdir(parents=True, exist_ok=True)
2259
- (state_dir / "pipeline.json").write_text("{}", encoding="utf-8")
2276
+ (state_dir / "pipeline.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
2260
2277
  item_state = state_dir / item_id
2261
2278
  item_state.mkdir(parents=True, exist_ok=True)
2262
2279
  (item_state / "status.json").write_text(
@@ -2517,7 +2534,7 @@ def test_invalid_existing_checkpoint_blocks_terminal_success_with_completion_sum
2517
2534
  artifact_dir.mkdir(parents=True)
2518
2535
  checkpoint = artifact_dir / "workflow-checkpoint.json"
2519
2536
  checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "not-valid"}]}), encoding="utf-8")
2520
- (artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
2537
+ (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
2521
2538
  prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", checkpoint_path=checkpoint, artifact_path=artifact_dir)
2522
2539
 
2523
2540
  classification = classify_session(_terminal_success_result(tmp_path), project_root=tmp_path, base_head=base_head, prompt=prompt)
@@ -2597,3 +2614,190 @@ def test_invalid_existing_checkpoint_blocks_workspace_auto_commit_success(tmp_pa
2597
2614
 
2598
2615
  assert classification.session_status == "crashed"
2599
2616
  assert classification.reason == "invalid_workflow_checkpoint:empty_steps"
2617
+
2618
+
2619
+ @pytest.mark.parametrize(
2620
+ ("kind", "item_id", "branch"),
2621
+ [
2622
+ ("feature", "F-001", "dev/F-001-ai-retry"),
2623
+ ("bugfix", "B-001", "bugfix/B-001-ai-retry"),
2624
+ ("refactor", "R-001", "refactor/R-001-ai-retry"),
2625
+ ],
2626
+ )
2627
+ @pytest.mark.parametrize("retry_status", ["infra_error", "crashed", "timed_out"])
2628
+ def test_retryable_ai_errors_continue_in_process_for_all_families(monkeypatch, tmp_path, kind, item_id, branch, retry_status):
2629
+ from prizmkit_runtime import runners
2630
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
2631
+
2632
+ monkeypatch.setenv("DEV_BRANCH", branch)
2633
+ paths = _make_paths(tmp_path / kind / retry_status)
2634
+ family = family_for(kind, paths)
2635
+ invocation = parse_invocation(family, "run", ())
2636
+ plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
2637
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
2638
+ observed = _install_runner_session_fakes(monkeypatch, runners, statuses=(retry_status, "success"))
2639
+ sleep_calls = []
2640
+ branch_plans = []
2641
+ monkeypatch.setattr(runners.time, "sleep", lambda seconds: sleep_calls.append(seconds))
2642
+ monkeypatch.setattr(
2643
+ runners,
2644
+ "run_git_plan",
2645
+ lambda project_root, plan: (branch_plans.append(plan.name) or SimpleNamespace(ok=True, plan=plan, results=())),
2646
+ )
2647
+
2648
+ status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
2649
+
2650
+ assert status == "completed"
2651
+ assert sleep_calls == [5]
2652
+ assert len(observed.launch_cwds) == 2
2653
+ assert observed.launch_cwds[0] == observed.launch_cwds[1]
2654
+ assert branch_plans.count("branch_create") == 1
2655
+ assert observed.updates[0][0] == retry_status
2656
+ continuation = observed.prompt_calls[1]["continuation"]
2657
+ assert continuation["previous_session_id"].endswith("session-1")
2658
+ assert continuation["failure_classification"] == retry_status
2659
+ assert continuation["active_dev_branch"] == branch
2660
+
2661
+
2662
+ def test_unique_session_ids_do_not_collide_when_wall_clock_second_is_frozen(monkeypatch):
2663
+ from prizmkit_runtime import runners
2664
+
2665
+ monkeypatch.setattr(runners.time, "strftime", lambda _fmt: "20260711030000")
2666
+ values = iter((101, 102))
2667
+ monkeypatch.setattr(runners.time, "time_ns", lambda: next(values))
2668
+
2669
+ first = runners._session_id("F-001")
2670
+ second = runners._session_id("F-001")
2671
+
2672
+ assert first != second
2673
+ assert first.endswith("-101")
2674
+ assert second.endswith("-102")
2675
+
2676
+
2677
+ @pytest.mark.parametrize("summary_text", ["{}", "{not-json", '{"completion_notes": []}', '{"completion_notes": [""]}'])
2678
+ def test_invalid_completion_summary_does_not_prove_late_runtime_success(tmp_path, summary_text):
2679
+ from prizmkit_runtime.runner_classification import classify_session
2680
+ from prizmkit_runtime.runner_models import PromptGenerationResult
2681
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
2682
+ from prizmkit_runtime.status import ProgressSummary
2683
+
2684
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2685
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
2686
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
2687
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
2688
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
2689
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2690
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
2691
+ artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-invalid"
2692
+ artifact_dir.mkdir(parents=True)
2693
+ (artifact_dir / "completion-summary.json").write_text(summary_text, encoding="utf-8")
2694
+ log = tmp_path / "session.log"
2695
+ log.write_text("quota exceeded", encoding="utf-8")
2696
+ result = AISessionResult(
2697
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
2698
+ exit_code=1,
2699
+ timed_out=False,
2700
+ termination_reason=None,
2701
+ stale_marker=None,
2702
+ fatal_error_code="api_error",
2703
+ progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
2704
+ session_log=log,
2705
+ backup_log=tmp_path / "backup.log",
2706
+ log_recovery=None,
2707
+ started_epoch=0,
2708
+ ended_epoch=1,
2709
+ )
2710
+
2711
+ classification = classify_session(
2712
+ result,
2713
+ project_root=tmp_path,
2714
+ base_head=base_head,
2715
+ prompt=PromptGenerationResult(output_path=tmp_path / "prompt.md", artifact_path=artifact_dir),
2716
+ )
2717
+
2718
+ assert classification.session_status == "infra_error"
2719
+
2720
+
2721
+ def test_non_utf8_completion_summary_does_not_abort_classification(tmp_path):
2722
+ from prizmkit_runtime.runner_classification import classify_session
2723
+ from prizmkit_runtime.runner_models import PromptGenerationResult
2724
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
2725
+ from prizmkit_runtime.status import ProgressSummary
2726
+
2727
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2728
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
2729
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
2730
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
2731
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
2732
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2733
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
2734
+ artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-corrupt"
2735
+ artifact_dir.mkdir(parents=True)
2736
+ (artifact_dir / "completion-summary.json").write_bytes(b"\xff\xfe")
2737
+ log = tmp_path / "session.log"
2738
+ log.write_text("quota exceeded", encoding="utf-8")
2739
+ result = AISessionResult(
2740
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
2741
+ exit_code=1,
2742
+ timed_out=False,
2743
+ termination_reason=None,
2744
+ stale_marker=None,
2745
+ fatal_error_code="api_error",
2746
+ progress_summary=ProgressSummary(terminal_result_text="quota exceeded"),
2747
+ session_log=log,
2748
+ backup_log=tmp_path / "backup.log",
2749
+ log_recovery=None,
2750
+ started_epoch=0,
2751
+ ended_epoch=1,
2752
+ )
2753
+
2754
+ classification = classify_session(
2755
+ result,
2756
+ project_root=tmp_path,
2757
+ base_head=base_head,
2758
+ prompt=PromptGenerationResult(output_path=tmp_path / "prompt.md", artifact_path=artifact_dir),
2759
+ )
2760
+
2761
+ assert classification.session_status == "infra_error"
2762
+
2763
+
2764
+ @pytest.mark.parametrize(
2765
+ ("timed_out", "termination_reason", "expected_status", "expected_reason"),
2766
+ [
2767
+ (True, "timeout", "timed_out", "timeout"),
2768
+ (False, "stale_session", "crashed", "stale_session"),
2769
+ ],
2770
+ )
2771
+ def test_timeout_and_stale_termination_override_provider_error_markers(tmp_path, timed_out, termination_reason, expected_status, expected_reason):
2772
+ from prizmkit_runtime.runner_classification import classify_session
2773
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
2774
+ from prizmkit_runtime.status import ProgressSummary
2775
+
2776
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2777
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
2778
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
2779
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
2780
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
2781
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2782
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
2783
+ log = tmp_path / "session.log"
2784
+ log.write_text("gateway API error", encoding="utf-8")
2785
+ result = AISessionResult(
2786
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
2787
+ exit_code=1,
2788
+ timed_out=timed_out,
2789
+ termination_reason=termination_reason,
2790
+ stale_marker=None,
2791
+ fatal_error_code="api_error",
2792
+ progress_summary=ProgressSummary(terminal_result_text="provider gateway error"),
2793
+ session_log=log,
2794
+ backup_log=tmp_path / "backup.log",
2795
+ log_recovery=None,
2796
+ started_epoch=0,
2797
+ ended_epoch=1,
2798
+ )
2799
+
2800
+ classification = classify_session(result, project_root=tmp_path, base_head=base_head)
2801
+
2802
+ assert classification.session_status == expected_status
2803
+ assert classification.reason == expected_reason
@@ -802,7 +802,7 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
802
802
  assert str(prompt) not in "\n".join(lines[:6])
803
803
 
804
804
 
805
- def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
805
+ def test_headless_review_guidance_uses_workspace_equivalence_protocol():
806
806
  guidance_files = [
807
807
  REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
808
808
  REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
@@ -811,15 +811,19 @@ def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
811
811
 
812
812
  for path in guidance_files:
813
813
  text = path.read_text(encoding="utf-8")
814
- assert (
815
- "default/no worktree" in text
816
- or "current-workspace / active-checkout / no-worktree" in text
817
- or "current workspace / active checkout / no worktree" in text
818
- ), path
819
- assert "do not request or create" in text.lower() or "do not request worktree isolation" in text.lower(), path
820
- assert ".claude/worktrees/..." in text, path
821
- assert ".prizmkit/state/worktrees/..." in text, path
822
- assert "USE_WORKTREE" in text, path
814
+ assert "platform-supported" in text, path
815
+ assert "snapshot" in text and "equivalent" in text, path
816
+ assert "Main-Agent self-review" in text, path
817
+ assert "current-workspace / active-checkout / no-worktree" not in text, path
818
+ assert ".claude/worktrees/..." not in text, path
819
+ assert ".prizmkit/state/worktrees/..." not in text, path
820
+ assert "USE_WORKTREE" not in text, path
821
+ assert "isolation:" not in text, path
822
+
823
+ skill_text = guidance_files[-1].read_text(encoding="utf-8")
824
+ assert "reviewer-workspace-protocol.md" in skill_text
825
+ assert "staged, unstaged, untracked, deleted, and renamed" in skill_text
826
+ assert "WORKSPACE_MISMATCH" in skill_text
823
827
 
824
828
  for path in guidance_files[:-1]:
825
829
  text = path.read_text(encoding="utf-8")
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.120",
2
+ "version": "1.1.121",
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.",
@@ -65,11 +65,11 @@
65
65
  "hasScripts": false
66
66
  },
67
67
  "prizmkit-test": {
68
- "description": "Full-stack test generation and orchestration. Detects architecture, discovers/runs existing tests, analyzes coverage gaps, generates missing tests (unit/integration/E2E), outputs unified report.",
68
+ "description": "Auditable project-native test evidence with request-driven execution, runner-generated receipts, differential proof, replay, and deterministic validation.",
69
69
  "tier": "1",
70
70
  "category": "prizmkit-skill",
71
- "hasAssets": false,
72
- "hasScripts": false
71
+ "hasAssets": true,
72
+ "hasScripts": true
73
73
  },
74
74
  "feature-workflow": {
75
75
  "description": "One-stop entry point for feature development. Orchestrates feature-planner → feature-pipeline-launcher → background execution. Handles multi-feature batch development from a single request.",
@@ -1,56 +1,53 @@
1
1
  ---
2
2
  name: "prizmkit-code-review"
3
- description: "Iterative review-fix loop against spec and plan. Always uses an independent inline reviewer subagent in the active checkout with a skill-owned prompt reference and no worktree/copy/remote isolation; the Main Agent filters findings and applies accepted fixes directly. If the platform cannot start an active-checkout inline reviewer, stop with checkout-isolation incompatibility instead of falling back to Main-Agent self-review. Loops until PASS or max 3 rounds. Use after /prizmkit-implement for Full path quality gate or when the user says 'review', 'check code', 'code review', or 'is it ready to commit'. (project)"
3
+ description: "Iterative independent review-fix loop against spec, plan, workspace changes, and test evidence. Establishes a verified review snapshot using any platform-supported subagent strategy while keeping review judgment separate from Main-Agent filtering and fixes. Use after /prizmkit-implement as a Full path quality gate or when the user asks to review, check code, validate implementation, or decide whether changes are ready to commit. Loops until PASS or max 3 rounds. (project)"
4
4
  ---
5
5
 
6
6
  # PrizmKit Code Review
7
7
 
8
- An iterative review-fix loop that checks workspace changes against the task spec and plan. It separates review judgment from fix application by requiring an independent inline reviewer subagent launched from the active checkout with a prompt loaded from this skill's references. Main Agent applies accepted fixes directly after filtering reviewer findings.
8
+ Run an iterative review-fix loop against the current change. Keep review judgment independent from fix application: an independent read-only Reviewer evaluates a verified snapshot, then the Main Agent filters findings and applies accepted fixes.
9
9
 
10
10
  Review modes:
11
- - `inline-reviewer-active-checkout`: required mode. Always spawn an independent reviewer subagent in the current active checkout with no worktree, copied checkout, remote checkout, or branch isolation.
11
+ - `independent-reviewer-verified-snapshot`: an independent Reviewer verified that its review snapshot is equivalent to the Main Agent's current change.
12
12
  - `not-run-no-changes`: no workspace changes were detected.
13
13
 
14
- If the platform cannot launch an inline reviewer subagent in current-workspace / active-checkout / no-worktree mode, stop with this checkout-isolation incompatibility message:
14
+ The skill defines review invariants rather than platform tool parameters. Choose any platform-supported subagent launch and snapshot transport strategy that satisfies `${SKILL_DIR}/references/reviewer-workspace-protocol.md`.
15
15
 
16
- ```text
17
- Cannot run /prizmkit-code-review because this platform cannot start an inline reviewer subagent in the active checkout without worktree, copy, remote, or branch isolation.
18
- ```
16
+ If no independent Reviewer can receive and verify an equivalent snapshot, stop normal review and report the workspace verification failure. Do not replace the independent review with Main-Agent self-review because that would remove the separation between judgment and implementation.
19
17
 
20
18
  ### When to Use
21
19
  - After `/prizmkit-implement` as the Full path quality gate
22
- - For Fast path only when risk, scope, or user request warrants review
23
- - User says "review", "check code", "review my implementation", or "is it ready to commit"
20
+ - For Fast path when risk, scope, or the user warrants review
21
+ - When the user says "review", "check code", "review my implementation", or "is it ready to commit"
24
22
  - Before `/prizmkit-committer` when the chosen lifecycle path requires review
25
23
 
26
24
  ### When NOT to Use
27
25
  - Direct edit or low-risk Fast path where review is not required
28
- - No spec.md or plan.md exists and there is no clear task goal to review against
26
+ - No `spec.md` or `plan.md` exists and there is no clear task goal to review against
29
27
  - Trivial typo, formatting, or config-only changes that the user wants committed directly
30
28
 
31
29
  ## Input
32
30
 
33
31
  | Parameter | Required | Description |
34
32
  |-----------|----------|-------------|
35
- | `artifact_dir` | No | Directory containing `spec.md` + `plan.md`. If omitted, scan `.prizmkit/` subdirectories for the most recently modified directory with a completed `plan.md`. When invoked as a handoff step, reuse the caller's `artifact_dir` rather than re-detecting. |
33
+ | `artifact_dir` | No | Directory containing `spec.md` and `plan.md`. If omitted, scan `.prizmkit/` subdirectories for the most recently modified directory with a completed `plan.md`. When invoked as a handoff step, reuse the caller's `artifact_dir` rather than re-detecting. |
36
34
 
37
- ## Phase 0: Context Loading
35
+ ## Phase 0: Context and Snapshot
38
36
 
39
37
  1. Read `spec.md` from the artifact directory and extract goals plus acceptance criteria.
40
38
  2. Read `plan.md` from the artifact directory and extract architecture decisions plus completed tasks.
41
- 3. Read scoped `/prizmkit-test` report when present:
39
+ 3. Read a scoped `/prizmkit-test` report when present:
42
40
  - If `{artifact_dir}/test-report-path.txt` exists, read the pointed report.
43
41
  - Extract Scope, Generated / Updated Tests, In-Scope Failures, Baseline Failures, Out of Scope Gaps, Boundary Matrix / Boundary Completion Gate, and Verdict when present.
44
- - If no test report exists, pass `No scoped /prizmkit-test report found` as informational context. Do not treat missing tests as a review finding unless the current lifecycle path explicitly required testing.
42
+ - If no report exists, pass `No scoped /prizmkit-test report found` as informational context. Treat missing tests as a finding only when the lifecycle path required testing before review.
45
43
  4. Read dev rules when configured: load `.prizmkit/prizm-docs/root.prizm`, then referenced `.prizmkit/rules/<layer>-rules.md` files if any exist.
46
- 5. Capture active checkout workspace context:
47
- - `git status`
48
- - `git diff`
49
- - `git diff --cached`
50
- - full paths for untracked and renamed files
51
- - expected active checkout git top-level from `git rev-parse --show-toplevel`
52
- - relevant root.prizm RULES and plan decisions
53
- 6. If no changes are detected, skip Phase 1 and write `review-report.md` with verdict PASS, rounds 0, and review mode `not-run-no-changes`.
44
+ 5. Capture the current workspace manifest and review payload according to `${SKILL_DIR}/references/reviewer-workspace-protocol.md`. Include:
45
+ - baseline repository and revision identity
46
+ - `git status`, `git diff`, and `git diff --cached` evidence
47
+ - staged, unstaged, untracked, deleted, and renamed path inventory
48
+ - changed content and stable content identities sufficient to detect missing, stale, or mixed snapshots
49
+ - relevant root Prizm rules and plan decisions
50
+ 6. If no changes are detected, skip Phase 1 and write `review-report.md` with verdict PASS, rounds 0, review mode `not-run-no-changes`, and snapshot verification `NOT_APPLICABLE`.
54
51
 
55
52
  ## Phase 1: Review-Fix Loop
56
53
 
@@ -70,42 +67,43 @@ python3 --version 2>/dev/null && echo "SCRIPT_MODE" || echo "TEXT_MODE"
70
67
  - `round = 1`
71
68
  - `max_rounds = 3`
72
69
  - `findings_history = []`
73
- - `review_mode = inline-reviewer-active-checkout` after confirming an inline reviewer subagent can launch in the active checkout
70
+ - `review_mode = independent-reviewer-verified-snapshot` after snapshot verification succeeds
74
71
 
75
- ### Step 1: Launch Required Inline Reviewer
72
+ ### Step 1: Establish Independent Review
76
73
 
77
- Always spawn an independent reviewer subagent using this skill-owned inline contract:
74
+ Use the skill-owned review contract:
78
75
 
79
76
  ```yaml
80
77
  prompt_reference: ${SKILL_DIR}/references/reviewer-agent-prompt.md
81
- isolation: current-workspace / active-checkout / no-worktree
82
- subagent_kind: inline reviewer prompt, not a platform-installed named agent
78
+ workspace_protocol: ${SKILL_DIR}/references/reviewer-workspace-protocol.md
79
+ reviewer_contract: independent, read-only, workspace-equivalent
83
80
  ```
84
81
 
85
- 1. Read `${SKILL_DIR}/references/reviewer-agent-prompt.md` and fill it with the active-checkout context from Phase 0.
86
- 2. The captured git diff, staged diff, untracked files, spec, plan, dev rules, and scoped test report are the primary review scope.
87
- 3. Never use worktree isolation, remote isolation, copied checkouts, branch switching, broad repository exploration, or a generic code explorer as a substitute for the required inline reviewer prompt. Do not request or create tool-managed worktrees, pipeline-managed worktrees, copied repositories, or remote checkouts. Do not use `.claude/worktrees/...` or `.prizmkit/state/worktrees/...`. Pipeline runner `USE_WORKTREE` controls outer infrastructure only; it does not change this skill's active-checkout reviewer requirement.
88
- 4. Never perform Main-Agent self-review or fallback review; the Main Agent's job is filtering and fixing, not acting as the reviewer.
89
- 5. If the platform cannot launch the reviewer in the active checkout without worktree/copy/remote/branch isolation, stop with the checkout-isolation incompatibility message from this skill's introduction.
82
+ 1. Read both references and fill the Reviewer prompt with Phase 0 context plus the workspace manifest and payload.
83
+ 2. Choose a subagent launch and snapshot transport strategy supported by the current platform. Strategy choice belongs to the Main Agent because platforms expose different capabilities; do not treat a particular tool parameter, checkout layout, branch name, or transport as part of the skill contract.
84
+ 3. Require the Reviewer to verify snapshot equivalence before reviewing. Different physical paths or execution environments are acceptable when the verified review view is content-equivalent to the captured Main-Agent workspace.
85
+ 4. If the Reviewer returns `WORKSPACE_MISMATCH`, repair or replace the strategy only when equivalence can be established safely. Otherwise write a `NEEDS_FIXES` report with the verification failure unresolved and stop; do not perform Main-Agent self-review.
86
+ 5. After the Main Agent applies fixes, capture and verify a fresh snapshot for the next round because the previous snapshot is stale by definition.
90
87
 
91
88
  ### Step 2: Run Review
92
89
 
93
90
  Round context:
94
91
  - Round 1: examine all current changes comprehensively.
95
- - Round 2+: focus on whether prior fixes are correct, whether fixes introduced new issues, and whether accepted findings remain.
92
+ - Round 2+: verify prior fixes, look for regressions introduced by the fixes, and identify accepted findings that remain.
96
93
 
97
94
  Review result:
98
95
  - `PASS`: no findings.
99
96
  - `NEEDS_FIXES`: one or more findings with severity, dimension, location, problem, and suggested fix.
97
+ - `WORKSPACE_MISMATCH`: snapshot equivalence was not established; this is a review transport failure, not a code verdict.
100
98
 
101
99
  ### Step 3: Loop Exit Gate
102
100
 
103
- Call the gate only in these cases:
101
+ Call the gate only when:
104
102
 
105
103
  1. Review returned `PASS`.
106
- 2. Main Agent has filtered `NEEDS_FIXES` findings.
104
+ 2. The Main Agent has filtered `NEEDS_FIXES` findings.
107
105
 
108
- Do not call the gate for `NEEDS_FIXES` before filtering because `accepted_count` is not known yet.
106
+ Do not call the gate for `NEEDS_FIXES` before filtering because `accepted_count` is not known. Handle `WORKSPACE_MISMATCH` through Step 1 rather than passing it to the code-verdict gate.
109
107
 
110
108
  Script mode invocation:
111
109
 
@@ -113,7 +111,7 @@ Script mode invocation:
113
111
  echo '{"reviewer_result":"NEEDS_FIXES","accepted_count":2,"findings_count":3,"round":1,"findings_history":[],"max_rounds":3,"filtering_done":true}' | python3 ${SKILL_DIR}/scripts/check_loop.py
114
112
  ```
115
113
 
116
- Gate output determines whether to end the loop. If `divergenceWarning` is true, warn the user that the loop may not be converging.
114
+ The gate output determines whether to end the loop. If `divergenceWarning` is true, warn the user that the loop may not be converging.
117
115
 
118
116
  ### Text Fallback Constraints
119
117
 
@@ -130,36 +128,39 @@ When Python is unavailable, apply these rules manually:
130
128
 
131
129
  For each finding, decide whether it is reasonable:
132
130
 
133
- - Is this relevant to the current changes?
134
- - Is this a real problem rather than subjective style preference?
135
- - Would fixing this improve the code without broad out-of-scope refactoring?
136
- - Does it conflict with spec, plan, dev rules, or current test evidence?
131
+ - Is the finding relevant to the current changes?
132
+ - Is it a real problem rather than a subjective style preference?
133
+ - Would the fix improve the code without broad out-of-scope refactoring?
134
+ - Does the finding conflict with the spec, plan, dev rules, or current test evidence?
137
135
 
138
136
  Output per finding:
139
137
  - Accepted: queue for direct Main Agent fix work.
140
- - Rejected: record a short reason such as out of scope, not a defect, or too risky for current task.
138
+ - Rejected: record a short reason such as out of scope, not a defect, or too risky for the current task.
141
139
 
142
140
  After filtering, call the Loop Exit Gate. If `endLoop=true`, proceed to Phase 2.
143
141
 
144
142
  ### Step 5: Main Agent Applies Accepted Fixes
145
143
 
146
- The Main Agent applies accepted findings directly in the active checkout. Do not spawn a separate implementation subagent for review fixes.
144
+ The Main Agent applies accepted findings directly in its current workspace. Keeping fix ownership with the Main Agent prevents the read-only Reviewer from becoming a second, conflicting implementation authority.
147
145
 
148
- If an accepted finding cannot be safely fixed within scope, record the reason and carry it into `review-report.md` as unresolved.
146
+ If an accepted finding cannot be fixed safely within scope, record the reason and carry the finding into `review-report.md` as unresolved.
149
147
 
150
- After fixes are complete, record results, increment the round, and return to Step 2.
148
+ After fixes are complete, record results, increment the round, capture a fresh workspace snapshot, and return to Step 1.
151
149
 
152
150
  ## Phase 2: Output
153
151
 
154
- Always write `review-report.md` to the artifact directory, including no-change PASS results. Read `${SKILL_DIR}/references/review-report-template.md` for the full output format.
152
+ Write `review-report.md` to the artifact directory, including no-change PASS and workspace-verification failure results. Read `${SKILL_DIR}/references/review-report-template.md` for the full output format.
155
153
 
156
154
  Required report fields:
157
155
  - Verdict: `PASS` or `NEEDS_FIXES`
158
- - Review Mode: `inline-reviewer-active-checkout` or `not-run-no-changes`
156
+ - Review Mode: `independent-reviewer-verified-snapshot` or `not-run-no-changes`
157
+ - Reviewer Strategy: concise strategy summary without relying on platform-specific parameter names
158
+ - Snapshot Verification: `VERIFIED`, `FAILED`, or `NOT_APPLICABLE`
159
+ - Snapshot Identity: manifest identity or failure reason
159
160
  - Rounds and total findings
160
161
  - Fixed, rejected, and unresolved findings
161
162
  - Final summary
162
163
 
163
164
  Completion summary:
164
165
  - `PASS`: hand off to `/prizmkit-retrospective` when docs or durable knowledge changed, otherwise `/prizmkit-committer`.
165
- - `NEEDS_FIXES`: inform the caller of remaining unresolved findings.
166
+ - `NEEDS_FIXES`: inform the caller of unresolved code findings or snapshot-verification blockers.