prizmkit 1.1.120 → 1.1.122

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 (46) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +1 -0
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +23 -25
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
  6. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +43 -29
  7. package/bundled/dev-pipeline/scripts/continuation.py +51 -6
  8. package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
  9. package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
  10. package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
  11. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
  12. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
  13. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  14. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  15. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
  16. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
  17. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  18. package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
  19. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +10 -3
  20. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +9 -2
  21. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +9 -1
  22. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +222 -18
  23. package/bundled/dev-pipeline/tests/test_unified_cli.py +24 -11
  24. package/bundled/skills/_metadata.json +4 -4
  25. package/bundled/skills/prizmkit-code-review/SKILL.md +49 -48
  26. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
  27. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +46 -32
  28. package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
  29. package/bundled/skills/prizmkit-test/SKILL.md +160 -155
  30. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
  31. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
  32. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
  33. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
  34. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
  35. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
  36. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +201 -0
  37. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
  38. package/bundled/skills/prizmkit-test/references/examples.md +65 -108
  39. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
  40. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
  41. package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
  42. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
  43. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +739 -0
  44. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1442 -0
  45. package/package.json +1 -1
  46. 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
@@ -30,10 +30,11 @@ if str(PIPELINE_ROOT) not in sys.path:
30
30
  sys.path.insert(0, str(PIPELINE_ROOT))
31
31
 
32
32
 
33
- def run_cli(*args):
33
+ def run_cli(*args, env=None):
34
34
  return subprocess.run(
35
35
  [PYTHON_310_PLUS, str(CLI_PATH), *args],
36
36
  cwd=REPO_ROOT,
37
+ env=env,
37
38
  text=True,
38
39
  stdout=subprocess.PIPE,
39
40
  stderr=subprocess.PIPE,
@@ -72,6 +73,14 @@ def test_cli_help_exposes_python_runtime_command_groups():
72
73
  assert "Traceback" not in result.stderr
73
74
 
74
75
 
76
+ def test_cli_help_preserves_runtime_identity_at_narrow_terminal_width():
77
+ result = run_cli("--help", env={**os.environ, "COLUMNS": "24"})
78
+
79
+ assert result.returncode == 0
80
+ assert "canonical Python runtime CLI" in result.stdout
81
+ assert "without shell wrappers" in result.stdout
82
+
83
+
75
84
  def test_module_entrypoint_help_exposes_same_cli():
76
85
  env = {**os.environ, "PYTHONPATH": str(PIPELINE_ROOT)}
77
86
  result = subprocess.run(
@@ -802,7 +811,7 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
802
811
  assert str(prompt) not in "\n".join(lines[:6])
803
812
 
804
813
 
805
- def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
814
+ def test_headless_review_guidance_uses_workspace_equivalence_protocol():
806
815
  guidance_files = [
807
816
  REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
808
817
  REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
@@ -811,15 +820,19 @@ def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
811
820
 
812
821
  for path in guidance_files:
813
822
  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
823
+ assert "platform-supported" in text, path
824
+ assert "snapshot" in text and "equivalent" in text, path
825
+ assert "Main-Agent self-review" in text, path
826
+ assert "current-workspace / active-checkout / no-worktree" not in text, path
827
+ assert ".claude/worktrees/..." not in text, path
828
+ assert ".prizmkit/state/worktrees/..." not in text, path
829
+ assert "USE_WORKTREE" not in text, path
830
+ assert "isolation:" not in text, path
831
+
832
+ skill_text = guidance_files[-1].read_text(encoding="utf-8")
833
+ assert "reviewer-workspace-protocol.md" in skill_text
834
+ assert "staged, unstaged, untracked, deleted, and renamed" in skill_text
835
+ assert "WORKSPACE_MISMATCH" in skill_text
823
836
 
824
837
  for path in guidance_files[:-1]:
825
838
  text = path.read_text(encoding="utf-8")
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.120",
2
+ "version": "1.1.122",
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.",