prizmkit 1.1.86 → 1.1.87

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 (41) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +462 -4
  3. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  4. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -0
  5. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +18 -3
  6. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -3
  7. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  8. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  9. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +260 -0
  10. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
  11. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
  12. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +429 -0
  13. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +462 -4
  14. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +1 -1
  15. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +14 -0
  16. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +18 -3
  17. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +18 -3
  18. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +2 -2
  19. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +2 -2
  20. package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +186 -0
  21. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
  22. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
  23. package/bundled/skills/_metadata.json +1 -1
  24. package/bundled/skills/prizmkit-code-review/SKILL.md +7 -2
  25. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  26. package/bundled/skills/prizmkit-test/SKILL.md +106 -14
  27. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  28. package/bundled/skills/prizmkit-test/references/examples.md +82 -9
  29. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +68 -12
  30. package/bundled/skills/prizmkit-test/references/test-report-template.md +73 -9
  31. package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  32. package/bundled/skills-windows/prizmkit-code-review/SKILL.md +7 -2
  33. package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  34. package/bundled/skills-windows/prizmkit-test/SKILL.md +106 -14
  35. package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  36. package/bundled/skills-windows/prizmkit-test/references/examples.md +82 -9
  37. package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +4 -4
  38. package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +68 -12
  39. package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +73 -9
  40. package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  41. package/package.json +1 -1
@@ -21,6 +21,7 @@ Usage:
21
21
  """
22
22
 
23
23
  import argparse
24
+ import glob
24
25
  import json
25
26
  import os
26
27
  import re
@@ -661,6 +662,11 @@ SECTION_TO_SKILL = {
661
662
  [".prizmkit/specs/{slug}/plan.md"]),
662
663
  "phase-critic-plan": ("critic-plan-review", "Critic: Plan Review", []),
663
664
  "phase-implement": ("prizmkit-implement", "Implement + Test", []),
665
+ "phase-prizmkit-test": (
666
+ "prizmkit-test", "Scoped Feature Test Gate",
667
+ [".prizmkit/specs/{slug}/test-report-path.txt",
668
+ ".prizmkit/test/*/test-report.md"],
669
+ ),
664
670
  "phase-review": ("prizmkit-code-review", "Code Review", []),
665
671
  "phase-browser": ("browser-verification", "Browser Verification", []),
666
672
  "phase-commit": None, # special: split into retrospective + committer
@@ -678,6 +684,446 @@ def _resolve_artifacts(artifact_templates, slug):
678
684
  return [a.replace("{slug}", slug) for a in artifact_templates]
679
685
 
680
686
 
687
+ def _artifact_exists(project_root, artifact):
688
+ """Return whether an artifact path or glob exists under project_root."""
689
+ full_path = os.path.join(project_root, artifact)
690
+ if glob.has_magic(full_path):
691
+ return bool(glob.glob(full_path))
692
+ return os.path.exists(full_path)
693
+
694
+
695
+ def _read_text(path):
696
+ """Read a UTF-8 text file, returning an empty string on IO errors."""
697
+ try:
698
+ with open(path, "r", encoding="utf-8") as f:
699
+ return f.read()
700
+ except IOError:
701
+ return ""
702
+
703
+
704
+ def _normalize_artifact_dir(path):
705
+ """Normalize report artifact_dir values for comparison."""
706
+ return path.strip().replace("\\", "/").rstrip("/")
707
+
708
+
709
+ def _report_path_matches_artifacts(project_root, report_path, artifacts):
710
+ """Return whether report_path is an in-project test report artifact."""
711
+ test_root = os.path.realpath(
712
+ os.path.join(project_root, ".prizmkit", "test")
713
+ )
714
+ resolved_report = os.path.realpath(report_path)
715
+
716
+ try:
717
+ if os.path.commonpath([test_root, resolved_report]) != test_root:
718
+ return False
719
+ except ValueError:
720
+ return False
721
+
722
+ report_artifacts = [a for a in artifacts if a.endswith("test-report.md")]
723
+ for artifact in report_artifacts:
724
+ artifact_path = os.path.join(project_root, artifact)
725
+ if glob.has_magic(artifact_path):
726
+ candidates = glob.glob(artifact_path)
727
+ else:
728
+ candidates = [artifact_path]
729
+ for candidate in candidates:
730
+ if os.path.realpath(candidate) == resolved_report:
731
+ return True
732
+ return False
733
+
734
+
735
+ def _extract_scope_field(report_text, field_name):
736
+ """Extract a '- Field: value' entry from the report Scope section."""
737
+ in_scope = False
738
+ prefix = "- {}:".format(field_name)
739
+ for raw_line in report_text.splitlines():
740
+ line = raw_line.strip()
741
+ if line == "## Scope":
742
+ in_scope = True
743
+ continue
744
+ if in_scope and line.startswith("## "):
745
+ break
746
+ if in_scope and line.startswith(prefix):
747
+ return line[len(prefix):].strip()
748
+ return ""
749
+
750
+
751
+ def _extract_report_verdict(report_text):
752
+ """Extract the first non-empty line after the report Verdict heading."""
753
+ in_verdict = False
754
+ for raw_line in report_text.splitlines():
755
+ line = raw_line.strip()
756
+ if line == "## Verdict":
757
+ in_verdict = True
758
+ continue
759
+ if in_verdict:
760
+ if line.startswith("## "):
761
+ break
762
+ if line:
763
+ return line
764
+ return ""
765
+
766
+
767
+ def _extract_report_section(report_text, heading):
768
+ """Extract text under a level-2 markdown heading."""
769
+ in_section = False
770
+ lines = []
771
+ for raw_line in report_text.splitlines():
772
+ line = raw_line.strip()
773
+ if line == heading:
774
+ in_section = True
775
+ continue
776
+ if in_section and line.startswith("## "):
777
+ break
778
+ if in_section:
779
+ lines.append(raw_line)
780
+ return "\n".join(lines)
781
+
782
+
783
+ def _discover_openapi_file(project_root):
784
+ """Return the first common OpenAPI/Swagger file under project_root."""
785
+ candidates = [
786
+ "openapi.yaml", "openapi.yml", "swagger.yaml", "swagger.yml",
787
+ "docs/openapi.yaml", "docs/openapi.yml",
788
+ "docs/swagger.yaml", "docs/swagger.yml",
789
+ "api/openapi.yaml", "api/openapi.yml",
790
+ "api/swagger.yaml", "api/swagger.yml",
791
+ ]
792
+ for candidate in candidates:
793
+ full_path = os.path.join(project_root, candidate)
794
+ if os.path.isfile(full_path):
795
+ return full_path
796
+ return ""
797
+
798
+
799
+ def _normalize_api_path(path):
800
+ """Normalize API path tokens for exact comparison."""
801
+ normalized = path.strip().strip("'\"`")
802
+ if len(normalized) > 1:
803
+ normalized = normalized.rstrip("/")
804
+ return normalized
805
+
806
+
807
+ def _extract_openapi_operations(openapi_text):
808
+ """Extract OpenAPI method+path operations without requiring PyYAML."""
809
+ methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"}
810
+ operations = []
811
+ in_paths = False
812
+ paths_indent = 0
813
+ current_path = ""
814
+ current_path_indent = 0
815
+ path_key_re = re.compile(
816
+ r"^\s*(?:(['\"])(/.*?)\1|(/[^:\s]+))\s*:\s*(?:#.*)?$"
817
+ )
818
+ method_re = re.compile(r"^\s*([A-Za-z]+)\s*:\s*(?:#.*)?$")
819
+
820
+ for raw_line in openapi_text.splitlines():
821
+ stripped = raw_line.strip()
822
+ if not stripped or stripped.startswith("#"):
823
+ continue
824
+
825
+ indent = len(raw_line) - len(raw_line.lstrip())
826
+ if not in_paths:
827
+ if re.match(r"^\s*paths\s*:\s*(?:#.*)?$", raw_line):
828
+ in_paths = True
829
+ paths_indent = indent
830
+ continue
831
+
832
+ if indent <= paths_indent:
833
+ break
834
+
835
+ path_match = path_key_re.match(raw_line)
836
+ if path_match:
837
+ current_path = path_match.group(2) or path_match.group(3) or ""
838
+ current_path_indent = indent
839
+ continue
840
+
841
+ method_match = method_re.match(raw_line)
842
+ if current_path and method_match and indent > current_path_indent:
843
+ method = method_match.group(1).upper()
844
+ if method in methods:
845
+ operations.append((method, _normalize_api_path(current_path)))
846
+
847
+ return operations
848
+
849
+
850
+ def _extract_matrix_operations(rows):
851
+ """Extract exact method+path tokens from Boundary Matrix rows."""
852
+ operations = set()
853
+ path_only = set()
854
+ token_re = re.compile(
855
+ r"(?:(\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b)\s+)?(/[^\s,`|)]+)",
856
+ flags=re.IGNORECASE,
857
+ )
858
+ for row in rows:
859
+ interface = row.get("Interface", "")
860
+ for match in token_re.finditer(interface):
861
+ method = match.group(1).upper() if match.group(1) else ""
862
+ path = _normalize_api_path(match.group(2))
863
+ if not path:
864
+ continue
865
+ if method:
866
+ operations.add((method, path))
867
+ else:
868
+ path_only.add(path)
869
+ return operations, path_only
870
+
871
+
872
+ def _boundary_report_sections_valid(project_root, report_text):
873
+ """Return whether required boundary sections do not report failure."""
874
+ required_columns = [
875
+ "Module",
876
+ "Interface",
877
+ "Service Type",
878
+ "Happy Path",
879
+ "Request Validation",
880
+ "Auth / Permission / Ownership",
881
+ "Domain Invariants",
882
+ "Collection / Pagination",
883
+ "Date / Time",
884
+ "State Transitions",
885
+ "Dependency Failure",
886
+ "Response Contract",
887
+ "N/A Reasons",
888
+ "Final Status",
889
+ ]
890
+ category_columns = [
891
+ "Happy Path",
892
+ "Request Validation",
893
+ "Auth / Permission / Ownership",
894
+ "Domain Invariants",
895
+ "Collection / Pagination",
896
+ "Date / Time",
897
+ "State Transitions",
898
+ "Dependency Failure",
899
+ "Response Contract",
900
+ ]
901
+ allowed_final_statuses = {
902
+ "boundary-covered",
903
+ "generated-needs-review",
904
+ "skipped-with-reason",
905
+ }
906
+ incomplete_markers = {
907
+ "boundary-missing",
908
+ "happy-path-only",
909
+ "unresolved",
910
+ }
911
+
912
+ matrix_section = _extract_report_section(report_text, "## Boundary Matrix")
913
+ if not matrix_section:
914
+ return False
915
+ matrix_lines = [line.strip() for line in matrix_section.splitlines()
916
+ if line.strip().startswith("|")]
917
+ headers = []
918
+ rows = []
919
+ for line in matrix_lines:
920
+ cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
921
+ if not headers:
922
+ if "Interface" in cells and "Final Status" in cells:
923
+ headers = cells
924
+ continue
925
+ if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells):
926
+ continue
927
+ if len(cells) < len(headers):
928
+ cells.extend([""] * (len(headers) - len(cells)))
929
+ row = {header: cells[i] if i < len(cells) else ""
930
+ for i, header in enumerate(headers)}
931
+ if row.get("Interface", "").strip(".` ") in {"", "..."}:
932
+ continue
933
+ rows.append(row)
934
+
935
+ if not headers or not rows:
936
+ return False
937
+ if any(column not in headers for column in required_columns):
938
+ return False
939
+
940
+ for row in rows:
941
+ final_status = row.get("Final Status", "").strip().lower()
942
+ if final_status not in allowed_final_statuses:
943
+ return False
944
+ for column in category_columns:
945
+ if not row.get(column, "").strip():
946
+ return False
947
+ for value in row.values():
948
+ normalized = value.strip().lower()
949
+ if any(marker in normalized for marker in incomplete_markers):
950
+ return False
951
+
952
+ openapi_file = _discover_openapi_file(project_root)
953
+ if openapi_file:
954
+ operations = _extract_openapi_operations(_read_text(openapi_file))
955
+ matrix_operations, _matrix_path_only = _extract_matrix_operations(rows)
956
+ for method, path in operations:
957
+ if (method, path) not in matrix_operations:
958
+ return False
959
+
960
+ gate_section = _extract_report_section(report_text,
961
+ "## Boundary Completion Gate")
962
+ if not gate_section:
963
+ return False
964
+ completion_gate_passed = False
965
+ boundary_missing_seen = False
966
+ happy_path_only_seen = False
967
+ for raw_line in gate_section.splitlines():
968
+ line = raw_line.strip()
969
+ match = re.match(r"-?\s*(Boundary-missing|Happy-path-only):\s*(\d+)\s*$",
970
+ line, re.IGNORECASE)
971
+ if match:
972
+ if match.group(1).lower() == "boundary-missing":
973
+ boundary_missing_seen = True
974
+ if match.group(1).lower() == "happy-path-only":
975
+ happy_path_only_seen = True
976
+ if int(match.group(2)) > 0:
977
+ return False
978
+ if re.match(r"-?\s*Completion gate passed:\s*yes\s*$",
979
+ line, re.IGNORECASE):
980
+ completion_gate_passed = True
981
+ if re.match(r"-?\s*Completion gate passed:\s*no\b",
982
+ line, re.IGNORECASE):
983
+ return False
984
+ if not (completion_gate_passed and boundary_missing_seen and
985
+ happy_path_only_seen):
986
+ return False
987
+
988
+ validation_section = _extract_report_section(report_text,
989
+ "## Boundary Validation")
990
+ if not validation_section:
991
+ return False
992
+ validator_result = ""
993
+ for raw_line in validation_section.splitlines():
994
+ line = raw_line.strip()
995
+ match = re.match(r"-?\s*Validator result:\s*(\S+)",
996
+ line, re.IGNORECASE)
997
+ if match:
998
+ validator_result = match.group(1).strip().lower()
999
+ break
1000
+
1001
+ return validator_result == "passed"
1002
+
1003
+
1004
+ def _extract_scope_list(report_text, list_name):
1005
+ """Extract a nested file list from the report Scope section."""
1006
+ in_scope = False
1007
+ in_list = False
1008
+ list_marker = "- {}:".format(list_name)
1009
+ values = []
1010
+ for raw_line in report_text.splitlines():
1011
+ line = raw_line.strip()
1012
+ if line == "## Scope":
1013
+ in_scope = True
1014
+ continue
1015
+ if in_scope and line.startswith("## "):
1016
+ break
1017
+ if not in_scope:
1018
+ continue
1019
+ if line == list_marker:
1020
+ in_list = True
1021
+ continue
1022
+ if in_list:
1023
+ if raw_line.startswith(" -") or raw_line.startswith("\t-"):
1024
+ value = line[2:].strip().strip("`")
1025
+ if value and value not in {"{...}", "..."}:
1026
+ values.append(value)
1027
+ continue
1028
+ if line.startswith("- "):
1029
+ break
1030
+ return values
1031
+
1032
+
1033
+ def _scoped_report_fresh(project_root, pointer_artifact, report_path, report_text):
1034
+ """Return whether the report is newer than the gate marker and in-scope files."""
1035
+ marker_path = os.path.join(
1036
+ project_root,
1037
+ os.path.dirname(pointer_artifact),
1038
+ ".prizmkit-test-started",
1039
+ )
1040
+ if not os.path.exists(marker_path):
1041
+ return False
1042
+
1043
+ try:
1044
+ report_mtime = os.path.getmtime(report_path)
1045
+ marker_mtime = os.path.getmtime(marker_path)
1046
+ except OSError:
1047
+ return False
1048
+
1049
+ if report_mtime < marker_mtime:
1050
+ return False
1051
+
1052
+ project_root_real = os.path.realpath(project_root)
1053
+ source_files = _extract_scope_list(report_text, "In-Scope Source Files")
1054
+ test_files = _extract_scope_list(report_text, "In-Scope Test Files")
1055
+ if not source_files or not test_files:
1056
+ return False
1057
+
1058
+ ignored = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
1059
+ for listed_file in source_files + test_files:
1060
+ normalized = listed_file.strip().strip("`")
1061
+ if not normalized or normalized.lower() in ignored:
1062
+ return False
1063
+ candidate = (normalized if os.path.isabs(normalized)
1064
+ else os.path.join(project_root, normalized))
1065
+ candidate_real = os.path.realpath(candidate)
1066
+ try:
1067
+ if os.path.commonpath([project_root_real, candidate_real]) != project_root_real:
1068
+ return False
1069
+ except ValueError:
1070
+ return False
1071
+ if not os.path.isfile(candidate_real):
1072
+ return False
1073
+ if os.path.getmtime(candidate_real) > report_mtime:
1074
+ return False
1075
+
1076
+ return True
1077
+
1078
+
1079
+ def _validate_scoped_test_report(project_root, artifacts):
1080
+ """Validate the feature prizmkit-test pointer and scoped PASS report."""
1081
+ pointer_artifacts = [a for a in artifacts
1082
+ if a.endswith("/test-report-path.txt")]
1083
+ if not pointer_artifacts:
1084
+ return False
1085
+
1086
+ pointer_artifact = pointer_artifacts[0]
1087
+ pointer_path = os.path.join(project_root, pointer_artifact)
1088
+ pointed_report = _read_text(pointer_path).strip().splitlines()
1089
+ if not pointed_report:
1090
+ return False
1091
+
1092
+ report_path = pointed_report[0].strip()
1093
+ if not os.path.isabs(report_path):
1094
+ report_path = os.path.join(project_root, report_path)
1095
+ if not os.path.exists(report_path):
1096
+ return False
1097
+ if not _report_path_matches_artifacts(project_root, report_path, artifacts):
1098
+ return False
1099
+
1100
+ report_text = _read_text(report_path)
1101
+ mode = _extract_scope_field(report_text, "Mode")
1102
+ artifact_dir = _extract_scope_field(report_text, "Artifact Dir")
1103
+ verdict = _extract_report_verdict(report_text)
1104
+
1105
+ expected_artifact_dir = _normalize_artifact_dir(
1106
+ os.path.dirname(pointer_artifact)
1107
+ )
1108
+ actual_artifact_dir = _normalize_artifact_dir(artifact_dir)
1109
+
1110
+ return (mode == "this-change" and
1111
+ actual_artifact_dir == expected_artifact_dir and
1112
+ verdict == "PASS" and
1113
+ _boundary_report_sections_valid(project_root, report_text) and
1114
+ _scoped_report_fresh(project_root, pointer_artifact,
1115
+ report_path, report_text))
1116
+
1117
+
1118
+ def _completed_step_artifacts_valid(project_root, skill_key, artifacts):
1119
+ """Return whether a completed checkpoint step can be preserved."""
1120
+ if not all(_artifact_exists(project_root, a) for a in artifacts):
1121
+ return False
1122
+ if skill_key == "prizmkit-test":
1123
+ return _validate_scoped_test_report(project_root, artifacts)
1124
+ return True
1125
+
1126
+
681
1127
  def generate_checkpoint_definition(sections, pipeline_mode, workflow_type,
682
1128
  item_id, item_slug, session_id,
683
1129
  init_done=False):
@@ -765,17 +1211,24 @@ def merge_checkpoint_state(existing, fresh, project_root):
765
1211
  """
