prizmkit 1.1.130 → 1.1.134

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 (119) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +0 -2
  3. package/bundled/dev-pipeline/.env.example +1 -1
  4. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +11 -6
  5. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -9
  7. package/bundled/dev-pipeline/prizmkit_runtime/processes.py +21 -1
  8. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  9. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  11. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  12. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +119 -18
  13. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +3 -2
  14. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +411 -17
  15. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +57 -14
  16. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +5 -4
  17. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +55 -2
  18. package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
  19. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  20. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  21. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  22. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  23. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +5 -5
  24. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +28 -22
  25. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +24 -10
  26. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +20 -2
  27. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +2 -2
  28. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +5 -10
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -19
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  32. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +1 -1
  33. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  34. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  35. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +15 -0
  36. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  37. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  38. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  39. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -5
  40. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +15 -11
  41. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +1 -1
  42. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +6 -6
  43. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  44. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  45. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
  46. package/bundled/dev-pipeline/templates/sections/task-contract.md +4 -0
  47. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  48. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +349 -48
  49. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +129 -0
  50. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  51. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +567 -23
  52. package/bundled/dev-pipeline/tests/test_unified_cli.py +37 -10
  53. package/bundled/skills/_metadata.json +63 -26
  54. package/bundled/skills/app-planner/SKILL.md +4 -3
  55. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  56. package/bundled/skills/bug-planner/SKILL.md +12 -27
  57. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  58. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  59. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  60. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  61. package/bundled/skills/feature-planner/SKILL.md +27 -22
  62. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  63. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  64. package/bundled/skills/prizmkit/SKILL.md +130 -94
  65. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  66. package/bundled/skills/prizmkit-code-review/SKILL.md +132 -86
  67. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +243 -0
  68. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +42 -1
  69. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +93 -1
  71. package/bundled/skills/prizmkit-committer/SKILL.md +125 -48
  72. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  73. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  74. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  75. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  76. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  77. package/bundled/skills/prizmkit-plan/SKILL.md +146 -105
  78. package/bundled/skills/prizmkit-plan/assets/plan-template.md +18 -0
  79. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +242 -0
  80. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +4 -4
  81. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  82. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  83. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  84. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  85. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  86. package/bundled/skills/prizmkit-test/SKILL.md +52 -6
  87. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +63 -12
  88. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -1
  89. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +13 -0
  90. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +6 -1
  91. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -1
  92. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  93. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +4 -1
  94. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  95. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +15 -0
  96. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  97. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +744 -23
  98. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +194 -15
  99. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  100. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  101. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  102. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  103. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  104. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  105. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  106. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  107. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  108. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  109. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  110. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  111. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  112. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  113. package/bundled/templates/project-memory-template.md +4 -4
  114. package/package.json +1 -1
  115. package/src/index.js +4 -0
  116. package/src/prompts.js +10 -3
  117. package/src/scaffold.js +1 -1
  118. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  119. 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
 
@@ -675,11 +681,13 @@ SECTION_TO_SKILL = {
675
681
  "prizmkit-plan", "Plan & Tasks",
676
682
  [".prizmkit/specs/{slug}/plan.md"],
677
683
  ),
678
- "phase-implement": checkpoint_step("prizmkit-implement", "Implement + Test", []),
684
+ "phase-implement": checkpoint_step("prizmkit-implement", "Implement", []),
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):
@@ -1237,10 +1619,10 @@ def _tier_header(pipeline_mode):
1237
1619
  "# Dev-Pipeline Session Bootstrap — Tier 3 "
1238
1620
  "(Main-Agent Full Guardrails)\n",
1239
1621
  "**Tier 3 — Main-Agent Full Guardrails**: For complex "
1240
- "features, the main orchestrator handles context, planning, "
1241
- "implementation, and Code Review directly. No top-level Dev "
1242
- "implementation subagent is spawned, and Code Review never uses "
1243
- "direct or indirect delegation.\n",
1622
+ "features, the main orchestrator handles context, planning, and "
1623
+ "implementation directly, then runs mandatory Main-Agent review. "
1624
+ "After convergence, the review skill may use only its strict-gated "
1625
+ "skill-owned independent Reviewer.\n",
1244
1626
  ),
1245
1627
  }
1246
1628
  return headers.get(pipeline_mode, headers["lite"])
@@ -1276,8 +1658,8 @@ def _tier_reminders(pipeline_mode):
1276
1658
  "Code Review writes only review-report.md",
1277
1659
  "- Gate checks require the Implementation Log (with Gate Evidence) "
1278
1660
  "and a valid review-report.md Final Result before proceeding",
