prizmkit 1.1.130 → 1.1.131

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 (88) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  7. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +396 -4
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +54 -1
  11. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +54 -1
  12. package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
  13. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  14. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  15. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  16. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  17. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
  19. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
  20. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
  21. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  24. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  25. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
  26. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  27. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
  28. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  30. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  31. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +310 -22
  32. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
  33. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
  36. package/bundled/skills/_metadata.json +62 -25
  37. package/bundled/skills/app-planner/SKILL.md +4 -3
  38. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  39. package/bundled/skills/bug-planner/SKILL.md +12 -27
  40. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  41. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  42. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  43. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  44. package/bundled/skills/feature-planner/SKILL.md +27 -22
  45. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  46. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  47. package/bundled/skills/prizmkit/SKILL.md +130 -94
  48. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  49. package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
  50. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
  51. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  52. package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
  53. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  54. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  55. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  57. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  58. package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
  59. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  60. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  61. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  62. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  63. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  64. package/bundled/skills/prizmkit-test/SKILL.md +28 -6
  65. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  66. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  67. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  68. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  69. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  71. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  72. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  73. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  74. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  75. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  76. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  77. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  78. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  79. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  80. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  81. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  82. package/bundled/templates/project-memory-template.md +4 -4
  83. package/package.json +1 -1
  84. package/src/index.js +4 -0
  85. package/src/prompts.js +10 -3
  86. package/src/scaffold.js +1 -1
  87. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  88. package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
@@ -10,6 +10,8 @@ from generate_refactor_prompt import (
10
10
  build_replacements,
11
11
  generate_refactor_checkpoint,
12
12
  merge_refactor_checkpoint_state,
13
+ refactor_checkpoint_metadata,
14
+ route_refactor_repair,
13
15
  )
14
16
 
15
17
  REMOVED_MAX_LOG_ENV = "MAX_" + "LOG_SIZE"
@@ -54,12 +56,24 @@ class TestRefactorCheckpointMerge:
54
56
  def test_preserves_skipped_without_artifacts(self, tmp_path):
55
57
  fresh = generate_refactor_checkpoint("R-008", "R-008-new")
56
58
  existing = generate_refactor_checkpoint("R-008", "R-008-old")
59
+ existing["refactor_state"] = refactor_checkpoint_metadata(
60
+ ".prizmkit/refactor/R-008",
61
+ stage="test",
62
+ status="TEST_FAIL",
63
+ stage_result="TEST_FAIL",
64
+ repair_scope="public-interface",
65
+ repair_round=2,
66
+ next_stage="prizmkit-implement",
67
+ resume_from="prizmkit-implement",
68
+ )
57
69
  existing["steps"][0]["status"] = "skipped"
58
70
 
59
71
  merged = merge_refactor_checkpoint_state(existing, fresh, str(tmp_path))
60
72
 
61
73
  assert merged["steps"][0]["status"] == "skipped"
62
74
  assert merged["steps"][1]["status"] == "pending"
75
+ assert merged["refactor_state"]["repair_scope"] == "public-interface"
76
+ assert merged["refactor_state"]["repair_round"] == 2
63
77
 
64
78
 
65
79
  class TestExplicitProjectRoot:
@@ -197,6 +211,11 @@ def test_template_source_is_clean_for_headless_prompt_contract():
197
211
  content = Path("dev-pipeline/templates/refactor-bootstrap-prompt.md").read_text(encoding="utf-8")
198
212
  for forbidden in HEADLESS_FORBIDDEN_PROMPT_STRINGS:
199
213
  assert forbidden not in content
214
+ assert "prizmkit-workflow" not in content
215
+ assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/refactor/{{REFACTOR_ID}}/" in content
216
+ assert "local_commit_authorized=true" in content
217
+ assert "push_authorized=false" in content
218
+ assert "Behavior-preservation checks remain supporting evidence" in content
200
219
 
201
220
 
202
221
  # ---------------------------------------------------------------------------
@@ -315,6 +334,78 @@ class TestRefactorBootstrapShellExtraction:
315
334
 
316
335
 
317
336
  class TestRefactorCheckpointGeneration:
337
+ def test_default_checkpoint_has_all_atomic_gates_in_order(self):
338
+ checkpoint = generate_refactor_checkpoint("R-001", "session-1")
339
+
340
+ assert [step["skill"] for step in checkpoint["steps"]] == [
341
+ "prizmkit-init",
342
+ "prizmkit-plan",
343
+ "prizmkit-implement",
344
+ "prizmkit-code-review",
345
+ "prizmkit-test",
346
+ "prizmkit-retrospective",
347
+ "prizmkit-committer",
348
+ "refactor-report",
349
+ "completion-summary",
350
+ ]
351
+ assert checkpoint["steps"][4]["depends_on"] == "S04"
352
+ assert checkpoint["steps"][5]["depends_on"] == "S05"
353
+
354
+ def test_refactor_repair_routing_preserves_behavior_review_and_test(self):
355
+ test_infra = route_refactor_repair(
356
+ "TEST_FAIL", "test-infrastructure", repair_round=1,
357
+ )
358
+ production = route_refactor_repair("TEST_FAIL", "public-interface", repair_round=1)
359
+ blocked = route_refactor_repair("TEST_BLOCKED", "production", repair_round=1)
360
+
361
+ assert test_infra["route"] == ["prizmkit-implement", "prizmkit-test"]
362
+ assert production["route"] == [
363
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
364
+ ]
365
+ unknown = route_refactor_repair("TEST_FAIL", "contract", repair_round=1)
366
+ exhausted = route_refactor_repair("TEST_FAIL", "production", repair_round=3)
367
+ assert unknown["blocked"] is True
368
+ assert unknown["route"] == []
369
+ assert exhausted["blocked"] is True
370
+ assert exhausted["repair_round"] == 3
371
+ assert blocked["blocked"] is True
372
+ assert blocked["route"] == []
373
+ assert blocked["next_stage"] is None
374
+ assert blocked["terminal_status"] == "WORKFLOW_BLOCKED"
375
+ assert blocked["blocked_reason"] == "test_blocked"
376
+
377
+ def test_refactor_checkpoint_metadata_preserves_artifact_and_evidence_handoff(self):
378
+ metadata = refactor_checkpoint_metadata(
379
+ ".prizmkit/refactor/R-001",
380
+ evidence={"manifest_path": "manifest.json"},
381
+ stage="test",
382
+ status="TEST_PASS",
383
+ stage_result="TEST_PASS",
384
+ next_stage="prizmkit-retrospective",
385
+ resume_from="prizmkit-retrospective",
386
+ )
387
+
388
+ assert metadata["artifact_dir"] == ".prizmkit/refactor/R-001"
389
+ assert metadata["authoritative_evidence_paths"]["manifest"] == "manifest.json"
390
+ assert metadata["next_stage"] == "prizmkit-retrospective"
391
+ assert metadata["resume_from"] == "prizmkit-retrospective"
392
+
393
+ def test_rendered_refactor_prompt_is_atomic_headless_and_local_commit_only(self, tmp_path):
394
+ prompt = _render_refactor_prompt_full(tmp_path)
395
+
396
+ assert prompt.index("/prizmkit-code-review") < prompt.index(
397
+ "/prizmkit-test scope=this-change artifact_dir=.prizmkit/refactor/R-001/"
398
+ ) < prompt.index("### Headless Commit Authorization")
399
+ assert "prizmkit-workflow" not in prompt
400
+ assert "Behavior-preservation checks collected during planning and implementation remain supporting evidence" in prompt
401
+ assert '"local_commit_authorized": true' in prompt
402
+ assert '"push_authorized": false' in prompt
403
+ assert "Do not ask questions, wait for user input" in prompt
404
+ assert "automatic push" not in prompt.lower()
405
+ assert "push_authorized\": true" not in prompt
406
+
407
+
408
+ class TestRefactorCheckpointGenerationLegacy:
318
409
  def test_setup_section_is_step_and_finalization_group_expands(self):
319
410
  checkpoint = generate_refactor_checkpoint(
320
411
  "R-001",
@@ -90,6 +90,93 @@ def _item_id_from_next_result(kind, result):
90
90
  return result.data[key]
91
91
 
92
92
 
93
+ def _write_semantic_completion_checkpoint(project_root, artifact_dir, workflow_type="feature-pipeline"):
94
+ validator = (
95
+ project_root / ".claude" / "command-assets" / "prizmkit-test"
96
+ / "scripts" / "validate_test_evidence.py"
97
+ )
98
+ validator.parent.mkdir(parents=True, exist_ok=True)
99
+ validator.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
100
+ contracts = {
101
+ "feature-pipeline": (
102
+ "feature_state",
103
+ [
104
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
105
+ "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
106
+ ],
107
+ ),
108
+ "bugfix-pipeline": (
109
+ "bugfix_state",
110
+ [
111
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
112
+ "prizmkit-retrospective", "prizmkit-committer", "bug-report",
113
+ ],
114
+ ),
115
+ "refactor-pipeline": (
116
+ "refactor_state",
117
+ [
118
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
119
+ "prizmkit-retrospective", "prizmkit-committer", "refactor-report",
120
+ "completion-summary",
121
+ ],
122
+ ),
123
+ }
124
+ state_key, skills = contracts[workflow_type]
125
+ evidence_id = "semantic-evidence"
126
+ evidence_dir = project_root / ".prizmkit" / "test" / "evidence" / evidence_id
127
+ evidence_dir.mkdir(parents=True, exist_ok=True)
128
+ evidence = {
129
+ "manifest": evidence_dir / "manifest.json",
130
+ "verdict": evidence_dir / "verdict.json",
131
+ "validation": evidence_dir / "validation.json",
132
+ }
133
+ evidence["manifest"].write_text(
134
+ json.dumps({"evidence_id": evidence_id, "final_verdict": "TEST_PASS"}),
135
+ encoding="utf-8",
136
+ )
137
+ evidence["verdict"].write_text(json.dumps({"verdict": "TEST_PASS"}), encoding="utf-8")
138
+ evidence["validation"].write_text(
139
+ json.dumps({"verdict": "TEST_PASS", "result": "passed"}), encoding="utf-8"
140
+ )
141
+ relative_evidence = {
142
+ key: str(path.relative_to(project_root)) for key, path in evidence.items()
143
+ }
144
+ steps = []
145
+ previous = None
146
+ for index, skill in enumerate(skills, start=1):
147
+ step = {
148
+ "id": f"S{index:02d}",
149
+ "skill": skill,
150
+ "name": skill,
151
+ "status": "completed",
152
+ "required_artifacts": [],
153
+ "depends_on": previous,
154
+ }
155
+ if skill == "prizmkit-code-review":
156
+ step["stage_result"] = "REVIEW_PASS"
157
+ if skill == "prizmkit-test":
158
+ step["stage_result"] = "TEST_PASS"
159
+ step["authoritative_evidence_paths"] = relative_evidence
160
+ steps.append(step)
161
+ previous = step["id"]
162
+ artifact_dir.mkdir(parents=True, exist_ok=True)
163
+ checkpoint = artifact_dir / "workflow-checkpoint.json"
164
+ checkpoint.write_text(
165
+ json.dumps({
166
+ "version": 1,
167
+ "workflow_type": workflow_type,
168
+ "steps": steps,
169
+ state_key: {
170
+ "repair_round": 0,
171
+ "authoritative_evidence_paths": relative_evidence,
172
+ "terminal_status": "WORKFLOW_COMPLETED",
173
+ },
174
+ }),
175
+ encoding="utf-8",
176
+ )
177
+ return checkpoint
178
+
179
+
93
180
  def test_retired_shell_runtime_files_are_absent_from_source():
94
181
  retired = [
95
182
  "run-feature.sh",
@@ -346,6 +433,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
346
433
  assert env.stale_kill_threshold_seconds == 9
347
434
  assert env.max_retries == 9
348
435
  assert env.strict_behavior_check is False
436
+ assert not hasattr(env, "enable_deploy")
349
437
  assert not hasattr(env, "max_" + "log_size")
350
438
 
351
439
 
@@ -1443,8 +1531,7 @@ def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_p
1443
1531
  subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
1444
1532
  subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1445
1533
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1446
- artifact_dir.mkdir(parents=True)
1447
- (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1534
+ _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
1448
1535
  (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1449
1536
  subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
1450
1537
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
@@ -1505,11 +1592,7 @@ def test_no_worktree_recovers_completed_pending_branch_before_new_session(monkey
1505
1592
  session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
1506
1593
  session_dir.mkdir(parents=True)
1507
1594
  artifact_dir = paths.project_root / ".prizmkit" / "specs" / "001-low"
1508
- artifact_dir.mkdir(parents=True)
1509
- (artifact_dir / "workflow-checkpoint.json").write_text(
1510
- json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}),
1511
- encoding="utf-8",
1512
- )
1595
+ _write_semantic_completion_checkpoint(paths.project_root, artifact_dir)
1513
1596
  updates = []
1514
1597
  events = []
1515
1598
 
@@ -2555,9 +2638,7 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
2555
2638
  subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
2556
2639
  subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2557
2640
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
2558
- artifact_dir.mkdir(parents=True)
2559
- checkpoint = artifact_dir / "workflow-checkpoint.json"
2560
- checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2641
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
2561
2642
  log = tmp_path / "session.log"
2562
2643
  log.write_text("model quota exceeded; API error after commit", encoding="utf-8")
2563
2644
  raw = AISessionResult(
@@ -2579,10 +2660,10 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
2579
2660
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
2580
2661
 
2581
2662
  assert classification.session_status == "success"
2582
- assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2663
+ assert classification.reason == "semantic_checkpoint_with_late_runtime_error"
2583
2664
 
2584
2665
 
2585
- def test_classification_accepts_late_infra_with_completion_artifact_without_new_commit(tmp_path):
2666
+ def test_classification_rejects_late_infra_with_completion_summary_only(tmp_path):
2586
2667
  from prizmkit_runtime.runner_classification import classify_session
2587
2668
  from prizmkit_runtime.runner_models import PromptGenerationResult
2588
2669
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -2627,8 +2708,8 @@ def test_classification_accepts_late_infra_with_completion_artifact_without_new_
2627
2708
 
2628
2709
  classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
2629
2710
 
2630
- assert classification.session_status == "success"
2631
- assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2711
+ assert classification.session_status == "infra_error"
2712
+ assert classification.reason == "ai_runtime_or_provider_error"
2632
2713
 
2633
2714
 
2634
2715
  def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
@@ -2681,9 +2762,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2681
2762
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2682
2763
  base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
2683
2764
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
2684
- artifact_dir.mkdir(parents=True)
2685
- checkpoint = artifact_dir / "workflow-checkpoint.json"
2686
- checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2765
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
2687
2766
  (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
2688
2767
  log = tmp_path / "session.log"
2689
2768
  log.write_text("terminal success", encoding="utf-8")
@@ -2709,7 +2788,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2709
2788
 
2710
2789
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
2711
2790
  assert classification.session_status == "success"
2712
- assert classification.reason == "terminal_success_with_completed_checkpoint"
2791
+ assert classification.reason == "terminal_success_with_semantic_checkpoint"
2713
2792
 
2714
2793
 
2715
2794
  def test_classification_preserves_success_before_context_overflow(tmp_path):
@@ -3475,3 +3554,317 @@ def test_timeout_and_stale_termination_override_provider_error_markers(tmp_path,
3475
3554
 
3476
3555
  assert classification.session_status == expected_status
3477
3556
  assert classification.reason == expected_reason
3557
+
3558
+
3559
+
3560
+ @pytest.mark.parametrize("workflow_type", ["feature-pipeline", "bugfix-pipeline", "refactor-pipeline"])
3561
+ def test_semantic_completion_classification_has_cross_family_parity(tmp_path, workflow_type):
3562
+ from prizmkit_runtime.runner_classification import classify_session
3563
+ from prizmkit_runtime.runner_models import PromptGenerationResult
3564
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
3565
+ from prizmkit_runtime.status import ProgressSummary
3566
+
3567
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3568
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3569
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3570
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3571
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3572
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3573
+ artifact = tmp_path / ".prizmkit" / "artifacts" / workflow_type
3574
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact, workflow_type)
3575
+ log = tmp_path / f"{workflow_type}.log"
3576
+ log.write_text("success", encoding="utf-8")
3577
+ raw = AISessionResult(
3578
+ command=AISessionCommand(("fake",), None, "claude", "stdin"),
3579
+ exit_code=0,
3580
+ timed_out=False,
3581
+ termination_reason=None,
3582
+ stale_marker=None,
3583
+ fatal_error_code="",
3584
+ progress_summary=ProgressSummary(result_subtype="success"),
3585
+ session_log=log,
3586
+ backup_log=tmp_path / "backup.log",
3587
+ log_recovery=None,
3588
+ started_epoch=0,
3589
+ ended_epoch=1,
3590
+ )
3591
+
3592
+ classification = classify_session(
3593
+ raw,
3594
+ project_root=tmp_path,
3595
+ base_head="",
3596
+ prompt=PromptGenerationResult(
3597
+ output_path=tmp_path / "prompt.md",
3598
+ checkpoint_path=checkpoint,
3599
+ artifact_path=artifact,
3600
+ ),
3601
+ )
3602
+
3603
+ assert classification.session_status == "success"
3604
+ assert classification.checkpoint_state["terminal_status"] == "WORKFLOW_COMPLETED"
3605
+
3606
+
3607
+ def test_semantic_blocked_classification_is_terminal_and_retry_neutral(tmp_path):
3608
+ from prizmkit_runtime.runner_classification import classify_session
3609
+ from prizmkit_runtime.runner_models import PromptGenerationResult
3610
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
3611
+ from prizmkit_runtime.status import ProgressSummary
3612
+
3613
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3614
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3615
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3616
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3617
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3618
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3619
+ artifact = tmp_path / ".prizmkit" / "specs" / "blocked"
3620
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
3621
+ data = json.loads(checkpoint.read_text(encoding="utf-8"))
3622
+ data["feature_state"].update(
3623
+ terminal_status="WORKFLOW_BLOCKED",
3624
+ blocked_reason="repair_round_limit",
3625
+ repair_round=3,
3626
+ )
3627
+ checkpoint.write_text(json.dumps(data), encoding="utf-8")
3628
+ log = tmp_path / "blocked.log"
3629
+ log.write_text("ambiguous failure", encoding="utf-8")
3630
+ raw = AISessionResult(
3631
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
3632
+ exit_code=1,
3633
+ timed_out=False,
3634
+ termination_reason=None,
3635
+ stale_marker=None,
3636
+ fatal_error_code="",
3637
+ progress_summary=ProgressSummary(terminal_result_text="ambiguous failure"),
3638
+ session_log=log,
3639
+ backup_log=tmp_path / "backup.log",
3640
+ log_recovery=None,
3641
+ started_epoch=0,
3642
+ ended_epoch=1,
3643
+ )
3644
+
3645
+ classification = classify_session(
3646
+ raw,
3647
+ project_root=tmp_path,
3648
+ base_head="",
3649
+ prompt=PromptGenerationResult(
3650
+ output_path=tmp_path / "prompt.md",
3651
+ checkpoint_path=checkpoint,
3652
+ artifact_path=artifact,
3653
+ ),
3654
+ )
3655
+
3656
+ assert classification.session_status == "workflow_blocked"
3657
+ assert classification.reason == "repair_round_limit"
3658
+ assert classification.checkpoint_state["repair_round"] == 3
3659
+
3660
+
3661
+ def test_recovery_outcome_preserves_dirty_partial_work_and_uses_semantic_checkpoint(tmp_path):
3662
+ from prizmkit_runtime.runner_recovery import _recovery_outcome
3663
+
3664
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3665
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3666
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3667
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3668
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3669
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3670
+ (tmp_path / "partial.txt").write_text("preserve me", encoding="utf-8")
3671
+ artifact = tmp_path / ".prizmkit" / "specs" / "done"
3672
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
3673
+
3674
+ outcome, semantic = _recovery_outcome(tmp_path, 1, checkpoint_path=checkpoint)
3675
+
3676
+ assert outcome == "success"
3677
+ assert semantic["terminal_status"] == "WORKFLOW_COMPLETED"
3678
+ assert (tmp_path / "partial.txt").read_text(encoding="utf-8") == "preserve me"
3679
+ assert subprocess.run(
3680
+ ["git", "status", "--porcelain", "--", "partial.txt"],
3681
+ cwd=tmp_path,
3682
+ check=True,
3683
+ stdout=subprocess.PIPE,
3684
+ text=True,
3685
+ ).stdout.startswith("??")
3686
+
3687
+
3688
+ def test_runtime_semantic_paths_do_not_invoke_composite_workflow():
3689
+ sources = [
3690
+ PIPELINE_ROOT / "prizmkit_runtime" / "checkpoint_state.py",
3691
+ PIPELINE_ROOT / "prizmkit_runtime" / "runner_classification.py",
3692
+ PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py",
3693
+ PIPELINE_ROOT / "prizmkit_runtime" / "runners.py",
3694
+ ]
3695
+
3696
+ for source in sources:
3697
+ assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
3698
+
3699
+
3700
+
3701
+ @pytest.mark.parametrize(
3702
+ ("kind", "item_id", "blocked_status"),
3703
+ [("feature", "F-001", "failed"), ("bugfix", "B-001", "needs_info"), ("refactor", "R-001", "failed")],
3704
+ )
3705
+ def test_family_status_persists_identical_semantic_blocked_snapshot_without_retry_spend(
3706
+ tmp_path, kind, item_id, blocked_status
3707
+ ):
3708
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
3709
+ from prizmkit_runtime.runner_status import start_item, update_item
3710
+
3711
+ paths = _make_paths(tmp_path)
3712
+ item = {
3713
+ "id": item_id,
3714
+ "title": "Semantic blocked",
3715
+ "status": "pending",
3716
+ "priority": "high",
3717
+ "dependencies": [],
3718
+ }
3719
+ if kind == "bugfix":
3720
+ item.update(severity="high", description="blocked", reproduction_steps=["run"])
3721
+ elif kind == "refactor":
3722
+ item.update(description="blocked", targets=["src"], goals=["preserve"])
3723
+ _write_family_plan(paths, kind, [item])
3724
+ family = family_for(kind, paths)
3725
+ invocation = parse_invocation(family, "run", ())
3726
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
3727
+ assert start_item(family, invocation, item_id, paths.project_root).ok is True
3728
+ semantic = {
3729
+ "current_stage": None,
3730
+ "next_stage": None,
3731
+ "stage_result": "TEST_BLOCKED",
3732
+ "repair_scope": "production",
3733
+ "repair_round": 2,
3734
+ "authoritative_evidence_paths": {
3735
+ "manifest": ".prizmkit/test/evidence/id/manifest.json",
3736
+ "verdict": ".prizmkit/test/evidence/id/verdict.json",
3737
+ "validation": ".prizmkit/test/evidence/id/validation.json",
3738
+ },
3739
+ "blocked_reason": "test_blocked",
3740
+ "terminal_status": "WORKFLOW_BLOCKED",
3741
+ }
3742
+
3743
+ result = update_item(
3744
+ family,
3745
+ invocation,
3746
+ item_id,
3747
+ "workflow_blocked",
3748
+ "session-1",
3749
+ paths.project_root,
3750
+ checkpoint_state=semantic,
3751
+ )
3752
+ status = json.loads((family.state_dir / item_id / "status.json").read_text(encoding="utf-8"))
3753
+
3754
+ assert result.ok is True
3755
+ assert result.data["new_status"] == blocked_status
3756
+ assert result.data["checkpoint_state"] == semantic
3757
+ assert result.data["restart_policy"] == "terminal_blocked"
3758
+ assert status["retry_count"] == 0
3759
+ assert status["infra_error_count"] == 0
3760
+ for key, value in semantic.items():
3761
+ assert status[key] == value
3762
+
3763
+
3764
+
3765
+ def test_recovery_detector_returns_no_candidate_for_empty_project(tmp_path):
3766
+ import importlib.util
3767
+
3768
+ detector_path = (
3769
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3770
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3771
+ )
3772
+ spec = importlib.util.spec_from_file_location("recovery_detector_empty", detector_path)
3773
+ module = importlib.util.module_from_spec(spec)
3774
+ spec.loader.exec_module(module)
3775
+
3776
+ report = module.build_report(tmp_path)
3777
+
3778
+ assert report["detected"] is False
3779
+ assert report["candidates"] == []
3780
+
3781
+
3782
+ def test_recovery_detector_returns_active_checkout_command(tmp_path, monkeypatch):
3783
+ import importlib.util
3784
+
3785
+ detector_path = (
3786
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3787
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3788
+ )
3789
+ spec = importlib.util.spec_from_file_location("recovery_detector_active", detector_path)
3790
+ module = importlib.util.module_from_spec(spec)
3791
+ spec.loader.exec_module(module)
3792
+
3793
+ checkout_path = tmp_path / ".prizmkit" / "state" / "features" / "F-001" / "checkout.json"
3794
+ checkout_path.parent.mkdir(parents=True)
3795
+ checkout_path.write_text(
3796
+ json.dumps({
3797
+ "task_type": "feature",
3798
+ "task_id": "F-001",
3799
+ "state": "active",
3800
+ "active_dev_branch": "dev/F-001-resume",
3801
+ "base_branch": "main",
3802
+ "checkout_mode": "branch",
3803
+ }),
3804
+ encoding="utf-8",
3805
+ )
3806
+ monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
3807
+
3808
+ report = module.build_report(tmp_path)
3809
+
3810
+ assert report["detected"] is True
3811
+ assert report["selection_required"] is False
3812
+ assert report["candidates"][0]["continue_command"] == (
3813
+ "python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001"
3814
+ )
3815
+
3816
+
3817
+ def test_recovery_detector_ignores_completed_checkout(tmp_path, monkeypatch):
3818
+ import importlib.util
3819
+
3820
+ detector_path = (
3821
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3822
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3823
+ )
3824
+ spec = importlib.util.spec_from_file_location("recovery_detector_completed", detector_path)
3825
+ module = importlib.util.module_from_spec(spec)
3826
+ spec.loader.exec_module(module)
3827
+
3828
+ checkout_path = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "checkout.json"
3829
+ checkout_path.parent.mkdir(parents=True)
3830
+ checkout_path.write_text(
3831
+ json.dumps({
3832
+ "task_type": "bugfix",
3833
+ "task_id": "B-001",
3834
+ "state": "completed",
3835
+ "active_dev_branch": "bugfix/B-001-done",
3836
+ "base_branch": "main",
3837
+ }),
3838
+ encoding="utf-8",
3839
+ )
3840
+ monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
3841
+
3842
+ report = module.build_report(tmp_path)
3843
+
3844
+ assert report["detected"] is False
3845
+
3846
+
3847
+ def test_l4_runtime_and_recovery_sources_do_not_depend_on_direct_l1_composite():
3848
+ runtime_root = REPO_ROOT / "dev-pipeline"
3849
+ source_files = list(runtime_root.rglob("*.py"))
3850
+ for source in source_files:
3851
+ if "__pycache__" in source.parts or "tests" in source.parts or not source.is_file():
3852
+ continue
3853
+ assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
3854
+
3855
+
3856
+ def test_recovery_detector_has_no_direct_lifecycle_fallback(tmp_path):
3857
+ import importlib.util
3858
+
3859
+ detector_path = (
3860
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3861
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3862
+ )
3863
+ spec = importlib.util.spec_from_file_location("recovery_detector_contract", detector_path)
3864
+ module = importlib.util.module_from_spec(spec)
3865
+ spec.loader.exec_module(module)
3866
+
3867
+ report = module.build_report(tmp_path)
3868
+
3869
+ assert "continue_command" not in report
3870
+ assert report["detected"] is False
@@ -839,8 +839,8 @@ def test_headless_review_guidance_uses_main_agent_review_loop():
839
839
  assert "isolation:" not in text, path
840
840
 
841
841
  skill_text = skill_path.read_text(encoding="utf-8")
842
- assert "staged and unstaged tracked changes" in skill_text
843
- assert "untracked files" in skill_text
842
+ assert "the staged diff, and the unstaged diff" in skill_text
843
+ assert "untracked, deleted, and renamed files" in skill_text
844
844
  assert "accepted = 0" in skill_text
845
845
  assert "ten completed review rounds" in skill_text
846
846
  assert "## Final Result" in skill_text
@@ -1576,7 +1576,7 @@ def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkey
1576
1576
  "run",
1577
1577
  lambda *_args, **_kwargs: type("Completed", (), {"returncode": 0, "stdout": "0", "stderr": ""})(),
1578
1578
  )
1579
- monkeypatch.setattr(runner_recovery, "_recovery_outcome", lambda *_args: "no_changes")
1579
+ monkeypatch.setattr(runner_recovery, "_recovery_outcome", lambda *_args, **_kwargs: ("no_changes", {}))
1580
1580
 
1581
1581
  class FakeLauncher:
1582
1582
  def __init__(self, config):