prizmkit 1.1.129 → 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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +398 -6
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +61 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +64 -4
- package/bundled/dev-pipeline/scripts/prompt_framework.py +156 -7
- package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
- package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +404 -24
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
- package/bundled/skills/_metadata.json +62 -25
- package/bundled/skills/app-planner/SKILL.md +4 -3
- package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
- package/bundled/skills/bug-planner/SKILL.md +12 -27
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
- package/bundled/skills/feature-planner/SKILL.md +27 -22
- package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +94 -121
- package/bundled/skills/prizmkit/SKILL.md +130 -94
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-init/SKILL.md +5 -5
- package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/SKILL.md +28 -6
- package/bundled/skills/prizmkit-test/references/examples.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/recovery-workflow/SKILL.md +62 -241
- package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
- package/bundled/skills/recovery-workflow/references/detection.md +28 -52
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
- package/bundled/skills/refactor-planner/SKILL.md +12 -57
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
- package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
- package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
- package/bundled/skills/refactor-workflow/SKILL.md +98 -139
- package/bundled/templates/project-memory-template.md +4 -4
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/prompts.js +10 -3
- package/src/scaffold.js +1 -1
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
- 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/*/
|
|
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
|
|
1095
|
-
"""Validate the
|
|
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):
|
|
@@ -1637,8 +2019,8 @@ def build_replacements(args, feature, features, global_context, script_dir):
|
|
|
1637
2019
|
effective_resume = "6"
|
|
1638
2020
|
|
|
1639
2021
|
|
|
1640
|
-
# Browser verification
|
|
1641
|
-
#
|
|
2022
|
+
# Browser verification is opt-in through a browser_interaction object and
|
|
2023
|
+
# can always be disabled globally with BROWSER_VERIFY=false.
|
|
1642
2024
|
browser_enabled, browser_tool, browser_verify_steps, browser_blocking = browser_enabled_from_item(feature)
|
|
1643
2025
|
|
|
1644
2026
|
if browser_tool not in ("playwright-cli", "opencli", "auto"):
|
|
@@ -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", []),
|
|
@@ -362,7 +396,7 @@ def _bugfix_header(pipeline_mode):
|
|
|
362
396
|
return title + "\n" + desc + "\n"
|
|
363
397
|
|
|
364
398
|
|
|
365
|
-
def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manual_gate):
|
|
399
|
+
def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manual_gate, browser_tool="auto"):
|
|
366
400
|
sections = [
|
|
367
401
|
("header", _bugfix_header(pipeline_mode)),
|
|
368
402
|
("bugfix-session-context", load_section(sections_dir, "bugfix-session-context.md")),
|
|
@@ -378,9 +412,14 @@ 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 = {
|
|
419
|
+
"playwright-cli": "phase-browser-verification.md",
|
|
420
|
+
"opencli": "phase-browser-verification-opencli.md",
|
|
421
|
+
}.get(browser_tool, "phase-browser-verification-auto.md")
|
|
422
|
+
sections.append(("phase-browser", load_section(sections_dir, browser_section_file)))
|
|
384
423
|
if manual_gate:
|
|
385
424
|
sections.append(("bugfix-manual-gate", load_section(sections_dir, "bugfix-phase-manual-verification.md")))
|
|
386
425
|
sections.extend([
|
|
@@ -391,6 +430,7 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
|
|
|
391
430
|
])
|
|
392
431
|
return sections
|
|
393
432
|
sections.extend([
|
|
433
|
+
("headless-commit-authorization", load_section(sections_dir, "headless-commit-authorization.md")),
|
|
394
434
|
("bugfix-phase-commit-report", load_section(sections_dir, "bugfix-phase-commit-report.md")),
|
|
395
435
|
("bugfix-session-status", load_section(sections_dir, "bugfix-session-status.md")),
|
|
396
436
|
("bugfix-critical-paths", load_section(sections_dir, "bugfix-critical-paths.md")),
|
|
@@ -408,6 +448,7 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
|
|
|
408
448
|
("bugfix-phase-diagnose-plan", ""),
|
|
409
449
|
("bugfix-phase-implement", ""),
|
|
410
450
|
("bugfix-phase-review", ""),
|
|
451
|
+
("bugfix-phase-test", ""),
|
|
411
452
|
("bugfix-phase-commit-report", ""),
|
|
412
453
|
]
|
|
413
454
|
return generate_checkpoint_from_sections(
|
|
@@ -424,7 +465,11 @@ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sec
|
|
|
424
465
|
|
|
425
466
|
def merge_bugfix_checkpoint_state(existing, fresh, project_root):
|
|
426
467
|
"""Merge existing bugfix checkpoint state into fresh definition."""
|
|
427
|
-
|
|
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
|
|
428
473
|
|
|
429
474
|
|
|
430
475
|
def emit_failure(message):
|
|
@@ -463,6 +508,7 @@ def main():
|
|
|
463
508
|
try:
|
|
464
509
|
sections = assemble_bugfix_sections(
|
|
465
510
|
pipeline_mode, sections_dir, browser_enabled, manual_gate,
|
|
511
|
+
browser_tool=replacements["{{BROWSER_TOOL}}"],
|
|
466
512
|
)
|
|
467
513
|
rendered = render_from_sections(sections, replacements)
|
|
468
514
|
except FileNotFoundError as exc:
|
|
@@ -498,6 +544,18 @@ def main():
|
|
|
498
544
|
checkpoint = generate_bugfix_checkpoint(
|
|
499
545
|
args.bug_id, args.session_id, pipeline_mode, sections if use_sections else None,
|
|
500
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
|
+
)
|
|
501
559
|
|
|
502
560
|
if (args.resume_phase != "null" or is_continuation(args)) and os.path.exists(checkpoint_path):
|
|
503
561
|
try:
|