1279
- "- Code Review runs only in the current Main Agent and never uses "
1280
- "direct or indirect delegation",
1661
+ "- Code Review keeps mandatory Main-Agent ownership; after convergence "
1662
+ "it may use only its strict-gated skill-owned independent Reviewer",
1281
1663
  "- On timeout outside Code Review: check snapshot + git diff HEAD "
1282
1664
  "→ model:lite → remaining steps only → max 2 retries per phase → "
1283
1665
  "Main Agent fallback",
@@ -1285,13 +1667,13 @@ def _tier_reminders(pipeline_mode):
1285
1667
  else: # full
1286
1668
  specific = [
1287
1669
  "- Tier 3: full orchestration — the Main Agent implements directly "
1288
- "and owns the complete Code Review loop",
1670
+ "and owns the mandatory Code Review loop",
1289
1671
  "- context-snapshot.md is the implementation knowledge base; "
1290
1672
  "Code Review writes only review-report.md",
1291
1673
  "- Gate checks require the Implementation Log and a valid "
1292
1674
  "review-report.md Final Result before proceeding",
1293
- "- Code Review is excluded from normal work delegation and never "
1294
- "uses direct or indirect delegation or another review entry point",
1675
+ "- Code Review excludes ordinary work delegation and permits only "
1676
+ "the skill-owned independent Reviewer under its strict structural gate",
1295
1677
  "- Do NOT use `run_in_background=true` when spawning normal work "
1296
1678
  "agents outside Code Review",
1297
1679
  "- If a normal work Agent call fails with team/config/lock errors, "
@@ -1483,11 +1865,13 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1483
1865
  load_section(sections_dir,
1484
1866
  browser_section_file)))
1485
1867
 
1868
+ # --- Headless commit authorization ---
1869
+ sections.append(("headless-commit-authorization",
1870
+ load_section(sections_dir, "headless-commit-authorization.md")))
1871
+
1486
1872
  # --- Commit (tier-dependent) ---
1487
1873
  if pipeline_mode == "full":
1488
- sections.append(("phase-commit",
1489
- load_section(sections_dir,
1490
- "phase-commit-full.md")))
1874
+ sections.append(("phase-commit", load_section(sections_dir, "phase-commit-full.md")))
1491
1875
  else:
