prizmkit 1.1.153 → 1.1.155

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 (178) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codex/skill-adapter.js +1 -1
  3. package/bundled/adapters/pi/skill-adapter.js +2 -1
  4. package/bundled/dev-pipeline/.env.example +2 -6
  5. package/bundled/dev-pipeline/README.md +120 -113
  6. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +355 -15
  8. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +189 -110
  9. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
  10. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +4 -20
  11. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +48 -14
  12. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
  13. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
  14. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
  15. package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
  16. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +50 -2
  17. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -10
  18. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +3 -3
  19. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +139 -74
  20. package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +482 -0
  21. package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
  22. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +64 -11
  23. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +145 -367
  24. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +91 -62
  25. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +105 -69
  26. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
  27. package/bundled/dev-pipeline/scripts/prompt_framework.py +36 -41
  28. package/bundled/dev-pipeline/scripts/update-bug-status.py +197 -71
  29. package/bundled/dev-pipeline/scripts/update-checkpoint.py +77 -16
  30. package/bundled/dev-pipeline/scripts/update-feature-status.py +46 -121
  31. package/bundled/dev-pipeline/scripts/update-refactor-status.py +46 -123
  32. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  33. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +22 -27
  34. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +71 -104
  35. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  36. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +59 -111
  37. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +37 -72
  38. package/bundled/dev-pipeline/templates/sections/artifact-manifest.md +39 -0
  39. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +3 -1
  40. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +8 -25
  41. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +3 -3
  42. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +2 -2
  43. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  44. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +2 -2
  45. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +4 -4
  46. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +0 -1
  47. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +7 -4
  48. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +14 -27
  49. package/bundled/dev-pipeline/templates/sections/feature-completion-summary.md +20 -0
  50. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -71
  51. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
  52. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +3 -3
  53. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
  54. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +5 -17
  55. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +3 -1
  56. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +10 -23
  57. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
  58. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +2 -2
  59. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  60. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +2 -2
  61. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +7 -7
  62. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +0 -1
  63. package/bundled/dev-pipeline/templates/sections/retrospective.md +12 -0
  64. package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +35 -0
  65. package/bundled/dev-pipeline/templates/session-status-schema.json +45 -62
  66. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  67. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +617 -30
  68. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +262 -194
  69. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +62 -23
  70. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +76 -28
  71. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +281 -206
  72. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +322 -0
  73. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  74. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -7
  75. package/bundled/dev-pipeline/tests/test_runtime_commit.py +608 -0
  76. package/bundled/dev-pipeline/tests/test_unified_cli.py +617 -188
  77. package/bundled/skills/_metadata.json +9 -9
  78. package/bundled/skills/app-planner/SKILL.md +3 -3
  79. package/bundled/skills/app-planner/references/generated-plan-review.md +2 -1
  80. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +56 -64
  81. package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +85 -240
  82. package/bundled/skills/app-planner/references/rules/backend/question-bank.md +25 -25
  83. package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +16 -7
  84. package/bundled/skills/app-planner/references/rules/backend/template.md +74 -92
  85. package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +93 -93
  86. package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +66 -173
  87. package/bundled/skills/app-planner/references/rules/database/question-bank.md +31 -26
  88. package/bundled/skills/app-planner/references/rules/database/question-manifest.json +13 -4
  89. package/bundled/skills/app-planner/references/rules/database/template.md +76 -98
  90. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +68 -95
  91. package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +93 -166
  92. package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +46 -36
  93. package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +11 -2
  94. package/bundled/skills/app-planner/references/rules/frontend/template.md +79 -257
  95. package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +91 -99
  96. package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +73 -246
  97. package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +17 -17
  98. package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +17 -2
  99. package/bundled/skills/app-planner/references/rules/mobile/template.md +73 -85
  100. package/bundled/skills/app-planner/references/rules-configuration.md +133 -65
  101. package/bundled/skills/app-planner/scripts/validate-rules-configuration.py +291 -0
  102. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  103. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  104. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  105. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  106. package/bundled/skills/prizmkit/SKILL.md +17 -18
  107. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +22 -18
  108. package/bundled/skills/prizmkit-code-review/SKILL.md +22 -58
  109. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +6 -6
  110. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +2 -2
  111. package/bundled/skills/prizmkit-committer/SKILL.md +82 -132
  112. package/bundled/skills/prizmkit-deploy/SKILL.md +27 -11
  113. package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +89 -0
  114. package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +44 -14
  115. package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +19 -3
  116. package/bundled/skills/prizmkit-deploy/references/database-setup.md +22 -12
  117. package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +3 -1
  118. package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +22 -6
  119. package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +49 -0
  120. package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +1 -1
  121. package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +164 -0
  122. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +9 -8
  123. package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +6 -6
  124. package/bundled/skills/prizmkit-implement/SKILL.md +30 -66
  125. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +4 -4
  126. package/bundled/skills/prizmkit-init/SKILL.md +4 -0
  127. package/bundled/skills/prizmkit-plan/SKILL.md +21 -64
  128. package/bundled/skills/prizmkit-plan/references/artifact-identity.md +38 -0
  129. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +4 -4
  130. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +5 -5
  131. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +11 -1
  132. package/bundled/skills/prizmkit-retrospective/SKILL.md +63 -77
  133. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -3
  134. package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
  135. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +17 -23
  136. package/bundled/skills/prizmkit-test/SKILL.md +64 -51
  137. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
  138. package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
  139. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
  140. package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
  141. package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
  142. package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
  143. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
  144. package/bundled/skills/recovery-workflow/SKILL.md +9 -6
  145. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  146. package/bundled/skills/recovery-workflow/references/detection.md +7 -6
  147. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +16 -6
  148. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  149. package/bundled/templates/project-memory-template.md +19 -11
  150. package/package.json +1 -1
  151. package/src/scaffold.js +33 -22
  152. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  153. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
  154. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
  155. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
  156. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
  157. package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
  158. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
  159. package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
  160. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
  161. package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
  162. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
  163. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
  164. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
  165. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
  166. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
  167. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
  168. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
  169. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
  170. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
  171. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
  172. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
  173. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
  174. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
  175. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
  176. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
  177. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
  178. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +0 -119
