prizmkit 1.1.141 → 1.1.143

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 (48) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +124 -4
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +13 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +4 -1
  6. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +20 -10
  7. package/bundled/dev-pipeline/scripts/check-session-status.py +11 -1
  8. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +2 -2
  9. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +2 -2
  10. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +2 -2
  11. package/bundled/dev-pipeline/scripts/prompt_framework.py +12 -1
  12. package/bundled/dev-pipeline/scripts/update-bug-status.py +20 -0
  13. package/bundled/dev-pipeline/scripts/update-checkpoint.py +38 -9
  14. package/bundled/dev-pipeline/scripts/update-feature-status.py +20 -0
  15. package/bundled/dev-pipeline/scripts/update-refactor-status.py +20 -0
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +1 -1
  17. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +1 -1
  18. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +5 -1
  19. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -2
  20. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  21. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  22. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +231 -0
  23. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +26 -1
  24. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +4 -2
  25. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +5 -2
  26. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +147 -4
  27. package/bundled/skills/_metadata.json +1 -1
  28. package/bundled/skills/prizmkit/SKILL.md +6 -6
  29. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +23 -11
  30. package/bundled/skills/prizmkit-code-review/SKILL.md +4 -4
  31. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +23 -11
  32. package/bundled/skills/prizmkit-committer/SKILL.md +5 -4
  33. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +23 -11
  34. package/bundled/skills/prizmkit-implement/SKILL.md +3 -2
  35. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +23 -11
  36. package/bundled/skills/prizmkit-plan/SKILL.md +2 -1
  37. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +23 -11
  38. package/bundled/skills/prizmkit-retrospective/SKILL.md +6 -4
  39. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +23 -11
  40. package/bundled/skills/prizmkit-test/SKILL.md +1 -1
  41. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  42. package/bundled/skills/prizmkit-workflow/SKILL.md +13 -13
  43. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +23 -11
  44. package/bundled/templates/hooks/commit-intent-status.py +50 -0
  45. package/bundled/templates/hooks/commit-intent.json +2 -2
  46. package/bundled/templates/hooks/post-command-prizm-drift.py +44 -0
  47. package/package.json +1 -1
  48. package/src/scaffold.js +2 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.141",
3
- "bundledAt": "2026-07-21T00:43:15.672Z",
4
- "bundledFrom": "df5581e"
2
+ "frameworkVersion": "1.1.143",
3
+ "bundledAt": "2026-07-21T14:41:54.248Z",
4
+ "bundledFrom": "1e6fbd1"
5
5
  }
@@ -359,7 +359,7 @@ python3 scripts/update-feature-status.py \
359
359
  | `complete` | `--feature-id` | Shortcut for manually marking completed |
360
360
 
361
361
  **Session status values (for `--session-status`):**
362
- `success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `commit_missing`, `docs_missing`, `merge_conflict`
362
+ `success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `timed_out`, `infra_error`, `context_overflow`, `stalled_context_continuation`, `workflow_blocked`, `workflow_skipped`, `commit_missing`, `docs_missing`, `merge_conflict`
363
363
 
364
364
  **Output (JSON to stdout):**
365
365
 
@@ -17,7 +17,37 @@ MAX_OUTER_REPAIR_ROUNDS = 3
17
17
 
18
18
  REVIEW_RESULTS = {"REVIEW_PASS", "REVIEW_NEEDS_FIXES"}
19
19
  TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
20
- VALID_STAGE_RESULTS = REVIEW_RESULTS | TEST_RESULTS
20
+ PLAN_RESULTS = {"PLAN_READY", "PLAN_BLOCKED"}
21
+ IMPLEMENT_RESULTS = {"IMPLEMENTED", "IMPLEMENT_REPAIR", "IMPLEMENT_BLOCKED"}
22
+ RETROSPECTIVE_RESULTS = {"RETRO_COMPLETE", "RETRO_BLOCKED"}
23
+ COMMIT_RESULTS = {"COMMIT_PENDING", "COMMITTED", "COMMIT_BLOCKED"}
24
+ VALID_STAGE_RESULTS = (
25
+ REVIEW_RESULTS
26
+ | TEST_RESULTS
27
+ | PLAN_RESULTS
28
+ | IMPLEMENT_RESULTS
29
+ | RETROSPECTIVE_RESULTS
30
+ | COMMIT_RESULTS
31
+ )
32
+ STAGE_RESULT_OWNERS = {
33
+ "prizmkit-plan": PLAN_RESULTS,
34
+ "prizmkit-implement": IMPLEMENT_RESULTS,
35
+ "prizmkit-code-review": REVIEW_RESULTS,
36
+ "prizmkit-test": TEST_RESULTS,
37
+ "prizmkit-retrospective": RETROSPECTIVE_RESULTS,
38
+ "prizmkit-committer": COMMIT_RESULTS,
39
+ }
40
+ WORKFLOW_STAGE_SKILLS = {
41
+ "plan": "prizmkit-plan",
42
+ "implement": "prizmkit-implement",
43
+ "code-review": "prizmkit-code-review",
44
+ "test": "prizmkit-test",
45
+ "retrospective": "prizmkit-retrospective",
46
+ "committer": "prizmkit-committer",
47
+ }
48
+ SKILL_WORKFLOW_STAGES = {
49
+ skill: stage for stage, skill in WORKFLOW_STAGE_SKILLS.items()
50
+ }
21
51
  VALID_TERMINAL_STATUSES = {"WORKFLOW_COMPLETED", "WORKFLOW_BLOCKED"}
22
52
  VALID_REPAIR_SCOPES = {
23
53
  "production",
@@ -270,6 +300,23 @@ def validate_checkpoint_data(
270
300
  stage_result = raw_step.get("stage_result")
271
301
  if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
272
302
  return _invalid(checkpoint_path, exists, "invalid_stage_result", f"Step {step_id} has invalid stage_result", data)
303
+ if stage_result is not None and not stage_result_allowed_for_skill(skill, stage_result):
304
+ return _invalid(
305
+ checkpoint_path,
306
+ exists,
307
+ "stage_result_owner_mismatch",
308
+ f"Step {step_id} cannot record {stage_result} for {skill}",
309
+ data,
310
+ )
311
+ expected_status = _expected_status_for_result(stage_result)
312
+ if expected_status is not None and status != expected_status:
313
+ return _invalid(
314
+ checkpoint_path,
315
+ exists,
316
+ "stage_result_status_mismatch",
317
+ f"Step {step_id} requires status={expected_status} for {stage_result}",
318
+ data,
319
+ )
273
320
 
274
321
  step = CheckpointStep(
275
322
  id=step_id,
@@ -401,6 +448,47 @@ def semantic_state_key(workflow_type: str) -> str:
401
448
  return SEMANTIC_STATE_KEYS.get(workflow_type, "l4_state")
402
449
 
403
450
 
451
+ def expected_status_for_stage_result(stage_result: str | None) -> str | None:
452
+ """Return the lifecycle status required by one domain stage result."""
453
+ return _expected_status_for_result(stage_result)
454
+
455
+
456
+ def stage_result_allowed_for_skill(skill: str, stage_result: str | None) -> bool:
457
+ """Return whether one stage owns the supplied domain result."""
458
+ if stage_result is None:
459
+ return True
460
+ allowed_results = STAGE_RESULT_OWNERS.get(skill)
461
+ return allowed_results is not None and stage_result in allowed_results
462
+
463
+
464
+ def _expected_status_for_result(stage_result: str | None) -> str | None:
465
+ if stage_result is None:
466
+ return None
467
+ if stage_result == "COMMIT_PENDING":
468
+ return "in_progress"
469
+ if stage_result in {
470
+ "REVIEW_PASS",
471
+ "TEST_PASS",
472
+ "PLAN_READY",
473
+ "IMPLEMENTED",
474
+ "RETRO_COMPLETE",
475
+ "COMMITTED",
476
+ }:
477
+ return "completed"
478
+ if stage_result in {
479
+ "REVIEW_NEEDS_FIXES",
480
+ "TEST_NEEDS_FIXES",
481
+ "TEST_BLOCKED",
482
+ "PLAN_BLOCKED",
483
+ "IMPLEMENT_REPAIR",
484
+ "IMPLEMENT_BLOCKED",
485
+ "RETRO_BLOCKED",
486
+ "COMMIT_BLOCKED",
487
+ }:
488
+ return "failed"
489
+ return None
490
+
491
+
404
492
  def _progress_error(steps: list[CheckpointStep]) -> str:
405
493
  active_seen = False
406
494
  for step in steps:
@@ -435,6 +523,32 @@ def _derive_semantic_state(
435
523
  repair_scope = raw_state.get("repair_scope")
436
524
  if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
437
525
  return _semantic_error(workflow_type, current_step, "invalid_repair_scope", "repair_scope is not a safe shared scope")
526
+ semantic_status = raw_state.get("status")
527
+ if semantic_status is not None and semantic_status not in VALID_CHECKPOINT_STATUSES:
528
+ return _semantic_error(workflow_type, current_step, "invalid_semantic_status", "semantic status is not a lifecycle status")
529
+ semantic_stage_result = raw_state.get("stage_result")
530
+ if semantic_stage_result is not None and semantic_stage_result not in VALID_STAGE_RESULTS:
531
+ return _semantic_error(workflow_type, current_step, "invalid_semantic_stage_result", "semantic stage_result is not recognized")
532
+ expected_semantic_status = _expected_status_for_result(semantic_stage_result)
533
+ if semantic_status is not None and expected_semantic_status is not None and semantic_status != expected_semantic_status:
534
+ return _semantic_error(
535
+ workflow_type,
536
+ current_step,
537
+ "semantic_stage_result_status_mismatch",
538
+ f"Semantic {semantic_stage_result} requires status={expected_semantic_status}",
539
+ )
540
+ semantic_stage = raw_state.get("stage")
541
+ if semantic_stage is not None and (not isinstance(semantic_stage, str) or not semantic_stage.strip()):
542
+ return _semantic_error(workflow_type, current_step, "invalid_semantic_stage", "semantic stage must be a non-empty string")
543
+ if semantic_stage_result is not None:
544
+ semantic_skill = WORKFLOW_STAGE_SKILLS.get(semantic_stage, semantic_stage)
545
+ if not stage_result_allowed_for_skill(semantic_skill, semantic_stage_result):
546
+ return _semantic_error(
547
+ workflow_type,
548
+ current_step,
549
+ "semantic_stage_result_owner_mismatch",
550
+ f"Semantic stage {semantic_stage} cannot record {semantic_stage_result}",
551
+ )
438
552
  terminal_status = raw_state.get("terminal_status")
439
553
  if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
440
554
  return _semantic_error(workflow_type, current_step, "invalid_terminal_status", "terminal_status is not recognized")
@@ -475,9 +589,15 @@ def _derive_semantic_state(
475
589
  current_stage = current_step.skill if current_step is not None else None
476
590
  next_stage = current_stage
477
591
  if current_step is not None and current_step.status == "in_progress":
478
- later = steps[current_step.index + 1:]
479
- next_step = next((step for step in later if step.status not in TERMINAL_STEP_STATUSES), None)
480
- next_stage = next_step.skill if next_step is not None else None
592
+ next_stage = current_stage
593
+ if (
594
+ current_step is not None
595
+ and current_step.status == "failed"
596
+ and current_step.stage_result == "REVIEW_NEEDS_FIXES"
597
+ and raw_state.get("stage_result") == "REVIEW_NEEDS_FIXES"
598
+ and raw_state.get("next_stage") == "prizmkit-implement"
599
+ ):
600
+ next_stage = "prizmkit-implement"
481
601
  if terminal_status is not None:
482
602
  current_stage = None
483
603
  next_stage = None
@@ -13,6 +13,7 @@ from .sessions import AISessionResult
13
13
 
14
14
  CONTEXT_OVERFLOW = "context_overflow"
15
15
  WORKFLOW_BLOCKED = "workflow_blocked"
16
+ WORKFLOW_SKIPPED = "workflow_skipped"
16
17
  STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
17
18
  CONTEXT_MARKERS = (
18
19
  "context_overflow",
@@ -50,8 +51,9 @@ def classify_session(
50
51
  fingerprint = progress_fingerprint(project_root, prompt)
51
52
  checkpoint = _workflow_checkpoint(prompt, project_root)
52
53
  if checkpoint is not None and not checkpoint.valid:
54
+ session_status = WORKFLOW_SKIPPED if _is_terminal_success(result) else "crashed"
53
55
  return SessionClassification(
54
- "crashed",
56
+ session_status,
55
57
  reason=f"invalid_workflow_checkpoint:{checkpoint.error_code}",
56
58
  progress_fingerprint=fingerprint,
57
59
  checkpoint_state=checkpoint.semantic_snapshot,
@@ -79,6 +81,16 @@ def classify_session(
79
81
  )
80
82
 
81
83
  semantic = checkpoint.semantic_snapshot if checkpoint is not None else {}
84
+ if _is_terminal_success(result):
85
+ reason = "no_semantic_completion"
86
+ if checkpoint is not None and checkpoint.semantic.error_code:
87
+ reason = f"{reason}:{checkpoint.semantic.error_code}"
88
+ return SessionClassification(
89
+ WORKFLOW_SKIPPED,
90
+ reason=reason,
91
+ progress_fingerprint=fingerprint,
92
+ checkpoint_state=semantic,
93
+ )
82
94
  if _is_context_overflow(result):
83
95
  no_progress_count = _next_no_progress_count(previous_fingerprint, fingerprint, previous_no_progress_count)
84
96
  if no_progress_count >= 2:
@@ -862,7 +862,10 @@ def _emit_interruption_cleanup_failure(result, message: str, recovery: str) -> N
862
862
 
863
863
  def _is_retryable_ai_outcome(session_status: str, new_status: str) -> bool:
864
864
  """Return whether a persisted post-launch outcome should continue in-process."""
865
- if session_status in {"success", "stalled_context_continuation", "workflow_blocked"}:
865
+ if session_status in {
866
+ "success", "stalled_context_continuation", "workflow_blocked",
867
+ "workflow_skipped",
868
+ }:
866
869
  return False
867
870
  return new_status in {"in_progress", "pending"} and session_status in {
868
871
  "context_overflow",
@@ -9,6 +9,12 @@ from typing import Any
9
9
 
10
10
  TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
11
11
  INDEPENDENT_REVIEW_STATUSES = {"completed", "downgraded", "not_applicable"}
12
+ _RESULT_LINE_PATTERN = re.compile(
13
+ r"(?m)^- Result:\s*(TEST_PASS|TEST_NEEDS_FIXES|TEST_BLOCKED)\s*$"
14
+ )
15
+ _BARE_RESULT_LINE_PATTERN = re.compile(
16
+ r"(?m)^(TEST_PASS|TEST_NEEDS_FIXES|TEST_BLOCKED)\s*$"
17
+ )
12
18
  _RESULT_KEYS = {
13
19
  "schema_version",
14
20
  "result",
@@ -128,18 +134,22 @@ def valid_test_artifact_pair(
128
134
 
129
135
 
130
136
  def _report_final_result(report: str) -> str | None:
131
- """Extract the result declared in the report's Final Result section."""
132
- match = re.search(
133
- r"(?ms)^## Final Result\s*$\n(?P<body>.*?)(?=^## |\Z)",
137
+ """Extract one unambiguous result from a supported terminal section."""
138
+ declared_results: list[str] = []
139
+ for match in re.finditer(
140
+ r"(?ms)^## (?P<title>Final Result|Terminal Result|Final Decision)\s*$\n"
141
+ r"(?P<body>.*?)(?=^## |\Z)",
134
142
  report,
135
- )
136
- if not match:
143
+ ):
144
+ body = match.group("body")
145
+ result_lines = _RESULT_LINE_PATTERN.findall(body)
146
+ if match.group("title") == "Final Decision":
147
+ result_lines.extend(_BARE_RESULT_LINE_PATTERN.findall(body))
148
+ declared_results.extend(result_lines)
149
+
150
+ if not declared_results or len(set(declared_results)) != 1:
137
151
  return None
138
- result = re.search(
139
- r"(?m)^- Result:\s*(TEST_PASS|TEST_NEEDS_FIXES|TEST_BLOCKED)\s*$",
140
- match.group("body"),
141
- )
142
- return result.group(1) if result else None
152
+ return declared_results[0]
143
153
 
144
154
 
145
155
  def _integer_in_range(value: object, minimum: int, maximum: int) -> bool:
@@ -150,7 +150,9 @@ def determine_status(data):
150
150
  """Determine the single-line status string from the parsed data.
151
151
 
152
152
  Returns one of: success, partial_resumable, partial_not_resumable,
153
- failed, commit_missing, docs_missing, merge_conflict, context_overflow.
153
+ failed, crashed, timed_out, infra_error, commit_missing, docs_missing,
154
+ merge_conflict, context_overflow, stalled_context_continuation,
155
+ workflow_blocked, workflow_skipped.
154
156
  """
155
157
  status = data.get("status", "")
156
158
 
@@ -174,12 +176,20 @@ def determine_status(data):
174
176
  return "crashed"
175
177
  elif status == "timed_out":
176
178
  return "timed_out"
179
+ elif status == "infra_error":
180
+ return "infra_error"
181
+ elif status == "stalled_context_continuation":
182
+ return "stalled_context_continuation"
177
183
  elif status == "commit_missing":
178
184
  return "commit_missing"
179
185
  elif status == "docs_missing":
180
186
  return "docs_missing"
181
187
  elif status == "merge_conflict":
182
188
  return "merge_conflict"
189
+ elif status == "workflow_blocked":
190
+ return "workflow_blocked"
191
+ elif status == "workflow_skipped":
192
+ return "workflow_skipped"
183
193
  else:
184
194
  # Unknown status value — treat as crashed
185
195
  return "crashed"
@@ -717,7 +717,7 @@ def route_feature_repair(stage_result, repair_scope=None, repair_round=0):
717
717
 
718
718
 
719
719
  def feature_checkpoint_metadata(artifact_dir, stage="plan",
720
- status="PLAN_READY", stage_result=None,
720
+ status="pending", stage_result=None,
721
721
  repair_scope=None, repair_round=0,
722
722
  next_stage="prizmkit-implement",
723
723
  resume_from="prizmkit-implement"):
@@ -1478,7 +1478,7 @@ def main():
1478
1478
  checkpoint["feature_state"] = feature_checkpoint_metadata(
1479
1479
  os.path.join(".prizmkit", "specs", feature_slug),
1480
1480
  stage="plan",
1481
- status="PLAN_READY",
1481
+ status="pending",
1482
1482
  stage_result=None,
1483
1483
  next_stage="prizmkit-implement",
1484
1484
  resume_from="prizmkit-implement",
@@ -41,7 +41,7 @@ def route_bugfix_repair(stage_result, repair_scope=None, repair_round=0):
41
41
 
42
42
 
43
43
  def bugfix_checkpoint_metadata(artifact_dir, stage="plan",
44
- status="PLAN_READY", stage_result=None,
44
+ status="pending", stage_result=None,
45
45
  repair_scope=None, repair_round=0,
46
46
  next_stage="prizmkit-implement",
47
47
  resume_from="prizmkit-implement"):
@@ -536,7 +536,7 @@ def main():
536
536
  checkpoint["bugfix_state"] = bugfix_checkpoint_metadata(
537
537
  replacements["{{ARTIFACT_DIR}}"],
538
538
  stage="plan",
539
- status="PLAN_READY",
539
+ status="pending",
540
540
  stage_result=None,
541
541
  repair_scope=None,
542
542
  repair_round=0,
@@ -41,7 +41,7 @@ def route_refactor_repair(stage_result, repair_scope=None, repair_round=0):
41
41
 
42
42
 
43
43
  def refactor_checkpoint_metadata(artifact_dir, stage="plan",
44
- status="PLAN_READY", stage_result=None,
44
+ status="pending", stage_result=None,
45
45
  repair_scope=None, repair_round=0,
46
46
  next_stage="prizmkit-implement",
47
47
  resume_from="prizmkit-implement"):
@@ -554,7 +554,7 @@ def main():
554
554
  checkpoint["refactor_state"] = refactor_checkpoint_metadata(
555
555
  replacements["{{ARTIFACT_DIR}}"],
556
556
  stage="plan",
557
- status="PLAN_READY",
557
+ status="pending",
558
558
  stage_result=None,
559
559
  repair_scope=None,
560
560
  repair_round=0,
@@ -14,6 +14,10 @@ _PIPELINE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
14
14
  if _PIPELINE_ROOT not in sys.path:
15
15
  sys.path.insert(0, _PIPELINE_ROOT)
16
16
 
17
+ from prizmkit_runtime.checkpoint_state import ( # noqa: E402
18
+ VALID_CHECKPOINT_STATUSES,
19
+ expected_status_for_stage_result,
20
+ )
17
21
  from prizmkit_runtime.test_result import valid_test_artifact_pair # noqa: E402
18
22
 
19
23
 
@@ -95,12 +99,19 @@ def route_pipeline_repair(stage_result, repair_scope=None, repair_round=0):
95
99
 
96
100
 
97
101
  def pipeline_checkpoint_metadata(artifact_dir, stage="plan",
98
- status="PLAN_READY", stage_result=None,
102
+ status="pending", stage_result=None,
99
103
  repair_scope=None, repair_round=0,
100
104
  next_stage="prizmkit-implement",
101
105
  resume_from="prizmkit-implement",
102
106
  blocked_reason=None, terminal_status=None):
103
107
  """Build family-neutral pipeline metadata without replacing skill state."""
108
+ if status not in VALID_CHECKPOINT_STATUSES:
109
+ raise ValueError("Invalid checkpoint lifecycle status: {}".format(status))
110
+ expected_status = expected_status_for_stage_result(stage_result)
111
+ if expected_status is None and stage_result is not None:
112
+ raise ValueError("Invalid checkpoint stage result: {}".format(stage_result))
113
+ if expected_status is not None and status != expected_status:
114
+ raise ValueError("{} requires status={}".format(stage_result, expected_status))
104
115
  current_stage = None if terminal_status else stage
105
116
  semantic_next_stage = None if terminal_status else next_stage
106
117
  return {
@@ -47,6 +47,7 @@ SESSION_STATUS_VALUES = [
47
47
  "context_overflow",
48
48
  "stalled_context_continuation",
49
49
  "workflow_blocked",
50
+ "workflow_skipped",
50
51
  "commit_missing",
51
52
  "docs_missing",
52
53
  "merge_conflict",
@@ -593,6 +594,20 @@ def action_update(args, bug_list_path, state_dir):
593
594
  if err:
594
595
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
595
596
  return
597
+ elif session_status == "workflow_skipped":
598
+ # A completed AI CLI session without authoritative workflow evidence is
599
+ # skipped, not crashed. Preserve diagnostics and spend no retry budget.
600
+ new_status = "skipped"
601
+ bs["continuation_pending"] = False
602
+ bs.pop("continuation_reason", None)
603
+ bs.pop("terminal_status", None)
604
+ bs.pop("blocked_reason", None)
605
+ bs["needs_attention"] = True
606
+ bs["skipped_reason"] = "workflow_completion_evidence_invalid"
607
+ err = update_bug_in_list(bug_list_path, bug_id, new_status)
608
+ if err:
609
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
610
+ return
596
611
  elif session_status == "workflow_blocked":
597
612
  # Semantic terminal state: preserve partial work without consuming any retry budget.
598
613
  new_status = "needs_info"
@@ -677,6 +692,11 @@ def action_update(args, bug_list_path, state_dir):
677
692
  summary["restart_policy"] = "context_overflow_continuation"
678
693
  summary.update(continuation_metadata_summary(bs))
679
694
  summary["artifacts_preserved"] = True
695
+ elif session_status == "workflow_skipped":
696
+ summary["restart_policy"] = "terminal_skipped"
697
+ summary.update(continuation_metadata_summary(bs))
698
+ summary["checkpoint_state"] = semantic_checkpoint
699
+ summary["artifacts_preserved"] = True
680
700
  elif session_status == "workflow_blocked":
681
701
  summary["restart_policy"] = "terminal_blocked"
682
702
  summary.update(continuation_metadata_summary(bs))
@@ -15,12 +15,16 @@ if _PIPELINE_ROOT not in sys.path:
15
15
  from prizmkit_runtime.checkpoint_state import ( # noqa: E402
16
16
  MAX_OUTER_REPAIR_ROUNDS,
17
17
  REVIEW_RESULTS,
18
+ SKILL_WORKFLOW_STAGES,
18
19
  STEP_ID_PATTERN,
19
20
  TERMINAL_STEP_STATUSES,
20
21
  TEST_RESULTS,
21
22
  VALID_CHECKPOINT_STATUSES,
23
+ VALID_STAGE_RESULTS,
22
24
  VALID_REPAIR_SCOPES,
23
25
  VALID_TERMINAL_STATUSES,
26
+ expected_status_for_stage_result,
27
+ stage_result_allowed_for_skill,
24
28
  load_checkpoint_state,
25
29
  semantic_state_key,
26
30
  validate_checkpoint_data,
@@ -134,6 +138,12 @@ def _validate_semantic_update(step, semantic_update):
134
138
  return "Review verdicts may only be recorded on prizmkit-code-review"
135
139
  if stage_result in TEST_RESULTS and skill != "prizmkit-test":
136
140
  return "Test verdicts may only be recorded on prizmkit-test"
141
+ if not stage_result_allowed_for_skill(skill, stage_result):
142
+ return "{} cannot record stage result {}".format(skill, stage_result)
143
+ expected_status = expected_status_for_stage_result(stage_result)
144
+ if expected_status is not None and semantic_update.get("status") is not None:
145
+ if semantic_update["status"] != expected_status:
146
+ return "{} requires status={}".format(stage_result, expected_status)
137
147
  repair_scope = semantic_update.get("repair_scope")
138
148
  if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
139
149
  return "Invalid repair_scope '{}'".format(repair_scope)
@@ -150,7 +160,7 @@ def _validate_semantic_update(step, semantic_update):
150
160
 
151
161
  def _apply_repair_policy(l4_state, stage_result, repair_scope):
152
162
  if stage_result not in {"REVIEW_NEEDS_FIXES", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}:
153
- return
163
+ return False
154
164
  current_round = l4_state.get("repair_round", 0)
155
165
  if not isinstance(current_round, int) or isinstance(current_round, bool):
156
166
  current_round = MAX_OUTER_REPAIR_ROUNDS
@@ -165,7 +175,7 @@ def _apply_repair_policy(l4_state, stage_result, repair_scope):
165
175
  current_stage=None,
166
176
  next_stage=None,
167
177
  )
168
- return
178
+ return False
169
179
  if current_round >= MAX_OUTER_REPAIR_ROUNDS:
170
180
  l4_state.update(
171
181
  repair_round=MAX_OUTER_REPAIR_ROUNDS,
@@ -174,7 +184,7 @@ def _apply_repair_policy(l4_state, stage_result, repair_scope):
174
184
  current_stage=None,
175
185
  next_stage=None,
176
186
  )
177
- return
187
+ return False
178
188
  if repair_scope not in VALID_REPAIR_SCOPES:
179
189
  l4_state.update(
180
190
  terminal_status="WORKFLOW_BLOCKED",
@@ -182,14 +192,15 @@ def _apply_repair_policy(l4_state, stage_result, repair_scope):
182
192
  current_stage=None,
183
193
  next_stage=None,
184
194
  )
185
- return
195
+ return False
186
196
  l4_state["repair_round"] = current_round + 1
187
197
  l4_state["terminal_status"] = None
188
198
  l4_state["blocked_reason"] = None
189
199
  l4_state["next_stage"] = "prizmkit-implement"
200
+ return True
190
201
 
191
202
 
192
- def _sync_cursor(data, l4_state):
203
+ def _sync_cursor(data, l4_state, preserve_next_stage=False):
193
204
  active = next(
194
205
  (step for step in data["steps"] if step.get("status") not in TERMINAL_STEP_STATUSES),
195
206
  None,
@@ -198,8 +209,9 @@ def _sync_cursor(data, l4_state):
198
209
  l4_state["current_stage"] = None
199
210
  l4_state["next_stage"] = None
200
211
  return
212
+ routed_stage = l4_state.get("next_stage") if preserve_next_stage else None
201
213
  l4_state["current_stage"] = active.get("skill") if active else None
202
- l4_state["next_stage"] = active.get("skill") if active else None
214
+ l4_state["next_stage"] = routed_stage or (active.get("skill") if active else None)
203
215
 
204
216
  def _checkpoint_project_root(checkpoint_path):
205
217
  """Resolve the project root from a conventional .prizmkit checkpoint path."""
@@ -296,6 +308,12 @@ def update_checkpoint(
296
308
  stage_result = stage_result or step.get("stage_result")
297
309
  if stage_result != "TEST_PASS":
298
310
  return {"ok": False, "error": "Test completion requires TEST_PASS"}
311
+ expected_status = expected_status_for_stage_result(stage_result)
312
+ if expected_status is not None and new_status != expected_status:
313
+ return {
314
+ "ok": False,
315
+ "error": "{} requires status={}".format(stage_result, expected_status),
316
+ }
299
317
 
300
318
  old_status = step.get("status", "unknown")
301
319
  step["status"] = new_status
@@ -303,6 +321,8 @@ def update_checkpoint(
303
321
  step["note"] = note
304
322
  if stage_result:
305
323
  step["stage_result"] = stage_result
324
+ else:
325
+ step.pop("stage_result", None)
306
326
 
307
327
  workflow_type = str(data.get("workflow_type") or "")
308
328
  state_key = semantic_state_key(workflow_type)
@@ -320,10 +340,19 @@ def update_checkpoint(
320
340
  l4_state[key] = semantic_update[key]
321
341
  l4_state["schema_version"] = 1
322
342
  l4_state["orchestrator"] = "prizmkit-l4"
343
+ skill = step.get("skill")
344
+ l4_state["stage"] = SKILL_WORKFLOW_STAGES.get(skill, skill)
345
+ l4_state["status"] = new_status
323
346
  if stage_result:
324
347
  l4_state["stage_result"] = stage_result
325
- _apply_repair_policy(l4_state, stage_result, semantic_update.get("repair_scope") or l4_state.get("repair_scope"))
326
- _sync_cursor(data, l4_state)
348
+ else:
349
+ l4_state.pop("stage_result", None)
350
+ repair_route_selected = _apply_repair_policy(
351
+ l4_state,
352
+ stage_result,
353
+ semantic_update.get("repair_scope") or l4_state.get("repair_scope"),
354
+ )
355
+ _sync_cursor(data, l4_state, preserve_next_stage=repair_route_selected)
327
356
  _finalize_semantics(checkpoint_path, data, state_key)
328
357
 
329
358
  final_state = validate_checkpoint_data(
@@ -362,7 +391,7 @@ def main():
362
391
  parser.add_argument("--step", required=True)
363
392
  parser.add_argument("--status", required=True, choices=sorted(VALID_STATUSES))
364
393
  parser.add_argument("--note", default=None)
365
- parser.add_argument("--stage-result", choices=sorted(REVIEW_RESULTS | TEST_RESULTS), default=None)
394
+ parser.add_argument("--stage-result", choices=sorted(VALID_STAGE_RESULTS), default=None)
366
395
  parser.add_argument("--repair-scope", choices=sorted(VALID_REPAIR_SCOPES), default=None)
367
396
  parser.add_argument("--repair-round", type=int, default=None)
368
397
  parser.add_argument("--blocked-reason", default=None)
@@ -51,6 +51,7 @@ SESSION_STATUS_VALUES = [
51
51
  "context_overflow",
52
52
  "stalled_context_continuation",
53
53
  "workflow_blocked",
54
+ "workflow_skipped",
54
55
  "commit_missing",
55
56
  "docs_missing",
56
57
  "merge_conflict",
@@ -1021,6 +1022,20 @@ def action_update(args, feature_list_path, state_dir):
1021
1022
  if err:
1022
1023
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
1023
1024
  return
1025
+ elif session_status == "workflow_skipped":
1026
+ # A completed AI CLI session without authoritative workflow evidence is
1027
+ # skipped, not crashed. Preserve diagnostics and spend no retry budget.
1028
+ new_status = "skipped"
1029
+ fs["continuation_pending"] = False
1030
+ fs.pop("continuation_reason", None)
1031
+ fs.pop("terminal_status", None)
1032
+ fs.pop("blocked_reason", None)
1033
+ fs["needs_attention"] = True
1034
+ fs["skipped_reason"] = "workflow_completion_evidence_invalid"
1035
+ err = update_feature_in_list(feature_list_path, feature_id, new_status)
1036
+ if err:
1037
+ error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
1038
+ return
1024
1039
  elif session_status == "workflow_blocked":
1025
1040
  # Semantic terminal state: preserve partial work without consuming any retry budget.
1026
1041
  new_status = "failed"
@@ -1119,6 +1134,11 @@ def action_update(args, feature_list_path, state_dir):
1119
1134
  summary["restart_policy"] = "context_overflow_continuation"
1120
1135
  summary.update(continuation_metadata_summary(fs))
1121
1136
  summary["artifacts_preserved"] = True
1137
+ elif session_status == "workflow_skipped":
1138
+ summary["restart_policy"] = "terminal_skipped"
1139
+ summary.update(continuation_metadata_summary(fs))
1140
+ summary["checkpoint_state"] = semantic_checkpoint
1141
+ summary["artifacts_preserved"] = True
1122
1142
  elif session_status == "workflow_blocked":
1123
1143
  summary["restart_policy"] = "terminal_blocked"
1124
1144
  summary.update(continuation_metadata_summary(fs))
@@ -48,6 +48,7 @@ SESSION_STATUS_VALUES = [
48
48
  "context_overflow",
49
49
  "stalled_context_continuation",
50
50
  "workflow_blocked",
51
+ "workflow_skipped",
51
52
  "commit_missing",
52
53
  "docs_missing",
53
54
  "merge_conflict",
@@ -597,6 +598,20 @@ def action_update(args, refactor_list_path, state_dir):
597
598
  if err:
598
599
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
599
600
  return
601
+ elif session_status == "workflow_skipped":
602
+ # A completed AI CLI session without authoritative workflow evidence is
603
+ # skipped, not crashed. Preserve diagnostics and spend no retry budget.
604
+ new_status = "skipped"
605
+ rs["continuation_pending"] = False
606
+ rs.pop("continuation_reason", None)
607
+ rs.pop("terminal_status", None)
608
+ rs.pop("blocked_reason", None)
609
+ rs["needs_attention"] = True
610
+ rs["skipped_reason"] = "workflow_completion_evidence_invalid"
611
+ err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
612
+ if err:
613
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
614
+ return
600
615
  elif session_status == "workflow_blocked":
601
616
  # Semantic terminal state: preserve partial work without consuming any retry budget.
602
617
  new_status = "failed"
@@ -690,6 +705,11 @@ def action_update(args, refactor_list_path, state_dir):
690
705
  summary["restart_policy"] = "context_overflow_continuation"
691
706
  summary.update(continuation_metadata_summary(rs))
692
707
  summary["artifacts_preserved"] = True
708
+ elif session_status == "workflow_skipped":
709
+ summary["restart_policy"] = "terminal_skipped"
710
+ summary.update(continuation_metadata_summary(rs))
711
+ summary["checkpoint_state"] = semantic_checkpoint
712
+ summary["artifacts_preserved"] = True
693
713
  elif session_status == "workflow_blocked":
694
714
  summary["restart_policy"] = "terminal_blocked"
695
715
  summary.update(continuation_metadata_summary(rs))