prizmkit 1.1.119 → 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 (91) hide show
  1. package/bin/create-prizmkit.js +1 -5
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/dev-pipeline/README.md +41 -38
  4. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +76 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/config.py +2 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +3 -3
  8. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +23 -25
  9. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
  11. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +88 -32
  12. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +28 -6
  13. package/bundled/dev-pipeline/scripts/continuation.py +51 -6
  14. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +60 -102
  15. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -19
  16. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -19
  17. package/bundled/dev-pipeline/scripts/{init-dev-team.py → init-change-artifact.py} +6 -16
  18. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +364 -76
  19. package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
  20. package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
  21. package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
  22. package/bundled/dev-pipeline/scripts/utils.py +1 -1
  23. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  24. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -11
  25. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
  26. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  27. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  28. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
  30. package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -2
  31. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -1
  32. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +6 -7
  33. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +6 -7
  34. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +4 -2
  35. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
  36. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -3
  37. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +20 -0
  38. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  39. package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
  40. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +214 -144
  41. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +25 -12
  42. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +11 -14
  43. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +235 -32
  44. package/bundled/dev-pipeline/tests/test_unified_cli.py +219 -16
  45. package/bundled/skills/_metadata.json +4 -4
  46. package/bundled/skills/app-planner/SKILL.md +2 -2
  47. package/bundled/skills/app-planner/references/architecture-decisions.md +1 -3
  48. package/bundled/skills/prizmkit-code-review/SKILL.md +50 -47
  49. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
  50. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +47 -33
  51. package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
  52. package/bundled/skills/prizmkit-implement/SKILL.md +21 -1
  53. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +67 -0
  54. package/bundled/skills/prizmkit-test/SKILL.md +159 -155
  55. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
  56. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
  57. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
  58. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
  59. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
  60. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
  61. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +194 -0
  62. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
  63. package/bundled/skills/prizmkit-test/references/examples.md +65 -108
  64. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
  65. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
  66. package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
  67. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
  68. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +723 -0
  69. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1426 -0
  70. package/package.json +1 -1
  71. package/src/clean.js +12 -8
  72. package/src/config.js +24 -42
  73. package/src/index.js +1 -10
  74. package/src/manifest.js +3 -9
  75. package/src/metadata.js +0 -26
  76. package/src/prompts.js +0 -13
  77. package/src/scaffold.js +76 -201
  78. package/src/upgrade.js +16 -33
  79. package/bundled/adapters/claude/agent-adapter.js +0 -96
  80. package/bundled/adapters/claude/team-adapter.js +0 -183
  81. package/bundled/adapters/codebuddy/agent-adapter.js +0 -42
  82. package/bundled/adapters/codebuddy/team-adapter.js +0 -46
  83. package/bundled/adapters/codex/agent-adapter.js +0 -38
  84. package/bundled/adapters/codex/team-adapter.js +0 -37
  85. package/bundled/agents/prizm-dev-team-dev.md +0 -123
  86. package/bundled/agents/prizm-dev-team-reviewer.md +0 -113
  87. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -65
  88. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +0 -436
  89. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +0 -510
  90. package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +0 -347
  91. package/bundled/team/prizm-dev-team.json +0 -27
@@ -1,5 +1,6 @@
1
1
  """Tests for generate-refactor-prompt.py continuation behavior."""
2
2
 
3
+ import json
3
4
  from argparse import Namespace
4
5
  from pathlib import Path
5
6
  import os
@@ -20,7 +21,14 @@ class TestRefactorContinuationHandoff:
20
21
  args = Namespace(
21
22
  output=str(tmp_path / ".prizmkit/state/refactor/R-007/sessions/R-007-new/bootstrap-prompt.md"),
22
23
  state_dir=str(tmp_path / ".prizmkit/state/refactor"),
23
- continuation_mode="context_overflow",
24
+ continuation_mode="ai_error",
25
+ failure_classification="ai_runtime_or_provider_error",
26
+ code_retry_count="1",
27
+ infra_error_count="2",
28
+ max_code_retries="3",
29
+ max_infra_retries="3",
30
+ no_progress_count="0",
31
+ progress_fingerprint=json.dumps({"git_diff_fingerprint": "diff", "head_commit": "abc", "checkpoint_cursor": "S03:pending"}),
24
32
  previous_session_id="R-007-old",
25
33
  continuation_count="1",
26
34
  context_overflow_count="1",
@@ -82,10 +90,7 @@ class TestExplicitProjectRoot:
82
90
  )
83
91
 
84
92
  assert replacements["{{PROJECT_ROOT}}"] == str(project.resolve())
85
- assert replacements["{{REVIEWER_SUBAGENT_PATH}}"].endswith(
86
- ".claude/agents/prizm-dev-team-reviewer.md"
87
- )
88
- assert replacements["{{REVIEWER_SUBAGENT_PATH}}"].startswith(str(project.resolve()))
93
+ assert "{{" + "REVIEWER_" + "SUBAGENT_PATH" + "}}" not in replacements
89
94
  assert replacements["{{SESSION_STATUS_PATH}}"] == str(
90
95
  project / ".prizmkit" / "state" / "refactor" / "R-001" /
91
96
  "sessions" / "session-1" / "session-status.json"
@@ -126,7 +131,7 @@ class TestExplicitProjectRoot:
126
131
  "sessions" / "session-1" / "session-status.json"
127
132
  )
128
133
  assert not replacements["{{SESSION_STATUS_PATH}}"].startswith(str(execution_root.resolve()))
129
- assert replacements["{{DEV_SUBAGENT_PATH}}"].startswith(str(execution_root.resolve()))
134
+ assert "{{" + "DEV_" + "SUBAGENT_PATH" + "}}" not in replacements
130
135
  assert "### Checkpoint Update Helper" in replacements["{{CHECKPOINT_SYSTEM}}"]
131
136
  assert "{{CHECKPOINT_PYTHON_CMD}}" in replacements["{{CHECKPOINT_SYSTEM}}"]
132
137
  assert "pipeline progress mechanism" not in replacements["{{CHECKPOINT_SYSTEM}}"]
@@ -234,14 +239,6 @@ def _render_refactor_prompt_full(tmp_path, platform="claude"):
234
239
  (project / ".prizmkit" / "config.json").write_text(
235
240
  json.dumps({"platform": platform, "ai_cli": platform}), encoding="utf-8"
236
241
  )
237
- # Ensure agent files
238
- for plat in ("claude", "codex"):
239
- platform_dir = ".codex" if plat == "codex" else ".claude"
240
- suffix = ".toml" if plat == "codex" else ".md"
241
- agents_dir = project / platform_dir / "agents"
242
- agents_dir.mkdir(parents=True, exist_ok=True)
243
- for name in ("dev", "reviewer"):
244
- (agents_dir / f"prizm-dev-team-{name}{suffix}").write_text("agent\n", encoding="utf-8")
245
242
 
246
243
  refactor_list = project / ".prizmkit" / "plans" / "refactor-list.json"
247
244
  refactor_list.parent.mkdir(parents=True)
@@ -811,8 +811,8 @@ def test_prompt_generation_passes_explicit_execution_root_to_generator(monkeypat
811
811
  "output_path": str(session_paths.prompt_path),
812
812
  "checkpoint_path": str(execution_root / ".prizmkit" / "specs" / "001-low" / "workflow-checkpoint.json"),
813
813
  "model": "",
814
- "pipeline_mode": "lite",
815
- "agent_count": 1,
814
+ "pipeline_mode": "full",
815
+ "agent_count": 3,
816
816
  }
817
817
  ),
818
818
  stderr="",
@@ -876,8 +876,8 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
876
876
  return PromptGenerationResult(
877
877
  output_path=session_paths.prompt_path,
878
878
  model="per-item-model",
879
- pipeline_mode="lite",
880
- agent_count=1,
879
+ pipeline_mode="full",
880
+ agent_count=3,
881
881
  checkpoint_path=execution_root / ".prizmkit" / "specs" / "001-low" / "workflow-checkpoint.json",
882
882
  artifact_path=execution_root / ".prizmkit" / "specs" / "001-low",
883
883
  raw={},
@@ -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))
@@ -1090,15 +1103,14 @@ def test_explicit_worktree_materializes_ignored_support_assets_before_prompt_and
1090
1103
  subprocess.run(["git", "add", ".gitignore", "tracked.txt"], cwd=project, check=True)
1091
1104
  subprocess.run(["git", "commit", "-m", "base"], cwd=project, check=True, stdout=subprocess.DEVNULL)
1092
1105
 
1093
- (project / ".claude" / "agents").mkdir(parents=True)
1094
- (project / ".claude" / "agents" / "prizm-dev-team-dev.md").write_text("dev agent", encoding="utf-8")
1095
- (project / ".claude" / "team-info.json").write_text("{}", encoding="utf-8")
1106
+ (project / ".claude" / "commands").mkdir(parents=True)
1107
+ (project / ".claude" / "commands" / "prizmkit.md").write_text("skill", encoding="utf-8")
1096
1108
  (project / ".prizmkit" / "prizm-docs").mkdir(parents=True)
1097
1109
  (project / ".prizmkit" / "prizm-docs" / "root.prizm").write_text("PRIZM_VERSION: 1\n", encoding="utf-8")
1098
1110
  (project / ".prizmkit" / "dev-pipeline" / "scripts").mkdir(parents=True)
