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.
Files changed (88) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  7. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +398 -6
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +61 -3
  11. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +64 -4
  12. package/bundled/dev-pipeline/scripts/prompt_framework.py +156 -7
  13. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  14. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  15. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  16. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  17. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
  19. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
  20. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
  21. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  24. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  25. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
  26. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  27. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
  28. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  30. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  31. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +404 -24
  32. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
  33. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
  36. package/bundled/skills/_metadata.json +62 -25
  37. package/bundled/skills/app-planner/SKILL.md +4 -3
  38. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  39. package/bundled/skills/bug-planner/SKILL.md +12 -27
  40. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  41. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  42. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  43. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  44. package/bundled/skills/feature-planner/SKILL.md +27 -22
  45. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  46. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  47. package/bundled/skills/prizmkit/SKILL.md +130 -94
  48. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  49. package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
  50. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
  51. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  52. package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
  53. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  54. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  55. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  57. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  58. package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
  59. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  60. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  61. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  62. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  63. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  64. package/bundled/skills/prizmkit-test/SKILL.md +28 -6
  65. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  66. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  67. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  68. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  69. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  71. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  72. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  73. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  74. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  75. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  76. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  77. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  78. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  79. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  80. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  81. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  82. package/bundled/templates/project-memory-template.md +4 -4
  83. package/package.json +1 -1
  84. package/src/index.js +4 -0
  85. package/src/prompts.js +10 -3
  86. package/src/scaffold.js +1 -1
  87. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  88. package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
@@ -1,6 +1,7 @@
1
1
  """Tests for generate-bootstrap-prompt.py core functions."""
2
2
 
3
3
  import json
4
+ import hashlib
4
5
  import importlib.util
5
6
  import os
6
7
  import re
@@ -27,6 +28,9 @@ from generate_bootstrap_prompt import (
27
28
  merge_checkpoint_state,
28
29
  load_active_agent_prompts,
29
30
  validate_rendered,
31
+ validate_feature_test_evidence,
32
+ route_feature_repair,
33
+ feature_checkpoint_metadata,
30
34
  main as generate_bootstrap_main,
31
35
  )
32
36
  from continuation import append_continuation_handoff, checkpoint_cursor
@@ -157,7 +161,7 @@ def scoped_report_text(verdict="PASS", boundary_missing=0,
157
161
 
158
162
 
159
163
  def write_fresh_scoped_report(project_root, report_path, report_text=None):
160
- """Write marker, in-scope files, and a fresh scoped report."""
164
+ """Write marker, in-scope files, and a fresh derived report."""
161
165
  source = project_root / "src" / "feature.js"
162
166
  test = project_root / "tests" / "feature.test.js"
163
167
  marker = project_root / ".prizmkit" / "specs" / "123-scope-test" / ".prizmkit-test-started"
@@ -173,6 +177,135 @@ def write_fresh_scoped_report(project_root, report_path, report_text=None):
173
177
  return source, test, marker
174
178
 
175
179
 
180
+ def write_fresh_structured_evidence(project_root):
181
+ """Create the smallest protocol-shaped, validator-attested pass package."""
182
+ source, test, marker = write_fresh_scoped_report(
183
+ project_root,
184
+ project_root / ".prizmkit" / "test" / "unused" / "test-report.md",
185
+ )
186
+ evidence_base = project_root / ".prizmkit" / "test" / "evidence"
187
+ environment = {}
188
+ plan_inputs = {"feature": "123-scope-test"}
189
+ categories = {
190
+ "source": [{"path": "src/feature.js", "sha256": hashlib.sha256(source.read_bytes()).hexdigest()}],
191
+ "tests": [{"path": "tests/feature.test.js", "sha256": hashlib.sha256(test.read_bytes()).hexdigest()}],
192
+ "contracts": [],
193
+ "lockfiles": [],
194
+ }
195
+ target_hashes = {
196
+ category: hashlib.sha256(json.dumps(
197
+ sorted(records, key=lambda item: item["path"]),
198
+ sort_keys=True, separators=(",", ":"),
199
+ ).encode()).hexdigest()
200
+ for category, records in categories.items()
201
+ }
202
+ target_hashes["environment"] = hashlib.sha256(b"{}").hexdigest()
203
+ target_hashes["plan"] = hashlib.sha256(json.dumps(
204
+ plan_inputs, sort_keys=True, separators=(",", ":"),
205
+ ).encode()).hexdigest()
206
+ inventory = {
207
+ "categories": categories,
208
+ "plan_inputs": plan_inputs,
209
+ }
210
+ scope = {"feature": "123-scope-test"}
211
+ zero_hash = "0" * 64
212
+ identity = {
213
+ "baseline_commit": "baseline",
214
+ "working_diff_sha256": zero_hash,
215
+ "scope_sha256": hashlib.sha256(json.dumps(
216
+ scope, sort_keys=True, separators=(",", ":"),
217
+ ).encode()).hexdigest(),
218
+ }
219
+ evidence_id = hashlib.sha256(json.dumps(
220
+ identity, sort_keys=True, separators=(",", ":"),
221
+ ).encode()).hexdigest()
222
+ evidence_root = evidence_base / evidence_id
223
+ evidence_root.mkdir(parents=True, exist_ok=True)
224
+ (evidence_root / "environment.json").write_text("{}\n", encoding="utf-8")
225
+ (evidence_root / "target-inventory.json").write_text(
226
+ json.dumps(inventory), encoding="utf-8",
227
+ )
228
+ (evidence_root / "scope.json").write_text(json.dumps(scope), encoding="utf-8")
229
+ verdict = {
230
+ "verdict": "TEST_PASS",
231
+ "testing_domain_only": True,
232
+ "authorizes_commit_or_release": False,
233
+ "real_environment_validated": False,
234
+ "repairs_business_defects": False,
235
+ "code_evidence_replayable": True,
236
+ "blockers": [],
237
+ "reproduced_failures": [],
238
+ }
239
+ (evidence_root / "verdict.json").write_text(json.dumps(verdict), encoding="utf-8")
240
+ report = scoped_report_text()
241
+ (evidence_root / "test-report.md").write_text(report, encoding="utf-8")
242
+ stages = []
243
+ stage_names = [
244
+ "CHANGE_CLASSIFY", "SCOPE_DISCOVER", "CONTRACT_MODEL", "TEST_PLAN",
245
+ "INFRA_READY", "TEST_BUILD", "EXECUTE_PROVE", "EVIDENCE_PACKAGE",
246
+ "EVIDENCE_VALIDATE",
247
+ ]
248
+ for name in stage_names:
249
+ stages.append({"name": name, "status": "complete", "input_sha256": zero_hash, "outputs": []})
250
+ manifest = {
251
+ "protocol_version": "1.0",
252
+ "evidence_id": evidence_id,
253
+ "evidence_id_inputs": identity,
254
+ "sensitivity": "project-controlled",
255
+ "environment_claim": "mocked-code-level-only",
256
+ "compatibility": "legacy-test-report-interface-not-supported",
257
+ "change_class": "lightweight",
258
+ "baseline_commit": "baseline",
259
+ "working_diff_sha256": zero_hash,
260
+ "target_hashes": target_hashes,
261
+ "stages": stages,
262
+ "files": [],
263
+ "final_verdict": "TEST_PASS",
264
+ }
265
+ file_records = []
266
+ for path in sorted(evidence_root.rglob("*")):
267
+ if path.is_file():
268
+ relative = path.relative_to(evidence_root).as_posix()
269
+ file_records.append({
270
+ "path": relative,
271
+ "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
272
+ "produced_by": "EVIDENCE_PACKAGE",
273
+ })
274
+ payload = sorted(
275
+ ({"path": item["path"], "sha256": item["sha256"]}
276
+ for item in file_records
277
+ if item["path"] not in {"verdict.json", "test-report.md"}),
278
+ key=lambda item: item["path"],
279
+ )
280
+ validation = {
281
+ "validator": "validate_test_evidence.py",
282
+ "validator_sha256": zero_hash,
283
+ "payload_sha256": hashlib.sha256(json.dumps(
284
+ payload, sort_keys=True, separators=(",", ":"),
285
+ ).encode()).hexdigest(),
286
+ "result": "passed",
287
+ "verdict": "TEST_PASS",
288
+ "semantics": "integrity-and-protocol-validation-not-hostile-producer-proof",
289
+ "replay_receipt_ids": [],
290
+ }
291
+ (evidence_root / "validation.json").write_text(json.dumps(validation), encoding="utf-8")
292
+ file_records.append({
293
+ "path": "validation.json",
294
+ "sha256": hashlib.sha256((evidence_root / "validation.json").read_bytes()).hexdigest(),
295
+ "produced_by": "EVIDENCE_VALIDATE",
296
+ })
297
+ manifest["files"] = file_records
298
+ (evidence_root / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
299
+ pointer = project_root / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
300
+ pointer.parent.mkdir(parents=True, exist_ok=True)
301
+ pointer.write_text(str(evidence_root / "test-report.md"), encoding="utf-8")
302
+ report_path = evidence_root / "test-report.md"
303
+ now = time.time()
304
+ os.utime(marker, (now, now))
305
+ os.utime(report_path, (now + 1, now + 1))
306
+ return evidence_root
307
+
308
+
176
309
  # ---------------------------------------------------------------------------
177
310
  # compute_feature_slug
178
311
  # ---------------------------------------------------------------------------
@@ -469,6 +602,13 @@ class TestProcessModeBlocks:
469
602
  # ---------------------------------------------------------------------------
470
603
 
471
604
  class TestScopedFeatureTestGate:
605
+ @staticmethod
606
+ def _trust_fixture_validator(monkeypatch):
607
+ monkeypatch.setattr(
608
+ "generate_bootstrap_prompt._evidence_validator_passes",
609
+ lambda _project_root, _evidence_root, _validation: True,
610
+ )
611
+
472
612
  def test_standard_sections_insert_prizmkit_test_after_review(self):
473
613
  sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
474
614
  sections = assemble_sections(
@@ -561,7 +701,9 @@ class TestScopedFeatureTestGate:
561
701
  test_step = next(step for step in checkpoint["steps"] if step["skill"] == "prizmkit-test")
562
702
  assert test_step["required_artifacts"] == [
563
703
  ".prizmkit/specs/123-scope-test/test-report-path.txt",
564
- ".prizmkit/test/*/test-report.md",
704
+ ".prizmkit/test/evidence/*/manifest.json",
705
+ ".prizmkit/test/evidence/*/verdict.json",
706
+ ".prizmkit/test/evidence/*/validation.json",
565
707
  ]
566
708
 
567
709
  def test_merge_resets_completed_prizmkit_test_without_report_artifact(self, tmp_path):
@@ -596,19 +738,19 @@ class TestScopedFeatureTestGate:
596
738
 
597
739
  assert [step["status"] for step in merged["steps"]] == ["pending", "pending"]
598
740
 
599
- def test_merge_keeps_completed_prizmkit_test_only_for_scoped_pass_report(self, tmp_path):
600
- report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
601
- write_fresh_scoped_report(tmp_path, report)
741
+ def test_merge_keeps_completed_prizmkit_test_only_for_scoped_pass_report(self, tmp_path, monkeypatch):
742
+ self._trust_fixture_validator(monkeypatch)
743
+ evidence_root = write_fresh_structured_evidence(tmp_path)
602
744
  pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
603
745
  pointer.parent.mkdir(parents=True, exist_ok=True)
604
- pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
746
+ pointer.write_text(str(evidence_root / "test-report.md") + "\n", encoding="utf-8")
605
747
  existing = {
606
748
  "steps": [
607
- {"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
749
+ {"id": "S01", "skill": "prizmkit-test", "status": "completed", "stage_result": "TEST_PASS", "required_artifacts": [
608
750
  ".prizmkit/specs/123-scope-test/test-report-path.txt",
609
751
  ".prizmkit/test/*/test-report.md",
610
752
  ]},
611
- {"id": "S02", "skill": "prizmkit-code-review", "status": "completed", "required_artifacts": []},
753
+ {"id": "S02", "skill": "prizmkit-code-review", "status": "completed", "stage_result": "REVIEW_PASS", "required_artifacts": []},
612
754
  ],
613
755
  }
614
756
  fresh = {
@@ -834,25 +976,171 @@ class TestScopedFeatureTestGate:
834
976
 
835
977
  assert merged["steps"][0]["status"] == "pending"
836
978
 
837
- def test_merge_resets_completed_prizmkit_test_without_in_scope_sources(self, tmp_path):
979
+ def test_structured_evidence_accepts_current_matching_package(self, tmp_path, monkeypatch):
980
+ self._trust_fixture_validator(monkeypatch)
981
+ evidence_root = write_fresh_structured_evidence(tmp_path)
982
+
983
+ evidence = validate_feature_test_evidence(
984
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
985
+ )
986
+
987
+ assert evidence is not None
988
+ assert evidence["verdict"] == "TEST_PASS"
989
+ assert evidence["manifest_path"] == str(
990
+ (evidence_root / "manifest.json").relative_to(tmp_path),
991
+ )
992
+ assert evidence["validation_path"].endswith("/validation.json")
993
+
994
+ def test_structured_evidence_requires_authoritative_validator(self, tmp_path, monkeypatch):
995
+ write_fresh_structured_evidence(tmp_path)
996
+ monkeypatch.setattr(
997
+ "generate_bootstrap_prompt._evidence_validator_passes",
998
+ lambda _project_root, _evidence_root, _validation: False,
999
+ )
1000
+
1001
+ assert validate_feature_test_evidence(
1002
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1003
+ ) is None
1004
+
1005
+ def test_structured_evidence_rejects_legacy_markdown_only_pass(self, tmp_path):
838
1006
  report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
839
- report_text = scoped_report_text().replace("- In-Scope Source Files:\n - src/feature.js\n", "- In-Scope Source Files:\n")
840
- write_fresh_scoped_report(tmp_path, report, report_text)
1007
+ write_fresh_scoped_report(tmp_path, report)
841
1008
  pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
842
- pointer.write_text(".prizmkit/test/run/test-report.md\n", encoding="utf-8")
843
- existing = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "completed", "required_artifacts": [
844
- ".prizmkit/specs/123-scope-test/test-report-path.txt",
845
- ".prizmkit/test/*/test-report.md",
846
- ]}]}
847
- fresh = {"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending", "required_artifacts": [
848
- ".prizmkit/specs/123-scope-test/test-report-path.txt",
849
- ".prizmkit/test/*/test-report.md",
850
- ]}]}
1009
+ pointer.write_text(str(report), encoding="utf-8")
1010
+
1011
+ assert validate_feature_test_evidence(
1012
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1013
+ ) is None
1014
+
1015
+ def test_structured_evidence_rejects_missing_validation(self, tmp_path):
1016
+ evidence_root = write_fresh_structured_evidence(tmp_path)
1017
+ (evidence_root / "validation.json").unlink()
1018
+
1019
+ assert validate_feature_test_evidence(
1020
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1021
+ ) is None
1022
+
1023
+ def test_structured_evidence_rejects_tampered_verdict(self, tmp_path, monkeypatch):
1024
+ self._trust_fixture_validator(monkeypatch)
1025
+ evidence_root = write_fresh_structured_evidence(tmp_path)
1026
+ verdict_path = evidence_root / "verdict.json"
1027
+ verdict = json.loads(verdict_path.read_text(encoding="utf-8"))
1028
+ verdict["verdict"] = "TEST_BLOCKED"
1029
+ verdict_path.write_text(json.dumps(verdict), encoding="utf-8")
1030
+
1031
+ assert validate_feature_test_evidence(
1032
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1033
+ ) is None
1034
+
1035
+ def test_structured_evidence_rejects_wrong_artifact_scope(self, tmp_path, monkeypatch):
1036
+ self._trust_fixture_validator(monkeypatch)
1037
+ evidence_root = write_fresh_structured_evidence(tmp_path)
1038
+ report_path = evidence_root / "test-report.md"
1039
+ report_path.write_text(
1040
+ scoped_report_text().replace(
1041
+ ".prizmkit/specs/123-scope-test/",
1042
+ ".prizmkit/specs/999-other/",
1043
+ ),
1044
+ encoding="utf-8",
1045
+ )
851
1046
 
852
- merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
1047
+ assert validate_feature_test_evidence(
1048
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1049
+ ) is None
853
1050
 
854
- assert merged["steps"][0]["status"] == "pending"
1051
+ def test_structured_evidence_rejects_stale_report(self, tmp_path, monkeypatch):
1052
+ self._trust_fixture_validator(monkeypatch)
1053
+ evidence_root = write_fresh_structured_evidence(tmp_path)
1054
+ report = evidence_root / "test-report.md"
1055
+ marker = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / ".prizmkit-test-started"
1056
+ now = time.time()
1057
+ os.utime(report, (now, now))
1058
+ os.utime(marker, (now + 10, now + 10))
1059
+
1060
+ assert validate_feature_test_evidence(
1061
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1062
+ ) is None
1063
+
1064
+ def test_structured_evidence_rejects_live_inventory_drift(self, tmp_path, monkeypatch):
1065
+ self._trust_fixture_validator(monkeypatch)
1066
+ write_fresh_structured_evidence(tmp_path)
1067
+ source = tmp_path / "src" / "feature.js"
1068
+ source.write_text("export const feature = false;\n", encoding="utf-8")
855
1069
 
1070
+ assert validate_feature_test_evidence(
1071
+ str(tmp_path), ".prizmkit/specs/123-scope-test",
1072
+ ) is None
1073
+
1074
+ def test_feature_repair_routing_preserves_review_and_test_boundaries(self):
1075
+ review = route_feature_repair("REVIEW_NEEDS_FIXES", repair_round=0)
1076
+ test_infra = route_feature_repair(
1077
+ "TEST_FAIL", "test-infrastructure", repair_round=1,
1078
+ )
1079
+ production = route_feature_repair("TEST_FAIL", "production", repair_round=1)
1080
+
1081
+ assert review["route"] == [
1082
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
1083
+ ]
1084
+ assert test_infra["route"] == ["prizmkit-implement", "prizmkit-test"]
1085
+ assert production["route"] == [
1086
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
1087
+ ]
1088
+ assert review["repair_round"] == 1
1089
+
1090
+ def test_feature_repair_routing_blocks_unsafe_and_exhausted_routes(self):
1091
+ unknown = route_feature_repair("TEST_FAIL", "unknown", repair_round=1)
1092
+ blocked = route_feature_repair("TEST_BLOCKED", "production", repair_round=1)
1093
+ exhausted = route_feature_repair("TEST_FAIL", "production", repair_round=3)
1094
+
1095
+ assert unknown["blocked"] is True
1096
+ assert unknown["route"] == []
1097
+ assert blocked["blocked"] is True
1098
+ assert blocked["resumable"] is False
1099
+ assert blocked["next_stage"] is None
1100
+ assert blocked["terminal_status"] == "WORKFLOW_BLOCKED"
1101
+ assert blocked["blocked_reason"] == "test_blocked"
1102
+ assert exhausted["blocked"] is True
1103
+ assert exhausted["repair_round"] == 3
1104
+
1105
+ def test_feature_checkpoint_metadata_records_l1_handoff_fields(self):
1106
+ evidence = {
1107
+ "manifest_path": ".prizmkit/test/evidence/id/manifest.json",
1108
+ "verdict_path": ".prizmkit/test/evidence/id/verdict.json",
1109
+ "validation_path": ".prizmkit/test/evidence/id/validation.json",
1110
+ }
1111
+
1112
+ metadata = feature_checkpoint_metadata(
1113
+ ".prizmkit/specs/123-scope-test",
1114
+ evidence=evidence,
1115
+ stage="test",
1116
+ status="TEST_FAIL",
1117
+ stage_result="TEST_FAIL",
1118
+ repair_scope="test-infrastructure",
1119
+ repair_round=2,
1120
+ next_stage="prizmkit-implement",
1121
+ resume_from="prizmkit-implement",
1122
+ )
1123
+
1124
+ assert metadata == {
1125
+ "schema_version": 1,
1126
+ "artifact_dir": ".prizmkit/specs/123-scope-test",
1127
+ "orchestrator": "prizmkit-l4",
1128
+ "stage": "test",
1129
+ "current_stage": "test",
1130
+ "status": "TEST_FAIL",
1131
+ "stage_result": "TEST_FAIL",
1132
+ "repair_scope": "test-infrastructure",
1133
+ "repair_round": 2,
1134
+ "authoritative_evidence_paths": {
1135
+ "manifest": ".prizmkit/test/evidence/id/manifest.json",
1136
+ "verdict": ".prizmkit/test/evidence/id/verdict.json",
1137
+ "validation": ".prizmkit/test/evidence/id/validation.json",
1138
+ },
1139
+ "next_stage": "prizmkit-implement",
1140
+ "resume_from": "prizmkit-implement",
1141
+ "blocked_reason": None,
1142
+ "terminal_status": None,
1143
+ }
856
1144
 
857
1145
  # ---------------------------------------------------------------------------
858
1146
  # Active compaction harness rollback
@@ -1310,7 +1598,7 @@ class TestFeaturePromptUnifiedFullGuidance:
1310
1598
  assert "### Scoped Feature Test Gate — PrizmKit Test" in prompt
1311
1599
  assert "## Test Failure Recovery Protocol" in prompt
1312
1600
  assert "Boundary Matrix" in prompt
1313
- assert "### Browser Verification — MANDATORY" in prompt
1601
+ assert "### Browser Verification — MANDATORY" not in prompt
1314
1602
  assert "### TERMINAL STOP — Commit Completed" in prompt
1315
1603
  assert "# Dev-Pipeline Session Bootstrap — Tier 1" not in prompt
1316
1604
  assert "# Dev-Pipeline Session Bootstrap — Tier 2" not in prompt
@@ -1420,7 +1708,99 @@ class TestBrowserBlockingPromptPolicy:
1420
1708
  assert "Failures do NOT block the commit" not in prompt
1421
1709
 
1422
1710
 
1423
- class TestManualHybridBugfixPromptPolicy:
1711
+ class TestBrowserOptInPromptPolicy:
1712
+ @staticmethod
1713
+ def _rendered_outputs(tmp_path, browser_interaction=None, env_value=None):
1714
+ previous_env = os.environ.get("BROWSER_VERIFY")
1715
+ if env_value is None:
1716
+ os.environ.pop("BROWSER_VERIFY", None)
1717
+ else:
1718
+ os.environ["BROWSER_VERIFY"] = env_value
1719
+ try:
1720
+ feature_prompt = _render_feature_prompt(
1721
+ tmp_path / "feature", browser_interaction=browser_interaction,
1722
+ )
1723
+ bugfix_prompt = _render_bugfix_prompt(
1724
+ tmp_path / "bugfix",
1725
+ bug_overrides=(
1726
+ {"browser_interaction": browser_interaction}
1727
+ if browser_interaction is not None else None
1728
+ ),
1729
+ )
1730
+ refactor_prompt = _render_refactor_prompt(
1731
+ tmp_path / "refactor",
1732
+ refactor_overrides=(
1733
+ {"browser_interaction": browser_interaction}
1734
+ if browser_interaction is not None else None
1735
+ ),
1736
+ )
1737
+ return feature_prompt, bugfix_prompt, refactor_prompt
1738
+ finally:
1739
+ if previous_env is None:
1740
+ os.environ.pop("BROWSER_VERIFY", None)
1741
+ else:
1742
+ os.environ["BROWSER_VERIFY"] = previous_env
1743
+
1744
+ @staticmethod
1745
+ def _checkpoint_skills(project_root, family):
1746
+ if family == "feature":
1747
+ pattern = "**/workflow-checkpoint.json"
1748
+ elif family == "bugfix":
1749
+ pattern = "**/bugfix/B-001/workflow-checkpoint.json"
1750
+ else:
1751
+ pattern = "**/refactor/R-001/workflow-checkpoint.json"
1752
+ path = next(project_root.glob(pattern))
1753
+ return [step["skill"] for step in json.loads(path.read_text(encoding="utf-8"))["steps"]]
1754
+
1755
+ def test_omitted_browser_interaction_disables_browser_for_all_families(self, tmp_path):
1756
+ prompts = self._rendered_outputs(tmp_path / "omitted")
1757
+
1758
+ for prompt in prompts:
1759
+ assert "Browser Verification" not in prompt
1760
+ assert "playwright-cli" not in prompt
1761
+ assert "opencli" not in prompt
1762
+ assert "development-server" not in prompt
1763
+ assert "browser-dev-server.pid" not in prompt
1764
+
1765
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "feature", "feature")
1766
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "bugfix", "bugfix")
1767
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "omitted" / "refactor", "refactor")
1768
+
1769
+ def test_explicit_browser_interaction_preserves_configuration_for_all_families(self, tmp_path):
1770
+ browser_interaction = {
1771
+ "tool": "playwright-cli",
1772
+ "verify_steps": ["Open the page", "Submit the form"],
1773
+ }
1774
+ prompts = self._rendered_outputs(tmp_path / "explicit", browser_interaction)
1775
+
1776
+ for prompt in prompts:
1777
+ assert "Browser Verification" in prompt
1778
+ assert "Browser Verification (playwright-cli)" in prompt
1779
+ assert "playwright-cli" in prompt
1780
+ assert "opencli" not in prompt
1781
+ assert "Goal 1: Open the page" in prompt
1782
+ assert "Goal 2: Submit the form" in prompt
1783
+
1784
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "feature", "feature")
1785
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "bugfix", "bugfix")
1786
+ assert "browser-verification" in self._checkpoint_skills(tmp_path / "explicit" / "refactor", "refactor")
1787
+
1788
+ def test_global_disable_suppresses_explicit_browser_for_all_families(self, tmp_path):
1789
+ prompts = self._rendered_outputs(
1790
+ tmp_path / "global-disable",
1791
+ {"tool": "opencli", "verify_steps": ["Open the page"]},
1792
+ env_value="false",
1793
+ )
1794
+
1795
+ for prompt in prompts:
1796
+ assert "Browser Verification" not in prompt
1797
+ assert "opencli" not in prompt
1798
+ assert "browser-dev-server.pid" not in prompt
1799
+
1800
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "feature", "feature")
1801
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "bugfix", "bugfix")
1802
+ assert "browser-verification" not in self._checkpoint_skills(tmp_path / "global-disable" / "refactor", "refactor")
1803
+
1424
1804
  def test_fresh_manual_bugfix_prompt_stops_before_commit_phase(self, tmp_path):
1425
1805
  prompt = _render_bugfix_prompt(
1426
1806
  tmp_path / "fresh-manual-bugfix",
@@ -13,8 +13,12 @@ from generate_bugfix_prompt import (
13
13
  format_error_source_details,
14
14
  format_environment,
15
15
  generate_bugfix_checkpoint,
16
+ bugfix_checkpoint_metadata,
17
+ merge_bugfix_checkpoint_state,
18
+ route_bugfix_repair,
16
19
  process_conditional_blocks,
17
20
  render_template,
21
+ assemble_bugfix_sections,
18
22
  )
19
23
  from continuation import append_continuation_handoff
20
24
 
@@ -312,6 +316,11 @@ def test_template_source_is_clean_for_headless_prompt_contract():
312
316
  content = Path("dev-pipeline/templates/bugfix-bootstrap-prompt.md").read_text(encoding="utf-8")
313
317
  for forbidden in HEADLESS_FORBIDDEN_PROMPT_STRINGS:
314
318
  assert forbidden not in content
319
+ assert "FAST_PATH=true" not in content
320
+ assert "prizmkit-workflow" not in content
321
+ assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/bugfix/{{BUG_ID}}/" in content
322
+ assert "local_commit_authorized=true" in content
323
+ assert "push_authorized=false" in content
315
324
 
316
325
 
317
326
  def test_bugfix_prompt_keeps_fine_grained_phases_out_of_external_status_contract():
@@ -455,6 +464,129 @@ class TestBugfixBootstrapShellExtraction:
455
464
 
456
465
 
457
466
  class TestBugfixCheckpointGeneration:
467
+ def test_checkpoint_merge_preserves_family_handoff_state(self, tmp_path):
468
+ fresh = generate_bugfix_checkpoint("B-001", "new")
469
+ existing = generate_bugfix_checkpoint("B-001", "old")
470
+ existing["bugfix_state"] = bugfix_checkpoint_metadata(
471
+ ".prizmkit/bugfix/B-001",
472
+ stage="test",
473
+ status="TEST_BLOCKED",
474
+ stage_result="TEST_BLOCKED",
475
+ repair_round=2,
476
+ next_stage="prizmkit-test",
477
+ resume_from="prizmkit-test",
478
+ )
479
+
480
+ merged = merge_bugfix_checkpoint_state(existing, fresh, str(tmp_path))
481
+
482
+ assert merged["bugfix_state"]["stage_result"] == "TEST_BLOCKED"
483
+ assert merged["bugfix_state"]["resume_from"] == "prizmkit-test"
484
+
485
+ def test_default_checkpoint_has_all_atomic_gates_in_order(self):
486
+ checkpoint = generate_bugfix_checkpoint("B-001", "session-1")
487
+
488
+ assert [step["skill"] for step in checkpoint["steps"]] == [
489
+ "prizmkit-init",
490
+ "bug-diagnosis-and-plan",
491
+ "prizmkit-implement",
492
+ "prizmkit-code-review",
493
+ "prizmkit-test",
494
+ "prizmkit-retrospective",
495
+ "prizmkit-committer",
496
+ "bug-report",
497
+ ]
498
+ test_step = checkpoint["steps"][4]
499
+ assert test_step["depends_on"] == "S04"
500
+ assert test_step["required_artifacts"] == [
501
+ ".prizmkit/test/evidence/*/manifest.json",
502
+ ".prizmkit/test/evidence/*/verdict.json",
503
+ ".prizmkit/test/evidence/*/validation.json",
504
+ ]
505
+ assert checkpoint["steps"][5]["depends_on"] == "S05"
506
+
507
+ def test_bugfix_repair_routing_preserves_review_and_test_boundaries(self):
508
+ review = route_bugfix_repair("REVIEW_NEEDS_FIXES", repair_round=0)
509
+ test_infra = route_bugfix_repair(
510
+ "TEST_FAIL", "test-infrastructure", repair_round=1,
511
+ )
512
+ production = route_bugfix_repair("TEST_FAIL", "public-interface", repair_round=2)
513
+
514
+ assert review["route"] == [
515
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
516
+ ]
517
+ assert test_infra["route"] == ["prizmkit-implement", "prizmkit-test"]
518
+ assert production["route"] == [
519
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
520
+ ]
521
+ assert production["repair_round"] == 3
522
+
523
+ def test_bugfix_repair_routing_blocks_unsafe_blocked_and_exhausted_results(self):
524
+ unknown = route_bugfix_repair("TEST_FAIL", "unknown", repair_round=1)
525
+ blocked = route_bugfix_repair("TEST_BLOCKED", "production", repair_round=1)
526
+ exhausted = route_bugfix_repair("TEST_FAIL", "production", repair_round=3)
527
+
528
+ assert unknown["blocked"] is True
529
+ assert unknown["route"] == []
530
+ assert blocked["blocked"] is True
531
+ assert blocked["resumable"] is False
532
+ assert blocked["next_stage"] is None
533
+ assert blocked["terminal_status"] == "WORKFLOW_BLOCKED"
534
+ assert blocked["blocked_reason"] == "test_blocked"
535
+ assert blocked["route"] == []
536
+ assert "cannot safely edit production" in blocked["reason"]
537
+ assert exhausted["blocked"] is True
538
+ assert exhausted["repair_round"] == 3
539
+
540
+ def test_bugfix_checkpoint_metadata_preserves_artifact_and_evidence_handoff(self):
541
+ metadata = bugfix_checkpoint_metadata(
542
+ ".prizmkit/bugfix/B-001",
543
+ evidence={
544
+ "manifest_path": ".prizmkit/test/evidence/id/manifest.json",
545
+ "verdict_path": ".prizmkit/test/evidence/id/verdict.json",
546
+ "validation_path": ".prizmkit/test/evidence/id/validation.json",
547
+ },
548
+ stage="test",
549
+ status="TEST_FAIL",
550
+ stage_result="TEST_FAIL",
551
+ repair_scope="test-infrastructure",
552
+ repair_round=2,
553
+ )
554
+
555
+ assert metadata["artifact_dir"] == ".prizmkit/bugfix/B-001"
556
+ assert metadata["authoritative_evidence_paths"]["validation"].endswith(
557
+ "/validation.json"
558
+ )
559
+ assert metadata["repair_round"] == 2
560
+ assert metadata["resume_from"] == "prizmkit-implement"
561
+
562
+ def test_manual_bugfix_keeps_test_gate_but_omits_commit_authorization(self):
563
+ sections_dir = Path("dev-pipeline/templates/sections")
564
+ sections = assemble_bugfix_sections(
565
+ "standard", str(sections_dir), False, True,
566
+ )
567
+ names = [name for name, _ in sections]
568
+
569
+ assert "bugfix-phase-review" in names
570
+ assert "bugfix-phase-test" in names
571
+ assert "bugfix-manual-gate" in names
572
+ assert "headless-commit-authorization" not in names
573
+ assert "bugfix-phase-commit-report" not in names
574
+
575
+ def test_rendered_bugfix_prompt_is_atomic_headless_and_local_commit_only(self, tmp_path):
576
+ prompt = _render_bugfix_prompt(tmp_path)
577
+
578
+ assert prompt.index("/prizmkit-code-review") < prompt.index(
579
+ "/prizmkit-test scope=this-change artifact_dir=.prizmkit/bugfix/B-001/"
580
+ ) < prompt.index("### Headless Commit Authorization")
581
+ assert "FAST_PATH=true" not in prompt
582
+ assert "prizmkit-workflow" not in prompt
583
+ assert '"local_commit_authorized": true' in prompt
584
+ assert '"push_authorized": false' in prompt
585
+ assert "Do not ask questions, wait for user input" in prompt
586
+ assert "push_authorized\": true" not in prompt
587
+ assert "automatic push" not in prompt.lower()
588
+ assert ".prizmkit/bugfix/B-001/" in prompt
589
+
458
590
  def test_setup_section_skips_and_finalization_group_expands(self):
459
591
  checkpoint = generate_bugfix_checkpoint(
460
592
  "B-001",