@@ -3,6 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import subprocess
7
+ import sys
8
+
9
+ import pytest
6
10
 
7
11
  from prizmkit_runtime.checkpoint_state import (
8
12
  VALID_CHECKPOINT_STATUSES,
@@ -12,6 +16,7 @@ from prizmkit_runtime.checkpoint_state import (
12
16
  read_checkpoint_cursor,
13
17
  validate_checkpoint_data,
14
18
  )
19
+ from prizmkit_runtime.test_result import load_test_result
15
20
  from update_checkpoint import update_checkpoint
16
21
 
17
22
 
@@ -56,8 +61,6 @@ def _write_test_artifacts(root, slug="feature", *, result="TEST_PASS"):
56
61
  },
57
62
  "repair_rounds": 0,
58
63
  "production_changed": False,
59
- "review_required": False,
60
- "review_scope": None,
61
64
  "unresolved_items": [],
62
65
  }
63
66
  (artifact_dir / "test-result.json").write_text(
@@ -71,14 +74,16 @@ def _family_checkpoint(root=None, *, semantic=None, terminal=True):
71
74
  "prizmkit-implement",
72
75
  "prizmkit-code-review",
73
76
  "prizmkit-test",
77
+ "completion-summary",
74
78
  "prizmkit-retrospective",
75
79
  "prizmkit-committer",
76
- "completion-summary",
77
80
  ]
78
81
  steps = []
79
82
  previous = None
80
83
  for index, skill in enumerate(skills, start=1):
81
84
  step = _step(f"S{index:02d}", skill, skill, "completed", previous)
85
+ if skill == "prizmkit-implement":
86
+ step["stage_result"] = "IMPLEMENTED"
82
87
  if skill == "prizmkit-code-review":
83
88
  step["stage_result"] = "REVIEW_PASS"
84
89
  if skill == "prizmkit-test":
@@ -87,6 +92,10 @@ def _family_checkpoint(root=None, *, semantic=None, terminal=True):
87
92
  ".prizmkit/specs/feature/test-report.md",
88
93
  ".prizmkit/specs/feature/test-result.json",
89
94
  ]
95
+ if skill == "prizmkit-retrospective":
96
+ step["stage_result"] = "RETRO_COMPLETE"
97
+ if skill == "prizmkit-committer":
98
+ step["stage_result"] = "COMMITTED"
90
99
  steps.append(step)
91
100
  previous = step["id"]
92
101
  if root is not None:
@@ -95,7 +104,14 @@ def _family_checkpoint(root=None, *, semantic=None, terminal=True):
95
104
  "version": 1,
96
105
  "workflow_type": "feature-pipeline",
97
106
  "steps": steps,
98
- "feature_state": semantic or ({"terminal_status": "WORKFLOW_COMPLETED"} if terminal else {}),
107
+ "feature_state": semantic or (
108
+ {
109
+ "terminal_status": "WORKFLOW_COMPLETED",
110
+ "runtime_base_head": "a" * 40,
111
+ "runtime_commit_hash": "b" * 40,
112
+ }
113
+ if terminal else {}
114
+ ),
99
115
  }
100
116
 
101
117
 
@@ -199,6 +215,16 @@ def test_structural_validation_rejects_status_result_mismatch(tmp_path):
199
215
  assert state.error_code == "stage_result_status_mismatch"
200
216
 
201
217
 
218
+ def test_family_planning_wrappers_own_plan_results(tmp_path):
219
+ for skill in ("context-snapshot-and-plan", "bug-diagnosis-and-plan"):
220
+ step = _step("S01", skill, "Plan", "completed")
221
+ step["stage_result"] = "PLAN_READY"
222
+ state = validate_checkpoint_data(
223
+ _checkpoint([step]), path=tmp_path / f"{skill}.json",
224
+ )
225
+ assert state.valid is True
226
+
227
+
202
228
  def test_structural_validation_rejects_stage_result_owner_mismatch(tmp_path):
203
229
  for skill in ("prizmkit-test", "custom-stage"):