1099
- (project / ".prizmkit" / "dev-pipeline" / "scripts" / "init-dev-team.py").write_text("# init", encoding="utf-8")
1100
- (project / ".prizmkit" / "config.json").write_text("{}", encoding="utf-8")
1101
- (project / ".prizmkit" / "manifest.json").write_text("{}", encoding="utf-8")
1111
+ (project / ".prizmkit" / "dev-pipeline" / "scripts" / "init-change-artifact.py").write_text("# init", 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")
1102
1114
 
1103
1115
  paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
1104
1116
  family = family_for("feature", paths)
@@ -1128,18 +1140,18 @@ def test_explicit_worktree_materializes_ignored_support_assets_before_prompt_and
1128
1140
  def fake_generate_prompt(family, invocation, **kwargs):
1129
1141
  execution_root = kwargs["execution_root"]
1130
1142
  assert execution_root != project
1131
- assert (execution_root / ".claude" / "agents" / "prizm-dev-team-dev.md").is_file()
1143
+ assert (execution_root / ".claude" / "commands" / "prizmkit.md").is_file()
1132
1144
  assert (execution_root / ".prizmkit" / "prizm-docs" / "root.prizm").is_file()
1133
1145
  assert (execution_root / ".prizmkit" / "config.json").is_file()
1134
1146
  assert (execution_root / ".prizmkit" / "manifest.json").is_file()
1135
- assert (execution_root / ".prizmkit" / "dev-pipeline" / "scripts" / "init-dev-team.py").is_file()
1147
+ assert (execution_root / ".prizmkit" / "dev-pipeline" / "scripts" / "init-change-artifact.py").is_file()
1136
1148
  assert not (execution_root / ".prizmkit" / "dev-pipeline" / "run-feature.sh").exists()
1137
1149
  session_paths = kwargs["session_paths"]
1138
1150
  return PromptGenerationResult(
1139
1151
  output_path=session_paths.prompt_path,
1140
1152
  model="",
1141
- pipeline_mode="lite",
1142
- agent_count=1,
1153
+ pipeline_mode="full",
1154
+ agent_count=3,
1143
1155
  checkpoint_path=execution_root / ".prizmkit" / "specs" / "001-support" / "workflow-checkpoint.json",
1144
1156
  artifact_path=execution_root / ".prizmkit" / "specs" / "001-support",
1145
1157
  raw={},
@@ -1226,7 +1238,7 @@ def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_p
1226
1238
  subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1227
1239
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1228
1240
  artifact_dir.mkdir(parents=True)
1229
- (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")
1230
1242
  (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1231
1243
  subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
1232
1244
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
@@ -1406,7 +1418,7 @@ def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch,
1406
1418
  ("refactor", "R-001", "refactor/R-001-20260707010203"),
1407
1419
  ],
1408
1420
  )
1409
- 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):
1410
1422
  from prizmkit_runtime import runners
1411
1423
  from prizmkit_runtime.runner_models import family_for, parse_invocation
1412
1424
 
@@ -1416,7 +1428,7 @@ def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families
1416
1428
  invocation = parse_invocation(family, "run", ())
1417
1429
  plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
1418
1430
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
1419
- _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="pending")
1431
+ _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="failed")
1420
1432
  events = []
1421
1433
  final_commits = []
1422
1434
  wip_commits = []
@@ -1432,12 +1444,12 @@ def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families
1432
1444
 
1433
1445
  status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
1434
1446
 
1435
- assert status == "pending"
1447
+ assert status == "failed"
1436
1448
  assert events[0] == "branch_create"
1437
1449
  assert "branch_merge" not in events
1438
1450
  assert events[-1] == ("return", "main", branch)
1439
- assert final_commits == [(paths.project_root, item_id, "pending")]
1440
- 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")]
1441
1453
 
1442
1454
 
1443
1455
  def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp_path):
@@ -1535,8 +1547,8 @@ def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_pa
1535
1547
  assert "Feature: F-001 — Readable output" in captured.err
1536
1548
  assert "Code retry: 1 / 3" in captured.err
1537
1549
  assert "Preflight: no user-visible changes to preserve before feature task F-001" in captured.err
1538
- assert "Pipeline mode: lite (Tier 1Single Agent)" in captured.err
1539
- assert "Agents: 1" in captured.err
1550
+ assert "Pipeline mode: full (Tier 3Orchestrator + Reviewer)" in captured.err
1551
+ assert "Agents: 3" in captured.err
1540
1552
  assert "Spawning AI CLI session: F-001-session-1" in captured.err
1541
1553
  assert "Session result: success" in captured.err
1542
1554
 
@@ -1846,7 +1858,7 @@ def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
1846
1858
  item_state.mkdir(parents=True)
1847
1859
  plan = plans / "feature-list.json"
1848
1860
  plan.write_text('{"features": []}', encoding="utf-8")
1849
- (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")
1850
1862
  source = tmp_path / "src.txt"
1851
1863
  source.write_text("base", encoding="utf-8")
1852
1864
  subprocess.run(["git", "add", ".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json", "src.txt"], cwd=tmp_path, check=True)
@@ -1900,6 +1912,8 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
1900
1912
  invocation = parse_invocation(family, "run", ())
1901
1913
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1902
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))
1903
1917
  setup_calls = []
1904
1918
  monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (setup_calls.append(runtime) or SimpleNamespace(ok=True, runtime=runtime, reason="")), raising=False)
1905
1919
 
@@ -1912,6 +1926,7 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
1912
1926
  assert observed.prompt_calls[0].get("continuation") is None
1913
1927
  assert observed.prompt_calls[1]["continuation"]["previous_session_id"] == "F-001-session-1"
1914
1928
  assert observed.updates[0][0] == "context_overflow"
1929
+ assert sleep_calls == [5]
1915
1930
  assert len(observed.cleanup_calls) == 1
1916
1931
  assert observed.cleanup_calls[0][1] is True
1917
1932
 
@@ -1960,7 +1975,7 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
1960
1975
  assert classification.reason == "completed_checkpoint_with_late_runtime_error"
1961
1976
 
1962
1977
 
1963
- 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):
1964
1979
  from prizmkit_runtime.runner_classification import classify_session
1965
1980
  from prizmkit_runtime.runner_models import PromptGenerationResult
1966
1981
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -1984,7 +1999,7 @@ def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tm
1984
1999
  subprocess.run(["git", "checkout", "side"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1985
2000
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1986
2001
  artifact_dir.mkdir(parents=True)
1987
- (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")
1988
2003
  log = tmp_path / "session.log"
1989
2004
  log.write_text("quota exceeded", encoding="utf-8")
1990
2005
  raw = AISessionResult(
@@ -2005,7 +2020,8 @@ def test_classification_rejects_late_infra_when_head_diff_has_no_commit_range(tm
2005
2020
 
2006
2021
  classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
2007
2022
 
2008
- assert classification.session_status == "infra_error"
2023
+ assert classification.session_status == "success"
2024
+ assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2009
2025
 
2010
2026
 
2011
2027
  def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
@@ -2061,7 +2077,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2061
2077
  artifact_dir.mkdir(parents=True)
2062
2078
  checkpoint = artifact_dir / "workflow-checkpoint.json"
2063
2079
  checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2064
- (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")
2065
2081
  log = tmp_path / "session.log"
2066
2082
  log.write_text("terminal success", encoding="utf-8")
2067
2083
  raw = AISessionResult(
@@ -2123,8 +2139,8 @@ def test_classification_preserves_success_before_context_overflow(tmp_path):
2123
2139
  )
2124
2140
 
2125
2141
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
2126
- assert classification.session_status == "success"
2127
- assert classification.reason == "commit_since_base"
2142
+ assert classification.session_status == "context_overflow"
2143
+ assert classification.reason == "context_overflow"
2128
2144
 
2129
2145
 
2130
2146
  def test_classification_crash_with_commit_since_base_does_not_become_success(tmp_path):
@@ -2257,7 +2273,7 @@ def _write_family_case(paths, kind, item_id=""):
2257
2273
  state_dir = paths.refactor_state_dir
2258
2274
  plan = paths.refactor_plan
2259
2275
  state_dir.mkdir(parents=True, exist_ok=True)
2260
- (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")
2261
2277
  item_state = state_dir / item_id
2262
2278
  item_state.mkdir(parents=True, exist_ok=True)
2263
2279
  (item_state / "status.json").write_text(
@@ -2518,7 +2534,7 @@ def test_invalid_existing_checkpoint_blocks_terminal_success_with_completion_sum
2518
2534
  artifact_dir.mkdir(parents=True)
2519
2535
  checkpoint = artifact_dir / "workflow-checkpoint.json"
2520
2536
  checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "not-valid"}]}), encoding="utf-8")
2521
- (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")
2522
2538
  prompt = PromptGenerationResult(output_path=tmp_path / "prompt.md", checkpoint_path=checkpoint, artifact_path=artifact_dir)
2523
2539
 
2524
2540
  classification = classify_session(_terminal_success_result(tmp_path), project_root=tmp_path, base_head=base_head, prompt=prompt)
@@ -2598,3 +2614,190 @@ def test_invalid_existing_checkpoint_blocks_workspace_auto_commit_success(tmp_pa
2598
2614
 
2599
2615
  assert classification.session_status == "crashed"
2600
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