766
1212
  existing_status = {}
767
1213
  existing_artifacts = {}
1214
+ fresh_artifacts = {step["skill"]: step.get("required_artifacts", [])
1215
+ for step in fresh.get("steps", [])}
768
1216
  for step in existing.get("steps", []):
769
1217
  existing_status[step["skill"]] = step["status"]
770
1218
  existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
771
1219
 
772
- # Determine which completed steps have valid artifacts
1220
+ # Determine which completed steps have valid artifacts. Prefer the freshly
1221
+ # generated artifact contract so newly added gates invalidate older
1222
+ # checkpoint entries that only satisfied a weaker contract.
773
1223
  valid_completed = set()
774
1224
  for skill_key, status in existing_status.items():
775
1225
  if status == "completed":
776
- artifacts = existing_artifacts.get(skill_key, [])
777
- if all(os.path.exists(os.path.join(project_root, a))
778
- for a in artifacts):
1226
+ artifacts = fresh_artifacts.get(
1227
+ skill_key, existing_artifacts.get(skill_key, [])
1228
+ )
1229
+ if _completed_step_artifacts_valid(project_root,
1230
+ skill_key,
1231
+ artifacts):
779
1232
  valid_completed.add(skill_key)
780
1233
  else:
781
1234
  LOGGER.warning(
@@ -1083,6 +1536,11 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1083
1536
  load_section(sections_dir,
1084
1537
  "test-failure-recovery-agent.md")))