204
230
  step = _step("S01", skill, "Step", "completed")
@@ -231,6 +257,120 @@ def test_semantic_metadata_rejects_legacy_or_mismatched_status(tmp_path):
231
257
  assert state.semantic.error_code == error_code
232
258
 
233
259
 
260
+ def test_semantic_blocker_metadata_requires_blocked_terminal_state(tmp_path):
261
+ cases = [
262
+ ({"blocked_reason": ""}, "invalid_blocked_reason"),
263
+ ({"blocked_reason": " "}, "invalid_blocked_reason"),
264
+ ({"blocked_reason": "unexpected blocker"}, "blocked_reason_without_blocked_terminal"),
265
+ (
266
+ {"blocked_reason": "unexpected blocker", "terminal_status": "WORKFLOW_COMPLETED"},
267
+ "completed_workflow_has_blocked_reason",
268
+ ),
269
+ ({"terminal_status": "WORKFLOW_BLOCKED"}, "missing_blocked_reason"),
270
+ ]
271
+
272
+ for index, (semantic, expected_error) in enumerate(cases):
273
+ payload = _family_checkpoint(tmp_path, semantic=semantic)
274
+ state = validate_checkpoint_data(
275
+ payload,
276
+ path=tmp_path / f"invalid-blocker-{index}.json",
277
+ project_root=tmp_path,
278
+ )
279
+
280
+ assert state.valid is True
281
+ assert state.semantic.error_code == expected_error
282
+ assert state.semantic_complete is False
283
+
284
+
285
+ def test_checkpoint_updater_rejects_invalid_blocker_metadata_without_mutation(tmp_path):
286
+ cases = [
287
+ ({"blocked_reason": "", "terminal_status": "WORKFLOW_COMPLETED"}, True),
288
+ ({"blocked_reason": " ", "terminal_status": "WORKFLOW_COMPLETED"}, True),
289
+ ({"blocked_reason": "unexpected blocker", "terminal_status": "WORKFLOW_COMPLETED"}, True),
290
+ ({"blocked_reason": "unexpected blocker"}, False),
291
+ ({"terminal_status": "WORKFLOW_BLOCKED"}, False),
292
+ ]
293
+
294
+ for index, (semantic_update, completed) in enumerate(cases):
295
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
296
+ if not completed:
297
+ payload["steps"][0]["status"] = "in_progress"
298
+ for step in payload["steps"][1:]:
299
+ step["status"] = "pending"
300
+ step.pop("stage_result", None)
301
+ checkpoint = _write(tmp_path / f"atomic-blocker-{index}.json", payload)
302
+ before = checkpoint.read_bytes()
303
+
304
+ result = update_checkpoint(
305
+ str(checkpoint),
306
+ "S06" if completed else "S01",
307
+ "completed" if completed else "in_progress",
308
+ semantic_update=semantic_update,
309
+ )
310
+
311
+ assert result["ok"] is False
312
+ assert checkpoint.read_bytes() == before
313
+
314
+
315
+ def test_checkpoint_updater_rejects_completed_terminal_for_blocking_stage_result(tmp_path):
316
+ for index, blocked_reason in enumerate((None, "safe environment unavailable")):
317
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
318
+ payload["steps"][2]["status"] = "in_progress"
319
+ payload["steps"][2].pop("stage_result", None)
320
+ for step in payload["steps"][3:]:
321
+ step["status"] = "pending"
322
+ step.pop("stage_result", None)
323
+ checkpoint = _write(tmp_path / f"conflicting-test-terminal-{index}.json", payload)
324
+ before = checkpoint.read_bytes()
325
+ semantic_update = {
326
+ "stage_result": "TEST_BLOCKED",
327
+ "terminal_status": "WORKFLOW_COMPLETED",
328
+ }
329
+ if blocked_reason is not None:
330
+ semantic_update["blocked_reason"] = blocked_reason
331
+
332
+ result = update_checkpoint(
333
+ str(checkpoint),
334
+ "prizmkit-test",
335
+ "failed",
336
+ semantic_update=semantic_update,
337
+ )
338
+
339
+ assert result["ok"] is False
340
+ assert checkpoint.read_bytes() == before
341
+
342
+
343
+ def test_checkpoint_updater_cli_returns_nonzero_without_mutating_invalid_blocker_input(tmp_path):
344
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
345
+ checkpoint = _write(tmp_path / "cli-invalid-blocker.json", payload)
346
+ before = checkpoint.read_bytes()
347
+ script = __import__("update_checkpoint").__file__
348
+
349
+ result = subprocess.run(
350
+ [
351
+ sys.executable,
352
+ script,
353
+ "--checkpoint-path",
354
+ str(checkpoint),
355
+ "--step",
356
+ "S06",
357
+ "--status",
358
+ "completed",
359
+ "--terminal-status",
360
+ "WORKFLOW_COMPLETED",
361
+ "--blocked-reason",
362
+ "",
363
+ ],
364
+ text=True,
365
+ capture_output=True,
366
+ check=False,
367
+ )
368
+
369
+ assert result.returncode == 1
370
+ assert json.loads(result.stdout)["ok"] is False
371
+ assert checkpoint.read_bytes() == before
372
+
373
+
234
374
  def test_update_checkpoint_reenters_failed_stage_without_stale_result(tmp_path):
235
375
  payload = _family_checkpoint(
236
376
  tmp_path,
@@ -275,16 +415,40 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
275
415
  ("prizmkit-test", "TEST_BLOCKED", "failed"),
276
416
  ("prizmkit-retrospective", "RETRO_COMPLETE", "completed"),
277
417
  ("prizmkit-committer", "COMMIT_PENDING", "in_progress"),
278
- ("prizmkit-committer", "COMMITTED", "completed"),
279
418
  ]
280
419
 
281
420
  assert {stage_result for _skill, stage_result, _status in cases} <= VALID_STAGE_RESULTS
282
421
  for index, (skill, stage_result, status) in enumerate(cases):
283
- step = _step("S01", skill, skill, "pending")
284
- checkpoint = _write(
285
- tmp_path / f"owned-result-{index}.json",
286
- _checkpoint([step]),
287
- )
422
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
423
+ if skill == "prizmkit-plan":
424
+ for offset, step in enumerate(payload["steps"], start=2):
425
+ step["id"] = f"S{offset:02d}"
426
+ step["depends_on"] = f"S{offset - 1:02d}"
427
+ payload["steps"].insert(0, _step("S01", skill, skill, "pending"))
428
+ target_index = 0
429
+ else:
430
+ payload["steps"].insert(
431
+ 0,
432
+ {
433
+ "id": "S00",
434
+ "skill": "context-snapshot-and-plan",
435
+ "name": "Plan",
436
+ "status": "completed",
437
+ "stage_result": "PLAN_READY",
438
+ "required_artifacts": [],
439
+ "depends_on": None,
440
+ },
441
+ )
442
+ payload["steps"][1]["depends_on"] = "S00"
443
+ target_index = next(
444
+ position
445
+ for position, step in enumerate(payload["steps"])
446
+ if step["skill"] == skill
447
+ )
448
+ for step in payload["steps"][target_index:]:
449
+ step["status"] = "pending"
450
+ step.pop("stage_result", None)
451
+ checkpoint = _write(tmp_path / f"owned-result-{index}.json", payload)
288
452
 
289
453
  result = update_checkpoint(
290
454
  str(checkpoint),
@@ -295,8 +459,9 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
295
459
 
296
460
  assert result["ok"] is True
297
461
  persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
298
- assert persisted["steps"][0]["status"] == status
299
- assert persisted["steps"][0]["stage_result"] == stage_result
462
+ target = next(step for step in persisted["steps"] if step["skill"] == skill)
463
+ assert target["status"] == status
464
+ assert target["stage_result"] == stage_result
300
465
  assert persisted["feature_state"]["stage"] == {
301
466
  "prizmkit-plan": "plan",
302
467
  "prizmkit-implement": "implement",
@@ -310,12 +475,19 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
310
475
 
311
476
 
312
477
  def test_duplicate_skills_require_exact_step_id(tmp_path):
313
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
314
- _step("S01", "prizmkit-committer", "First", "completed"),
315
- _step("S02", "prizmkit-committer", "Second", "pending", "S01"),
316
- ]))
478
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
479
+ for offset, step in enumerate(payload["steps"], start=3):
480
+ step["id"] = f"S{offset:02d}"
481
+ step["depends_on"] = f"S{offset - 1:02d}"
482
+ step["status"] = "pending"
483
+ step.pop("stage_result", None)
484
+ payload["steps"][:0] = [
485
+ _step("S01", "custom-stage", "First", "completed"),
486
+ _step("S02", "custom-stage", "Second", "pending", "S01"),
487
+ ]
488
+ path = _write(tmp_path / "workflow-checkpoint.json", payload)
317
489
 
318
- rejected = update_checkpoint(str(path), "prizmkit-committer", "completed")
490
+ rejected = update_checkpoint(str(path), "custom-stage", "completed")
319
491
  accepted = update_checkpoint(str(path), "S02", "completed")
320
492
 
321
493
  assert rejected["ok"] is False
@@ -375,11 +547,26 @@ def test_family_sequence_mismatch_cannot_complete(tmp_path):
375
547
  assert state.semantic.error_code == "invalid_stage_sequence"
376
548
 
377
549
 
378
- def test_test_needs_fixes_is_valid_domain_result_and_blocks_l4(tmp_path):
550
+ def test_test_needs_fixes_routes_to_implementation_and_rewinds_stale_gates(tmp_path):
379
551
  payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
380
- payload["steps"][2]["status"] = "in_progress"
381
- payload["steps"][2].pop("stage_result")
382
- for step in payload["steps"][3:]:
552
+ payload["steps"].insert(
553
+ 0,
554
+ {
555
+ "id": "S00",
556
+ "skill": "context-snapshot-and-plan",
557
+ "name": "Plan",
558
+ "status": "completed",
559
+ "stage_result": "PLAN_READY",
560
+ "required_artifacts": [],
561
+ "depends_on": None,
562
+ },
563
+ )
564
+ payload["steps"][1]["depends_on"] = "S00"
565
+ test_step = next(step for step in payload["steps"] if step["skill"] == "prizmkit-test")
566
+ test_index = payload["steps"].index(test_step)
567
+ test_step["status"] = "in_progress"
568
+ test_step.pop("stage_result")
569
+ for step in payload["steps"][test_index + 1:]:
383
570
  step["status"] = "pending"