1492
1876
  sections.append(("phase-commit",
1493
1877
  load_section(sections_dir,
@@ -1886,6 +2270,16 @@ def main():
1886
2270
  checkpoint_dir, "workflow-checkpoint.json",
1887
2271
  )
1888
2272
 
2273
+ # L4 metadata remains separate from the L1 workflow-state contract.
2274
+ checkpoint["feature_state"] = feature_checkpoint_metadata(
2275
+ os.path.join(".prizmkit", "specs", feature_slug),
2276
+ stage="plan",
2277
+ status="PLAN_READY",
2278
+ stage_result=None,
2279
+ next_stage="prizmkit-implement",
2280
+ resume_from="prizmkit-implement",
2281
+ )
2282
+
1889
2283
  # On resume, merge existing completed state (with artifact validation)
1890
2284
  if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
1891
2285
  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", []),
@@ -266,12 +300,9 @@ def build_replacements(args, bug, global_context, script_dir):
266
300
  dev_url = "http://localhost:{}".format(dev_port) if dev_port.isdigit() else "<UNKNOWN_DETECT_FROM_PROJECT_CONFIG>"
267
301
 
268
302
  pipeline_mode = determine_bugfix_mode(args, bug)
269
- recovery_like = is_recovery_like(args)
270
303
  manual_or_hybrid = verification_type in ("manual", "hybrid")
271
304
  manual_policy = (
272
- "Recovery-class session: automated verification may substitute for manual/hybrid UAT when evidence is documented."
273
- if manual_or_hybrid and recovery_like
274
- else "Fresh manual/hybrid bugfix: stop as partial before commit after automated checks; manual UAT is required."
305
+ "Manual/hybrid verification is executed automatically in headless mode: use available browser, CLI, API, fixture, and test evidence; document the verification evidence and proceed through review, test, retrospective, and commit."
275
306
  if manual_or_hybrid
276
307
  else "Automated verification: complete tests/review and proceed to commit when gates pass."
277
308
  )
@@ -356,7 +387,7 @@ def _bugfix_header(pipeline_mode):
356
387
  title = "# Dev-Pipeline Bug Fix Session Bootstrap — {}\n".format(pipeline_mode.title())
357
388
  desc = {
358
389
  "lite": "**Tier 1 — Single Agent**: direct root-cause fix by the main orchestrator.",
359
- "standard": "**Tier 2 — Main-Agent Review Gates**: direct implementation with Main-Agent-only review gates.",
390
+ "standard": "**Tier 2 — Main-Agent Review Gates**: direct implementation with mandatory Main-Agent review and optional strict capability-gated independent correctness review.",
360
391
  "full": "**Tier 3 — Full Bugfix Guardrails**: direct implementation with stronger diagnosis and review gates.",
361
392
  }.get(pipeline_mode, "**Bugfix pipeline**")
362
393
  return title + "\n" + desc + "\n"
@@ -378,6 +409,7 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
378
409
  sections.extend([
379
410
  ("bugfix-phase-implement", load_section(sections_dir, "bugfix-phase-implement.md")),
380
411
  ("bugfix-phase-review", load_section(sections_dir, "bugfix-phase-review.md")),
412
+ ("bugfix-phase-test", load_section(sections_dir, "bugfix-phase-test.md")),
381
413
  ])
382
414
  if browser_enabled:
383
415
  browser_section_file = {
@@ -387,14 +419,8 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
387
419
  sections.append(("phase-browser", load_section(sections_dir, browser_section_file)))
388
420
  if manual_gate:
389
421
  sections.append(("bugfix-manual-gate", load_section(sections_dir, "bugfix-phase-manual-verification.md")))
390
- sections.extend([
391
- ("bugfix-session-status", load_section(sections_dir, "bugfix-session-status.md")),
392
- ("bugfix-critical-paths", load_section(sections_dir, "bugfix-critical-paths.md")),
393
- ("failure-capture", load_section(sections_dir, "failure-capture.md")),
394
- ("bugfix-reminders", load_section(sections_dir, "bugfix-reminders.md")),
395
- ])
396
- return sections
397
422
  sections.extend([
423
+ ("headless-commit-authorization", load_section(sections_dir, "headless-commit-authorization.md")),
398
424
  ("bugfix-phase-commit-report", load_section(sections_dir, "bugfix-phase-commit-report.md")),
399
425
  ("bugfix-session-status", load_section(sections_dir, "bugfix-session-status.md")),
400
426
  ("bugfix-critical-paths", load_section(sections_dir, "bugfix-critical-paths.md")),
@@ -412,6 +438,7 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
412
438
  ("bugfix-phase-diagnose-plan", ""),
413
439
  ("bugfix-phase-implement", ""),
414
440
  ("bugfix-phase-review", ""),
441
+ ("bugfix-phase-test", ""),
415
442
  ("bugfix-phase-commit-report", ""),
416
443
  ]
417
444
  return generate_checkpoint_from_sections(
@@ -428,7 +455,11 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
428
455
 
429
456
  def merge_bugfix_checkpoint_state(existing, fresh, project_root):
430
457
  """Merge existing bugfix checkpoint state into fresh definition."""
431
- return merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
458
+ merged = merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
459
+ existing_state = existing.get("bugfix_state")
460
+ if isinstance(existing_state, dict):
461
+ merged["bugfix_state"] = existing_state
462
+ return merged
432
463
 
433
464
 
434
465
  def emit_failure(message):
@@ -459,7 +490,7 @@ def main():
459
490
  replacements = build_replacements(args, bug, global_context, script_dir)
460
491
  pipeline_mode = replacements["{{PIPELINE_MODE}}"]
461
492
  browser_enabled = replacements["{{BROWSER_ENABLED}}"] == "true"
462
- manual_gate = bug.get("verification_type", "automated") in ("manual", "hybrid") and not is_recovery_like(args)
493
+ manual_gate = bug.get("verification_type", "automated") in ("manual", "hybrid")
463
494
 
464
495
  use_sections = os.path.isdir(sections_dir) and not args.template
465
496
  sections = []
@@ -503,6 +534,18 @@ def main():
503
534
  checkpoint = generate_bugfix_checkpoint(
504
535
  args.bug_id, args.session_id, pipeline_mode, sections if use_sections else None,
505
536
  )
537
+ if not any(step.get("skill") == "prizmkit-test" for step in checkpoint["steps"]):
538
+ emit_failure("Bugfix checkpoint is missing the mandatory prizmkit-test stage")
539
+ checkpoint["bugfix_state"] = bugfix_checkpoint_metadata(
540
+ replacements["{{ARTIFACT_DIR}}"],
541
+ stage="plan",
542
+ status="PLAN_READY",
543
+ stage_result=None,
544
+ repair_scope=None,
545
+ repair_round=0,
546
+ next_stage="prizmkit-implement",
547
+ resume_from="prizmkit-implement",
548
+ )
506
549
 
507
550
  if (args.resume_phase != "null" or is_continuation(args)) and os.path.exists(checkpoint_path):
508
551
  try: