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
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.130",
3
- "bundledAt": "2026-07-15T08:17:59.816Z",
4
- "bundledFrom": "81d54a2"
2
+ "frameworkVersion": "1.1.131",
3
+ "bundledAt": "2026-07-18T08:21:47.386Z",
4
+ "bundledFrom": "c1378b1"
5
5
  }
@@ -30,7 +30,7 @@
30
30
  # Unset = auto-detect from estimated_complexity
31
31
 
32
32
  # ─── Refactor Pipeline Only ──────────────────────────────────────────
33
- # STRICT_BEHAVIOR_CHECK=1 # Run full test suite after each refactor (1=on, 0=off)
33
+ # STRICT_BEHAVIOR_CHECK=1 # Enable strict behavior-preservation gates for refactor tasks (1=on, 0=off)
34
34
 
35
35
  # ─── Branch Management ────────────────────────────────────────────────
36
36
  # AUTO_PUSH=0 # Auto-push to remote after successful task (0=off, 1=on)
@@ -1,9 +1,11 @@
1
- """Shared workflow-checkpoint.json reader and validator."""
1
+ """Shared workflow-checkpoint.json reader and semantic validator."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
+ import subprocess
8
+ import sys
7
9
  from dataclasses import dataclass, field
8
10
  from pathlib import Path
9
11
  from typing import Any, Mapping
@@ -11,6 +13,57 @@ from typing import Any, Mapping
11
13
  VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
12
14
  TERMINAL_STEP_STATUSES = {"completed", "skipped"}
13
15
  STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
16
+ MAX_OUTER_REPAIR_ROUNDS = 3
17
+
18
+ REVIEW_RESULTS = {"REVIEW_PASS", "REVIEW_NEEDS_FIXES"}
19
+ TEST_RESULTS = {"TEST_PASS", "TEST_FAIL", "TEST_BLOCKED"}
20
+ VALID_STAGE_RESULTS = REVIEW_RESULTS | TEST_RESULTS
21
+ VALID_TERMINAL_STATUSES = {"WORKFLOW_COMPLETED", "WORKFLOW_BLOCKED"}
22
+ VALID_REPAIR_SCOPES = {
23
+ "production",
24
+ "runtime",
25
+ "schema",
26
+ "dependency",
27
+ "public-interface",
28
+ "test-infrastructure",
29
+ }
30
+ SEMANTIC_STATE_KEYS = {
31
+ "feature-pipeline": "feature_state",
32
+ "bugfix-pipeline": "bugfix_state",
33
+ "refactor-pipeline": "refactor_state",
34
+ }
35
+ FAMILY_REQUIRED_STAGES = {
36
+ "feature-pipeline": (
37
+ "prizmkit-implement",
38
+ "prizmkit-code-review",
39
+ "prizmkit-test",
40
+ "prizmkit-retrospective",
41
+ "prizmkit-committer",
42
+ "completion-summary",
43
+ ),
44
+ "bugfix-pipeline": (
45
+ "prizmkit-implement",
46
+ "prizmkit-code-review",
47
+ "prizmkit-test",
48
+ "prizmkit-retrospective",
49
+ "prizmkit-committer",
50
+ "bug-report",
51
+ ),
52
+ "refactor-pipeline": (
53
+ "prizmkit-implement",
54
+ "prizmkit-code-review",
55
+ "prizmkit-test",
56
+ "prizmkit-retrospective",
57
+ "prizmkit-committer",
58
+ "refactor-report",
59
+ "completion-summary",
60
+ ),
61
+ }
62
+ EVIDENCE_FILENAMES = {
63
+ "manifest": "manifest.json",
64
+ "verdict": "verdict.json",
65
+ "validation": "validation.json",
66
+ }
14
67
 
15
68
 
16
69
  @dataclass(frozen=True)
@@ -23,6 +76,8 @@ class CheckpointStep:
23
76
  status: str
24
77
  depends_on: str | None = None
25
78
  required_artifacts: tuple[str, ...] = ()
79
+ stage_result: str | None = None
80
+ authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
26
81
  index: int = 0
27
82
 
28
83
  @property
@@ -36,6 +91,42 @@ class CheckpointStep:
36
91
  return f"{self.id} ({self.skill})"
37
92
 
38
93
 
94
+ @dataclass(frozen=True)
95
+ class SemanticCheckpoint:
96
+ """Provider-neutral L4 state shared by runners, status, and recovery."""
97
+
98
+ workflow_type: str = ""
99
+ current_stage: str | None = None
100
+ next_stage: str | None = None
101
+ stage_result: str | None = None
102
+ repair_scope: str | None = None
103
+ repair_round: int = 0
104
+ authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
105
+ blocked_reason: str | None = None
106
+ terminal_status: str | None = None
107
+ sequence_valid: bool = False
108
+ evidence_valid: bool = False
109
+ artifacts_valid: bool = False
110
+ completion_ready: bool = False
111
+ complete: bool = False
112
+ blocked: bool = False
113
+ error_code: str = ""
114
+ error_message: str = ""
115
+
116
+ def as_dict(self) -> dict[str, object]:
117
+ """Return the additive runtime-state representation."""
118
+ return {
119
+ "current_stage": self.current_stage,
120
+ "next_stage": self.next_stage,
121
+ "stage_result": self.stage_result,
122
+ "repair_scope": self.repair_scope,
123
+ "repair_round": self.repair_round,
124
+ "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
125
+ "blocked_reason": self.blocked_reason,
126
+ "terminal_status": self.terminal_status,
127
+ }
128
+
129
+
39
130
  @dataclass(frozen=True)
40
131
  class CheckpointState:
41
132
  """Result of loading and validating one workflow checkpoint."""
@@ -50,6 +141,7 @@ class CheckpointState:
50
141
  all_completed: bool = False
51
142
  duplicate_skills: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
52
143
  raw_data: Mapping[str, Any] | None = None
144
+ semantic: SemanticCheckpoint = field(default_factory=SemanticCheckpoint)
53
145
 
54
146
  @property
55
147
  def current_step_name(self) -> str:
@@ -69,17 +161,38 @@ class CheckpointState:
69
161
 
70
162
  @property
71
163
  def cursor_signature(self) -> str:
72
- """Return a stable progress fingerprint cursor string."""
164
+ """Return a stable semantic progress fingerprint cursor string."""
73
165
  if not self.valid:
74
166
  return f"invalid:{self.error_code}" if self.exists else ""
75
- if self.all_completed:
76
- return "complete"
167
+ if self.semantic.complete:
168
+ return "complete:WORKFLOW_COMPLETED"
169
+ if self.semantic.blocked:
170
+ return f"blocked:{self.semantic.blocked_reason or 'unspecified'}"
77
171
  if self.current_step is None:
78
- return ""
172
+ return "complete"
79
173
  return f"{self.current_step.id}:{self.current_step.skill}:{self.current_step.status}"
80
174
 
175
+ @property
176
+ def semantic_complete(self) -> bool:
177
+ """Return whether this checkpoint authoritatively proves task completion."""
178
+ return self.valid and self.semantic.complete
81
179
 
82
- def load_checkpoint_state(path: str | Path | None) -> CheckpointState:
180
+ @property
181
+ def semantic_blocked(self) -> bool:
182
+ """Return whether this checkpoint has a deterministic blocked terminal state."""
183
+ return self.valid and self.semantic.blocked
184
+
185
+ @property
186
+ def semantic_snapshot(self) -> dict[str, object]:
187
+ """Return the normalized state persisted by runtime consumers."""
188
+ return self.semantic.as_dict()
189
+
190
+
191
+ def load_checkpoint_state(
192
+ path: str | Path | None,
193
+ *,
194
+ project_root: str | Path | None = None,
195
+ ) -> CheckpointState:
83
196
  """Load and validate one workflow-checkpoint.json file."""
84
197
  checkpoint_path = Path(path) if path is not None else None
85
198
  if checkpoint_path is None or not checkpoint_path.is_file():
@@ -90,7 +203,12 @@ def load_checkpoint_state(path: str | Path | None) -> CheckpointState:
90
203
  return _invalid(checkpoint_path, True, "malformed_json", f"Malformed checkpoint JSON: {exc}")
91
204
  except OSError as exc:
92
205
  return _invalid(checkpoint_path, True, "read_error", f"Cannot read checkpoint: {exc}")
93
- return validate_checkpoint_data(data, path=checkpoint_path, exists=True)
206
+ return validate_checkpoint_data(
207
+ data,
208
+ path=checkpoint_path,
209
+ exists=True,
210
+ project_root=project_root,
211
+ )
94
212
 
95
213
 
96
214
  def validate_checkpoint_data(
@@ -98,8 +216,9 @@ def validate_checkpoint_data(
98
216
  *,
99
217
  path: str | Path | None = None,
100
218
  exists: bool = True,
219
+ project_root: str | Path | None = None,
101
220
  ) -> CheckpointState:
102
- """Validate already-loaded checkpoint data."""
221
+ """Validate already-loaded checkpoint data and derive semantic L4 state."""
103
222
  checkpoint_path = Path(path) if path is not None else None
104
223
  if not isinstance(data, dict):
105
224
  return _invalid(checkpoint_path, exists, "invalid_root", "Checkpoint root must be an object")
@@ -156,6 +275,15 @@ def validate_checkpoint_data(
156
275
  if not isinstance(artifacts, list) or any(not isinstance(item, str) for item in artifacts):
157
276
  return _invalid(checkpoint_path, exists, "invalid_required_artifacts", f"Step {step_id} has invalid required_artifacts", data)
158
277
 
278
+ stage_result = raw_step.get("stage_result")
279
+ if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
280
+ return _invalid(checkpoint_path, exists, "invalid_stage_result", f"Step {step_id} has invalid stage_result", data)
281
+ evidence = raw_step.get("authoritative_evidence_paths", {})
282
+ if evidence is None:
283
+ evidence = {}
284
+ if not _valid_evidence_mapping(evidence):
285
+ return _invalid(checkpoint_path, exists, "invalid_evidence_paths", f"Step {step_id} has invalid evidence paths", data)
286
+
159
287
  step = CheckpointStep(
160
288
  id=step_id,
161
289
  skill=skill,
@@ -163,6 +291,8 @@ def validate_checkpoint_data(
163
291
  status=str(status),
164
292
  depends_on=depends_on,
165
293
  required_artifacts=tuple(artifacts),
294
+ stage_result=stage_result,
295
+ authoritative_evidence_paths=dict(evidence),
166
296
  index=index,
167
297
  )
168
298
  steps.append(step)
@@ -186,12 +316,23 @@ def validate_checkpoint_data(
186
316
  data,
187
317
  )
188
318
 
319
+ progress_error = _progress_error(steps)
320
+ if progress_error:
321
+ return _invalid(checkpoint_path, exists, "invalid_step_progress", progress_error, data)
322
+
189
323
  duplicate_skills = {
190
324
  skill: tuple(ids)
191
325
  for skill, ids in skill_to_ids.items()
192
326
  if len(ids) > 1
193
327
  }
194
328
  current_step = next((step for step in steps if step.status not in TERMINAL_STEP_STATUSES), None)
329
+ semantic = _derive_semantic_state(
330
+ data,
331
+ tuple(steps),
332
+ current_step,
333
+ checkpoint_path,
334
+ Path(project_root) if project_root is not None else None,
335
+ )
195
336
  return CheckpointState(
196
337
  path=checkpoint_path,
197
338
  exists=exists,
@@ -201,6 +342,7 @@ def validate_checkpoint_data(
201
342
  all_completed=current_step is None,
202
343
  duplicate_skills=duplicate_skills,
203
344
  raw_data=data,
345
+ semantic=semantic,
204
346
  )
205
347
 
206
348
 
@@ -224,6 +366,7 @@ def checkpoint_cursor(checkpoint: Mapping[str, Any] | None) -> dict[str, object]
224
366
  "failed": [],
225
367
  "first_cursor": f"(invalid checkpoint: {state.error_code})",
226
368
  "current_step_name": "",
369
+ "semantic": state.semantic_snapshot,
227
370
  }
228
371
 
229
372
  grouped: dict[str, list[str]] = {
@@ -250,6 +393,7 @@ def checkpoint_cursor(checkpoint: Mapping[str, Any] | None) -> dict[str, object]
250
393
  "failed": grouped["failed"],
251
394
  "first_cursor": first_cursor,
252
395
  "current_step_name": state.current_step_name,
396
+ "semantic": state.semantic_snapshot,
253
397
  }
254
398
 
255
399
 
@@ -266,6 +410,298 @@ def invalid_existing_checkpoint(path: str | Path | None) -> CheckpointState | No
266
410
  return None
267
411
 
268
412
 
413
+ def semantic_state_key(workflow_type: str) -> str:
414
+ """Return the family-owned L4 semantic metadata key."""
415
+ return SEMANTIC_STATE_KEYS.get(workflow_type, "l4_state")
416
+
417
+
418
+ def _progress_error(steps: list[CheckpointStep]) -> str:
419
+ active_seen = False
420
+ for step in steps:
421
+ if step.status in TERMINAL_STEP_STATUSES:
422
+ if active_seen:
423
+ return f"Step {step.id} is terminal after an unfinished predecessor"
424
+ else:
425
+ active_seen = True
426
+ return ""
427
+
428
+
429
+ def _derive_semantic_state(
430
+ data: Mapping[str, Any],
431
+ steps: tuple[CheckpointStep, ...],
432
+ current_step: CheckpointStep | None,
433
+ checkpoint_path: Path | None,
434
+ project_root: Path | None,
435
+ ) -> SemanticCheckpoint:
436
+ workflow_type = str(data.get("workflow_type") or "")
437
+ state_key = semantic_state_key(workflow_type)
438
+ raw_state = data.get(state_key, {})
439
+ if not isinstance(raw_state, dict):
440
+ return _semantic_error(workflow_type, current_step, "invalid_semantic_state", f"{state_key} must be an object")
441
+
442
+ required = FAMILY_REQUIRED_STAGES.get(workflow_type)
443
+ sequence_error = _required_sequence_error(steps, required) if required else "unsupported workflow family"
444
+ sequence_valid = not sequence_error
445
+
446
+ repair_round = raw_state.get("repair_round", 0)
447
+ if isinstance(repair_round, bool) or not isinstance(repair_round, int) or not 0 <= repair_round <= MAX_OUTER_REPAIR_ROUNDS:
448
+ return _semantic_error(workflow_type, current_step, "invalid_repair_round", "repair_round must be an integer from 0 through 3")
449
+ repair_scope = raw_state.get("repair_scope")
450
+ if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
451
+ return _semantic_error(workflow_type, current_step, "invalid_repair_scope", "repair_scope is not a safe shared scope")
452
+ terminal_status = raw_state.get("terminal_status")
453
+ if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
454
+ return _semantic_error(workflow_type, current_step, "invalid_terminal_status", "terminal_status is not recognized")
455
+ blocked_reason = raw_state.get("blocked_reason")
456
+ if blocked_reason is not None and (not isinstance(blocked_reason, str) or not blocked_reason.strip()):
457
+ return _semantic_error(workflow_type, current_step, "invalid_blocked_reason", "blocked_reason must be a non-empty string")
458
+ if terminal_status == "WORKFLOW_BLOCKED" and not blocked_reason:
459
+ return _semantic_error(workflow_type, current_step, "missing_blocked_reason", "WORKFLOW_BLOCKED requires blocked_reason")
460
+
461
+ review_step = _single_step(steps, "prizmkit-code-review")
462
+ test_step = _single_step(steps, "prizmkit-test")
463
+ review_valid = bool(
464
+ review_step
465
+ and review_step.status == "completed"
466
+ and review_step.stage_result == "REVIEW_PASS"
467
+ )
468
+ evidence_paths = (
469
+ dict(test_step.authoritative_evidence_paths)
470
+ if test_step and test_step.authoritative_evidence_paths
471
+ else dict(raw_state.get("authoritative_evidence_paths") or {})
472
+ )
473
+ evidence_valid = bool(
474
+ test_step
475
+ and test_step.status == "completed"
476
+ and test_step.stage_result == "TEST_PASS"
477
+ and _validate_test_evidence(evidence_paths, checkpoint_path, project_root)
478
+ )
479
+ all_required_completed = bool(required) and all(
480
+ (step := _single_step(steps, skill)) is not None and step.status == "completed"
481
+ for skill in required
482
+ )
483
+ artifacts_valid = all(
484
+ _required_artifacts_exist(step, checkpoint_path, project_root)
485
+ for step in steps
486
+ if step.status == "completed"
487
+ )
488
+ completion_ready = (
489
+ sequence_valid and all_required_completed and review_valid
490
+ and evidence_valid and artifacts_valid
491
+ )
492
+ blocked = terminal_status == "WORKFLOW_BLOCKED" and bool(blocked_reason)
493
+ complete = completion_ready and terminal_status == "WORKFLOW_COMPLETED" and not blocked
494
+
495
+ current_stage = current_step.skill if current_step is not None else None
496
+ next_stage = current_stage
497
+ if current_step is not None and current_step.status == "in_progress":
498
+ later = steps[current_step.index + 1:]
499
+ next_step = next((step for step in later if step.status not in TERMINAL_STEP_STATUSES), None)
500
+ next_stage = next_step.skill if next_step is not None else None
501
+ if terminal_status is not None:
502
+ current_stage = None
503
+ next_stage = None
504
+
505
+ error_code = ""
506
+ error_message = ""
507
+ if sequence_error:
508
+ error_code = "invalid_stage_sequence"
509
+ error_message = sequence_error
510
+ elif all(step.status in TERMINAL_STEP_STATUSES for step in steps) and not completion_ready and not blocked:
511
+ error_code = "incomplete_semantic_evidence"
512
+ if not review_valid:
513
+ error_message = "completed checkpoint lacks REVIEW_PASS on the review stage"
514
+ elif not evidence_valid:
515
+ error_message = "completed checkpoint lacks valid TEST_PASS structured evidence"
516
+ else:
517
+ error_message = "completed checkpoint lacks mandatory completed stages"
518
+ elif terminal_status == "WORKFLOW_COMPLETED" and not completion_ready:
519
+ error_code = "invalid_completed_terminal"
520
+ error_message = "WORKFLOW_COMPLETED does not have semantic completion evidence"
521
+
522
+ return SemanticCheckpoint(
523
+ workflow_type=workflow_type,
524
+ current_stage=current_stage,
525
+ next_stage=next_stage,
526
+ stage_result=raw_state.get("stage_result"),
527
+ repair_scope=repair_scope,
528
+ repair_round=repair_round,
529
+ authoritative_evidence_paths=evidence_paths,
530
+ blocked_reason=blocked_reason,
531
+ terminal_status=terminal_status,
532
+ sequence_valid=sequence_valid,
533
+ evidence_valid=evidence_valid,
534
+ artifacts_valid=artifacts_valid,
535
+ completion_ready=completion_ready,
536
+ complete=complete,
537
+ blocked=blocked,
538
+ error_code=error_code,
539
+ error_message=error_message,
540
+ )
541
+
542
+
543
+ def _required_sequence_error(
544
+ steps: tuple[CheckpointStep, ...],
545
+ required: tuple[str, ...] | None,
546
+ ) -> str:
547
+ if not required:
548
+ return ""
549
+ positions: list[int] = []
550
+ for skill in required:
551
+ matches = [step.index for step in steps if step.skill == skill]
552
+ if len(matches) != 1:
553
+ return f"Expected exactly one mandatory stage {skill}"
554
+ positions.append(matches[0])
555
+ if positions != sorted(positions):
556
+ return "Mandatory stages are not in the expected family order"
557
+ return ""
558
+
559
+
560
+ def _single_step(steps: tuple[CheckpointStep, ...], skill: str) -> CheckpointStep | None:
561
+ matches = [step for step in steps if step.skill == skill]
562
+ return matches[0] if len(matches) == 1 else None
563
+
564
+
565
+
566
+ def _required_artifacts_exist(
567
+ step: CheckpointStep,
568
+ checkpoint_path: Path | None,
569
+ project_root: Path | None,
570
+ ) -> bool:
571
+ if not step.required_artifacts:
572
+ return True
573
+ import glob
574
+
575
+ root = (project_root or _infer_project_root(checkpoint_path)).resolve()
576
+ for artifact in step.required_artifacts:
577
+ candidate = root / artifact
578
+ if glob.has_magic(str(candidate)):
579
+ if not glob.glob(str(candidate)):
580
+ return False
581
+ elif not candidate.exists():
582
+ return False
583
+ return True
584
+
585
+ def _valid_evidence_mapping(value: object) -> bool:
586
+ if not isinstance(value, dict):
587
+ return False
588
+ return all(
589
+ key in EVIDENCE_FILENAMES
590
+ and (path is None or isinstance(path, str))
591
+ for key, path in value.items()
592
+ )
593
+
594
+
595
+ def _validate_test_evidence(
596
+ paths: Mapping[str, str | None],
597
+ checkpoint_path: Path | None,
598
+ project_root: Path | None,
599
+ ) -> bool:
600
+ if set(paths) != set(EVIDENCE_FILENAMES):
601
+ return False
602
+ root = (project_root or _infer_project_root(checkpoint_path)).resolve()
603
+ resolved: dict[str, Path] = {}
604
+ for key, filename in EVIDENCE_FILENAMES.items():
605
+ raw_path = paths.get(key)
606
+ if not isinstance(raw_path, str) or not raw_path.strip():
607
+ return False
608
+ candidate = Path(raw_path)
609
+ if not candidate.is_absolute():
610
+ candidate = root / candidate
611
+ try:
612
+ candidate = candidate.resolve()
613
+ candidate.relative_to(root)
614
+ except (OSError, ValueError):
615
+ return False
616
+ if candidate.name != filename or not candidate.is_file():
617
+ return False
618
+ resolved[key] = candidate
619
+ parents = {path.parent for path in resolved.values()}
620
+ if len(parents) != 1:
621
+ return False
622
+ try:
623
+ manifest = json.loads(resolved["manifest"].read_text(encoding="utf-8"))
624
+ verdict = json.loads(resolved["verdict"].read_text(encoding="utf-8"))
625
+ validation = json.loads(resolved["validation"].read_text(encoding="utf-8"))
626
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
627
+ return False
628
+ if not all(isinstance(record, dict) for record in (manifest, verdict, validation)):
629
+ return False
630
+ evidence_id = manifest.get("evidence_id")
631
+ if isinstance(evidence_id, str) and evidence_id and resolved["manifest"].parent.name != evidence_id:
632
+ return False
633
+ records_agree = (
634
+ manifest.get("final_verdict") == "TEST_PASS"
635
+ and verdict.get("verdict") == "TEST_PASS"
636
+ and validation.get("verdict") == "TEST_PASS"
637
+ and validation.get("result") == "passed"
638
+ )
639
+ if not records_agree:
640
+ return False
641
+ validator = _resolve_evidence_validator(root)
642
+ if validator is None:
643
+ return False
644
+ return _evidence_validator_passes(root, resolved["manifest"].parent, validator)
645
+
646
+
647
+ def _evidence_validator_passes(
648
+ project_root: Path,
649
+ evidence_dir: Path,
650
+ validator: Path,
651
+ ) -> bool:
652
+ try:
653
+ result = subprocess.run(
654
+ [
655
+ sys.executable, str(validator),
656
+ "--evidence-dir", str(evidence_dir),
657
+ "--project-root", str(project_root),
658
+ ],
659
+ stdout=subprocess.DEVNULL,
660
+ stderr=subprocess.DEVNULL,
661
+ timeout=120,
662
+ check=False,
663
+ )
664
+ except (OSError, subprocess.SubprocessError):
665
+ return False
666
+ return result.returncode == 0
667
+
668
+
669
+ def _resolve_evidence_validator(project_root: Path) -> Path | None:
670
+ candidates = (
671
+ project_root / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
672
+ project_root / ".agents" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
673
+ project_root / ".codebuddy" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
674
+ project_root / "core" / "skills" / "prizmkit-skill" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
675
+ )
676
+ return next((path for path in candidates if path.is_file()), None)
677
+
678
+
679
+ def _infer_project_root(checkpoint_path: Path | None) -> Path:
680
+ if checkpoint_path is None:
681
+ return Path.cwd()
682
+ resolved = checkpoint_path.resolve()
683
+ for parent in (resolved.parent, *resolved.parents):
684
+ if (parent / ".prizmkit").is_dir():
685
+ return parent
686
+ return resolved.parent
687
+
688
+
689
+ def _semantic_error(
690
+ workflow_type: str,
691
+ current_step: CheckpointStep | None,
692
+ code: str,
693
+ message: str,
694
+ ) -> SemanticCheckpoint:
695
+ stage = current_step.skill if current_step is not None else None
696
+ return SemanticCheckpoint(
697
+ workflow_type=workflow_type,
698
+ current_stage=stage,
699
+ next_stage=stage,
700
+ error_code=code,
701
+ error_message=message,
702
+ )
703
+
704
+
269
705
  def _invalid(
270
706
  path: Path | None,
271
707
  exists: bool,
@@ -280,4 +716,5 @@ def _invalid(
280
716
  error_code=code,
281
717
  error_message=message,
282
718
  raw_data=raw_data,
719
+ semantic=SemanticCheckpoint(error_code=code, error_message=message),
283
720
  )