384
571
  step.pop("stage_result", None)
385
572
  checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
@@ -388,16 +575,80 @@ def test_test_needs_fixes_is_valid_domain_result_and_blocks_l4(tmp_path):
388
575
  str(checkpoint),
389
576
  "prizmkit-test",
390
577
  "failed",
391
- semantic_update={"stage_result": "TEST_NEEDS_FIXES"},
578
+ semantic_update={
579
+ "stage_result": "TEST_NEEDS_FIXES",
580
+ "repair_scope": "production",
581
+ },
392
582
  )
393
- persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
583
+ routed = json.loads(checkpoint.read_text(encoding="utf-8"))
394
584
 
395
585
  assert result["ok"] is True
396
- assert persisted["steps"][2]["stage_result"] == "TEST_NEEDS_FIXES"
397
- assert persisted["feature_state"]["stage_result"] == "TEST_NEEDS_FIXES"
398
- assert persisted["feature_state"]["blocked_reason"] == "test_needs_fixes"
399
- assert persisted["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"
400
- assert "authoritative_evidence_paths" not in persisted["steps"][2]
586
+ assert test_step["skill"] == "prizmkit-test"
587
+ persisted_test = next(
588
+ step for step in routed["steps"] if step["skill"] == "prizmkit-test"
589
+ )
590
+ assert persisted_test["stage_result"] == "TEST_NEEDS_FIXES"
591
+ assert routed["feature_state"]["stage_result"] == "TEST_NEEDS_FIXES"
592
+ assert routed["feature_state"]["next_stage"] == "prizmkit-implement"
593
+ assert routed["feature_state"]["repair_round"] == 1
594
+
595
+ restarted = update_checkpoint(
596
+ str(checkpoint), "prizmkit-implement", "in_progress"
597
+ )
598
+ rewound = json.loads(checkpoint.read_text(encoding="utf-8"))
599
+
600
+ assert restarted["ok"] is True
601
+ implementation = next(
602
+ step for step in rewound["steps"] if step["skill"] == "prizmkit-implement"
603
+ )
604
+ assert implementation["status"] == "in_progress"
605
+ for step in rewound["steps"][rewound["steps"].index(implementation) + 1:]:
606
+ assert step["status"] == "pending"
607
+ assert "stage_result" not in step
608
+
609
+
610
+ def test_terminal_atomic_blockers_become_workflow_blocked(tmp_path):
611
+ cases = [
612
+ ("context-snapshot-and-plan", "PLAN_BLOCKED"),
613
+ ("prizmkit-implement", "IMPLEMENT_BLOCKED"),
614
+ ("prizmkit-retrospective", "RETRO_BLOCKED"),
615
+ ("prizmkit-committer", "COMMIT_BLOCKED"),
616
+ ]
617
+ for index, (skill, stage_result) in enumerate(cases):
618
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
619
+ if skill == "context-snapshot-and-plan":
620
+ payload["steps"].insert(
621
+ 0,
622
+ {
623
+ "id": "S00",
624
+ "skill": skill,
625
+ "name": "Plan",
626
+ "status": "pending",
627
+ "required_artifacts": [],
628
+ "depends_on": None,
629
+ },
630
+ )
631
+ payload["steps"][1]["depends_on"] = "S00"
632
+ target_index = 0
633
+ else:
634
+ target_index = next(
635
+ position for position, step in enumerate(payload["steps"])
636
+ if step["skill"] == skill
637
+ )
638
+ for step in payload["steps"][target_index:]:
639
+ step["status"] = "pending"
640
+ step.pop("stage_result", None)
641
+ checkpoint = _write(tmp_path / f"terminal-blocker-{index}.json", payload)
642
+
643
+ result = update_checkpoint(
644
+ str(checkpoint), skill, "failed",
645
+ semantic_update={"stage_result": stage_result},
646
+ )
647
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
648
+
649
+ assert result["ok"] is True
650
+ assert persisted["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"
651
+ assert persisted["feature_state"]["blocked_reason"]
401
652
 
402
653
 
403
654
  def test_test_blocked_preserves_l1_result_without_evidence_discovery(tmp_path):
@@ -466,6 +717,31 @@ def test_update_checkpoint_rejects_test_completion_for_non_pass(tmp_path):
466
717
  assert "requires TEST_PASS" in result["error"]
467
718
 
468
719
 
720
+ def test_prompt_checkpoint_helper_rejects_runtime_owned_committed_result(tmp_path):
721
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
722
+ committer = next(
723
+ step for step in payload["steps"] if step["skill"] == "prizmkit-committer"
724
+ )
725
+ committer["status"] = "in_progress"
726
+ committer["stage_result"] = "COMMIT_PENDING"
727
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
728
+
729
+ result = update_checkpoint(
730
+ str(checkpoint),
731
+ "prizmkit-committer",
732
+ "completed",
733
+ semantic_update={"stage_result": "COMMITTED"},
734
+ )
735
+
736
+ assert result["ok"] is False
737
+ assert "Runtime-owned" in result["error"]
738
+ without_result = update_checkpoint(
739
+ str(checkpoint), "prizmkit-committer", "completed"
740
+ )
741
+ assert without_result["ok"] is False
742
+ assert "Runtime-owned COMMITTED" in without_result["error"]
743
+
744
+
469
745
  def test_update_checkpoint_rejects_test_before_review_pass(tmp_path):
470
746
  payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
471
747
  payload["steps"][1]["status"] = "skipped"
@@ -487,7 +763,7 @@ def test_update_checkpoint_rejects_finalization_before_test_pass(tmp_path):
487
763
  payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
488
764
  payload["steps"][2]["status"] = "skipped"
489
765
  payload["steps"][2].pop("stage_result", None)
490
- payload["steps"][3]["status"] = "pending"
766
+ payload["steps"][3]["status"] = "completed"
491
767
  for step in payload["steps"][4:]:
492
768
  step["status"] = "pending"
493
769
  step.pop("stage_result", None)
@@ -499,9 +775,53 @@ def test_update_checkpoint_rejects_finalization_before_test_pass(tmp_path):
499
775
  assert "TEST_PASS" in result["error"]
500
776
 
501
777
 
778
+ def test_commit_pending_is_runtime_ready_but_not_workflow_complete(tmp_path):
779
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
780
+ payload["steps"].insert(
781
+ 0,
782
+ {
783
+ "id": "S00",
784
+ "skill": "context-snapshot-and-plan",
785
+ "name": "Plan",
786
+ "status": "completed",
787
+ "stage_result": "PLAN_READY",
788
+ "required_artifacts": [],
789
+ "depends_on": None,
790
+ },
791
+ )
792
+ payload["steps"][1]["depends_on"] = "S00"
793
+ committer = next(
794
+ step for step in payload["steps"] if step["skill"] == "prizmkit-committer"
795
+ )
796
+ committer["status"] = "in_progress"
797
+ committer["stage_result"] = "COMMIT_PENDING"
798
+ committer["required_artifacts"] = [
799
+ ".prizmkit/specs/feature/runtime-commit-request.json"
800
+ ]
801
+ artifact_dir = tmp_path / ".prizmkit/specs/feature"
802
+ (artifact_dir / "runtime-commit-request.json").write_text("{}", encoding="utf-8")
803
+ payload["feature_state"] = {
804
+ "stage": "committer",
805
+ "status": "in_progress",
806
+ "stage_result": "COMMIT_PENDING",
807
+ "repair_round": 0,
808
+ }
809
+ checkpoint = _write(artifact_dir / "workflow-checkpoint.json", payload)
810
+
811
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
812
+
813
+ assert state.valid is True
814
+ assert state.semantic_commit_ready is True
815
+ assert state.semantic_complete is False
816
+ assert state.semantic_snapshot["commit_ready"] is True
817
+
818
+
502
819
  def test_declared_final_artifact_must_exist_for_completion(tmp_path):
503
820
  payload = _family_checkpoint(tmp_path)
504
- payload["steps"][-1]["required_artifacts"] = [
821
+ summary_step = next(
822
+ step for step in payload["steps"] if step["skill"] == "completion-summary"
823
+ )
824
+ summary_step["required_artifacts"] = [
505
825
  ".prizmkit/specs/feature/completion-summary.json"
506
826
  ]
507
827
  checkpoint = _write(
@@ -519,6 +839,239 @@ def test_declared_final_artifact_must_exist_for_completion(tmp_path):
519
839
  assert present.semantic.artifacts_valid is True
520
840
 
521
841
 
842
+ def test_completion_requires_declared_review_report_with_terminal_pass(tmp_path):
843
+ payload = _family_checkpoint(tmp_path)
844
+ review = next(
845
+ step for step in payload["steps"]
846
+ if step["skill"] == "prizmkit-code-review"
847
+ )
848
+ review["required_artifacts"] = [
849
+ ".prizmkit/specs/feature/review-report.md"
850
+ ]
851
+ checkpoint = _write(
852
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json", payload,
853
+ )
854
+
855
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is False
856
+ (checkpoint.parent / "review-report.md").write_text(
857
+ "# Review Report\n\n## Final Result\n\n- Verdict: NEEDS_FIXES\n",
858
+ encoding="utf-8",
859
+ )
860
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is False
861
+ (checkpoint.parent / "review-report.md").write_text(
862
+ "# Review Report\n\n## Final Result\n\n- Verdict: PASS\n",
863
+ encoding="utf-8",
864
+ )
865
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is True
866
+
867
+
868
+ def test_completion_requires_valid_declared_retrospective_result(tmp_path):
869
+ payload = _family_checkpoint(tmp_path)
870
+ retrospective = next(
871
+ step for step in payload["steps"]
872
+ if step["skill"] == "prizmkit-retrospective"
873
+ )
874
+ retrospective["required_artifacts"] = [
875
+ ".prizmkit/specs/feature/retrospective-result.json"
876
+ ]
877
+ checkpoint = _write(
878
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json", payload,
879
+ )
880
+
881
+ (checkpoint.parent / "retrospective-result.json").write_text(
882
+ "{}\n", encoding="utf-8",
883
+ )
884
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is False
885
+ (checkpoint.parent / "retrospective-result.json").write_text(
886
+ json.dumps(
887
+ {
888
+ "schema_version": 1,
889
+ "outcome": "RETRO_COMPLETE",
890
+ "result": "NO_DOC_CHANGE",
891
+ "reason": "No durable change",
892
+ "change_paths": ["src/example.py"],
893
+ "documentation_paths": [],
894
+ "validation": [],
895
+ }
896
+ ),
897
+ encoding="utf-8",
898
+ )
899
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is True
900
+
901
+
902
+ @pytest.mark.parametrize(
903
+ "invalid_path",
904
+ [
905
+ ".prizmkit/prizm-docs/../escape.prizm",
906
+ ".prizmkit/prizm-docs/./module.prizm",
907
+ ".prizmkit/prizm-docs//module.prizm",
908
+ ".prizmkit\\prizm-docs\\module.prizm",
909
+ ".prizmkit/prizm-docs/module.md",
910
+ ],
911
+ )
912
+ def test_completion_rejects_unsafe_retrospective_documentation_path(
913
+ tmp_path, invalid_path,
914
+ ):
915
+ payload = _family_checkpoint(tmp_path)
916
+ retrospective = next(
917
+ step for step in payload["steps"]
918
+ if step["skill"] == "prizmkit-retrospective"
919
+ )
920
+ retrospective["required_artifacts"] = [
921
+ ".prizmkit/specs/feature/retrospective-result.json"
922
+ ]
923
+ checkpoint = _write(
924
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json", payload,
925
+ )
926
+ (checkpoint.parent / "retrospective-result.json").write_text(
927
+ json.dumps(
928
+ {
929
+ "schema_version": 1,
930
+ "outcome": "RETRO_COMPLETE",
931
+ "result": "DOCS_UPDATED",
932
+ "reason": "Durable docs changed",
933
+ "change_paths": ["src/example.py"],
934
+ "documentation_paths": [invalid_path],
935
+ "validation": ["validate docs"],
936
+ }
937
+ ),
938
+ encoding="utf-8",
939
+ )
940
+
941
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is False
942
+
943
+
944
+ @pytest.mark.parametrize(
945
+ "invalid_path",
946
+ ["../escape.py", "./src/example.py", "src//example.py", "src\\example.py"],
947
+ )
948
+ def test_completion_rejects_unsafe_retrospective_change_path(tmp_path, invalid_path):
949
+ payload = _family_checkpoint(tmp_path)
950
+ retrospective = next(
951
+ step for step in payload["steps"]
952
+ if step["skill"] == "prizmkit-retrospective"
953
+ )
954
+ retrospective["required_artifacts"] = [
955
+ ".prizmkit/specs/feature/retrospective-result.json"
956
+ ]
957
+ checkpoint = _write(
958
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json", payload,
959
+ )
960
+ (checkpoint.parent / "retrospective-result.json").write_text(
961
+ json.dumps(
962
+ {
963
+ "schema_version": 1,
964
+ "outcome": "RETRO_COMPLETE",
965
+ "result": "NO_DOC_CHANGE",
966
+ "reason": "No durable change",
967
+ "change_paths": [invalid_path],
968
+ "documentation_paths": [],
969
+ "validation": [],
970
+ }
971
+ ),
972
+ encoding="utf-8",
973
+ )
974
+
975
+ assert load_checkpoint_state(checkpoint, project_root=tmp_path).semantic_complete is False
976
+
977
+
978
+ def test_completion_rejects_configured_verification_after_test(tmp_path):
979
+ payload = _family_checkpoint(tmp_path)
980
+ test_index = next(
981
+ index for index, step in enumerate(payload["steps"])
982
+ if step["skill"] == "prizmkit-test"
983
+ )
984
+ test_step = payload["steps"][test_index]
985
+ following = payload["steps"][test_index + 1]
986
+ payload["steps"].insert(
987
+ test_index + 1,
988
+ {
989
+ "id": "S99",
990
+ "skill": "browser-verification",
991
+ "name": "Browser Verification",
992
+ "status": "completed",
993
+ "required_artifacts": [],
994
+ "depends_on": test_step["id"],
995
+ },
996
+ )
997
+ following["depends_on"] = "S99"
998
+
999
+ state = validate_checkpoint_data(payload, project_root=tmp_path)
1000
+
1001
+ assert state.semantic.sequence_valid is False
1002
+ assert "after review and before test" in state.semantic.error_message
1003
+
1004
+
1005
+ def test_configured_manual_verification_cannot_be_skipped_for_commit_readiness(tmp_path):
1006
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
1007
+ payload["steps"].insert(
1008
+ 0,
1009
+ {
1010
+ "id": "S00",
1011
+ "skill": "context-snapshot-and-plan",
1012
+ "name": "Plan",
1013
+ "status": "completed",
1014
+ "stage_result": "PLAN_READY",
1015
+ "required_artifacts": [],
1016
+ "depends_on": None,
1017
+ },
1018
+ )
1019
+ payload["steps"][1]["depends_on"] = "S00"
1020
+ review = next(
1021
+ step for step in payload["steps"]
1022
+ if step["skill"] == "prizmkit-code-review"
1023
+ )
1024
+ test = next(
1025
+ step for step in payload["steps"] if step["skill"] == "prizmkit-test"
1026
+ )
1027
+ test_index = payload["steps"].index(test)
1028
+ payload["steps"].insert(
1029
+ test_index,
1030
+ {
1031
+ "id": "S99",
1032
+ "skill": "manual-verification",
1033
+ "name": "Manual Verification",
1034
+ "status": "skipped",
1035
+ "required_artifacts": [],
1036
+ "depends_on": review["id"],
1037
+ },
1038
+ )
1039
+ test["depends_on"] = "S99"
1040
+ committer = next(
1041
+ step for step in payload["steps"]
1042
+ if step["skill"] == "prizmkit-committer"
1043
+ )
1044
+ committer["status"] = "in_progress"
1045
+ committer["stage_result"] = "COMMIT_PENDING"
1046
+ committer["required_artifacts"] = [
1047
+ ".prizmkit/specs/feature/runtime-commit-request.json"
1048
+ ]
1049
+ artifact_dir = tmp_path / ".prizmkit/specs/feature"
1050
+ (artifact_dir / "runtime-commit-request.json").write_text("{}", encoding="utf-8")
1051
+ checkpoint = _write(artifact_dir / "workflow-checkpoint.json", payload)
1052
+
1053
+ skipped = load_checkpoint_state(checkpoint, project_root=tmp_path)
1054
+ payload["steps"][test_index]["status"] = "completed"
1055
+ checkpoint.write_text(json.dumps(payload), encoding="utf-8")
1056
+ completed = load_checkpoint_state(checkpoint, project_root=tmp_path)
1057
+
1058
+ assert skipped.semantic_commit_ready is False
1059
+ assert completed.semantic_commit_ready is True
1060
+
1061
+
1062
+ def test_nonpass_test_projection_requires_exact_unresolved_item(tmp_path):
1063
+ artifact_dir = _write_test_artifacts(
1064
+ tmp_path, slug="nonpass", result="TEST_NEEDS_FIXES"
1065
+ )
1066
+ result_path = artifact_dir / "test-result.json"
1067
+
1068
+ assert load_test_result(result_path) is None
1069
+ projection = json.loads(result_path.read_text(encoding="utf-8"))
1070
+ projection["unresolved_items"] = ["Public API correction remains"]
1071
+ result_path.write_text(json.dumps(projection), encoding="utf-8")
1072
+ assert load_test_result(result_path) is not None
1073
+
1074
+
522
1075
  def test_completion_rejects_invalid_test_result_projection(tmp_path):
523
1076
  payload = _family_checkpoint(tmp_path)
524
1077
  checkpoint = _write(
@@ -558,6 +1111,40 @@ def test_completion_rejects_inconsistent_independent_review_metadata(tmp_path):
558
1111
  assert state.semantic.artifacts_valid is False
559
1112
 
560
1113
 
1114
+ def test_legacy_completed_checkpoint_accepts_historical_test_projection_read_only(tmp_path):
1115
+ payload = _family_checkpoint(tmp_path)
1116
+ by_skill = {step["skill"]: step for step in payload["steps"]}
1117
+ legacy_order = [
1118
+ "prizmkit-implement",
1119
+ "prizmkit-code-review",
1120
+ "prizmkit-test",
1121
+ "prizmkit-retrospective",
1122
+ "prizmkit-committer",
1123
+ "completion-summary",
1124
+ ]
1125
+ payload["steps"] = [by_skill[skill] for skill in legacy_order]
1126
+ previous = None
1127
+ for index, step in enumerate(payload["steps"], start=1):
1128
+ step["id"] = f"S{index:02d}"
1129
+ step["depends_on"] = previous
1130
+ previous = step["id"]
1131
+ checkpoint = _write(
1132
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json", payload,
1133
+ )
1134
+ result_path = checkpoint.parent / "test-result.json"
1135
+ projection = json.loads(result_path.read_text(encoding="utf-8"))
1136
+ projection["review_required"] = False
1137
+ projection["review_scope"] = None
1138
+ result_path.write_text(json.dumps(projection), encoding="utf-8")
1139
+ assert load_test_result(result_path) is None
1140
+
1141
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
1142
+
1143
+ assert state.semantic_complete is True
1144
+ assert state.semantic.artifacts_valid is True
1145
+ assert state.semantic_commit_ready is False
1146
+
1147
+
561
1148
  def test_completion_accepts_legacy_terminal_result_report(tmp_path):
562
1149
  payload = _family_checkpoint(tmp_path)
563
1150
  checkpoint = _write(