1085
1538
 
1539
+ # --- Scoped Feature Test Gate ---
1540
+ sections.append(("phase-prizmkit-test",
1541
+ load_section(sections_dir,
1542
+ "phase-prizmkit-test.md")))
1543
+
1086
1544
  # Verification Gates are included in Task Contract. Keep AC in one place so
1087
1545
  # background context and implementation prompts cannot redefine scope.
1088
1546
  # --- Review (only for agent tiers) ---
@@ -36,7 +36,7 @@ Infer what needs to be done from the feature context above and follow the standa
36
36
 
37
37
  3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
38
38
 
39
- 4. **Test**: Run the project test suite to verify all tests pass with no regressions.
39
+ 4. **Test**: Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate and verify tests only for this feature's changed scope. Do not use bugfix/refactor artifact directories for this gate.
40
40
 
41
41
  5. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to review and auto-fix changes against the spec (internal review-fix loop, max 3 rounds).
42
42
 
@@ -197,6 +197,20 @@ When tests fail, use convergence recovery — keep fixing while progress is bein
197
197
  **Key rule**: If failures decrease (even by 1), plateau counter resets. Do NOT block commit for unresolved failures — document and defer to next session.
198
198
 
199
199
 
200
+ ### Scoped Feature Test Gate — PrizmKit Test
201
+
202
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
203
+
204
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
205
+
206
+ Gate requirements:
207
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
208
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
209
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
210
+ - Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
211
+ - Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
212
+ - If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
213
+
200
214
  {{IF_BROWSER_INTERACTION}}
201
215
  ### Phase 3.5: Browser Verification — MANDATORY
202
216
 
@@ -276,6 +276,20 @@ if (Select-String -Path ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md" -
276
276
  ```
277
277
  If GATE:MISSING — send message to Dev (re-spawn if needed): "Write the '## Implementation Log' section to context-snapshot.md before I can proceed to review. Include: files changed/created, key decisions, deviations from plan, notable discoveries."
278
278
 
279
+ ### Scoped Feature Test Gate — PrizmKit Test
280
+
281
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
282
+
283
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
284
+
285
+ Gate requirements:
286
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
287
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
288
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
289
+ - Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
290
+ - Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
291
+ - If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
292
+
279
293
  ### Phase 5: Review + Test — Reviewer Subagent
280
294
 
281
295
  Spawn Reviewer subagent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
@@ -286,9 +300,10 @@ Prompt:
286
300
  > "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
287
301
  > 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
288
302
  > 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
289
- > 3. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer filter Dev fix, max 3 rounds) and writes review-report.md.
290
- > 4. Run the full test suite using `{{TEST_CMD}}`. When running: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then use Select-String on the file for details — do NOT re-run the suite multiple times.
291
- > 5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
303
+ > 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
304
+ > 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer filter Dev fix, max 3 rounds) and writes review-report.md.
305
+ > 5. Run the full test suite using `{{TEST_CMD}}`. When running: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then use Select-String on the file for details — do NOT re-run the suite multiple times.
306
+ > 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
292
307
  > Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
293
308
 
294
309
  Wait for Reviewer to return.
@@ -337,6 +337,20 @@ Wait for Dev to return. **If Dev times out before all tasks are `[x]`**:
337
337
 
338
338
  All tasks `[x]`, tests pass.
339
339
 
340
+ ### Scoped Feature Test Gate — PrizmKit Test
341
+
342
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
343
+
344
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
345
+
346
+ Gate requirements:
347
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
348
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
349
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
350
+ - Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
351
+ - Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
352
+ - If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
353
+
340
354
  ### Phase 5: Review + Test — Reviewer Agent
341
355
 
342
356
  Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
@@ -347,9 +361,10 @@ Prompt:
347
361
  > "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
348
362
  > 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
349
363
  > 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
350
- > 3. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer filter Dev fix, max 3 rounds) and writes review-report.md.
351
- > 4. Run the full test suite using `{{TEST_CMD}}`. When running tests: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then Select-String `(Join-Path $env:TEMP "review-test-out.txt")` for details — do NOT re-run the suite multiple times.
352
- > 5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
364
+ > 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
365
+ > 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer filter Dev fix, max 3 rounds) and writes review-report.md.
366
+ > 5. Run the full test suite using `{{TEST_CMD}}`. When running tests: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then Select-String `(Join-Path $env:TEMP "review-test-out.txt")` for details — do NOT re-run the suite multiple times.
367
+ > 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
353
368
  > Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
354
369
 
355
370
  Wait for Reviewer to return.
@@ -70,13 +70,13 @@ Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\update-checkpoint.py `
70
70
  --status completed
71
71
  ```
72
72
 
73
- ### TERMINAL STOP — S09 Completed
73
+ ### TERMINAL STOP — Commit Completed
74
74
 
75
75
  After all of the following are true, the session is **TERMINAL**:
76
76
 
77
77
  - `/prizmkit-committer` has completed
78
78
  - `git status --short` is clean (except ignored/local-only pipeline artifacts)
79
79
  - `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
80
- - the S09 / `prizmkit-committer` checkpoint is marked `completed`
80
+ - the `prizmkit-committer` checkpoint is marked `completed`
81
81
 
82
82
  At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.
@@ -63,13 +63,13 @@ Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\update-checkpoint.py `
63
63
  --status completed
64
64
  ```
65
65
 
66
- ### TERMINAL STOP — S09 Completed
66
+ ### TERMINAL STOP — Commit Completed
67
67
 
68
68
  After all of the following are true, the session is **TERMINAL**:
69
69
 
70
70
  - `/prizmkit-committer` has completed
71
71
  - `git status --short` is clean (except ignored/local-only pipeline artifacts)
72
72
  - `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
73
- - the S09 / `prizmkit-committer` checkpoint is marked `completed`
73
+ - the `prizmkit-committer` checkpoint is marked `completed`
74
74
 
75
75
  At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.