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
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.86",
3
- "bundledAt": "2026-06-30T15:38:43.553Z",
4
- "bundledFrom": "d83ca7c"
2
+ "frameworkVersion": "1.1.87",
3
+ "bundledAt": "2026-07-01T13:22:59.563Z",
4
+ "bundledFrom": "3ecfd63"
5
5
  }
@@ -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
@@ -623,6 +624,11 @@ SECTION_TO_SKILL = {
623
624
  [".prizmkit/specs/{slug}/plan.md"]),
624
625
  "phase-critic-plan": ("critic-plan-review", "Critic: Plan Review", []),
625
626
  "phase-implement": ("prizmkit-implement", "Implement + Test", []),
627
+ "phase-prizmkit-test": (
628
+ "prizmkit-test", "Scoped Feature Test Gate",
629
+ [".prizmkit/specs/{slug}/test-report-path.txt",
630
+ ".prizmkit/test/*/test-report.md"],
631
+ ),
626
632
  "phase-review": ("prizmkit-code-review", "Code Review", []),
627
633
  "phase-browser": ("browser-verification", "Browser Verification", []),
628
634
  "phase-commit": None, # special: split into retrospective + committer
@@ -640,6 +646,446 @@ def _resolve_artifacts(artifact_templates, slug):
640
646
  return [a.replace("{slug}", slug) for a in artifact_templates]
641
647
 
642
648
 
