prizmkit 1.1.140 → 1.1.142

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 (83) 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 +165 -125
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +13 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -6
  6. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +5 -3
  7. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +160 -0
  8. package/bundled/dev-pipeline/scripts/check-session-status.py +11 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +23 -806
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +6 -8
  11. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +6 -8
  13. package/bundled/dev-pipeline/scripts/prompt_framework.py +68 -56
  14. package/bundled/dev-pipeline/scripts/update-bug-status.py +21 -2
  15. package/bundled/dev-pipeline/scripts/update-checkpoint.py +52 -131
  16. package/bundled/dev-pipeline/scripts/update-feature-status.py +21 -2
  17. package/bundled/dev-pipeline/scripts/update-refactor-status.py +21 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
  19. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +8 -19
  20. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
  21. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +13 -10
  27. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +18 -9
  29. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +2 -2
  33. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
  34. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +1 -1
  35. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +1 -1
  36. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +14 -26
  37. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -2
  38. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
  39. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  40. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +13 -10
  41. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  42. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  43. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +494 -413
  44. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +144 -684
  45. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +20 -41
  46. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +24 -24
  47. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +176 -36
  48. package/bundled/skills/_metadata.json +6 -6
  49. package/bundled/skills/prizmkit/SKILL.md +13 -18
  50. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +73 -80
  51. package/bundled/skills/prizmkit-code-review/SKILL.md +7 -7
  52. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +73 -80
  53. package/bundled/skills/prizmkit-committer/SKILL.md +8 -7
  54. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +73 -80
  55. package/bundled/skills/prizmkit-implement/SKILL.md +3 -2
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +73 -80
  57. package/bundled/skills/prizmkit-plan/SKILL.md +2 -1
  58. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +73 -80
  59. package/bundled/skills/prizmkit-retrospective/SKILL.md +8 -6
  60. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +73 -80
  61. package/bundled/skills/prizmkit-test/SKILL.md +217 -189
  62. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +119 -0
  63. package/bundled/skills/prizmkit-test/references/independent-test-review.md +269 -0
  64. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +193 -0
  65. package/bundled/skills/prizmkit-test/references/test-report-template.md +164 -79
  66. package/bundled/skills/prizmkit-workflow/SKILL.md +28 -36
  67. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +73 -80
  68. package/package.json +1 -1
  69. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +0 -477
  70. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +0 -116
  71. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +0 -111
  72. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -66
  73. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +0 -104
  74. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +0 -65
  75. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +0 -208
  76. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +0 -80
  77. package/bundled/skills/prizmkit-test/references/examples.md +0 -100
  78. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +0 -228
  79. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +0 -118
  80. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +0 -107
  81. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +0 -181
  82. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +0 -1930
  83. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +0 -1847
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.140",
3
- "bundledAt": "2026-07-20T13:39:17.949Z",
4
- "bundledFrom": "9a25298"
2
+ "frameworkVersion": "1.1.142",
3
+ "bundledAt": "2026-07-21T07:43:37.944Z",
4
+ "bundledFrom": "deb1402"
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
 
@@ -4,20 +4,50 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
- import subprocess
8
- import sys
9
7
  from dataclasses import dataclass, field
10
8
  from pathlib import Path
11
9
  from typing import Any, Mapping
12
10
 
11
+ from .test_result import valid_test_artifact_pair
12
+
13
13
  VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
14
14
  TERMINAL_STEP_STATUSES = {"completed", "skipped"}
15
15
  STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
16
16
  MAX_OUTER_REPAIR_ROUNDS = 3
17
17
 
18
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
19
+ TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
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",
@@ -59,11 +89,6 @@ FAMILY_REQUIRED_STAGES = {
59
89
  "completion-summary",
60
90
  ),
61
91
  }
62
- EVIDENCE_FILENAMES = {
63
- "manifest": "manifest.json",
64
- "verdict": "verdict.json",
65
- "validation": "validation.json",
66
- }
67
92
 
68
93
 
69
94
  @dataclass(frozen=True)
@@ -77,7 +102,6 @@ class CheckpointStep:
77
102
  depends_on: str | None = None
78
103
  required_artifacts: tuple[str, ...] = ()
79
104
  stage_result: str | None = None
80
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
81
105
  index: int = 0
82
106
 
83
107
  @property
@@ -101,11 +125,10 @@ class SemanticCheckpoint:
101
125
  stage_result: str | None = None
102
126
  repair_scope: str | None = None
103
127
  repair_round: int = 0
104
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
105
128
  blocked_reason: str | None = None
106
129
  terminal_status: str | None = None
107
130
  sequence_valid: bool = False
108
- evidence_valid: bool = False
131
+ test_result_valid: bool = False
109
132
  artifacts_valid: bool = False
110
133
  completion_ready: bool = False
111
134
  complete: bool = False
@@ -121,7 +144,6 @@ class SemanticCheckpoint:
121
144
  "stage_result": self.stage_result,
122
145
  "repair_scope": self.repair_scope,
123
146
  "repair_round": self.repair_round,
124
- "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
125
147
  "blocked_reason": self.blocked_reason,
126
148
  "terminal_status": self.terminal_status,
127
149
  }
@@ -278,11 +300,23 @@ def validate_checkpoint_data(
278
300
  stage_result = raw_step.get("stage_result")
279
301
  if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
280
302
  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)
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
+ )
286
320
 
287
321
  step = CheckpointStep(
288
322
  id=step_id,
@@ -292,7 +326,6 @@ def validate_checkpoint_data(
292
326
  depends_on=depends_on,
293
327
  required_artifacts=tuple(artifacts),
294
328
  stage_result=stage_result,
295
- authoritative_evidence_paths=dict(evidence),
296
329
  index=index,
297
330
  )
298
331
  steps.append(step)
@@ -415,6 +448,47 @@ def semantic_state_key(workflow_type: str) -> str:
415
448
  return SEMANTIC_STATE_KEYS.get(workflow_type, "l4_state")
416
449
 
417
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
+
418
492
  def _progress_error(steps: list[CheckpointStep]) -> str:
419
493
  active_seen = False
420
494
  for step in steps:
@@ -449,6 +523,32 @@ def _derive_semantic_state(
449
523
  repair_scope = raw_state.get("repair_scope")
450
524
  if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
451
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
+ )
452
552
  terminal_status = raw_state.get("terminal_status")
453
553
  if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
454
554
  return _semantic_error(workflow_type, current_step, "invalid_terminal_status", "terminal_status is not recognized")
@@ -465,29 +565,23 @@ def _derive_semantic_state(
465
565
  and review_step.status == "completed"
466
566
  and review_step.stage_result == "REVIEW_PASS"
467
567
  )
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(
568
+ test_result_valid = bool(
474
569
  test_step
475
570
  and test_step.status == "completed"
476
571
  and test_step.stage_result == "TEST_PASS"
477
- and _validate_test_evidence(evidence_paths, checkpoint_path, project_root)
478
572
  )
479
573
  all_required_completed = bool(required) and all(
480
574
  (step := _single_step(steps, skill)) is not None and step.status == "completed"
481
575
  for skill in required
482
576
  )
483
577
  artifacts_valid = all(
484
- _required_artifacts_exist(step, checkpoint_path, project_root)
578
+ _required_step_artifacts_valid(step, checkpoint_path, project_root)
485
579
  for step in steps
486
580
  if step.status == "completed"
487
581
  )
488
582
  completion_ready = (
489
583
  sequence_valid and all_required_completed and review_valid
490
- and evidence_valid and artifacts_valid
584
+ and test_result_valid and artifacts_valid
491
585
  )
492
586
  blocked = terminal_status == "WORKFLOW_BLOCKED" and bool(blocked_reason)
493
587
  complete = completion_ready and terminal_status == "WORKFLOW_COMPLETED" and not blocked
@@ -495,9 +589,15 @@ def _derive_semantic_state(
495
589
  current_stage = current_step.skill if current_step is not None else None
496
590
  next_stage = current_stage
497
591
  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
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"
501
601
  if terminal_status is not None:
502
602
  current_stage = None
503
603
  next_stage = None
@@ -511,8 +611,8 @@ def _derive_semantic_state(
511
611
  error_code = "incomplete_semantic_evidence"
512
612
  if not review_valid:
513
613
  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"
614
+ elif not test_result_valid:
615
+ error_message = "completed checkpoint lacks TEST_PASS on the test stage"
516
616
  else:
517
617
  error_message = "completed checkpoint lacks mandatory completed stages"
518
618
  elif terminal_status == "WORKFLOW_COMPLETED" and not completion_ready:
@@ -526,11 +626,10 @@ def _derive_semantic_state(
526
626
  stage_result=raw_state.get("stage_result"),
527
627
  repair_scope=repair_scope,
528
628
  repair_round=repair_round,
529
- authoritative_evidence_paths=evidence_paths,
530
629
  blocked_reason=blocked_reason,
531
630
  terminal_status=terminal_status,
532
631
  sequence_valid=sequence_valid,
533
- evidence_valid=evidence_valid,
632
+ test_result_valid=test_result_valid,
534
633
  artifacts_valid=artifacts_valid,
535
634
  completion_ready=completion_ready,
536
635
  complete=complete,
@@ -563,6 +662,30 @@ def _single_step(steps: tuple[CheckpointStep, ...], skill: str) -> CheckpointSte
563
662
 
564
663
 
565
664
 
665
+ def _required_step_artifacts_valid(
666
+ step: CheckpointStep,
667
+ checkpoint_path: Path | None,
668
+ project_root: Path | None,
669
+ ) -> bool:
670
+ """Validate declared artifacts, including semantic test terminal output."""
671
+ if step.skill == "prizmkit-test":
672
+ root = (project_root or _infer_project_root(checkpoint_path)).resolve()
673
+ reports = [item for item in step.required_artifacts if item.endswith("/test-report.md")]
674
+ results = [item for item in step.required_artifacts if item.endswith("/test-result.json")]
675
+ if len(reports) != 1 or len(results) != 1:
676
+ return False
677
+ report_path = (root / reports[0]).resolve()
678
+ result_path = (root / results[0]).resolve()
679
+ if not _is_within_root(report_path, root) or not _is_within_root(result_path, root):
680
+ return False
681
+ return valid_test_artifact_pair(
682
+ report_path,
683
+ result_path,
684
+ required_result="TEST_PASS",
685
+ )
686
+ return _required_artifacts_exist(step, checkpoint_path, project_root)
687
+
688
+
566
689
  def _required_artifacts_exist(
567
690
  step: CheckpointStep,
568
691
  checkpoint_path: Path | None,
@@ -574,7 +697,9 @@ def _required_artifacts_exist(
574
697
 
575
698
  root = (project_root or _infer_project_root(checkpoint_path)).resolve()
576
699
  for artifact in step.required_artifacts:
577
- candidate = root / artifact
700
+ candidate = (root / artifact).resolve()
701
+ if not _is_within_root(candidate, root):
702
+ return False
578
703
  if glob.has_magic(str(candidate)):
579
704
  if not glob.glob(str(candidate)):
580
705
  return False
@@ -582,98 +707,13 @@ def _required_artifacts_exist(
582
707
  return False
583
708
  return True
584
709
 
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
710
 
647
- def _evidence_validator_passes(
648
- project_root: Path,
649
- evidence_dir: Path,
650
- validator: Path,
651
- ) -> bool:
711
+ def _is_within_root(path: Path, root: Path) -> bool:
652
712
  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):
713
+ path.relative_to(root)
714
+ except ValueError:
665
715
  return False
666
- return result.returncode == 0
667
-
668
-
669
- def _resolve_evidence_validator(project_root: Path) -> Path | None:
670
- candidates = (
671
- project_root / "core" / "skills" / "prizmkit-skill" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
672
- project_root / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
673
- project_root / ".agents" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
674
- project_root / ".codebuddy" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
675
- )
676
- return next((path for path in candidates if path.is_file()), None)
716
+ return True
677
717
 
678
718
 
679
719
  def _infer_project_root(checkpoint_path: Path | None) -> Path:
@@ -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:
@@ -137,23 +137,18 @@ class SemanticCheckpointState:
137
137
  stage_result: str | None = None
138
138
  repair_scope: str | None = None
139
139
  repair_round: int = 0
140
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
141
140
  blocked_reason: str | None = None
142
141
  terminal_status: str | None = None
143
142
 
144
143
  @classmethod
145
144
  def from_mapping(cls, value: Mapping[str, object] | None) -> "SemanticCheckpointState":
146
145
  data = value or {}
147
- evidence = data.get("authoritative_evidence_paths")
148
146
  return cls(
149
147
  current_stage=_optional_text(data.get("current_stage")),
150
148
  next_stage=_optional_text(data.get("next_stage")),
151
149
  stage_result=_optional_text(data.get("stage_result")),
152
150
  repair_scope=_optional_text(data.get("repair_scope")),
153
151
  repair_round=max(0, _safe_int(data.get("repair_round"), 0)),
154
- authoritative_evidence_paths=(
155
- dict(evidence) if isinstance(evidence, Mapping) else {}
156
- ),
157
152
  blocked_reason=_optional_text(data.get("blocked_reason")),
158
153
  terminal_status=_optional_text(data.get("terminal_status")),
159
154
  )
@@ -165,7 +160,6 @@ class SemanticCheckpointState:
165
160
  "stage_result": self.stage_result,
166
161
  "repair_scope": self.repair_scope,
167
162
  "repair_round": self.repair_round,
168
- "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
169
163
  "blocked_reason": self.blocked_reason,
170
164
  "terminal_status": self.terminal_status,
171
165
  }
@@ -650,8 +650,7 @@ def _process_item_locked(
650
650
  key: metadata.get(key)
651
651
  for key in (
652
652
  "current_stage", "next_stage", "stage_result",
653
- "repair_scope", "repair_round",
654
- "authoritative_evidence_paths", "blocked_reason",
653
+ "repair_scope", "repair_round", "blocked_reason",
655
654
  "terminal_status",
656
655
  )
657
656
  },
@@ -863,7 +862,10 @@ def _emit_interruption_cleanup_failure(result, message: str, recovery: str) -> N
863
862
 
864
863
  def _is_retryable_ai_outcome(session_status: str, new_status: str) -> bool:
865
864
  """Return whether a persisted post-launch outcome should continue in-process."""
866
- 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
+ }:
867
869
  return False
868
870
  return new_status in {"in_progress", "pending"} and session_status in {
869
871
  "context_overflow",