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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +23 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +43 -29
- package/bundled/dev-pipeline/scripts/continuation.py +51 -6
- package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
- package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
- package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +10 -3
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +9 -2
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +9 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +222 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +24 -11
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/prizmkit-code-review/SKILL.md +49 -48
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +46 -32
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
- package/bundled/skills/prizmkit-test/SKILL.md +160 -155
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
- package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
- package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +201 -0
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
- package/bundled/skills/prizmkit-test/references/examples.md +65 -108
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
- package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +739 -0
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1442 -0
- package/package.json +1 -1
- 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
|
-
|
|
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("
|
|
1100
|
-
(project / ".prizmkit" / "manifest.json").write_text("
|
|
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("
|
|
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
|
|
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="
|
|
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 == "
|
|
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, "
|
|
1439
|
-
assert wip_commits == [(paths.project_root, item_id, "
|
|
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("
|
|
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
|
|
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("
|
|
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 == "
|
|
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("
|
|
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 == "
|
|
2126
|
-
assert classification.reason == "
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
assert "
|
|
820
|
-
assert "
|
|
821
|
-
assert "
|
|
822
|
-
|
|
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.
|
|
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": "
|
|
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":
|
|
72
|
-
"hasScripts":
|
|
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.",
|