649
+ def _artifact_exists(project_root, artifact):
650
+ """Return whether an artifact path or glob exists under project_root."""
651
+ full_path = os.path.join(project_root, artifact)
652
+ if glob.has_magic(full_path):
653
+ return bool(glob.glob(full_path))
654
+ return os.path.exists(full_path)
655
+
656
+
657
+ def _read_text(path):
658
+ """Read a UTF-8 text file, returning an empty string on IO errors."""
659
+ try:
660
+ with open(path, "r", encoding="utf-8") as f:
661
+ return f.read()
662
+ except IOError:
663
+ return ""
664
+
665
+
666
+ def _normalize_artifact_dir(path):
667
+ """Normalize report artifact_dir values for comparison."""
668
+ return path.strip().replace("\\", "/").rstrip("/")
669
+
670
+
671
+ def _report_path_matches_artifacts(project_root, report_path, artifacts):
672
+ """Return whether report_path is an in-project test report artifact."""
673
+ test_root = os.path.realpath(
674
+ os.path.join(project_root, ".prizmkit", "test")
675
+ )
676
+ resolved_report = os.path.realpath(report_path)
677
+
678
+ try:
679
+ if os.path.commonpath([test_root, resolved_report]) != test_root:
680
+ return False
681
+ except ValueError:
682
+ return False
683
+
684
+ report_artifacts = [a for a in artifacts if a.endswith("test-report.md")]
685
+ for artifact in report_artifacts:
686
+ artifact_path = os.path.join(project_root, artifact)
687
+ if glob.has_magic(artifact_path):
688
+ candidates = glob.glob(artifact_path)
689
+ else:
690
+ candidates = [artifact_path]
691
+ for candidate in candidates:
692
+ if os.path.realpath(candidate) == resolved_report:
693
+ return True
694
+ return False
695
+
696
+
697
+ def _extract_scope_field(report_text, field_name):
698
+ """Extract a '- Field: value' entry from the report Scope section."""
699
+ in_scope = False
700
+ prefix = "- {}:".format(field_name)
701
+ for raw_line in report_text.splitlines():
702
+ line = raw_line.strip()
703
+ if line == "## Scope":
704
+ in_scope = True
705
+ continue
706
+ if in_scope and line.startswith("## "):
707
+ break
708
+ if in_scope and line.startswith(prefix):
709
+ return line[len(prefix):].strip()
710
+ return ""
711
+
712
+
713
+ def _extract_report_verdict(report_text):
714
+ """Extract the first non-empty line after the report Verdict heading."""
715
+ in_verdict = False
716
+ for raw_line in report_text.splitlines():
717
+ line = raw_line.strip()
718
+ if line == "## Verdict":
719
+ in_verdict = True
720
+ continue
721
+ if in_verdict:
722
+ if line.startswith("## "):
723
+ break
724
+ if line:
725
+ return line
726
+ return ""
727
+
728
+
729
+ def _extract_report_section(report_text, heading):
730
+ """Extract text under a level-2 markdown heading."""
731
+ in_section = False
732
+ lines = []
733
+ for raw_line in report_text.splitlines():
734
+ line = raw_line.strip()
735
+ if line == heading:
736
+ in_section = True
737
+ continue
738
+ if in_section and line.startswith("## "):
739
+ break
740
+ if in_section:
741
+ lines.append(raw_line)
742
+ return "\n".join(lines)
743
+
744
+
745
+ def _discover_openapi_file(project_root):
746
+ """Return the first common OpenAPI/Swagger file under project_root."""
747
+ candidates = [
748
+ "openapi.yaml", "openapi.yml", "swagger.yaml", "swagger.yml",
749
+ "docs/openapi.yaml", "docs/openapi.yml",
750
+ "docs/swagger.yaml", "docs/swagger.yml",
751
+ "api/openapi.yaml", "api/openapi.yml",
752
+ "api/swagger.yaml", "api/swagger.yml",
753
+ ]
754
+ for candidate in candidates:
755
+ full_path = os.path.join(project_root, candidate)
756
+ if os.path.isfile(full_path):
757
+ return full_path
758
+ return ""
759
+
760
+
761
+ def _normalize_api_path(path):
762
+ """Normalize API path tokens for exact comparison."""
763
+ normalized = path.strip().strip("'\"`")
764
+ if len(normalized) > 1:
765
+ normalized = normalized.rstrip("/")
766
+ return normalized
767
+
768
+
769
+ def _extract_openapi_operations(openapi_text):
770
+ """Extract OpenAPI method+path operations without requiring PyYAML."""
771
+ methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"}
772
+ operations = []
773
+ in_paths = False
774
+ paths_indent = 0
775
+ current_path = ""
776
+ current_path_indent = 0
777
+ path_key_re = re.compile(
778
+ r"^\s*(?:(['\"])(/.*?)\1|(/[^:\s]+))\s*:\s*(?:#.*)?$"
779
+ )
780
+ method_re = re.compile(r"^\s*([A-Za-z]+)\s*:\s*(?:#.*)?$")
781
+
782
+ for raw_line in openapi_text.splitlines():
783
+ stripped = raw_line.strip()
784
+ if not stripped or stripped.startswith("#"):
785
+ continue
786
+
787
+ indent = len(raw_line) - len(raw_line.lstrip())
788
+ if not in_paths:
789
+ if re.match(r"^\s*paths\s*:\s*(?:#.*)?$", raw_line):
790
+ in_paths = True
791
+ paths_indent = indent
792
+ continue
793
+
794
+ if indent <= paths_indent:
795
+ break
796
+
797
+ path_match = path_key_re.match(raw_line)
798
+ if path_match:
799
+ current_path = path_match.group(2) or path_match.group(3) or ""
800
+ current_path_indent = indent
801
+ continue
802
+
803
+ method_match = method_re.match(raw_line)
804
+ if current_path and method_match and indent > current_path_indent:
805
+ method = method_match.group(1).upper()
806
+ if method in methods:
807
+ operations.append((method, _normalize_api_path(current_path)))
808
+
809
+ return operations
810
+
811
+
812
+ def _extract_matrix_operations(rows):
813
+ """Extract exact method+path tokens from Boundary Matrix rows."""
814
+ operations = set()
815
+ path_only = set()
816
+ token_re = re.compile(
817
+ r"(?:(\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b)\s+)?(/[^\s,`|)]+)",
818
+ flags=re.IGNORECASE,
819
+ )
820
+ for row in rows:
821
+ interface = row.get("Interface", "")
822
+ for match in token_re.finditer(interface):
823
+ method = match.group(1).upper() if match.group(1) else ""
824
+ path = _normalize_api_path(match.group(2))
825
+ if not path:
826
+ continue
827
+ if method:
828
+ operations.add((method, path))
829
+ else:
830
+ path_only.add(path)
831
+ return operations, path_only
832
+
833
+
834
+ def _boundary_report_sections_valid(project_root, report_text):
835
+ """Return whether required boundary sections do not report failure."""
836
+ required_columns = [
837
+ "Module",
838
+ "Interface",
839
+ "Service Type",
840
+ "Happy Path",
841
+ "Request Validation",
842
+ "Auth / Permission / Ownership",
843
+ "Domain Invariants",
844
+ "Collection / Pagination",
845
+ "Date / Time",
846
+ "State Transitions",
847
+ "Dependency Failure",
848
+ "Response Contract",
849
+ "N/A Reasons",
850
+ "Final Status",
851
+ ]
852
+ category_columns = [
853
+ "Happy Path",
854
+ "Request Validation",
855
+ "Auth / Permission / Ownership",
856
+ "Domain Invariants",
857
+ "Collection / Pagination",
858
+ "Date / Time",
859
+ "State Transitions",
860
+ "Dependency Failure",
861
+ "Response Contract",
862
+ ]
863
+ allowed_final_statuses = {
864
+ "boundary-covered",
865
+ "generated-needs-review",
866
+ "skipped-with-reason",
867
+ }
868
+ incomplete_markers = {
869
+ "boundary-missing",
870
+ "happy-path-only",
871
+ "unresolved",
872
+ }
873
+
874
+ matrix_section = _extract_report_section(report_text, "## Boundary Matrix")
875
+ if not matrix_section:
876
+ return False
877
+ matrix_lines = [line.strip() for line in matrix_section.splitlines()
878
+ if line.strip().startswith("|")]
879
+ headers = []
880
+ rows = []
881
+ for line in matrix_lines:
882
+ cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
883
+ if not headers:
884
+ if "Interface" in cells and "Final Status" in cells:
885
+ headers = cells
886
+ continue
887
+ if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells):
888
+ continue
889
+ if len(cells) < len(headers):
890
+ cells.extend([""] * (len(headers) - len(cells)))
891
+ row = {header: cells[i] if i < len(cells) else ""
892
+ for i, header in enumerate(headers)}
893
+ if row.get("Interface", "").strip(".` ") in {"", "..."}:
894
+ continue
895
+ rows.append(row)
896
+
897
+ if not headers or not rows:
898
+ return False
899
+ if any(column not in headers for column in required_columns):
900
+ return False
901
+
902
+ for row in rows:
903
+ final_status = row.get("Final Status", "").strip().lower()
904
+ if final_status not in allowed_final_statuses:
905
+ return False
906
+ for column in category_columns:
907
+ if not row.get(column, "").strip():
908
+ return False
909
+ for value in row.values():
910
+ normalized = value.strip().lower()
911
+ if any(marker in normalized for marker in incomplete_markers):
912
+ return False
913
+
914
+ openapi_file = _discover_openapi_file(project_root)
915
+ if openapi_file:
916
+ operations = _extract_openapi_operations(_read_text(openapi_file))
917
+ matrix_operations, _matrix_path_only = _extract_matrix_operations(rows)
918
+ for method, path in operations:
919
+ if (method, path) not in matrix_operations:
920
+ return False
921
+
922
+ gate_section = _extract_report_section(report_text,
923
+ "## Boundary Completion Gate")
924
+ if not gate_section:
925
+ return False
926
+ completion_gate_passed = False
927
+ boundary_missing_seen = False
928
+ happy_path_only_seen = False
929
+ for raw_line in gate_section.splitlines():
930
+ line = raw_line.strip()
931
+ match = re.match(r"-?\s*(Boundary-missing|Happy-path-only):\s*(\d+)\s*$",
932
+ line, re.IGNORECASE)
933
+ if match:
934
+ if match.group(1).lower() == "boundary-missing":
935
+ boundary_missing_seen = True
936
+ if match.group(1).lower() == "happy-path-only":
937
+ happy_path_only_seen = True
938
+ if int(match.group(2)) > 0:
939
+ return False
940
+ if re.match(r"-?\s*Completion gate passed:\s*yes\s*$",
941
+ line, re.IGNORECASE):
942
+ completion_gate_passed = True
943
+ if re.match(r"-?\s*Completion gate passed:\s*no\b",
944
+ line, re.IGNORECASE):
945
+ return False
946
+ if not (completion_gate_passed and boundary_missing_seen and
947
+ happy_path_only_seen):
948
+ return False
949
+
950
+ validation_section = _extract_report_section(report_text,
951
+ "## Boundary Validation")
952
+ if not validation_section:
953
+ return False
954
+ validator_result = ""
955
+ for raw_line in validation_section.splitlines():
956
+ line = raw_line.strip()
957
+ match = re.match(r"-?\s*Validator result:\s*(\S+)",
958
+ line, re.IGNORECASE)
959
+ if match:
960
+ validator_result = match.group(1).strip().lower()
961
+ break
962
+
963
+ return validator_result == "passed"
964
+
965
+
966
+ def _extract_scope_list(report_text, list_name):
967
+ """Extract a nested file list from the report Scope section."""
968
+ in_scope = False
969
+ in_list = False
970
+ list_marker = "- {}:".format(list_name)
971
+ values = []
972
+ for raw_line in report_text.splitlines():
973
+ line = raw_line.strip()
974
+ if line == "## Scope":
975
+ in_scope = True
976
+ continue
977
+ if in_scope and line.startswith("## "):
978
+ break
979
+ if not in_scope:
980
+ continue
981
+ if line == list_marker:
982
+ in_list = True
983
+ continue
984
+ if in_list:
985
+ if raw_line.startswith(" -") or raw_line.startswith("\t-"):
986
+ value = line[2:].strip().strip("`")
987
+ if value and value not in {"{...}", "..."}:
988
+ values.append(value)
989
+ continue
990
+ if line.startswith("- "):
991
+ break
992
+ return values
993
+
994
+
995
+ def _scoped_report_fresh(project_root, pointer_artifact, report_path, report_text):
996
+ """Return whether the report is newer than the gate marker and in-scope files."""
997
+ marker_path = os.path.join(
998
+ project_root,
999
+ os.path.dirname(pointer_artifact),
1000
+ ".prizmkit-test-started",
1001
+ )
1002
+ if not os.path.exists(marker_path):
1003
+ return False
1004
+
1005
+ try:
1006
+ report_mtime = os.path.getmtime(report_path)
1007
+ marker_mtime = os.path.getmtime(marker_path)
1008
+ except OSError:
1009
+ return False
1010
+
1011
+ if report_mtime < marker_mtime:
1012
+ return False
1013
+
1014
+ project_root_real = os.path.realpath(project_root)
1015
+ source_files = _extract_scope_list(report_text, "In-Scope Source Files")
1016
+ test_files = _extract_scope_list(report_text, "In-Scope Test Files")
1017
+ if not source_files or not test_files:
1018
+ return False
1019
+
1020
+ ignored = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
1021
+ for listed_file in source_files + test_files:
1022
+ normalized = listed_file.strip().strip("`")
1023
+ if not normalized or normalized.lower() in ignored:
1024
+ return False
1025
+ candidate = (normalized if os.path.isabs(normalized)
1026
+ else os.path.join(project_root, normalized))
1027
+ candidate_real = os.path.realpath(candidate)
1028
+ try:
1029
+ if os.path.commonpath([project_root_real, candidate_real]) != project_root_real:
1030
+ return False
1031
+ except ValueError:
1032
+ return False
1033
+ if not os.path.isfile(candidate_real):
1034
+ return False
1035
+ if os.path.getmtime(candidate_real) > report_mtime:
1036
+ return False
1037
+
1038
+ return True
1039
+
1040
+
1041
+ def _validate_scoped_test_report(project_root, artifacts):
1042
+ """Validate the feature prizmkit-test pointer and scoped PASS report."""
1043
+ pointer_artifacts = [a for a in artifacts
1044
+ if a.endswith("/test-report-path.txt")]
1045
+ if not pointer_artifacts:
1046
+ return False
1047
+
1048
+ pointer_artifact = pointer_artifacts[0]
1049
+ pointer_path = os.path.join(project_root, pointer_artifact)
1050
+ pointed_report = _read_text(pointer_path).strip().splitlines()
1051
+ if not pointed_report:
1052
+ return False
1053
+
1054
+ report_path = pointed_report[0].strip()
1055
+ if not os.path.isabs(report_path):
1056
+ report_path = os.path.join(project_root, report_path)
1057
+ if not os.path.exists(report_path):
1058
+ return False
1059
+ if not _report_path_matches_artifacts(project_root, report_path, artifacts):
1060
+ return False
1061
+
1062
+ report_text = _read_text(report_path)
1063
+ mode = _extract_scope_field(report_text, "Mode")
1064
+ artifact_dir = _extract_scope_field(report_text, "Artifact Dir")
1065
+ verdict = _extract_report_verdict(report_text)
1066
+
1067
+ expected_artifact_dir = _normalize_artifact_dir(
1068
+ os.path.dirname(pointer_artifact)
1069
+ )
1070
+ actual_artifact_dir = _normalize_artifact_dir(artifact_dir)
1071
+
1072
+ return (mode == "this-change" and
1073
+ actual_artifact_dir == expected_artifact_dir and
1074
+ verdict == "PASS" and
1075
+ _boundary_report_sections_valid(project_root, report_text) and
1076
+ _scoped_report_fresh(project_root, pointer_artifact,
1077
+ report_path, report_text))
1078
+
1079
+
1080
+ def _completed_step_artifacts_valid(project_root, skill_key, artifacts):
1081
+ """Return whether a completed checkpoint step can be preserved."""
1082
+ if not all(_artifact_exists(project_root, a) for a in artifacts):
1083
+ return False
1084
+ if skill_key == "prizmkit-test":
1085
+ return _validate_scoped_test_report(project_root, artifacts)
1086
+ return True
1087
+
1088
+
643
1089
  def generate_checkpoint_definition(sections, pipeline_mode, workflow_type,
644
1090
  item_id, item_slug, session_id,
645
1091
  init_done=False):
@@ -727,17 +1173,24 @@ def merge_checkpoint_state(existing, fresh, project_root):
727
1173
  """
728
1174
  existing_status = {}
729
1175
  existing_artifacts = {}
1176
+ fresh_artifacts = {step["skill"]: step.get("required_artifacts", [])
1177
+ for step in fresh.get("steps", [])}
730
1178
  for step in existing.get("steps", []):
731
1179
  existing_status[step["skill"]] = step["status"]
732
1180
  existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
733
1181
 
734
- # Determine which completed steps have valid artifacts
1182
+ # Determine which completed steps have valid artifacts. Prefer the freshly
1183
+ # generated artifact contract so newly added gates invalidate older
1184
+ # checkpoint entries that only satisfied a weaker contract.
735
1185
  valid_completed = set()
736
1186
  for skill_key, status in existing_status.items():
737
1187
  if status == "completed":
738
- artifacts = existing_artifacts.get(skill_key, [])
739
- if all(os.path.exists(os.path.join(project_root, a))
740
- for a in artifacts):
1188
+ artifacts = fresh_artifacts.get(
1189
+ skill_key, existing_artifacts.get(skill_key, [])
1190
+ )
1191
+ if _completed_step_artifacts_valid(project_root,
1192
+ skill_key,
1193
+ artifacts):
741
1194
  valid_completed.add(skill_key)
742
1195
  else:
743
1196
  LOGGER.warning(
@@ -1045,6 +1498,11 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1045
1498
  load_section(sections_dir,
1046
1499
  "test-failure-recovery-agent.md")))
1047
1500
 
1501
+ # --- Scoped Feature Test Gate ---
1502
+ sections.append(("phase-prizmkit-test",
1503
+ load_section(sections_dir,
1504
+ "phase-prizmkit-test.md")))
1505
+
1048
1506
  # Verification Gates are included in Task Contract. Keep AC in one place so
1049
1507
  # background context and implementation prompts cannot redefine scope.
1050
1508
  # --- Review (only for agent tiers) ---
@@ -30,7 +30,7 @@ Infer what needs to be done from the feature context above and follow the standa
30
30
 
31
31
  3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
32
32
 
33
- 4. **Test**: Run the project test suite to verify all tests pass with no regressions.
33
+ 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.
34
34
 
35
35
  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).
36
36
 
@@ -163,6 +163,20 @@ When tests fail, use convergence recovery — keep fixing while progress is bein
163
163
  **Key rule**: If failures decrease (even by 1), plateau counter resets. Do NOT block commit for unresolved failures — document and defer to next session.
164
164
 
165
165
 
166
+ ### Scoped Feature Test Gate — PrizmKit Test
167
+
168
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
169
+
170
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
171
+
172
+ Gate requirements:
173
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
174
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
175
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
176
+ - 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`.
177
+ - 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`.
178
+ - 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.
179
+
166
180
  {{IF_BROWSER_INTERACTION}}
167
181
  ### Phase 3.5: Browser Verification — MANDATORY
168
182
 
@@ -240,6 +240,20 @@ grep -q "## Implementation Log" .prizmkit/specs/{{FEATURE_SLUG}}/context-snapsho
240
240
  ```
241
241
  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."
242
242
 
243
+ ### Scoped Feature Test Gate — PrizmKit Test
244
+
245
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
246
+
247
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
248
+
249
+ Gate requirements:
250
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
251
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
252
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
253
+ - 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`.
254
+ - 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`.
255
+ - 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.
256
+
243
257
  ### Phase 5: Review + Test — Reviewer Subagent
244
258
 
245
259
  Spawn Reviewer subagent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
@@ -250,9 +264,10 @@ Prompt:
250
264
  > "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
251
265
  > 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
252
266
  > 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
253
- > 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.
254
- > 4. Run the full test suite using `{{TEST_CMD}}`. When running: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep the file for details do NOT re-run the suite multiple times.
255
- > 5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
267
+ > 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.
268
+ > 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.
269
+ > 5. Run the full test suite using `{{TEST_CMD}}`. When running: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep the file for details — do NOT re-run the suite multiple times.
270
+ > 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
256
271
  > Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
257
272
 
258
273
  Wait for Reviewer to return.
@@ -301,6 +301,20 @@ Wait for Dev to return. **If Dev times out before all tasks are `[x]`**:
301
301
 
302
302
  All tasks `[x]`, tests pass.
303
303
 
304
+ ### Scoped Feature Test Gate — PrizmKit Test
305
+
306
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
307
+
308
+ Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
309
+
310
+ Gate requirements:
311
+ - Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
312
+ - Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
313
+ - Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
314
+ - 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`.
315
+ - 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`.
316
+ - 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.
317
+
304
318
  ### Phase 5: Review + Test — Reviewer Agent
305
319
 
306
320
  Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
@@ -311,9 +325,10 @@ Prompt:
311
325
  > "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
312
326
  > 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
313
327
  > 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
314
- > 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.
315
- > 4. Run the full test suite using `{{TEST_CMD}}`. When running tests: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep `/tmp/review-test-out.txt` for details do NOT re-run the suite multiple times.
316
- > 5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
328
+ > 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.
329
+ > 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.
330
+ > 5. Run the full test suite using `{{TEST_CMD}}`. When running tests: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep `/tmp/review-test-out.txt` for details — do NOT re-run the suite multiple times.
331
+ > 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
317
332
  > Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
318
333
 
319
334
  Wait for Reviewer to return.
@@ -70,13 +70,13 @@ python3 $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 @@ python3 $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.