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
@@ -24,17 +24,23 @@ Usage:
24
24
 
25
25
  import argparse
26
26
  import glob
27
+ import hashlib
27
28
  import json
28
29
  import os
29
30
  import re
31
+ import subprocess
30
32
  import sys
31
33
 
32
34
  from utils import enrich_global_context, helper_replacements, load_json_file, resolve_prompt_platform, setup_logging
33
35
  from continuation import add_continuation_args, append_continuation_handoff, is_continuation
34
- from prompt_framework import browser_enabled_from_item, checkpoint_group, checkpoint_skip, checkpoint_step, detect_dev_server_port, generate_checkpoint_from_sections, merge_checkpoint_state as merge_shared_checkpoint_state
36
+ from prompt_framework import browser_enabled_from_item, checkpoint_group, checkpoint_skip, checkpoint_step, detect_dev_server_port, generate_checkpoint_from_sections, merge_checkpoint_state as merge_shared_checkpoint_state, pipeline_checkpoint_metadata, route_pipeline_repair
35
37
 
36
38
 
37
39
  DEFAULT_MAX_RETRIES = 3
40
+ MAX_OUTER_REPAIR_ROUNDS = 3
41
+ EVIDENCE_FINAL_RECORDS = {"validation.json", "verdict.json", "test-report.md"}
42
+ EVIDENCE_SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
43
+ EVIDENCE_TARGET_HASH_KEYS = ("source", "tests", "contracts", "lockfiles", "environment", "plan")
38
44
 
39
45
  LOGGER = setup_logging("generate-bootstrap-prompt")
40
46
 
@@ -679,7 +685,9 @@ SECTION_TO_SKILL = {
679
685
  "phase-prizmkit-test": checkpoint_step(
680
686
  "prizmkit-test", "Scoped Feature Test Gate",
681
687
  [".prizmkit/specs/{slug}/test-report-path.txt",
682
- ".prizmkit/test/*/test-report.md"],
688
+ ".prizmkit/test/evidence/*/manifest.json",
689
+ ".prizmkit/test/evidence/*/verdict.json",
690
+ ".prizmkit/test/evidence/*/validation.json"],
683
691
  ),
684
692
  "phase-review": checkpoint_step("prizmkit-code-review", "Code Review", []),
685
693
  "phase-browser": checkpoint_step("browser-verification", "Browser Verification", []),
@@ -716,6 +724,366 @@ def _read_text(path):
716
724
  return ""
717
725
 
718
726
 
727
+ def _canonical_sha256(value):
728
+ """Return the protocol's compact, key-sorted JSON SHA-256."""
729
+ payload = json.dumps(
730
+ value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
731
+ )
732
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
733
+
734
+
735
+ def _file_sha256(path):
736
+ """Hash a file without loading large evidence records into memory."""
737
+ digest = hashlib.sha256()
738
+ try:
739
+ with open(path, "rb") as f:
740
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
741
+ digest.update(chunk)
742
+ except OSError:
743
+ return ""
744
+ return digest.hexdigest()
745
+
746
+
747
+ def _load_strict_json(path):
748
+ """Load JSON while rejecting duplicate keys and non-JSON constants."""
749
+ def reject_duplicates(pairs):
750
+ result = {}
751
+ for key, value in pairs:
752
+ if key in result:
753
+ raise ValueError("duplicate object key: {}".format(key))
754
+ result[key] = value
755
+ return result
756
+
757
+ def reject_constant(value):
758
+ raise ValueError("non-JSON numeric constant: {}".format(value))
759
+
760
+ try:
761
+ with open(path, "r", encoding="utf-8") as f:
762
+ return json.load(
763
+ f, object_pairs_hook=reject_duplicates,
764
+ parse_constant=reject_constant,
765
+ )
766
+ except (OSError, ValueError, TypeError):
767
+ return None
768
+
769
+
770
+ def _is_sha256(value):
771
+ return isinstance(value, str) and bool(EVIDENCE_SHA256_RE.fullmatch(value))
772
+
773
+
774
+ def _path_is_within(root, candidate):
775
+ """Return whether candidate resolves below root without escaping it."""
776
+ try:
777
+ return os.path.commonpath([
778
+ os.path.realpath(root), os.path.realpath(candidate),
779
+ ]) == os.path.realpath(root)
780
+ except ValueError:
781
+ return False
782
+
783
+
784
+ def _resolve_test_evidence_validator(project_root):
785
+ """Return the installed validator path used by the current project."""
786
+ candidates = [
787
+ os.path.join(
788
+ project_root, ".claude", "command-assets", "prizmkit-test",
789
+ "scripts", "validate_test_evidence.py",
790
+ ),
791
+ os.path.join(
792
+ project_root, ".agents", "skills", "prizmkit-test", "scripts",
793
+ "validate_test_evidence.py",
794
+ ),
795
+ os.path.join(
796
+ project_root, ".codebuddy", "skills", "prizmkit-test", "scripts",
797
+ "validate_test_evidence.py",
798
+ ),
799
+ ]
800
+ return next((path for path in candidates if os.path.isfile(path)), "")
801
+
802
+
803
+ def _evidence_validator_passes(project_root, evidence_root, validation):
804
+ """Re-run the authoritative L1 validator before accepting any verdict."""
805
+ validator_path = _resolve_test_evidence_validator(project_root)
806
+ if (not validator_path or
807
+ validation.get("validator_sha256") != _file_sha256(validator_path)):
808
+ return False
809
+ try:
810
+ result = subprocess.run(
811
+ [sys.executable, validator_path,
812
+ "--evidence-dir", evidence_root,
813
+ "--project-root", project_root],
814
+ stdout=subprocess.DEVNULL,
815
+ stderr=subprocess.DEVNULL,
816
+ timeout=120,
817
+ check=False,
818
+ )
819
+ except (OSError, subprocess.SubprocessError):
820
+ return False
821
+ return result.returncode == 0
822
+
823
+
824
+ def _evidence_package_from_pointer(project_root, pointer_artifact):
825
+ """Resolve a Feature report pointer to the structured evidence package."""
826
+ pointer_path = os.path.join(project_root, pointer_artifact)
827
+ pointed = _read_text(pointer_path).strip().splitlines()
828
+ if not pointed:
829
+ return None, None
830
+
831
+ report_path = pointed[0].strip()
832
+ if not os.path.isabs(report_path):
833
+ report_path = os.path.join(project_root, report_path)
834
+ evidence_root = os.path.dirname(report_path)
835
+ expected_root = os.path.join(project_root, ".prizmkit", "test", "evidence")
836
+ if not _path_is_within(expected_root, evidence_root):
837
+ return None, None
838
+ relative_root = os.path.relpath(evidence_root, expected_root)
839
+ if (not relative_root or relative_root.startswith("..") or
840
+ os.sep in relative_root):
841
+ return None, None
842
+ if os.path.basename(report_path) != "test-report.md":
843
+ return None, None
844
+ if not os.path.isfile(report_path):
845
+ return None, None
846
+ return os.path.realpath(evidence_root), os.path.realpath(report_path)
847
+
848
+
849
+ def _manifest_entries(evidence_root, manifest):
850
+ """Validate the manifest file list and return path -> entry."""
851
+ entries = manifest.get("files")
852
+ if not isinstance(entries, list):
853
+ return None
854
+ result = {}
855
+ for entry in entries:
856
+ if not isinstance(entry, dict):
857
+ return None
858
+ relative = entry.get("path")
859
+ if (not isinstance(relative, str) or not relative or
860
+ os.path.isabs(relative) or relative in result or
861
+ ".." in relative.split("/") or "\\" in relative):
862
+ return None
863
+ path = os.path.join(evidence_root, relative)
864
+ if not _path_is_within(evidence_root, path) or not os.path.isfile(path):
865
+ return None
866
+ if not _is_sha256(entry.get("sha256")):
867
+ return None
868
+ if not isinstance(entry.get("produced_by"), str) or not entry["produced_by"].strip():
869
+ return None
870
+ if _file_sha256(path) != entry["sha256"]:
871
+ return None
872
+ result[relative] = entry
873
+
874
+ actual = set()
875
+ for root, _dirs, files in os.walk(evidence_root):
876
+ for filename in files:
877
+ relative = os.path.relpath(os.path.join(root, filename), evidence_root)
878
+ if relative != "manifest.json":
879
+ actual.add(relative)
880
+ return result if set(result) == actual else None
881
+
882
+
883
+ def _extract_report_scope(report_text):
884
+ """Read only the derived report's artifact identity, never its verdict."""
885
+ in_scope = False
886
+ for raw_line in report_text.splitlines():
887
+ line = raw_line.strip()
888
+ if line == "## Scope":
889
+ in_scope = True
890
+ continue
891
+ if in_scope and line.startswith("## "):
892
+ break
893
+ if in_scope and line.startswith("- Artifact Dir:"):
894
+ return line.split(":", 1)[1].strip()
895
+ return ""
896
+
897
+
898
+ def _validate_target_hashes(evidence_root, project_root, manifest, entry_map):
899
+ """Validate live inventoried files against the manifest target hashes."""
900
+ inventory_path = os.path.join(evidence_root, "target-inventory.json")
901
+ environment_path = os.path.join(evidence_root, "environment.json")
902
+ inventory = _load_strict_json(inventory_path)
903
+ environment = _load_strict_json(environment_path)
904
+ if not isinstance(inventory, dict) or not isinstance(environment, dict):
905
+ return False
906
+ if "target-inventory.json" not in entry_map or "environment.json" not in entry_map:
907
+ return False
908
+
909
+ categories = inventory.get("categories")
910
+ expected = set(EVIDENCE_TARGET_HASH_KEYS[:4])
911
+ if not isinstance(categories, dict) or set(categories) != expected:
912
+ return False
913
+ aggregates = {}
914
+ for category in sorted(expected):
915
+ records = categories.get(category)
916
+ if not isinstance(records, list):
917
+ return False
918
+ normalized = []
919
+ for record in records:
920
+ if not isinstance(record, dict):
921
+ return False
922
+ relative = record.get("path")
923
+ expected_hash = record.get("sha256")
924
+ if (not isinstance(relative, str) or os.path.isabs(relative) or
925
+ not _is_sha256(expected_hash)):
926
+ return False
927
+ live_path = os.path.join(project_root, relative)
928
+ if (not _path_is_within(project_root, live_path) or
929
+ not os.path.isfile(live_path) or
930
+ _file_sha256(live_path) != expected_hash):
931
+ return False
932
+ normalized.append({"path": relative, "sha256": expected_hash})
933
+ aggregates[category] = _canonical_sha256(
934
+ sorted(normalized, key=lambda item: item["path"]),
935
+ )
936
+
937
+ aggregates["environment"] = _canonical_sha256(environment)
938
+ plan_inputs = inventory.get("plan_inputs")
939
+ if not isinstance(plan_inputs, dict) or not plan_inputs:
940
+ return False
941
+ aggregates["plan"] = _canonical_sha256(plan_inputs)
942
+ target_hashes = manifest.get("target_hashes")
943
+ return target_hashes == aggregates
944
+
945
+
946
+ def validate_feature_test_evidence(project_root, artifact_dir):
947
+ """Return structured Feature test evidence, or None when it is unsafe."""
948
+ pointer_artifact = os.path.join(artifact_dir, "test-report-path.txt")
949
+ evidence_root, report_path = _evidence_package_from_pointer(
950
+ project_root, pointer_artifact,
951
+ )
952
+ if not evidence_root or not report_path:
953
+ return None
954
+
955
+ marker_path = os.path.join(project_root, artifact_dir, ".prizmkit-test-started")
956
+ if not os.path.isfile(marker_path):
957
+ return None
958
+ try:
959
+ if os.path.getmtime(report_path) < os.path.getmtime(marker_path):
960
+ return None
961
+ except OSError:
962
+ return None
963
+
964
+ manifest = _load_strict_json(os.path.join(evidence_root, "manifest.json"))
965
+ verdict = _load_strict_json(os.path.join(evidence_root, "verdict.json"))
966
+ validation = _load_strict_json(os.path.join(evidence_root, "validation.json"))
967
+ if not all(isinstance(record, dict) for record in (manifest, verdict, validation)):
968
+ return None
969
+ entry_map = _manifest_entries(evidence_root, manifest)
970
+ if entry_map is None or not {"verdict.json", "validation.json"}.issubset(entry_map):
971
+ return None
972
+
973
+ identity = manifest.get("evidence_id_inputs")
974
+ evidence_id = manifest.get("evidence_id")
975
+ if (not isinstance(identity, dict) or not _is_sha256(identity.get("working_diff_sha256")) or
976
+ not _is_sha256(identity.get("scope_sha256")) or
977
+ not isinstance(identity.get("baseline_commit"), str) or
978
+ evidence_id != _canonical_sha256(identity) or
979
+ os.path.basename(evidence_root) != evidence_id):
980
+ return None
981
+ if (manifest.get("protocol_version") != "1.0" or
982
+ manifest.get("sensitivity") != "project-controlled" or
983
+ manifest.get("environment_claim") != "mocked-code-level-only" or
984
+ manifest.get("compatibility") != "legacy-test-report-interface-not-supported" or
985
+ manifest.get("final_verdict") not in {"TEST_PASS", "TEST_FAIL", "TEST_BLOCKED"}):
986
+ return None
987
+ stages = manifest.get("stages")
988
+ stage_names = [stage.get("name") for stage in stages] if isinstance(stages, list) else []
989
+ if stage_names != [
990
+ "CHANGE_CLASSIFY", "SCOPE_DISCOVER", "CONTRACT_MODEL", "TEST_PLAN",
991
+ "INFRA_READY", "TEST_BUILD", "EXECUTE_PROVE", "EVIDENCE_PACKAGE",
992
+ "EVIDENCE_VALIDATE"]:
993
+ return None
994
+ for stage in stages:
995
+ if (not isinstance(stage, dict) or stage.get("status") not in {"complete", "not_applicable"} or
996
+ not _is_sha256(stage.get("input_sha256")) or
997
+ not isinstance(stage.get("outputs"), list)):
998
+ return None
999
+ if (not _is_sha256(manifest.get("working_diff_sha256")) or
1000
+ manifest.get("working_diff_sha256") != identity.get("working_diff_sha256")):
1001
+ return None
1002
+
1003
+ final_verdict = manifest["final_verdict"]
1004
+ if (verdict.get("verdict") != final_verdict or
1005
+ verdict.get("testing_domain_only") is not True or
1006
+ verdict.get("authorizes_commit_or_release") is not False or
1007
+ verdict.get("real_environment_validated") is not False or
1008
+ verdict.get("repairs_business_defects") is not False or
1009
+ not isinstance(verdict.get("blockers"), list) or
1010
+ not isinstance(verdict.get("reproduced_failures"), list)):
1011
+ return None
1012
+ if final_verdict == "TEST_PASS" and (
1013
+ verdict.get("code_evidence_replayable") is not True or
1014
+ verdict.get("blockers") or verdict.get("reproduced_failures")):
1015
+ return None
1016
+ if final_verdict == "TEST_FAIL" and not verdict.get("reproduced_failures"):
1017
+ return None
1018
+ if final_verdict == "TEST_BLOCKED" and not verdict.get("blockers"):
1019
+ return None
1020
+
1021
+ payload = sorted(
1022
+ ({"path": path, "sha256": entry["sha256"]}
1023
+ for path, entry in entry_map.items()
1024
+ if path not in EVIDENCE_FINAL_RECORDS),
1025
+ key=lambda item: item["path"],
1026
+ )
1027
+ if (validation.get("validator") != "validate_test_evidence.py" or
1028
+ not _is_sha256(validation.get("validator_sha256")) or
1029
+ validation.get("payload_sha256") != _canonical_sha256(payload) or
1030
+ validation.get("result") != "passed" or
1031
+ validation.get("verdict") != final_verdict or
1032
+ validation.get("semantics") != "integrity-and-protocol-validation-not-hostile-producer-proof" or
1033
+ not isinstance(validation.get("replay_receipt_ids"), list)):
1034
+ return None
1035
+ if not _evidence_validator_passes(project_root, evidence_root, validation):
1036
+ return None
1037
+
1038
+ report_scope = _normalize_artifact_dir(_extract_report_scope(_read_text(report_path)))
1039
+ expected_scope = _normalize_artifact_dir(artifact_dir)
1040
+ if report_scope != expected_scope:
1041
+ return None
1042
+ if not _validate_target_hashes(evidence_root, project_root, manifest, entry_map):
1043
+ return None
1044
+ scope = _load_strict_json(os.path.join(evidence_root, "scope.json"))
1045
+ if not isinstance(scope, dict) or identity.get("scope_sha256") != _canonical_sha256(scope):
1046
+ return None
1047
+
1048
+ state_path = os.path.join(
1049
+ project_root, ".prizmkit", "state", "workflows",
1050
+ os.path.basename(artifact_dir) + ".json",
1051
+ )
1052
+ state = _load_strict_json(state_path) if os.path.isfile(state_path) else {}
1053
+ if state and (not isinstance(state, dict) or state.get("stage_result") not in {None, final_verdict, "TEST_PASS", "TEST_FAIL", "TEST_BLOCKED"}):
1054
+ return None
1055
+ return {
1056
+ "evidence_id": evidence_id,
1057
+ "evidence_dir": os.path.relpath(evidence_root, project_root),
1058
+ "manifest_path": os.path.relpath(os.path.join(evidence_root, "manifest.json"), project_root),
1059
+ "verdict_path": os.path.relpath(os.path.join(evidence_root, "verdict.json"), project_root),
1060
+ "validation_path": os.path.relpath(os.path.join(evidence_root, "validation.json"), project_root),
1061
+ "verdict": final_verdict,
1062
+ "repair_scope": state.get("repair_scope") if isinstance(state, dict) else None,
1063
+ "repair_round": state.get("repair_round", 0) if isinstance(state, dict) else 0,
1064
+ "target_hashes": manifest.get("target_hashes"),
1065
+ }
1066
+
1067
+
1068
+ def route_feature_repair(stage_result, repair_scope=None, repair_round=0):
1069
+ """Map Feature review/test outcomes to the shared safe route contract."""
1070
+ return route_pipeline_repair(stage_result, repair_scope, repair_round)
1071
+
1072
+
1073
+ def feature_checkpoint_metadata(artifact_dir, evidence=None, stage="plan",
1074
+ status="PLAN_READY", stage_result=None,
1075
+ repair_scope=None, repair_round=0,
1076
+ next_stage="prizmkit-implement",
1077
+ resume_from="prizmkit-implement"):
1078
+ """Build Feature L4 handoff metadata through the shared semantic contract."""
1079
+ return pipeline_checkpoint_metadata(
1080
+ artifact_dir, evidence=evidence, stage=stage, status=status,
1081
+ stage_result=stage_result, repair_scope=repair_scope,
1082
+ repair_round=repair_round, next_stage=next_stage,
1083
+ resume_from=resume_from,
1084
+ )
1085
+
1086
+
719
1087
  def _normalize_artifact_dir(path):
720
1088
  """Normalize report artifact_dir values for comparison."""
721
1089
  return path.strip().replace("\\", "/").rstrip("/")
@@ -1091,8 +1459,12 @@ def _scoped_report_fresh(project_root, pointer_artifact, report_path, report_tex
1091
1459
  return True
1092
1460
 
1093
1461
 
1094
- def _validate_scoped_test_report(project_root, artifacts):
1095
- """Validate the feature prizmkit-test pointer and scoped PASS report."""
1462
+ def _validate_legacy_scoped_test_report(project_root, artifacts):
1463
+ """Validate the legacy Markdown report for old callers only.
1464
+
1465
+ Feature checkpoints must use ``_validate_scoped_test_report`` below; this
1466
+ retained helper is intentionally not used by Feature prompt generation.
1467
+ """
1096
1468
  pointer_artifacts = [a for a in artifacts
1097
1469
  if a.endswith("/test-report-path.txt")]
1098
1470
  if not pointer_artifacts:
@@ -1130,6 +1502,16 @@ def _validate_scoped_test_report(project_root, artifacts):
1130
1502
  report_path, report_text))
1131
1503
 
1132
1504
 
1505
+ def _validate_scoped_test_report(project_root, artifacts):
1506
+ """Validate current Feature structured evidence, never Markdown authority."""
1507
+ pointer_artifacts = [a for a in artifacts if a.endswith("/test-report-path.txt")]
1508
+ if not pointer_artifacts:
1509
+ return False
1510
+ return validate_feature_test_evidence(
1511
+ project_root, os.path.dirname(pointer_artifacts[0]),
1512
+ ) is not None
1513
+
1514
+
1133
1515
  def _completed_step_artifacts_valid(project_root, skill_key, artifacts):
1134
1516
  """Return whether a completed checkpoint step can be preserved."""
1135
1517
  if not all(_artifact_exists(project_root, a) for a in artifacts):
@@ -1886,6 +2268,16 @@ def main():
1886
2268
  checkpoint_dir, "workflow-checkpoint.json",
1887
2269
  )
1888
2270
 
2271
+ # L4 metadata remains separate from the L1 workflow-state contract.
2272
+ checkpoint["feature_state"] = feature_checkpoint_metadata(
2273
+ os.path.join(".prizmkit", "specs", feature_slug),
2274
+ stage="plan",
2275
+ status="PLAN_READY",
2276
+ stage_result=None,
2277
+ next_stage="prizmkit-implement",
2278
+ resume_from="prizmkit-implement",
2279
+ )
2280
+
1889
2281
  # On resume, merge existing completed state (with artifact validation)
1890
2282
  if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
1891
2283
  try:
@@ -22,9 +22,11 @@ from prompt_framework import (
22
22
  merge_checkpoint_state,
23
23
  mode_agent_count,
24
24
  normalize_mode,
25
+ pipeline_checkpoint_metadata,
25
26
  read_text_file,
26
27
  render_from_sections,
27
28
  replace_placeholders,
29
+ route_pipeline_repair,
28
30
  validate_rendered,
29
31
  write_output,
30
32
  )
@@ -34,6 +36,30 @@ from utils import enrich_global_context, helper_replacements, load_json_file, re
34
36
  LOGGER = setup_logging("generate-bugfix-prompt")
35
37
 
36
38
 
39
+ def route_bugfix_repair(stage_result, repair_scope=None, repair_round=0):
40
+ """Map Bugfix review/test outcomes to the shared safe route contract."""
41
+ return route_pipeline_repair(stage_result, repair_scope, repair_round)
42
+
43
+
44
+ def bugfix_checkpoint_metadata(artifact_dir, evidence=None, stage="plan",
45
+ status="PLAN_READY", stage_result=None,
46
+ repair_scope=None, repair_round=0,
47
+ next_stage="prizmkit-implement",
48
+ resume_from="prizmkit-implement"):
49
+ """Build Bugfix L4 handoff metadata while preserving Bugfix L1 state."""
50
+ return pipeline_checkpoint_metadata(
51
+ artifact_dir,
52
+ evidence=evidence,
53
+ stage=stage,
54
+ status=status,
55
+ stage_result=stage_result,
56
+ repair_scope=repair_scope,
57
+ repair_round=repair_round,
58
+ next_stage=next_stage,
59
+ resume_from=resume_from,
60
+ )
61
+
62
+
37
63
  # Section name -> checkpoint contract.
38
64
  BUGFIX_SECTION_TO_SKILL = {
39
65
  "bugfix-phase-init": checkpoint_step("prizmkit-init", "Initialize", [".prizmkit/bugfix/{slug}"]),
@@ -43,6 +69,14 @@ BUGFIX_SECTION_TO_SKILL = {
43
69
  ),
44
70
  "bugfix-phase-implement": checkpoint_step("prizmkit-implement", "Implement Fix", []),
45
71
  "bugfix-phase-review": checkpoint_step("prizmkit-code-review", "Code Review", []),
72
+ "bugfix-phase-test": checkpoint_step(
73
+ "prizmkit-test", "Post-Review Test Gate",
74
+ [
75
+ ".prizmkit/test/evidence/*/manifest.json",
76
+ ".prizmkit/test/evidence/*/verdict.json",
77
+ ".prizmkit/test/evidence/*/validation.json",
78
+ ],
79
+ ),
46
80
  "phase-browser": checkpoint_step("browser-verification", "Browser Verification", []),
47
81
  "bugfix-phase-commit-report": checkpoint_group([
48
82
  ("prizmkit-retrospective", "Retrospective", []),
@@ -378,6 +412,7 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
378
412
  sections.extend([
379
413
  ("bugfix-phase-implement", load_section(sections_dir, "bugfix-phase-implement.md")),
380
414
  ("bugfix-phase-review", load_section(sections_dir, "bugfix-phase-review.md")),
415
+ ("bugfix-phase-test", load_section(sections_dir, "bugfix-phase-test.md")),
381
416
  ])
382
417
  if browser_enabled:
383
418
  browser_section_file = {
@@ -395,6 +430,7 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
395
430
  ])
396
431
  return sections
397
432
  sections.extend([
433
+ ("headless-commit-authorization", load_section(sections_dir, "headless-commit-authorization.md")),
398
434
  ("bugfix-phase-commit-report", load_section(sections_dir, "bugfix-phase-commit-report.md")),
399
435
  ("bugfix-session-status", load_section(sections_dir, "bugfix-session-status.md")),
400
436
  ("bugfix-critical-paths", load_section(sections_dir, "bugfix-critical-paths.md")),
@@ -412,6 +448,7 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
412
448
  ("bugfix-phase-diagnose-plan", ""),
413
449
  ("bugfix-phase-implement", ""),
414
450
  ("bugfix-phase-review", ""),
451
+ ("bugfix-phase-test", ""),
415
452
  ("bugfix-phase-commit-report", ""),
416
453
  ]
417
454
  return generate_checkpoint_from_sections(
@@ -428,7 +465,11 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
428
465
 
429
466
  def merge_bugfix_checkpoint_state(existing, fresh, project_root):
430
467
  """Merge existing bugfix checkpoint state into fresh definition."""
431
- return merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
468
+ merged = merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
469
+ existing_state = existing.get("bugfix_state")
470
+ if isinstance(existing_state, dict):
471
+ merged["bugfix_state"] = existing_state
472
+ return merged
432
473
 
433
474
 
434
475
  def emit_failure(message):
@@ -503,6 +544,18 @@ def main():
503
544
  checkpoint = generate_bugfix_checkpoint(
504
545
  args.bug_id, args.session_id, pipeline_mode, sections if use_sections else None,
505
546
  )
547
+ if not any(step.get("skill") == "prizmkit-test" for step in checkpoint["steps"]):
548
+ emit_failure("Bugfix checkpoint is missing the mandatory prizmkit-test stage")
549
+ checkpoint["bugfix_state"] = bugfix_checkpoint_metadata(
550
+ replacements["{{ARTIFACT_DIR}}"],
551
+ stage="plan",
552
+ status="PLAN_READY",
553
+ stage_result=None,
554
+ repair_scope=None,
555
+ repair_round=0,
556
+ next_stage="prizmkit-implement",
557
+ resume_from="prizmkit-implement",
558
+ )
506
559
 
507
560
  if (args.resume_phase != "null" or is_continuation(args)) and os.path.exists(checkpoint_path):
508
561
  try:
@@ -22,9 +22,11 @@ from prompt_framework import (
22
22
  merge_checkpoint_state,
23
23
  mode_agent_count,
24
24
  normalize_mode,
25
+ pipeline_checkpoint_metadata,
25
26
  read_text_file,
26
27
  render_from_sections,
27
28
  replace_placeholders,
29
+ route_pipeline_repair,
28
30
  validate_rendered,
29
31
  write_output,
30
32
  )
@@ -34,6 +36,30 @@ from utils import enrich_global_context, helper_replacements, load_json_file, re
34
36
  LOGGER = setup_logging("generate-refactor-prompt")
35
37
 
36
38
 
39
+ def route_refactor_repair(stage_result, repair_scope=None, repair_round=0):
40
+ """Map Refactor review/test outcomes to the shared safe route contract."""
41
+ return route_pipeline_repair(stage_result, repair_scope, repair_round)
42
+
43
+
44
+ def refactor_checkpoint_metadata(artifact_dir, evidence=None, stage="plan",
45
+ status="PLAN_READY", stage_result=None,
46
+ repair_scope=None, repair_round=0,
47
+ next_stage="prizmkit-implement",
48
+ resume_from="prizmkit-implement"):
49
+ """Build Refactor L4 handoff metadata while preserving Refactor L1 state."""
50
+ return pipeline_checkpoint_metadata(
51
+ artifact_dir,
52
+ evidence=evidence,
53
+ stage=stage,
54
+ status=status,
55
+ stage_result=stage_result,
56
+ repair_scope=repair_scope,
57
+ repair_round=repair_round,
58
+ next_stage=next_stage,
59
+ resume_from=resume_from,
60
+ )
61
+
62
+
37
63
  REFACTOR_SECTION_TO_SKILL = {
38
64
  "refactor-phase-init": checkpoint_step("prizmkit-init", "Initialize", [".prizmkit/refactor/{slug}"]),
39
65
  "refactor-phase-plan": checkpoint_step(
@@ -48,6 +74,14 @@ REFACTOR_SECTION_TO_SKILL = {
48
74
  "prizmkit-code-review", "Review — Code Review & Behavior Verification",
49
75
  [".prizmkit/refactor/{slug}/review-report.md"],
50
76
  ),
77
+ "refactor-phase-test": checkpoint_step(
78
+ "prizmkit-test", "Post-Review Test Gate",
79
+ [
80
+ ".prizmkit/test/evidence/*/manifest.json",
81
+ ".prizmkit/test/evidence/*/verdict.json",
82
+ ".prizmkit/test/evidence/*/validation.json",
83
+ ],
84
+ ),
51
85
  "phase-browser": checkpoint_step("browser-verification", "Browser Verification", []),
52
86
  "refactor-phase-commit-report": checkpoint_group([
53
87
  ("prizmkit-retrospective", "Retrospective", []),
@@ -399,6 +433,7 @@ def assemble_refactor_sections(pipeline_mode, sections_dir, browser_enabled, bro
399
433
  sections.extend([
400
434
  ("refactor-phase-implement", load_section(sections_dir, "refactor-phase-implement.md")),
401
435
  ("refactor-phase-review", load_section(sections_dir, "refactor-phase-review.md")),
436
+ ("refactor-phase-test", load_section(sections_dir, "refactor-phase-test.md")),
402
437
  ])
403
438
  if browser_enabled:
404
439
  browser_section_file = {
@@ -407,6 +442,7 @@ def assemble_refactor_sections(pipeline_mode, sections_dir, browser_enabled, bro
407
442
  }.get(browser_tool, "phase-browser-verification-auto.md")
408
443
  sections.append(("phase-browser", load_section(sections_dir, browser_section_file)))
409
444
  sections.extend([
445
+ ("headless-commit-authorization", load_section(sections_dir, "headless-commit-authorization.md")),
410
446
  ("refactor-phase-commit-report", load_section(sections_dir, "refactor-phase-commit-report.md")),
411
447
  ("refactor-session-status", load_section(sections_dir, "refactor-session-status.md")),
412
448
  ("refactor-critical-paths", load_section(sections_dir, "refactor-critical-paths.md")),
@@ -423,6 +459,7 @@ def generate_refactor_checkpoint(refactor_id, session_id, pipeline_mode="standar
423
459
  ("refactor-phase-plan", ""),
424
460
  ("refactor-phase-implement", ""),
425
461
  ("refactor-phase-review", ""),
462
+ ("refactor-phase-test", ""),
426
463
  ("refactor-phase-commit-report", ""),
427
464
  ]
428
465
  return generate_checkpoint_from_sections(
@@ -438,7 +475,11 @@ def generate_refactor_checkpoint(refactor_id, session_id, pipeline_mode="standar
438
475
 
439
476
 
440
477
  def merge_refactor_checkpoint_state(existing, fresh, project_root):
441
- return merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
478
+ merged = merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
479
+ existing_state = existing.get("refactor_state")
480
+ if isinstance(existing_state, dict):
481
+ merged["refactor_state"] = existing_state
482
+ return merged
442
483
 
443
484
 
444
485
  def emit_failure(message):
@@ -511,6 +552,18 @@ def main():
511
552
  checkpoint = generate_refactor_checkpoint(
512
553
  args.refactor_id, args.session_id, pipeline_mode, sections if use_sections else None,
513
554
  )
555
+ if not any(step.get("skill") == "prizmkit-test" for step in checkpoint["steps"]):
556
+ emit_failure("Refactor checkpoint is missing the mandatory prizmkit-test stage")
557
+ checkpoint["refactor_state"] = refactor_checkpoint_metadata(
558
+ replacements["{{ARTIFACT_DIR}}"],
559
+ stage="plan",
560
+ status="PLAN_READY",
561
+ stage_result=None,
562
+ repair_scope=None,
563
+ repair_round=0,
564
+ next_stage="prizmkit-implement",
565
+ resume_from="prizmkit-implement",
566
+ )
514
567
 
515
568
  if (args.resume_phase != "null" or is_continuation(args)) and os.path.exists(checkpoint_path):
516
569
  try: