prizmkit 1.1.131 → 1.1.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +0 -2
  3. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +11 -6
  4. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -9
  5. package/bundled/dev-pipeline/prizmkit_runtime/processes.py +21 -1
  6. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +104 -17
  7. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +3 -2
  8. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +15 -13
  9. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -13
  10. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +5 -4
  11. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
  12. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -4
  13. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -9
  14. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -7
  15. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  16. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +2 -2
  17. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +5 -10
  18. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -5
  19. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +1 -1
  20. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +2 -3
  22. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  23. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  24. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -5
  25. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -5
  26. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +5 -5
  28. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/task-contract.md +4 -0
  31. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +39 -26
  32. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +7 -10
  33. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +3 -3
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +156 -5
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +34 -7
  36. package/bundled/skills/_metadata.json +3 -3
  37. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +3 -3
  38. package/bundled/skills/prizmkit-code-review/SKILL.md +22 -6
  39. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +243 -0
  40. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +41 -0
  41. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +3 -3
  42. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +93 -1
  43. package/bundled/skills/prizmkit-committer/SKILL.md +6 -13
  44. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +3 -3
  45. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +3 -3
  46. package/bundled/skills/prizmkit-plan/SKILL.md +17 -3
  47. package/bundled/skills/prizmkit-plan/assets/plan-template.md +18 -0
  48. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +242 -0
  49. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +4 -4
  50. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +3 -3
  51. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +3 -3
  52. package/bundled/skills/prizmkit-test/SKILL.md +25 -1
  53. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +63 -12
  54. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -1
  55. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +13 -0
  56. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +6 -1
  57. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -1
  58. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +4 -1
  59. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +15 -0
  60. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +3 -3
  61. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +744 -23
  62. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +194 -15
  63. package/bundled/skills/prizmkit-workflow/SKILL.md +1 -1
  64. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +3 -3
  65. package/package.json +1 -1
@@ -38,6 +38,7 @@ LAYER_ORDER = {
38
38
  "regression-ring": 4,
39
39
  }
40
40
  FINAL_RECORDS = {"validation.json", "verdict.json", "test-report.md"}
41
+ ATTESTATION_MUTABLE = {"validation.json"}
41
42
  COMMON_RECORDS = {
42
43
  "change-classification.json", "scope.json", "target-inventory.json",
43
44
  "environment.json", "executions.json", "verdict.json", "validation.json",
@@ -54,9 +55,13 @@ BLOCKED_EXTERNAL_CLASSES = {"production", "unknown"}
54
55
  SNAPSHOT_VOLATILE_ROOTS = (
55
56
  ".prizmkit/state",
56
57
  ".prizmkit/test/evidence",
58
+ ".prizmkit/" + "dev-" + "pipeline",
57
59
  ".claude/worktrees",
60
+ ".agents/worktrees",
61
+ ".codebuddy/worktrees",
62
+ ".codex/worktrees",
58
63
  )
59
- SNAPSHOT_VOLATILE_NAMES = {".git", "__pycache__"}
64
+ SNAPSHOT_VOLATILE_NAMES = {".git", "__pycache__", ".pytest_cache", ".mypy_cache"}
60
65
  RISK_CONFLICT_PATTERNS = {
61
66
  "permission": re.compile(r"auth|tenant|role|permission|access|acl|rbac", re.I),
62
67
  "concurrency": re.compile(r"lock|shared[ _-]?state|worker|queue|concurr|parallel|race", re.I),
@@ -219,6 +224,18 @@ def validate_schema(
219
224
  """Validate the strict JSON-Schema subset used by skill-owned schemas."""
220
225
  schema = resolve_ref(schema, root_schema)
221
226
 
227
+ one_of = schema.get("oneOf")
228
+ if isinstance(one_of, list):
229
+ matching = 0
230
+ for branch in one_of:
231
+ if not isinstance(branch, dict):
232
+ continue
233
+ branch_errors: list[str] = []
234
+ validate_schema(value, branch, root_schema, location, branch_errors)
235
+ if not branch_errors:
236
+ matching += 1
237
+ require(matching == 1, f"{location}: value must match exactly one schema branch", errors)
238
+
222
239
  for child in schema.get("allOf", []):
223
240
  if isinstance(child, dict):
224
241
  validate_schema(value, child, root_schema, location, errors)
@@ -341,6 +358,31 @@ def require_manifest_entry(
341
358
  return path
342
359
 
343
360
 
361
+ def validate_differential_na(
362
+ value: Any,
363
+ location: str,
364
+ errors: list[str],
365
+ ) -> None:
366
+ """Validate the differential-only N/A shape without weakening risk-cell N/A."""
367
+ if not isinstance(value, dict):
368
+ errors.append(f"{location}: differential N/A must be an object")
369
+ return
370
+ required = {"reason", "rationale", "evidence", "considered_signals", "conflicts"}
371
+ require(set(value) == required, f"{location}: differential N/A fields are incomplete", errors)
372
+ require(value.get("reason") in {"new-behavior", "textual-contract"}, f"{location}: invalid differential N/A reason", errors)
373
+ rationale = value.get("rationale")
374
+ require(isinstance(rationale, str) and len(rationale.strip()) >= 16, f"{location}: differential N/A rationale is too short", errors)
375
+ if value.get("reason") == "new-behavior" and isinstance(rationale, str):
376
+ require("no stable pre-change observable" in rationale.lower(), f"{location}: new behavior N/A lacks baseline rationale", errors)
377
+ if value.get("reason") == "textual-contract" and isinstance(rationale, str):
378
+ require("textual" in rationale.lower(), f"{location}: textual contract N/A lacks textual rationale", errors)
379
+ evidence = value.get("evidence")
380
+ require(isinstance(evidence, list) and bool(evidence) and all(isinstance(item, str) and item.strip() for item in evidence), f"{location}: differential N/A needs evidence strings", errors)
381
+ signals = value.get("considered_signals")
382
+ require(isinstance(signals, list) and bool(signals) and all(isinstance(item, str) and item.strip() for item in signals), f"{location}: differential N/A needs considered signals", errors)
383
+ require(isinstance(value.get("conflicts"), list), f"{location}: differential N/A conflicts must be an array", errors)
384
+
385
+
344
386
  def structured_na(
345
387
  value: Any,
346
388
  records_schema: dict[str, Any],
@@ -503,6 +545,25 @@ def validate_manifest(
503
545
  return entry_map
504
546
 
505
547
 
548
+ def get_captured_deleted_paths(root: Path) -> set[str]:
549
+ capture_path = root / "change-capture.json"
550
+ if not capture_path.is_file():
551
+ return set()
552
+ try:
553
+ capture = load_json(capture_path)
554
+ except EvidenceError:
555
+ return set()
556
+ if not isinstance(capture, dict):
557
+ return set()
558
+ return {
559
+ item["path"]
560
+ for item in capture.get("changed_files", [])
561
+ if isinstance(item, dict)
562
+ and isinstance(item.get("path"), str)
563
+ and (item.get("status", "").strip() == "D" or item.get("role") == "source")
564
+ }
565
+
566
+
506
567
  def validate_inventory(
507
568
  root: Path,
508
569
  project_root: Path,
@@ -569,6 +630,7 @@ def validate_inventory(
569
630
  require(plan.get("plan_inputs") == plan_inputs, "test plan inputs do not match target inventory", errors)
570
631
 
571
632
  patch = require_manifest_entry("source-change.patch", entry_map, root, errors, "source-change patch")
633
+ captured_deleted_paths = get_captured_deleted_paths(root)
572
634
  if patch is not None:
573
635
  require(manifest.get("working_diff_sha256") == file_sha256(patch), "working_diff_sha256 does not match source-change.patch", errors)
574
636
 
@@ -579,7 +641,19 @@ def validate_inventory(
579
641
  request = load_json(request_path)
580
642
  if isinstance(request, dict):
581
643
  require(request.get("changed_files") == inventory.get("changed_files"), "inventory request changed_files mismatch", errors)
582
- require(request.get("module_roots") == inventory.get("module_roots"), "inventory request module_roots mismatch", errors)
644
+ request_roots = request.get("module_roots")
645
+ normalized_request_roots = []
646
+ if isinstance(request_roots, list):
647
+ for item in request_roots:
648
+ if isinstance(item, str):
649
+ normalized_request_roots.append(item)
650
+ elif isinstance(item, dict) and isinstance(item.get("path"), str):
651
+ normalized_request_roots.append(item["path"])
652
+ require(
653
+ normalized_request_roots == inventory.get("module_roots"),
654
+ "inventory request module_roots mismatch",
655
+ errors,
656
+ )
583
657
  require(request.get("exclusions") == scope.get("exclusions"), "inventory request exclusions mismatch", errors)
584
658
  require(request.get("plan_inputs") == plan_inputs, "inventory request plan_inputs mismatch", errors)
585
659
  else:
@@ -594,6 +668,7 @@ def validate_scope(
594
668
  inventory_by_category: dict[str, dict[str, str]],
595
669
  project_root: Path,
596
670
  evidence_root: Path,
671
+ captured_deleted_paths: set[str],
597
672
  errors: list[str],
598
673
  ) -> None:
599
674
  changed_files = scope.get("changed_files")
@@ -614,8 +689,14 @@ def validate_scope(
614
689
  if relative in exclusion_paths:
615
690
  require(relative not in inventory_paths, f"excluded changed file is also inventoried: {relative}", errors)
616
691
  else:
617
- require(candidate is not None and candidate.is_file(), f"changed file is not live: {relative}", errors)
618
- require(relative in inventory_paths, f"changed file is neither inventoried nor excluded: {relative}", errors)
692
+ candidate = safe_path(project_root, relative, errors, "changed file")
693
+ deleted = relative in captured_deleted_paths
694
+ require(
695
+ deleted or (candidate is not None and candidate.is_file()),
696
+ f"changed file is not live: {relative}",
697
+ errors,
698
+ )
699
+ require(relative in inventory_paths or deleted, f"changed file is neither inventoried nor excluded: {relative}", errors)
619
700
 
620
701
  roots = scope.get("module_roots")
621
702
  require(isinstance(roots, list) and bool(roots), "scope module_roots is empty", errors)
@@ -695,7 +776,13 @@ def validate_requests_and_receipts(
695
776
  if not isinstance(request, dict):
696
777
  errors.append(f"request must be an object: {relative}")
697
778
  continue
698
- if "execution_request" in request:
779
+ if request.get("method") == "not-applicable":
780
+ validate_definition(request, "differentialRequest", records_schema, relative, errors)
781
+ not_applicable = request.get("not_applicable")
782
+ if isinstance(not_applicable, dict):
783
+ validate_differential_na(not_applicable, f"{relative}.not_applicable", errors)
784
+ differential_requests[relative] = request
785
+ elif "execution_request" in request:
699
786
  validate_definition(request, "differentialRequest", records_schema, relative, errors)
700
787
  nested = request.get("execution_request")
701
788
  if isinstance(nested, dict):
@@ -707,6 +794,17 @@ def validate_requests_and_receipts(
707
794
  validate_execution_request_semantics(request, relative, errors)
708
795
  validate_external_targets(request.get("external_targets"), f"{relative}.external_targets", errors)
709
796
  execution_requests[relative] = request
797
+ elif set(request) == {
798
+ "request_version", "change_class", "artifact_dir",
799
+ } or (
800
+ "request_version" in request
801
+ and "change_class" in request
802
+ and set(request) <= {
803
+ "request_version", "change_class", "baseline_commit", "scope_path",
804
+ "environment_path", "artifact_dir", "blockers", "repair_scope",
805
+ }
806
+ ):
807
+ require(request.get("request_version", "1.0") == "1.0", f"unsupported finalize request version: {relative}", errors)
710
808
  elif set(request) == {
711
809
  "request_version", "categories", "changed_files", "module_roots",
712
810
  "exclusions", "discovery_evidence", "plan_inputs",
@@ -922,12 +1020,18 @@ def validate_plan_and_scope_bindings(
922
1020
  project_relative = test.get("project_path")
923
1021
  inventory_relative = test.get("inventory_path")
924
1022
  snapshot_relative = test.get("snapshot_path")
1023
+ change_status = test.get("change_status", "modified" if isinstance(snapshot_relative, str) else "existing")
1024
+ require(change_status in {"existing", "added", "modified"}, f"planned test has invalid change_status: {test_id}", errors)
925
1025
  project_path = safe_path(project_root, project_relative, errors, f"planned test {test_id} project_path")
926
1026
  require(project_path is not None and project_path.is_file(), f"planned test live path is missing: {test_id}", errors)
927
1027
  require(inventory_relative == project_relative, f"planned test inventory path differs from live project path: {test_id}", errors)
928
1028
  require(inventory_relative in inventory_by_category.get("tests", {}), f"planned test is not in the test inventory: {test_id}", errors)
929
- snapshot_path = require_manifest_entry(snapshot_relative, entry_map, root, errors, f"planned test snapshot {test_id}") if isinstance(snapshot_relative, str) else None
930
- require(isinstance(snapshot_relative, str) and path_under(snapshot_relative, "generated-tests"), f"planned test snapshot must be under generated-tests/: {test_id}", errors)
1029
+ if change_status == "existing":
1030
+ require(snapshot_relative is None, f"existing test must not have a generated snapshot: {test_id}", errors)
1031
+ snapshot_path = None
1032
+ else:
1033
+ snapshot_path = require_manifest_entry(snapshot_relative, entry_map, root, errors, f"planned test snapshot {test_id}") if isinstance(snapshot_relative, str) else None
1034
+ require(isinstance(snapshot_relative, str) and path_under(snapshot_relative, "generated-tests"), f"planned test snapshot must be under generated-tests/: {test_id}", errors)
931
1035
  if project_path is not None and project_path.is_file() and snapshot_path is not None and snapshot_path.is_file():
932
1036
  require(file_sha256(project_path) == file_sha256(snapshot_path), f"generated test snapshot differs from live test: {test_id}", errors)
933
1037
  test_layers = test.get("layers")
@@ -1082,7 +1186,7 @@ def validate_differential_proofs(
1082
1186
  proof_by_behavior[behavior_id] = item
1083
1187
  classification = item.get("classification")
1084
1188
  if classification == "NOT_APPLICABLE":
1085
- structured_na(item.get("not_applicable"), records_schema, f"differential proof {behavior_id} not_applicable", errors)
1189
+ validate_differential_na(item.get("not_applicable"), f"differential proof {behavior_id} not_applicable", errors)
1086
1190
  for key in (
1087
1191
  "method", "differential_request_path", "differential_request_sha256", "baseline_commit",
1088
1192
  "current_tree_sha256", "baseline_execution_id", "mutation_execution_id", "current_execution_id",
@@ -1245,6 +1349,13 @@ def validate_verdict_and_attestation(
1245
1349
  require(bool(verdict_record.get("blockers")), "TEST_BLOCKED lacks blocker reasons", errors)
1246
1350
  elif verdict == "TEST_FAIL":
1247
1351
  require(bool(verdict_record.get("reproduced_failures")), "TEST_FAIL lacks reproduced failures", errors)
1352
+ require(
1353
+ verdict_record.get("repair_scope") in {
1354
+ "test-infrastructure", "production", "runtime", "schema", "dependency", "public-interface",
1355
+ },
1356
+ "TEST_FAIL lacks a valid evidence-backed repair_scope",
1357
+ errors,
1358
+ )
1248
1359
 
1249
1360
  if verify_attestation:
1250
1361
  validate_definition(validation, "validation", records_schema, "validation", errors)
@@ -1264,6 +1375,10 @@ def validate_verdict_and_attestation(
1264
1375
  require(validation.get("semantics") == "integrity-and-protocol-validation-not-hostile-producer-proof", "validation attestation semantics exceed integrity/protocol validation", errors)
1265
1376
  require(validation.get("replay_receipt_ids") == replayed_ids, "validation replay receipt IDs do not match actual replay executions", errors)
1266
1377
 
1378
+ if root.joinpath("lifecycle.json").is_file():
1379
+ expected_report = render_report_text(root, manifest, verdict_record, validation)
1380
+ require(report == expected_report, "derived report does not match canonical rendering", errors)
1381
+
1267
1382
  fragments = [
1268
1383
  f"Verdict: {verdict}", f"Evidence ID: {manifest.get('evidence_id')}",
1269
1384
  "Derived from structured evidence", "sensitivity=project-controlled",
@@ -1299,7 +1414,10 @@ def validate_evidence(
1299
1414
 
1300
1415
  entry_map = validate_manifest(root, manifest, manifest_schema, records_schema, errors, ignored_hash_paths)
1301
1416
  change_class = manifest.get("change_class")
1302
- required = COMMON_RECORDS | (BEHAVIOR_RECORDS if change_class == "behavior" else LIGHTWEIGHT_RECORDS)
1417
+ final_verdict = manifest.get("final_verdict")
1418
+ required = COMMON_RECORDS
1419
+ if final_verdict != "TEST_BLOCKED":
1420
+ required = required | (BEHAVIOR_RECORDS if change_class == "behavior" else LIGHTWEIGHT_RECORDS)
1303
1421
  for relative in required:
1304
1422
  require(relative in entry_map, f"required evidence record not hashed: {relative}", errors)
1305
1423
  validate_definition(classification, "changeClassification", records_schema, "change-classification", errors)
@@ -1322,7 +1440,10 @@ def validate_evidence(
1322
1440
 
1323
1441
  inventory_by_category, _ = validate_inventory(root, project_root, manifest, inventory, scope, plan, entry_map, errors)
1324
1442
  require(manifest.get("evidence_id_inputs", {}).get("scope_sha256") == canonical_sha256(scope), "scope identity hash mismatch", errors)
1325
- validate_scope(scope, inventory, inventory_by_category, project_root, root, errors)
1443
+ validate_scope(
1444
+ scope, inventory, inventory_by_category, project_root, root,
1445
+ get_captured_deleted_paths(root), errors,
1446
+ )
1326
1447
  execution_requests, differential_requests, executions = validate_requests_and_receipts(root, entry_map, records_schema, errors)
1327
1448
 
1328
1449
  if change_class == "behavior":
@@ -1373,6 +1494,64 @@ def validate_evidence(
1373
1494
  return errors
1374
1495
 
1375
1496
 
1497
+ def atomic_write_text(path: Path, value: str) -> None:
1498
+ path.parent.mkdir(parents=True, exist_ok=True)
1499
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
1500
+ try:
1501
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1502
+ handle.write(value)
1503
+ os.replace(temporary_name, path)
1504
+ except BaseException:
1505
+ try:
1506
+ os.unlink(temporary_name)
1507
+ except FileNotFoundError:
1508
+ pass
1509
+ raise
1510
+
1511
+
1512
+ def render_report_text(
1513
+ root: Path,
1514
+ manifest: dict[str, Any],
1515
+ verdict: dict[str, Any],
1516
+ validation: dict[str, Any],
1517
+ ) -> str:
1518
+ lifecycle_path = root / "lifecycle.json"
1519
+ lifecycle = load_json(lifecycle_path) if lifecycle_path.exists() else {}
1520
+ proof_path = root / "differential-proof.json"
1521
+ proof_record = load_json(proof_path) if proof_path.exists() else {}
1522
+ executions_record = load_json(root / "executions.json")
1523
+ proofs = proof_record.get("proofs", []) if isinstance(proof_record, dict) else []
1524
+ executions = executions_record if isinstance(executions_record, list) else []
1525
+ artifact_dir = lifecycle.get("artifact_dir") if isinstance(lifecycle, dict) else None
1526
+ lines = [
1527
+ "# Test Evidence Report",
1528
+ "",
1529
+ "Derived from structured evidence",
1530
+ f"Evidence ID: {manifest.get('evidence_id')}",
1531
+ f"Verdict: {verdict.get('verdict')}",
1532
+ f"Validator result: {validation.get('result', 'pending')}",
1533
+ "sensitivity=project-controlled",
1534
+ "The project owns access control, retention, and upload policy.",
1535
+ "Mocked code-level evidence does not verify a real deployed environment.",
1536
+ "",
1537
+ "## Scope",
1538
+ f"- Artifact Dir: {artifact_dir or 'not provided'}",
1539
+ "",
1540
+ f"Execution receipts: {len(executions)}",
1541
+ f"Differential proofs: {len(proofs) if isinstance(proofs, list) else 0}",
1542
+ "The legacy test-report interface is not supported.",
1543
+ "",
1544
+ ]
1545
+ return "\n".join(lines)
1546
+
1547
+
1548
+ def refresh_final_record_hashes(root: Path, manifest: dict[str, Any]) -> None:
1549
+ for entry in manifest.get("files", []):
1550
+ if isinstance(entry, dict) and entry.get("path") in FINAL_RECORDS:
1551
+ entry["sha256"] = file_sha256(root / entry["path"])
1552
+ atomic_write_json(root / "manifest.json", manifest)
1553
+
1554
+
1376
1555
  def main() -> int:
1377
1556
  parser = argparse.ArgumentParser(description="Validate a PrizmKit test evidence package")
1378
1557
  parser.add_argument("--evidence-dir", required=True, help="Path to .prizmkit/test/evidence/<evidence-id>")
@@ -1388,7 +1567,7 @@ def main() -> int:
1388
1567
  except IndexError:
1389
1568
  print("Test evidence validation: FAIL\n- cannot infer project root from evidence directory")
1390
1569
  return 1
1391
- errors = validate_evidence(root, project_root, FINAL_RECORDS if args.attest else None)
1570
+ errors = validate_evidence(root, project_root, ATTESTATION_MUTABLE if args.attest else None)
1392
1571
  if errors:
1393
1572
  print("Test evidence validation: FAIL")
1394
1573
  for error in errors:
@@ -1396,6 +1575,7 @@ def main() -> int:
1396
1575
  return 1
1397
1576
 
1398
1577
  manifest = load_json(root / "manifest.json")
1578
+ verdict_record = load_json(root / "verdict.json")
1399
1579
  if args.attest:
1400
1580
  executions = load_json(root / "executions.json")
1401
1581
  replayed_ids = [
@@ -1421,10 +1601,9 @@ def main() -> int:
1421
1601
  "replay_receipt_ids": replayed_ids,
1422
1602
  }
1423
1603
  atomic_write_json(root / "validation.json", validation)
1424
- for entry in manifest["files"]:
1425
- if entry["path"] in FINAL_RECORDS:
1426
- entry["sha256"] = file_sha256(root / entry["path"])
1427
- atomic_write_json(root / "manifest.json", manifest)
1604
+ report_text = render_report_text(root, manifest, verdict_record, validation)
1605
+ atomic_write_text(root / "test-report.md", report_text)
1606
+ refresh_final_record_hashes(root, manifest)
1428
1607
  errors = validate_evidence(root, project_root)
1429
1608
  if errors:
1430
1609
  print("Test evidence validation: FAIL")
@@ -199,7 +199,7 @@ Interactive execution requires the committer to:
199
199
  4. wait for explicit user confirmation from the current user;
200
200
  5. create and verify the local commit only after confirmation.
201
201
 
202
- Trusted headless execution is a separate atomic-stage path. It requires a host-defined non-interactive `mode`, a trusted `owner` identifier, `local_commit_authorized=true`, and `push_authorized=false`; it does not ask or wait and never pushes. Unknown headless contexts are blocked. Remote push is never part of this composite workflow's automatic path.
202
+ Trusted headless execution is a separate atomic-stage path. It requires a host-defined non-interactive `mode`, a trusted `owner` identifier, and `local_commit_authorized=true`; it does not ask or wait. Unknown headless contexts are blocked. Remote publication is a separate host-runtime operation and is never decided by this composite workflow.
203
203
 
204
204
  ## Automatic Handoff and Manual Fallback
205
205
 
@@ -157,12 +157,12 @@ Trusted headless execution:
157
157
  ```text
158
158
  a trusted host explicitly authorizes the current local task commit
159
159
  → host injects mode=<host-defined-headless-mode>, owner=<trusted-host-identifier>,
160
- local_commit_authorized=true, push_authorized=false
160
+ local_commit_authorized=true
161
161
  → committer verifies gates and commits automatically
162
- → no question, wait, or push is permitted
162
+ → no question or wait is permitted
163
163
  ```
164
164
 
165
- A self-declared or otherwise untrusted headless context does not authorize a commit. Push is never implied by either mode.
165
+ A self-declared or otherwise untrusted headless context does not authorize a commit. Remote publication remains a separate host-runtime decision.
166
166
 
167
167
  ## Recovery
168
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.131",
3
+ "version": "1.1.134",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {