ultra-builder-pro-cli 0.26.1 → 3.0.2

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 (75) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +64 -29
  2. package/.ultra-template/north-star.md +36 -30
  3. package/.ultra-template/project-brief.md +28 -0
  4. package/.ultra-template/specs/product.md +4 -0
  5. package/.ultra-template/specs/research-distillate.md +8 -0
  6. package/.ultra-template/tasks.json +1 -0
  7. package/.ultra-template/test-report.json +18 -1
  8. package/CHANGELOG.md +227 -1
  9. package/README.md +161 -59
  10. package/adapters/_shared/host-profile.cjs +68 -12
  11. package/adapters/_shared/plugin-core.cjs +4 -5
  12. package/adapters/_shared/runtime-assets.cjs +11 -2
  13. package/adapters/zcode.js +375 -0
  14. package/bin/delegate-worker.cjs +20 -0
  15. package/bin/delegate.cjs +94 -25
  16. package/bin/install.js +11 -6
  17. package/docs/ARCHITECTURE.md +109 -25
  18. package/docs/ARTIFACT-AUTHORITY.md +204 -30
  19. package/docs/DECISIONS.md +141 -10
  20. package/docs/PHILOSOPHY.md +61 -8
  21. package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
  23. package/docs/SKILL-AUTHORING.md +14 -9
  24. package/docs/WORKFLOW-LIFECYCLE.md +211 -53
  25. package/docs/evals/adversarial-review-2026-08-14.md +84 -0
  26. package/docs/evals/zcode-automation-2026-08-14.md +103 -0
  27. package/hooks/README.md +31 -3
  28. package/hooks/_common.py +1699 -33
  29. package/hooks/adapters/zcode.py +227 -0
  30. package/hooks/compact_context.py +176 -17
  31. package/hooks/mid_workflow_recall.py +27 -5
  32. package/hooks/post_edit_guard.py +133 -23
  33. package/hooks/session_context.py +722 -11
  34. package/package.json +6 -3
  35. package/skills/ultra-change/SKILL.md +84 -64
  36. package/skills/ultra-change/references/change-contract.md +130 -32
  37. package/skills/ultra-change/references/execution-grant.md +109 -0
  38. package/skills/ultra-change/references/primary-transfer.md +273 -0
  39. package/skills/ultra-change/scripts/validate_primary_transfer.cjs +1928 -0
  40. package/skills/ultra-delegate/SKILL.md +38 -5
  41. package/skills/ultra-delegate/references/delegation-contract.md +21 -1
  42. package/skills/ultra-deliver/SKILL.md +123 -44
  43. package/skills/ultra-deliver/references/baseline-reconciliation.md +5 -3
  44. package/skills/ultra-dev/SKILL.md +173 -43
  45. package/skills/ultra-grilling/SKILL.md +19 -16
  46. package/skills/ultra-grilling/references/reframing.md +7 -3
  47. package/skills/ultra-init/SKILL.md +99 -87
  48. package/skills/ultra-init/references/brownfield-adoption.md +23 -40
  49. package/skills/ultra-init/scripts/init_project.cjs +968 -21
  50. package/skills/ultra-plan/SKILL.md +83 -46
  51. package/skills/ultra-plan/references/task-evidence-v2.md +300 -0
  52. package/skills/ultra-plan/scripts/validate_task_evidence.cjs +859 -0
  53. package/skills/ultra-research/SKILL.md +119 -68
  54. package/skills/ultra-research/references/{03-competitive-landscape.md → 03-alternatives.md} +6 -6
  55. package/skills/ultra-research/references/04-product-strategy.md +11 -2
  56. package/skills/ultra-research/references/21-features-scope.md +8 -0
  57. package/skills/ultra-research/references/22-success-metrics.md +9 -4
  58. package/skills/ultra-research/references/99-synthesis.md +25 -1
  59. package/skills/ultra-research/references/north-star-v2.md +168 -0
  60. package/skills/ultra-research/references/wayfinding.md +50 -0
  61. package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
  62. package/skills/ultra-review/SKILL.md +201 -24
  63. package/skills/ultra-review/references/adversarial-evaluation.md +31 -0
  64. package/skills/ultra-review/references/spec.md +5 -0
  65. package/skills/ultra-review/references/unified-schema.md +146 -11
  66. package/skills/ultra-review/references/worker-packet.md +151 -8
  67. package/skills/ultra-review/scripts/review_wait.py +2653 -63
  68. package/skills/ultra-status/SKILL.md +146 -32
  69. package/skills/ultra-tdd/SKILL.md +14 -4
  70. package/skills/ultra-tdd/references/test-execution.md +6 -1
  71. package/skills/ultra-test/SKILL.md +157 -15
  72. package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
  73. package/skills/ultra-test/scripts/worktree_digest.cjs +551 -29
  74. package/skills/ultra-think/SKILL.md +5 -10
  75. package/skills/ultra-think/references/interaction-boundary.md +2 -1
@@ -1,29 +1,50 @@
1
1
  #!/usr/bin/env python3
2
- """Wait for and validate Ultra review v2 artifacts.
2
+ """Wait for and validate Ultra review artifacts.
3
3
 
4
4
  Usage:
5
+ python3 review_wait.py <session_path> packet --packet-digest <sha256>
5
6
  python3 review_wait.py <session_path> agents --packet-digest <sha256> <artifact-stem> [<artifact-stem> ...]
6
7
  python3 review_wait.py <session_path> summary --packet-digest <sha256>
8
+ python3 review_wait.py <session_path> summary --packet-digest <sha256> --summary-snapshot-digest <sha256>
9
+
10
+ New sessions require admitted v4. Add ``--legacy-v4`` immediately after the packet
11
+ digest only when reading an immutable pre-admission v4 session, or ``--legacy-v3`` for
12
+ an immutable historical v3 session; neither flag writes or upgrades artifacts.
7
13
 
8
14
  The agents mode validates the exact named JSON files. The summary mode validates the
9
15
  two-axis verdict contract. Timeout and poll intervals can be shortened in tests with
10
16
  UBP_REVIEW_WAIT_TIMEOUT and UBP_REVIEW_WAIT_POLL.
11
17
  """
12
18
 
19
+ import hashlib
13
20
  import json
14
21
  import os
15
22
  import re
23
+ import selectors
24
+ import shutil
25
+ import stat
26
+ import subprocess
16
27
  import sys
28
+ import tempfile
17
29
  import time
18
- from pathlib import Path
30
+ from datetime import datetime
31
+ from pathlib import Path, PurePosixPath
19
32
 
20
- FINDINGS_SCHEMA = "ultra-review-findings-v2"
21
- SUMMARY_SCHEMA = "ultra-review-summary-v2"
33
+ FINDINGS_SCHEMA = "ultra-review-findings-v4"
34
+ SUMMARY_SCHEMA = "ultra-review-summary-v4"
35
+ LEGACY_FINDINGS_SCHEMA = "ultra-review-findings-v3"
36
+ LEGACY_SUMMARY_SCHEMA = "ultra-review-summary-v3"
37
+ ADMISSION_SCHEMA_V1 = "ultra-review-admission-v1"
38
+ ADMISSION_SCHEMA_V2 = "ultra-review-admission-v2"
39
+ ADMISSION_CONTRACT_V1 = "ultra-review-admission-required-v1"
40
+ ADMISSION_CONTRACT_V2 = "ultra-review-admission-required-v2"
41
+ STRICT_ADMISSION_CONTRACTS = {ADMISSION_CONTRACT_V1, ADMISSION_CONTRACT_V2}
22
42
  AXES = {"spec_fidelity", "engineering_standards"}
23
43
  SEVERITIES = {"P0", "P1", "P2", "P3"}
24
44
  AXIS_VERDICTS = {"PASS", "FAIL", "INCOMPLETE"}
25
45
  OVERALL_VERDICTS = {"APPROVE", "REQUEST_CHANGES", "INCOMPLETE"}
26
46
  REVIEW_MODES = {"task", "change", "plan"}
47
+ EXECUTION_MODES = {"isolated", "sequential-shared-context"}
27
48
  SELECTION_STATUSES = {"selected", "skipped"}
28
49
  REVIEW_WORKERS = {
29
50
  "review-spec",
@@ -33,11 +54,83 @@ REVIEW_WORKERS = {
33
54
  "review-design",
34
55
  "review-comments",
35
56
  }
57
+ CANONICAL_WORKERS = (
58
+ ("review-spec", "spec_fidelity", "skills/ultra-review/references/spec.md"),
59
+ ("review-code", "engineering_standards", "skills/ultra-review/references/code.md"),
60
+ ("review-tests", "engineering_standards", "skills/ultra-review/references/tests.md"),
61
+ ("review-errors", "engineering_standards", "skills/ultra-review/references/errors.md"),
62
+ ("review-design", "engineering_standards", "skills/ultra-review/references/design.md"),
63
+ ("review-comments", "engineering_standards", "skills/ultra-review/references/comments.md"),
64
+ )
65
+ CANONICAL_WORKER_BY_AGENT = {
66
+ agent: {"axis": axis, "lens": lens, "order": order}
67
+ for order, (agent, axis, lens) in enumerate(CANONICAL_WORKERS)
68
+ }
69
+ PACKET_REQUIRED_FIELDS = {
70
+ "$schema",
71
+ "session",
72
+ "mode",
73
+ "created_at",
74
+ "head",
75
+ "range",
76
+ "change_id",
77
+ "task_ids",
78
+ "acceptance",
79
+ "public_seams",
80
+ "north_star_trace",
81
+ "context_files",
82
+ "workers",
83
+ "diff_files",
84
+ "output_directory",
85
+ }
86
+ TRACE_FIELDS = {
87
+ "path",
88
+ "first_principles",
89
+ "serves",
90
+ "touches",
91
+ "north_star_revision",
92
+ "north_star_digest",
93
+ }
94
+ CONTEXT_FIELDS = {"path", "sha256"}
95
+ WORKER_FIELDS = {"agent", "axis", "lens", "output"}
96
+ REVIEW_HISTORY_FIELDS = {
97
+ "parent_summary_ref",
98
+ "parent_summary_digest",
99
+ "unresolved_blocking_ids",
100
+ }
101
+ FOREIGN_REVIEW_SESSION = re.compile(
102
+ r"\.ultra/reviews/([A-Za-z0-9][A-Za-z0-9._-]*)/"
103
+ )
36
104
  ARTIFACT_STEM = re.compile(r"^[a-z][a-z0-9-]*$")
37
105
  FINDING_FIELDS = {
38
106
  "id", "axis", "severity", "category", "title", "file", "line", "trigger",
39
107
  "impact", "evidence", "suggestion",
40
108
  }
109
+ MAX_JSON_BYTES = 8 * 1024 * 1024
110
+ READ_CHUNK_BYTES = 64 * 1024
111
+ MAX_VALIDATOR_REPORT_BYTES = 1024 * 1024
112
+ VALIDATOR_TIMEOUT_SECONDS = 5
113
+ ADMISSION_FIELDS = {
114
+ "$schema",
115
+ "version",
116
+ "session",
117
+ "packet_digest",
118
+ "head",
119
+ "observations",
120
+ "north_star_report",
121
+ "subject_digest",
122
+ }
123
+ ADMISSION_OBSERVATION_FIELDS_V1 = {"role", "path", "sha256"}
124
+ ADMISSION_OBSERVATION_FIELDS_V2 = {"role", "path", "sha256", "byte_length"}
125
+ PACKET_SUBJECT_OBSERVATION_FIELDS = {"role", "path", "sha256", "byte_length"}
126
+ NORTH_STAR_REPORT_RECEIPT_FIELDS = {
127
+ "schema",
128
+ "sha256",
129
+ "input_sha256",
130
+ "input_byte_length",
131
+ "source_observations",
132
+ }
133
+ NORTH_STAR_SOURCE_OBSERVATION_FIELDS = {"role", "path", "sha256", "byte_length"}
41
134
 
42
135
 
43
136
  def env_seconds(name: str, default: float) -> float:
@@ -52,16 +145,229 @@ DEFAULT_TIMEOUT = env_seconds("UBP_REVIEW_WAIT_TIMEOUT", 300.0)
52
145
  POLL_INTERVAL = env_seconds("UBP_REVIEW_WAIT_POLL", 2.0)
53
146
 
54
147
 
55
- def read_json(path: Path):
148
+ def decode_json_bytes(data_bytes: bytes, label: str):
56
149
  try:
57
- data = json.loads(path.read_text(encoding="utf-8"))
58
- except (OSError, json.JSONDecodeError) as error:
59
- return None, f"unreadable JSON: {error}"
150
+ data = json.loads(data_bytes.decode("utf-8"))
151
+ except UnicodeDecodeError as error:
152
+ return None, f"{label} is unreadable UTF-8 JSON: {error}"
153
+ except json.JSONDecodeError as error:
154
+ return None, f"{label} is unreadable JSON: {error}"
60
155
  if not isinstance(data, dict):
61
- return None, "top-level value must be an object"
156
+ return None, f"{label} top-level value must be an object"
62
157
  return data, None
63
158
 
64
159
 
160
+ def _stat_identity(value):
161
+ return (
162
+ value.st_dev,
163
+ value.st_ino,
164
+ value.st_mode,
165
+ value.st_size,
166
+ value.st_mtime_ns,
167
+ value.st_ctime_ns,
168
+ )
169
+
170
+
171
+ def _component_identity(value):
172
+ return (value.st_dev, value.st_ino, value.st_mode)
173
+
174
+
175
+ def _verify_fresh_repo_walk(
176
+ root: Path,
177
+ segments,
178
+ directory_identities,
179
+ file_identity,
180
+ label: str,
181
+ ):
182
+ """Rewalk from the current root and reject any swapped path component."""
183
+ directory_flags = os.O_RDONLY
184
+ directory_flags |= getattr(os, "O_DIRECTORY", 0)
185
+ directory_flags |= getattr(os, "O_NOFOLLOW", 0)
186
+ descriptors = []
187
+ try:
188
+ root_path = os.stat(root, follow_symlinks=False)
189
+ descriptor = os.open(root, directory_flags)
190
+ descriptors.append(descriptor)
191
+ root_fd = os.fstat(descriptor)
192
+ if (
193
+ _component_identity(root_path) != directory_identities[0]
194
+ or _component_identity(root_fd) != directory_identities[0]
195
+ ):
196
+ return f"{label} repository root changed after its stable byte snapshot"
197
+
198
+ for index, segment in enumerate(segments[:-1], start=1):
199
+ component = os.stat(segment, dir_fd=descriptor, follow_symlinks=False)
200
+ if stat.S_ISLNK(component.st_mode) or not stat.S_ISDIR(component.st_mode):
201
+ return f"{label} path component changed after its stable byte snapshot"
202
+ next_descriptor = os.open(segment, directory_flags, dir_fd=descriptor)
203
+ descriptors.append(next_descriptor)
204
+ opened = os.fstat(next_descriptor)
205
+ if (
206
+ _component_identity(component) != directory_identities[index]
207
+ or _component_identity(opened) != directory_identities[index]
208
+ ):
209
+ return (
210
+ f"{label} path component {'/'.join(segments[:index])} changed "
211
+ "after its stable byte snapshot"
212
+ )
213
+ descriptor = next_descriptor
214
+
215
+ current_file = os.stat(
216
+ segments[-1],
217
+ dir_fd=descriptor,
218
+ follow_symlinks=False,
219
+ )
220
+ if (
221
+ not stat.S_ISREG(current_file.st_mode)
222
+ or _stat_identity(current_file) != file_identity
223
+ ):
224
+ return f"{label} changed after its stable byte snapshot"
225
+ except (FileNotFoundError, NotADirectoryError):
226
+ return f"{label} path changed after its stable byte snapshot"
227
+ except OSError as error:
228
+ return f"{label} fresh path identity cannot be verified: {error}"
229
+ finally:
230
+ for descriptor in reversed(descriptors):
231
+ os.close(descriptor)
232
+ return None
233
+
234
+
235
+ def stable_read_repo_bytes(
236
+ repo_root: Path,
237
+ repo_path: str,
238
+ label: str,
239
+ max_bytes: int = MAX_JSON_BYTES,
240
+ ):
241
+ """Read one bounded file through a no-symlink repository descriptor walk."""
242
+ normalized, path_error = validate_repo_path(repo_path, repo_root, label)
243
+ if path_error:
244
+ return None, path_error, False
245
+ segments = normalized.split("/")
246
+ directory_flags = os.O_RDONLY
247
+ directory_flags |= getattr(os, "O_DIRECTORY", 0)
248
+ directory_flags |= getattr(os, "O_NOFOLLOW", 0)
249
+ file_flags = os.O_RDONLY
250
+ file_flags |= getattr(os, "O_NONBLOCK", 0)
251
+ file_flags |= getattr(os, "O_NOFOLLOW", 0)
252
+ directory_descriptors = []
253
+ descriptor = None
254
+ directory_identities = []
255
+ try:
256
+ root = repo_root.resolve(strict=True)
257
+ root_path = os.stat(root, follow_symlinks=False)
258
+ directory_descriptor = os.open(root, directory_flags)
259
+ directory_descriptors.append(directory_descriptor)
260
+ root_fd = os.fstat(directory_descriptor)
261
+ if _component_identity(root_path) != _component_identity(root_fd):
262
+ return None, f"{label} repository root changed before its stable byte snapshot", False
263
+ directory_identities.append(_component_identity(root_fd))
264
+ for segment in segments[:-1]:
265
+ component = os.stat(
266
+ segment,
267
+ dir_fd=directory_descriptor,
268
+ follow_symlinks=False,
269
+ )
270
+ if stat.S_ISLNK(component.st_mode):
271
+ return None, f"{label} path components must not be symlinks", False
272
+ if not stat.S_ISDIR(component.st_mode):
273
+ return None, f"{label} path component is not a directory", False
274
+ next_descriptor = os.open(
275
+ segment,
276
+ directory_flags,
277
+ dir_fd=directory_descriptor,
278
+ )
279
+ directory_descriptors.append(next_descriptor)
280
+ opened_component = os.fstat(next_descriptor)
281
+ if _component_identity(component) != _component_identity(opened_component):
282
+ return None, (
283
+ f"{label} path component changed before its stable byte snapshot"
284
+ ), False
285
+ directory_identities.append(_component_identity(opened_component))
286
+ directory_descriptor = next_descriptor
287
+
288
+ filename = segments[-1]
289
+ before_path = os.stat(
290
+ filename,
291
+ dir_fd=directory_descriptor,
292
+ follow_symlinks=False,
293
+ )
294
+ if stat.S_ISLNK(before_path.st_mode):
295
+ return None, (
296
+ f"{label} must be a stable repository-contained regular file, not a symlink"
297
+ ), False
298
+ if not stat.S_ISREG(before_path.st_mode):
299
+ return None, f"{label} must be a stable repository-contained regular file", False
300
+ if before_path.st_size > max_bytes:
301
+ return None, f"{label} exceeds the {max_bytes}-byte artifact limit", False
302
+
303
+ descriptor = os.open(filename, file_flags, dir_fd=directory_descriptor)
304
+ before_fd = os.fstat(descriptor)
305
+ if not stat.S_ISREG(before_fd.st_mode):
306
+ return None, f"{label} must be a stable repository-contained regular file", False
307
+ if _stat_identity(before_path) != _stat_identity(before_fd):
308
+ return None, f"{label} changed before its stable byte snapshot could be read", False
309
+
310
+ chunks = []
311
+ observed = 0
312
+ while True:
313
+ chunk = os.read(descriptor, min(READ_CHUNK_BYTES, max_bytes + 1 - observed))
314
+ if not chunk:
315
+ break
316
+ chunks.append(chunk)
317
+ observed += len(chunk)
318
+ if observed > max_bytes:
319
+ return None, f"{label} exceeds the {max_bytes}-byte artifact limit", False
320
+ after_fd = os.fstat(descriptor)
321
+ after_path = os.stat(
322
+ filename,
323
+ dir_fd=directory_descriptor,
324
+ follow_symlinks=False,
325
+ )
326
+ except FileNotFoundError:
327
+ return None, f"{label} is missing", True
328
+ except OSError as error:
329
+ return None, f"{label} cannot be read as a stable regular file: {error}", False
330
+ finally:
331
+ if descriptor is not None:
332
+ os.close(descriptor)
333
+ for directory_descriptor in reversed(directory_descriptors):
334
+ os.close(directory_descriptor)
335
+
336
+ if (
337
+ _stat_identity(before_fd) != _stat_identity(after_fd)
338
+ or _stat_identity(after_fd) != _stat_identity(after_path)
339
+ ):
340
+ return None, f"{label} changed while its stable byte snapshot was read", False
341
+
342
+ data_bytes = b"".join(chunks)
343
+ if len(data_bytes) != after_fd.st_size:
344
+ return None, f"{label} size changed while its stable byte snapshot was read", False
345
+ rewalk_error = _verify_fresh_repo_walk(
346
+ root,
347
+ segments,
348
+ directory_identities,
349
+ _stat_identity(after_fd),
350
+ label,
351
+ )
352
+ if rewalk_error:
353
+ return None, rewalk_error, False
354
+ return {
355
+ "bytes": data_bytes,
356
+ "sha256": hashlib.sha256(data_bytes).hexdigest(),
357
+ "identity": _stat_identity(after_fd),
358
+ "component_identities": tuple(directory_identities),
359
+ "path": normalized,
360
+ }, None, False
361
+
362
+
363
+ def stable_read_repo_json(repo_root: Path, repo_path: str, label: str):
364
+ snapshot, error, missing = stable_read_repo_bytes(repo_root, repo_path, label)
365
+ if error:
366
+ return None, snapshot, error, missing
367
+ data, error = decode_json_bytes(snapshot["bytes"], label)
368
+ return data, snapshot, error, False
369
+
370
+
65
371
  def nonempty_string(value) -> bool:
66
372
  return isinstance(value, str) and bool(value.strip())
67
373
 
@@ -70,7 +376,1612 @@ def digest_string(value) -> bool:
70
376
  return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None
71
377
 
72
378
 
73
- def validate_finding(finding, artifact_axis: str, seen_ids):
379
+ def git_blob_digest_string(value) -> bool:
380
+ return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{40}", value) is not None
381
+
382
+
383
+ def timestamp_string(value) -> bool:
384
+ if not isinstance(value, str) or re.fullmatch(
385
+ r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
386
+ value,
387
+ ) is None:
388
+ return False
389
+ try:
390
+ parsed = datetime.fromisoformat(value[:-1] + "+00:00" if value.endswith("Z") else value)
391
+ except ValueError:
392
+ return False
393
+ return parsed.tzinfo is not None
394
+
395
+
396
+ def canonical_repo_root(session_path: Path):
397
+ resolved_session = session_path.resolve()
398
+ if (
399
+ resolved_session.parent.name != "reviews"
400
+ or resolved_session.parent.parent.name != ".ultra"
401
+ ):
402
+ return None, (
403
+ "session path must resolve to the canonical repository location "
404
+ "<repo>/.ultra/reviews/<session>"
405
+ )
406
+ return resolved_session.parent.parent.parent, None
407
+
408
+
409
+ def validate_repo_path(value, repo_root: Path, field: str, allow_fragment=False):
410
+ if not nonempty_string(value):
411
+ return None, f"{field} must be a non-empty normalized repository-relative path"
412
+
413
+ path_value = value
414
+ if allow_fragment:
415
+ if value.count("#") != 1:
416
+ return None, f"{field} must contain one non-empty fragment"
417
+ path_value, fragment = value.split("#", 1)
418
+ if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", fragment) is None:
419
+ return None, f"{field} has an invalid fragment"
420
+ elif "#" in value:
421
+ return None, f"{field} must not contain a fragment"
422
+
423
+ if (
424
+ "\\" in path_value
425
+ or "\x00" in path_value
426
+ or path_value.startswith("/")
427
+ or re.match(r"^[A-Za-z]:", path_value)
428
+ ):
429
+ return None, f"{field} must be a normalized repository-relative POSIX path"
430
+ segments = path_value.split("/")
431
+ if any(segment in {"", ".", ".."} for segment in segments):
432
+ return None, f"{field} must not contain empty, dot, or parent path segments"
433
+ if PurePosixPath(path_value).as_posix() != path_value:
434
+ return None, f"{field} must be normalized"
435
+
436
+ try:
437
+ resolved_root = repo_root.resolve()
438
+ resolved_target = (resolved_root / Path(*segments)).resolve(strict=False)
439
+ except (OSError, RuntimeError) as error:
440
+ return None, f"{field} cannot be resolved safely: {error}"
441
+ try:
442
+ resolved_target.relative_to(resolved_root)
443
+ except ValueError:
444
+ return None, f"{field} escapes the repository root"
445
+
446
+ # Path.resolve(strict=False) stopped raising on symlink loops in Python
447
+ # 3.13, so a looping or later-swapped component can survive resolution
448
+ # while the stable-read descriptor walks would still refuse it. Refuse
449
+ # statable symlink or non-directory components here too; components that
450
+ # merely do not exist yet stay valid at validation time.
451
+ probe = resolved_root
452
+ for segment in segments[:-1]:
453
+ probe = probe / segment
454
+ try:
455
+ component_stat = os.stat(probe, follow_symlinks=False)
456
+ except FileNotFoundError:
457
+ continue
458
+ except NotADirectoryError:
459
+ return None, f"{field} must traverse ordinary directories"
460
+ if stat.S_ISLNK(component_stat.st_mode) or not stat.S_ISDIR(component_stat.st_mode):
461
+ return None, f"{field} must traverse ordinary non-symlink directories"
462
+ return path_value, None
463
+
464
+
465
+ def validate_string_array(value, field: str, require_nonempty=False):
466
+ if not isinstance(value, list) or not all(nonempty_string(item) for item in value):
467
+ return f"{field} must be a string array"
468
+ if require_nonempty and not value:
469
+ return f"{field} must not be empty"
470
+ if len(value) != len(set(value)):
471
+ return f"{field} must not contain duplicates"
472
+ return None
473
+
474
+
475
+ def canonical_context_digest(context_files) -> str:
476
+ if len(context_files) == 1:
477
+ return context_files[0]["sha256"]
478
+ canonical = sorted(
479
+ (
480
+ {"path": item["path"], "sha256": item["sha256"]}
481
+ for item in context_files
482
+ ),
483
+ key=lambda item: item["path"].encode("utf-8"),
484
+ )
485
+ payload = json.dumps(
486
+ canonical,
487
+ ensure_ascii=False,
488
+ separators=(",", ":"),
489
+ sort_keys=True,
490
+ ).encode("utf-8")
491
+ return hashlib.sha256(payload).hexdigest()
492
+
493
+
494
+ def _trace_arrays(value):
495
+ if not isinstance(value, dict):
496
+ return None
497
+ result = {}
498
+ patterns = {
499
+ "first_principles": r"FP-[A-Za-z0-9][A-Za-z0-9._-]*",
500
+ "serves": r"NS-[A-Za-z0-9][A-Za-z0-9._-]*",
501
+ "touches": r"HC-[A-Za-z0-9][A-Za-z0-9._-]*",
502
+ }
503
+ for field, pattern in patterns.items():
504
+ values = value.get(field)
505
+ if not isinstance(values, list) or not all(
506
+ isinstance(item, str) and re.fullmatch(pattern, item) for item in values
507
+ ) or len(values) != len(set(values)):
508
+ return None
509
+ result[field] = values
510
+ return result
511
+
512
+
513
+ def _terminate_and_reap(process):
514
+ if process.poll() is None:
515
+ process.terminate()
516
+ try:
517
+ process.wait(timeout=0.25)
518
+ except subprocess.TimeoutExpired:
519
+ process.kill()
520
+ process.wait()
521
+ else:
522
+ process.wait()
523
+
524
+
525
+ def _bounded_process_output(argv, input_bytes: bytes, timeout: float, max_output_bytes: int):
526
+ try:
527
+ process = subprocess.Popen(
528
+ argv,
529
+ stdin=subprocess.PIPE,
530
+ stdout=subprocess.PIPE,
531
+ stderr=subprocess.PIPE,
532
+ bufsize=0,
533
+ )
534
+ except OSError as error:
535
+ return None, None, None, f"canonical North Star validator could not run: {error}"
536
+
537
+ selector = selectors.DefaultSelector()
538
+ buffers = {"stdout": bytearray(), "stderr": bytearray()}
539
+ input_view = memoryview(input_bytes)
540
+ input_offset = 0
541
+ combined_output = 0
542
+ failure = None
543
+ returncode = None
544
+
545
+ def stop_watching(stream):
546
+ try:
547
+ selector.unregister(stream)
548
+ except (KeyError, ValueError):
549
+ pass
550
+ try:
551
+ stream.close()
552
+ except OSError:
553
+ pass
554
+
555
+ try:
556
+ for stream, role, events in [
557
+ (process.stdout, "stdout", selectors.EVENT_READ),
558
+ (process.stderr, "stderr", selectors.EVENT_READ),
559
+ (process.stdin, "stdin", selectors.EVENT_WRITE),
560
+ ]:
561
+ os.set_blocking(stream.fileno(), False)
562
+ selector.register(stream, events, role)
563
+ if not input_bytes:
564
+ stop_watching(process.stdin)
565
+
566
+ deadline = time.monotonic() + timeout
567
+ while selector.get_map():
568
+ remaining = deadline - time.monotonic()
569
+ if remaining <= 0:
570
+ failure = "canonical North Star validator timed out"
571
+ break
572
+ events = selector.select(min(0.05, remaining))
573
+ for key, _mask in events:
574
+ stream = key.fileobj
575
+ role = key.data
576
+ if role == "stdin":
577
+ try:
578
+ written = os.write(
579
+ stream.fileno(),
580
+ input_view[input_offset:input_offset + READ_CHUNK_BYTES],
581
+ )
582
+ except BlockingIOError:
583
+ continue
584
+ except BrokenPipeError:
585
+ stop_watching(stream)
586
+ continue
587
+ input_offset += written
588
+ if input_offset == len(input_view):
589
+ stop_watching(stream)
590
+ continue
591
+
592
+ try:
593
+ chunk = os.read(stream.fileno(), READ_CHUNK_BYTES)
594
+ except BlockingIOError:
595
+ continue
596
+ if not chunk:
597
+ stop_watching(stream)
598
+ continue
599
+ combined_output += len(chunk)
600
+ if combined_output > max_output_bytes:
601
+ failure = (
602
+ "canonical North Star validator output exceeded its bounded "
603
+ "combined report limit"
604
+ )
605
+ break
606
+ buffers[role].extend(chunk)
607
+ if failure:
608
+ break
609
+
610
+ if failure:
611
+ _terminate_and_reap(process)
612
+ else:
613
+ remaining = max(0.0, deadline - time.monotonic())
614
+ try:
615
+ returncode = process.wait(timeout=remaining)
616
+ except subprocess.TimeoutExpired:
617
+ failure = "canonical North Star validator timed out"
618
+ _terminate_and_reap(process)
619
+ finally:
620
+ selector.close()
621
+ for stream in (process.stdin, process.stdout, process.stderr):
622
+ if stream is not None and not stream.closed:
623
+ try:
624
+ stream.close()
625
+ except OSError:
626
+ pass
627
+ if process.poll() is None:
628
+ _terminate_and_reap(process)
629
+
630
+ return returncode, bytes(buffers["stdout"]), bytes(buffers["stderr"]), failure
631
+
632
+
633
+ def validate_north_star_snapshot(repo_root: Path, snapshot):
634
+ node = shutil.which("node")
635
+ if node is None:
636
+ return None, "canonical North Star validation requires the installed node runtime"
637
+ validator = (
638
+ Path(__file__).resolve().parents[2]
639
+ / "ultra-research"
640
+ / "scripts"
641
+ / "validate_north_star.cjs"
642
+ )
643
+ if not validator.is_file():
644
+ return None, f"canonical North Star validator is missing: {validator}"
645
+ canonical_path = repo_root.resolve() / ".ultra" / "north-star.md"
646
+ returncode, stdout, stderr, process_error = _bounded_process_output(
647
+ [node, str(validator), "--stdin", "--path", str(canonical_path)],
648
+ snapshot["bytes"],
649
+ VALIDATOR_TIMEOUT_SECONDS,
650
+ MAX_VALIDATOR_REPORT_BYTES,
651
+ )
652
+ if process_error:
653
+ return None, process_error
654
+ report, report_error = decode_json_bytes(
655
+ stdout,
656
+ "canonical North Star validator report",
657
+ )
658
+ if report_error:
659
+ detail = stderr.decode("utf-8", errors="replace").strip()
660
+ return None, f"{report_error}{': ' + detail if detail else ''}"
661
+ input_receipt = report.get("input")
662
+ expected_input = {
663
+ "path": str(canonical_path),
664
+ "byte_length": len(snapshot["bytes"]),
665
+ "sha256": snapshot["sha256"],
666
+ }
667
+ if input_receipt != expected_input:
668
+ return None, (
669
+ "canonical North Star validator report is not bound to the exact stdin "
670
+ "snapshot bytes and canonical path"
671
+ )
672
+ binding = report.get("acceptance_binding")
673
+ source_observations = report.get("source_observations")
674
+ expected_sources = None
675
+ if isinstance(binding, dict):
676
+ source = binding.get("source")
677
+ accepted_snapshot = binding.get("snapshot")
678
+ if isinstance(source, str) and "#" in source and isinstance(accepted_snapshot, str):
679
+ expected_sources = [
680
+ ("decision", source.split("#", 1)[0]),
681
+ ("snapshot", accepted_snapshot),
682
+ ]
683
+ if expected_sources is None or not isinstance(source_observations, list):
684
+ return None, "canonical North Star validator report lacks exact source observations"
685
+ observed_sources = []
686
+ for index, observation in enumerate(source_observations):
687
+ if (
688
+ not isinstance(observation, dict)
689
+ or set(observation) != NORTH_STAR_SOURCE_OBSERVATION_FIELDS
690
+ or observation.get("role") not in {"decision", "snapshot"}
691
+ or not digest_string(observation.get("sha256"))
692
+ or not isinstance(observation.get("byte_length"), int)
693
+ or isinstance(observation.get("byte_length"), bool)
694
+ or observation["byte_length"] < 0
695
+ ):
696
+ return None, (
697
+ "canonical North Star validator report has an invalid "
698
+ f"source_observations[{index}] receipt"
699
+ )
700
+ normalized, path_error = validate_repo_path(
701
+ observation.get("path"),
702
+ repo_root,
703
+ f"canonical North Star validator source_observations[{index}].path",
704
+ )
705
+ if path_error:
706
+ return None, path_error
707
+ observed_sources.append((observation["role"], normalized))
708
+ if observed_sources != expected_sources:
709
+ return None, (
710
+ "canonical North Star validator report source observations do not exactly "
711
+ "match its decision and accepted snapshot binding"
712
+ )
713
+ if returncode != 0 or report.get("valid") is not True:
714
+ codes = [
715
+ item.get("code")
716
+ for item in report.get("diagnostics", [])
717
+ if isinstance(item, dict) and nonempty_string(item.get("code"))
718
+ ]
719
+ return None, (
720
+ "canonical North Star validator rejected the current snapshot"
721
+ + (f": {', '.join(codes)}" if codes else "")
722
+ )
723
+ return {
724
+ "data": report,
725
+ "sha256": hashlib.sha256(stdout).hexdigest(),
726
+ }, None
727
+
728
+
729
+ def current_git_head(repo_root: Path):
730
+ try:
731
+ completed = subprocess.run(
732
+ ["git", "-C", str(repo_root), "rev-parse", "--verify", "HEAD"],
733
+ stdout=subprocess.PIPE,
734
+ stderr=subprocess.PIPE,
735
+ timeout=VALIDATOR_TIMEOUT_SECONDS,
736
+ check=False,
737
+ )
738
+ except subprocess.TimeoutExpired:
739
+ return None, "current Git HEAD observation timed out"
740
+ except OSError as error:
741
+ return None, f"current Git HEAD cannot be observed: {error}"
742
+ if completed.returncode != 0:
743
+ detail = completed.stderr.decode("utf-8", errors="replace").strip()
744
+ return None, "current Git HEAD cannot be observed" + (f": {detail}" if detail else "")
745
+ try:
746
+ head = completed.stdout.decode("ascii").strip()
747
+ except UnicodeDecodeError:
748
+ return None, "current Git HEAD is not ASCII"
749
+ if not git_blob_digest_string(head):
750
+ return None, f"current Git HEAD is not a lowercase 40-hex commit digest: {head!r}"
751
+ return head, None
752
+
753
+
754
+ def acceptance_source_paths(packet):
755
+ return sorted(
756
+ {
757
+ acceptance.partition(": ")[0].split("#", 1)[0]
758
+ for acceptance in packet["acceptance"]
759
+ },
760
+ key=lambda value: value.encode("utf-8"),
761
+ )
762
+
763
+
764
+ def trace_source_path(packet):
765
+ return packet["north_star_trace"]["path"].split("#", 1)[0]
766
+
767
+
768
+ def validate_packet_subject_observations(packet, repo_root: Path):
769
+ observations = packet.get("subject_observations")
770
+ acceptance_paths = acceptance_source_paths(packet)
771
+ expected_count = 1 + len(acceptance_paths) + 2
772
+ if not isinstance(observations, list) or len(observations) != expected_count:
773
+ return (
774
+ "WORKER-PACKET.json subject_observations must be the exact ordered "
775
+ "Change, acceptance-source, decision, and snapshot observation array"
776
+ )
777
+
778
+ expected_prefix = [
779
+ ("change", trace_source_path(packet)),
780
+ *(("acceptance_source", path) for path in acceptance_paths),
781
+ ]
782
+ expected_roles = [
783
+ *(role for role, _path in expected_prefix),
784
+ "decision",
785
+ "snapshot",
786
+ ]
787
+ observed_roles_and_paths = []
788
+ for index, observation in enumerate(observations):
789
+ if (
790
+ not isinstance(observation, dict)
791
+ or set(observation) != PACKET_SUBJECT_OBSERVATION_FIELDS
792
+ ):
793
+ return (
794
+ f"WORKER-PACKET.json subject_observations[{index}] must contain "
795
+ "exactly role, path, sha256, and byte_length"
796
+ )
797
+ if observation.get("role") != expected_roles[index]:
798
+ return (
799
+ "WORKER-PACKET.json subject_observations must use exact ordered roles: "
800
+ + ", ".join(expected_roles)
801
+ )
802
+ normalized, path_error = validate_repo_path(
803
+ observation.get("path"),
804
+ repo_root,
805
+ f"WORKER-PACKET.json subject_observations[{index}].path",
806
+ )
807
+ if path_error:
808
+ return path_error
809
+ if not digest_string(observation.get("sha256")):
810
+ return (
811
+ f"WORKER-PACKET.json subject_observations[{index}].sha256 must be a "
812
+ "lowercase SHA-256 digest"
813
+ )
814
+ byte_length = observation.get("byte_length")
815
+ if (
816
+ not isinstance(byte_length, int)
817
+ or isinstance(byte_length, bool)
818
+ or byte_length < 0
819
+ ):
820
+ return (
821
+ f"WORKER-PACKET.json subject_observations[{index}].byte_length must "
822
+ "be a non-negative integer"
823
+ )
824
+ observed_roles_and_paths.append((observation["role"], normalized))
825
+
826
+ if observed_roles_and_paths[:len(expected_prefix)] != expected_prefix:
827
+ return (
828
+ "WORKER-PACKET.json subject_observations must bind the exact validated "
829
+ "Change path and ordered acceptance sources"
830
+ )
831
+ return None
832
+
833
+
834
+ def canonical_admission_subject(value):
835
+ return {
836
+ "version": value["version"],
837
+ "session": value["session"],
838
+ "packet_digest": value["packet_digest"],
839
+ "head": value["head"],
840
+ "observations": value["observations"],
841
+ "north_star_report": value["north_star_report"],
842
+ }
843
+
844
+
845
+ def canonical_json_digest(value):
846
+ payload = json.dumps(
847
+ value,
848
+ ensure_ascii=False,
849
+ separators=(",", ":"),
850
+ sort_keys=True,
851
+ ).encode("utf-8")
852
+ return hashlib.sha256(payload).hexdigest()
853
+
854
+
855
+ def validate_packet_admission(packet, repo_root: Path, packet_digest: str):
856
+ observed_head, error = current_git_head(repo_root)
857
+ if error:
858
+ return None, error
859
+ if observed_head != packet["head"]:
860
+ return None, (
861
+ f"WORKER-PACKET.json head does not match current Git HEAD {observed_head}; "
862
+ "create a new review session for the current subject"
863
+ )
864
+
865
+ snapshots = {}
866
+
867
+ def subject_snapshot(repo_path, label):
868
+ if repo_path not in snapshots:
869
+ snapshot, snapshot_error, _missing = stable_read_repo_bytes(
870
+ repo_root,
871
+ repo_path,
872
+ label,
873
+ )
874
+ if snapshot_error:
875
+ return None, snapshot_error
876
+ snapshots[repo_path] = snapshot
877
+ return snapshots[repo_path], None
878
+
879
+ observations = []
880
+ for index, context_file in enumerate(packet["context_files"]):
881
+ snapshot, error = subject_snapshot(
882
+ context_file["path"],
883
+ f"WORKER-PACKET.json context_files[{index}] subject",
884
+ )
885
+ if error:
886
+ return None, error
887
+ observed = snapshot["sha256"]
888
+ if observed != context_file["sha256"]:
889
+ return None, (
890
+ f"WORKER-PACKET.json context_files[{index}] subject bytes do not match "
891
+ f"recorded sha256 {context_file['sha256']}; observed {observed}"
892
+ )
893
+ observations.append({
894
+ "role": "context",
895
+ "path": context_file["path"],
896
+ "sha256": observed,
897
+ "byte_length": len(snapshot["bytes"]),
898
+ })
899
+
900
+ north_star_path = ".ultra/north-star.md"
901
+ north_star, error = subject_snapshot(
902
+ north_star_path,
903
+ "WORKER-PACKET.json canonical North Star subject",
904
+ )
905
+ if error:
906
+ return None, error
907
+ validated_report, error = validate_north_star_snapshot(repo_root, north_star)
908
+ if error:
909
+ return None, error
910
+ report = validated_report["data"]
911
+ if (
912
+ report.get("$schema") != "ultra-north-star-validation-v1"
913
+ or report.get("kind") != "north-star-v2"
914
+ or report.get("status") != "accepted"
915
+ or report.get("classification") != "accepted"
916
+ ):
917
+ return None, "canonical North Star validator report must describe an accepted v2 authority"
918
+
919
+ trace = packet["north_star_trace"]
920
+ if report.get("revision") != trace["north_star_revision"]:
921
+ return None, (
922
+ "north_star_trace.north_star_revision does not match the canonical "
923
+ f"validator report; observed {report.get('revision')!r}"
924
+ )
925
+ binding = report.get("acceptance_binding")
926
+ if not isinstance(binding, dict):
927
+ return None, "canonical North Star validator report lacks acceptance binding evidence"
928
+ if binding.get("git_blob_digest") != trace["north_star_digest"]:
929
+ return None, (
930
+ "north_star_trace.north_star_digest does not match the canonical validator "
931
+ f"report; observed {binding.get('git_blob_digest')!r}"
932
+ )
933
+ if binding.get("content_sha256") != north_star["sha256"]:
934
+ return None, "canonical North Star validator report is not bound to the stable input bytes"
935
+
936
+ ids = report.get("ids")
937
+ if not isinstance(ids, dict) or set(ids) != {"FP", "NS", "HC"}:
938
+ return None, "canonical North Star validator report has an invalid ID inventory"
939
+ for field, kind in [
940
+ ("first_principles", "FP"),
941
+ ("serves", "NS"),
942
+ ("touches", "HC"),
943
+ ]:
944
+ available = ids.get(kind)
945
+ if not isinstance(available, list) or not all(nonempty_string(item) for item in available):
946
+ return None, f"canonical North Star validator report has invalid {kind} IDs"
947
+ unresolved = [item for item in trace[field] if item not in set(available)]
948
+ if unresolved:
949
+ return None, (
950
+ f"north_star_trace.{field} contains IDs unresolved by the canonical "
951
+ f"validator report: {', '.join(unresolved)}"
952
+ )
953
+
954
+ observations.append({
955
+ "role": "north_star",
956
+ "path": north_star_path,
957
+ "sha256": north_star["sha256"],
958
+ "byte_length": len(north_star["bytes"]),
959
+ })
960
+
961
+ packet_subjects = packet["subject_observations"]
962
+ canonical_sources = packet_subjects[-2:]
963
+ if report.get("source_observations") != canonical_sources:
964
+ return None, (
965
+ "canonical North Star validator decision and snapshot observations do not "
966
+ "match WORKER-PACKET.json subject_observations"
967
+ )
968
+ for index, expected_observation in enumerate(packet_subjects):
969
+ source, error = subject_snapshot(
970
+ expected_observation["path"],
971
+ f"WORKER-PACKET.json subject_observations[{index}] subject",
972
+ )
973
+ if error:
974
+ return None, error
975
+ observed_length = len(source["bytes"])
976
+ if (
977
+ source["sha256"] != expected_observation["sha256"]
978
+ or observed_length != expected_observation["byte_length"]
979
+ ):
980
+ return None, (
981
+ f"WORKER-PACKET.json subject_observations[{index}] bytes do not match "
982
+ f"recorded byte_length and sha256 for {expected_observation['path']}"
983
+ )
984
+ if expected_observation["role"] in {"change", "acceptance_source"}:
985
+ observations.append(dict(expected_observation))
986
+
987
+ final_head, error = current_git_head(repo_root)
988
+ if error:
989
+ return None, error
990
+ if final_head != observed_head:
991
+ return None, "current Git HEAD changed during packet admission; retry with a new session"
992
+ for repo_path, expected in snapshots.items():
993
+ current, snapshot_error, _missing = stable_read_repo_bytes(
994
+ repo_root,
995
+ repo_path,
996
+ f"WORKER-PACKET.json admitted subject {repo_path}",
997
+ )
998
+ if snapshot_error:
999
+ return None, snapshot_error
1000
+ if (
1001
+ current["bytes"] != expected["bytes"]
1002
+ or len(current["bytes"]) != len(expected["bytes"])
1003
+ or current["sha256"] != expected["sha256"]
1004
+ ):
1005
+ return None, (
1006
+ f"admitted subject {repo_path} bytes, byte length, or sha256 changed "
1007
+ "during packet admission"
1008
+ )
1009
+
1010
+ report_receipt = {
1011
+ "schema": report["$schema"],
1012
+ "sha256": validated_report["sha256"],
1013
+ "input_sha256": north_star["sha256"],
1014
+ "input_byte_length": len(north_star["bytes"]),
1015
+ "source_observations": report["source_observations"],
1016
+ }
1017
+ admission = {
1018
+ "$schema": ADMISSION_SCHEMA_V2,
1019
+ "version": 2,
1020
+ "session": packet["session"],
1021
+ "packet_digest": packet_digest,
1022
+ "head": packet["head"],
1023
+ "observations": observations,
1024
+ "north_star_report": report_receipt,
1025
+ }
1026
+ admission["subject_digest"] = canonical_json_digest(
1027
+ canonical_admission_subject(admission)
1028
+ )
1029
+ return admission, None
1030
+
1031
+
1032
+ def cleanup_admission_temporary(
1033
+ session_descriptor: int,
1034
+ temporary: str,
1035
+ publication_descriptor: int,
1036
+ ):
1037
+ """Remove only the temporary pathname that still names our open inode."""
1038
+ filename = os.path.basename(os.fspath(temporary))
1039
+ try:
1040
+ current = os.lstat(filename, dir_fd=session_descriptor)
1041
+ owned = os.fstat(publication_descriptor)
1042
+ except FileNotFoundError:
1043
+ return True, None
1044
+ except OSError as error:
1045
+ return False, error
1046
+ if (
1047
+ not stat.S_ISREG(current.st_mode)
1048
+ or _stat_identity(current) != _stat_identity(owned)
1049
+ ):
1050
+ return False, (
1051
+ "temporary pathname identity differs from the owned admission file; "
1052
+ "the replacement was retained for manual recovery"
1053
+ )
1054
+ try:
1055
+ os.unlink(filename, dir_fd=session_descriptor)
1056
+ except OSError as error:
1057
+ return False, error
1058
+ return True, None
1059
+
1060
+
1061
+ def atomic_write_admission(session_path: Path, admission):
1062
+ target = session_path / "ADMISSION.json"
1063
+ payload = (json.dumps(admission, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
1064
+ payload_digest = hashlib.sha256(payload).hexdigest()
1065
+ descriptor = None
1066
+ session_descriptor = None
1067
+ publication_descriptor = None
1068
+ temporary = None
1069
+ published = False
1070
+ publication = "created"
1071
+ result_digest = payload_digest
1072
+ durability_warnings = []
1073
+ temporary_cleanup_error = None
1074
+ try:
1075
+ session_flags = os.O_RDONLY
1076
+ session_flags |= getattr(os, "O_DIRECTORY", 0)
1077
+ session_flags |= getattr(os, "O_NOFOLLOW", 0)
1078
+ session_path_identity = os.stat(session_path, follow_symlinks=False)
1079
+ session_descriptor = os.open(session_path, session_flags)
1080
+ session_fd_identity = os.fstat(session_descriptor)
1081
+ if (
1082
+ not stat.S_ISDIR(session_path_identity.st_mode)
1083
+ or _component_identity(session_path_identity)
1084
+ != _component_identity(session_fd_identity)
1085
+ ):
1086
+ return None, {
1087
+ "code": "admission_publish_failed",
1088
+ "message": "review session changed before ADMISSION.json publication",
1089
+ }
1090
+ descriptor, temporary = tempfile.mkstemp(prefix=".ADMISSION.", dir=session_path)
1091
+ with os.fdopen(descriptor, "wb", closefd=True) as stream:
1092
+ descriptor = None
1093
+ stream.write(payload)
1094
+ stream.flush()
1095
+ os.fsync(stream.fileno())
1096
+ publication_flags = os.O_RDONLY
1097
+ publication_flags |= getattr(os, "O_NONBLOCK", 0)
1098
+ publication_flags |= getattr(os, "O_NOFOLLOW", 0)
1099
+ publication_descriptor = os.open(temporary, publication_flags)
1100
+ try:
1101
+ # A same-directory hard link is an atomic create-if-absent publication.
1102
+ # Unlike replace/rename, it can never overwrite an admitted session.
1103
+ os.link(temporary, target, follow_symlinks=False)
1104
+ published = True
1105
+ temporary_identity = _stat_identity(os.fstat(publication_descriptor))
1106
+ repo_root, root_error = canonical_repo_root(session_path)
1107
+ if root_error:
1108
+ return None, {
1109
+ "code": "admission_publication_changed",
1110
+ "message": (
1111
+ "ADMISSION.json publication cannot be verified from the canonical "
1112
+ f"session path: {root_error}"
1113
+ ),
1114
+ }
1115
+ published_snapshot, published_error, _missing = stable_read_repo_bytes(
1116
+ repo_root,
1117
+ f".ultra/reviews/{session_path.resolve().name}/ADMISSION.json",
1118
+ "published ADMISSION.json",
1119
+ )
1120
+ if (
1121
+ published_error
1122
+ or published_snapshot["identity"] != temporary_identity
1123
+ or published_snapshot["bytes"] != payload
1124
+ ):
1125
+ detail = published_error or (
1126
+ "the canonical receipt identity or bytes differ from the newly "
1127
+ "published file"
1128
+ )
1129
+ return None, {
1130
+ "code": "admission_publication_changed",
1131
+ "message": (
1132
+ "ADMISSION.json canonical session or receipt changed during its "
1133
+ f"post-publication fresh rewalk: {detail}"
1134
+ ),
1135
+ }
1136
+ except FileExistsError:
1137
+ repo_root, root_error = canonical_repo_root(session_path)
1138
+ if root_error:
1139
+ return None, {
1140
+ "code": "admission_conflict",
1141
+ "message": f"existing ADMISSION.json cannot be verified: {root_error}",
1142
+ }
1143
+ existing, existing_error, _missing = stable_read_repo_bytes(
1144
+ repo_root,
1145
+ f".ultra/reviews/{session_path.resolve().name}/ADMISSION.json",
1146
+ "existing ADMISSION.json",
1147
+ )
1148
+ if existing_error or existing["bytes"] != payload:
1149
+ detail = existing_error or "its bytes differ from the newly observed receipt"
1150
+ return None, {
1151
+ "code": "admission_conflict",
1152
+ "message": (
1153
+ "ADMISSION.json is immutable once published and the existing "
1154
+ f"receipt cannot be reused because {detail}; start a fresh review session"
1155
+ ),
1156
+ }
1157
+ published = True
1158
+ publication = "existing"
1159
+ result_digest = existing["sha256"]
1160
+ except OSError as error:
1161
+ return None, {
1162
+ "code": "admission_publish_failed",
1163
+ "message": f"ADMISSION.json could not be published atomically: {error}",
1164
+ }
1165
+
1166
+ removed, cleanup_error = cleanup_admission_temporary(
1167
+ session_descriptor,
1168
+ temporary,
1169
+ publication_descriptor,
1170
+ )
1171
+ if removed:
1172
+ temporary = None
1173
+ else:
1174
+ temporary_cleanup_error = cleanup_error
1175
+
1176
+ try:
1177
+ os.fsync(session_descriptor)
1178
+ except OSError as error:
1179
+ durability_warnings.append({
1180
+ "code": "admission_directory_fsync_failed",
1181
+ "message": (
1182
+ "ADMISSION.json is published and usable, but directory durability "
1183
+ f"could not be confirmed: {error}"
1184
+ ),
1185
+ })
1186
+ except OSError as error:
1187
+ if published:
1188
+ if not any(
1189
+ warning["code"] == "admission_directory_fsync_failed"
1190
+ for warning in durability_warnings
1191
+ ):
1192
+ durability_warnings.append({
1193
+ "code": "admission_directory_fsync_failed",
1194
+ "message": (
1195
+ "ADMISSION.json is published and usable, but directory durability "
1196
+ f"could not be confirmed: {error}"
1197
+ ),
1198
+ })
1199
+ else:
1200
+ return None, {
1201
+ "code": "admission_publish_failed",
1202
+ "message": f"ADMISSION.json could not be published atomically: {error}",
1203
+ }
1204
+ finally:
1205
+ if descriptor is not None:
1206
+ os.close(descriptor)
1207
+ if (
1208
+ temporary is not None
1209
+ and session_descriptor is not None
1210
+ and publication_descriptor is not None
1211
+ ):
1212
+ removed, cleanup_error = cleanup_admission_temporary(
1213
+ session_descriptor,
1214
+ temporary,
1215
+ publication_descriptor,
1216
+ )
1217
+ if removed:
1218
+ temporary = None
1219
+ temporary_cleanup_error = None
1220
+ else:
1221
+ temporary_cleanup_error = cleanup_error
1222
+ if publication_descriptor is not None:
1223
+ os.close(publication_descriptor)
1224
+ if session_descriptor is not None:
1225
+ os.close(session_descriptor)
1226
+ if temporary is not None and temporary_cleanup_error is not None:
1227
+ durability_warnings.append({
1228
+ "code": "admission_temp_cleanup_failed",
1229
+ "message": (
1230
+ "ADMISSION.json is published and usable, but its temporary pathname "
1231
+ f"could not be removed: {temporary_cleanup_error}"
1232
+ ),
1233
+ })
1234
+ result = {
1235
+ "digest": result_digest,
1236
+ "publication": publication,
1237
+ }
1238
+ if len(durability_warnings) == 1:
1239
+ result["durability_warning"] = durability_warnings[0]
1240
+ elif durability_warnings:
1241
+ result["durability_warning"] = {
1242
+ "code": "admission_multiple_durability_warnings",
1243
+ "message": (
1244
+ "ADMISSION.json is published and usable, but multiple durability "
1245
+ "operations remain unresolved"
1246
+ ),
1247
+ "warnings": durability_warnings,
1248
+ }
1249
+ return result, None
1250
+
1251
+
1252
+ def validate_admission_receipt(receipt, packet):
1253
+ if not isinstance(receipt, dict) or set(receipt) != ADMISSION_FIELDS:
1254
+ return "ADMISSION.json must contain exactly the strict admission receipt fields"
1255
+ contract = packet["data"].get("admission_contract")
1256
+ if contract == ADMISSION_CONTRACT_V1:
1257
+ expected_schema = ADMISSION_SCHEMA_V1
1258
+ expected_version = 1
1259
+ observation_fields = ADMISSION_OBSERVATION_FIELDS_V1
1260
+ elif contract == ADMISSION_CONTRACT_V2:
1261
+ expected_schema = ADMISSION_SCHEMA_V2
1262
+ expected_version = 2
1263
+ observation_fields = ADMISSION_OBSERVATION_FIELDS_V2
1264
+ else:
1265
+ return "ADMISSION.json requires a recognized strict packet admission_contract"
1266
+ if (
1267
+ receipt.get("$schema") != expected_schema
1268
+ or receipt.get("version") != expected_version
1269
+ ):
1270
+ return (
1271
+ f"ADMISSION.json must use {expected_schema} version {expected_version} "
1272
+ "for its packet admission_contract"
1273
+ )
1274
+ if receipt.get("session") != packet["data"]["session"]:
1275
+ return "ADMISSION.json session does not match WORKER-PACKET.json"
1276
+ if receipt.get("packet_digest") != packet["digest"]:
1277
+ return "ADMISSION.json packet_digest does not match WORKER-PACKET.json"
1278
+ if receipt.get("head") != packet["data"]["head"]:
1279
+ return "ADMISSION.json head does not match WORKER-PACKET.json"
1280
+ if not digest_string(receipt.get("subject_digest")):
1281
+ return "ADMISSION.json subject_digest must be a lowercase SHA-256 digest"
1282
+
1283
+ observations = receipt.get("observations")
1284
+ if not isinstance(observations, list):
1285
+ return "ADMISSION.json observations must be an array"
1286
+ for index, observation in enumerate(observations):
1287
+ if not isinstance(observation, dict) or set(observation) != observation_fields:
1288
+ return f"ADMISSION.json observations[{index}] has invalid fields"
1289
+ if observation.get("role") not in {
1290
+ "context", "north_star", "change", "acceptance_source",
1291
+ }:
1292
+ return f"ADMISSION.json observations[{index}] has an invalid role"
1293
+ _, path_error = validate_repo_path(
1294
+ observation.get("path"),
1295
+ packet["repo_root"],
1296
+ f"ADMISSION.json observations[{index}].path",
1297
+ )
1298
+ if path_error:
1299
+ return path_error
1300
+ if not digest_string(observation.get("sha256")):
1301
+ return f"ADMISSION.json observations[{index}].sha256 is invalid"
1302
+ if expected_version == 2:
1303
+ byte_length = observation.get("byte_length")
1304
+ if (
1305
+ not isinstance(byte_length, int)
1306
+ or isinstance(byte_length, bool)
1307
+ or byte_length < 0
1308
+ ):
1309
+ return f"ADMISSION.json observations[{index}].byte_length is invalid"
1310
+
1311
+ subject_projection = (
1312
+ packet["data"]["subject_observations"][:-2]
1313
+ if expected_version == 2
1314
+ else [
1315
+ {
1316
+ "role": "change",
1317
+ "path": f".ultra/changes/active/{packet['data']['change_id']}/intent.md",
1318
+ },
1319
+ *(
1320
+ {"role": "acceptance_source", "path": path}
1321
+ for path in acceptance_source_paths(packet["data"])
1322
+ ),
1323
+ ]
1324
+ )
1325
+ expected_roles_and_paths = [
1326
+ *(('context', item["path"]) for item in packet["data"]["context_files"]),
1327
+ ("north_star", ".ultra/north-star.md"),
1328
+ *((item["role"], item["path"]) for item in subject_projection),
1329
+ ]
1330
+ observed_roles_and_paths = [
1331
+ (item["role"], item["path"]) for item in observations
1332
+ ]
1333
+ if observed_roles_and_paths != expected_roles_and_paths:
1334
+ return "ADMISSION.json observations are not the exact packet-derived subject projection"
1335
+ for index, context_file in enumerate(packet["data"]["context_files"]):
1336
+ if observations[index]["sha256"] != context_file["sha256"]:
1337
+ return "ADMISSION.json context observation does not match WORKER-PACKET.json"
1338
+ if expected_version == 2:
1339
+ subject_offset = len(packet["data"]["context_files"]) + 1
1340
+ if observations[subject_offset:] != subject_projection:
1341
+ return (
1342
+ "ADMISSION.json Change and acceptance observations do not exactly "
1343
+ "match WORKER-PACKET.json subject_observations"
1344
+ )
1345
+
1346
+ report_receipt = receipt.get("north_star_report")
1347
+ if (
1348
+ not isinstance(report_receipt, dict)
1349
+ or set(report_receipt) != NORTH_STAR_REPORT_RECEIPT_FIELDS
1350
+ or report_receipt.get("schema") != "ultra-north-star-validation-v1"
1351
+ or not digest_string(report_receipt.get("sha256"))
1352
+ or not digest_string(report_receipt.get("input_sha256"))
1353
+ or not isinstance(report_receipt.get("input_byte_length"), int)
1354
+ or isinstance(report_receipt.get("input_byte_length"), bool)
1355
+ or report_receipt["input_byte_length"] < 0
1356
+ ):
1357
+ return "ADMISSION.json north_star_report receipt is invalid"
1358
+ source_observations = report_receipt.get("source_observations")
1359
+ if not isinstance(source_observations, list) or len(source_observations) != 2:
1360
+ return "ADMISSION.json north_star_report source observations are invalid"
1361
+ observed_source_roles = []
1362
+ for index, observation in enumerate(source_observations):
1363
+ if (
1364
+ not isinstance(observation, dict)
1365
+ or set(observation) != NORTH_STAR_SOURCE_OBSERVATION_FIELDS
1366
+ or observation.get("role") not in {"decision", "snapshot"}
1367
+ or not digest_string(observation.get("sha256"))
1368
+ or not isinstance(observation.get("byte_length"), int)
1369
+ or isinstance(observation.get("byte_length"), bool)
1370
+ or observation["byte_length"] < 0
1371
+ ):
1372
+ return (
1373
+ "ADMISSION.json north_star_report "
1374
+ f"source_observations[{index}] is invalid"
1375
+ )
1376
+ _, path_error = validate_repo_path(
1377
+ observation.get("path"),
1378
+ packet["repo_root"],
1379
+ f"ADMISSION.json north_star_report source_observations[{index}].path",
1380
+ )
1381
+ if path_error:
1382
+ return path_error
1383
+ observed_source_roles.append(observation["role"])
1384
+ if observed_source_roles != ["decision", "snapshot"]:
1385
+ return (
1386
+ "ADMISSION.json north_star_report source observations must be the exact "
1387
+ "decision then snapshot pair"
1388
+ )
1389
+ if (
1390
+ expected_version == 2
1391
+ and source_observations != packet["data"]["subject_observations"][-2:]
1392
+ ):
1393
+ return (
1394
+ "ADMISSION.json canonical report decision and snapshot observations do not "
1395
+ "exactly match WORKER-PACKET.json subject_observations"
1396
+ )
1397
+ north_star_observation = next(
1398
+ item for item in observations if item["role"] == "north_star"
1399
+ )
1400
+ if report_receipt["input_sha256"] != north_star_observation["sha256"]:
1401
+ return "ADMISSION.json canonical report input does not match its North Star observation"
1402
+ expected_subject_digest = canonical_json_digest(canonical_admission_subject(receipt))
1403
+ if receipt["subject_digest"] != expected_subject_digest:
1404
+ return "ADMISSION.json subject_digest does not match its exact admission observations"
1405
+ return None
1406
+
1407
+
1408
+ def load_admission_receipt(session_path: Path, packet):
1409
+ admission_repo_path = (
1410
+ f".ultra/reviews/{packet['data']['session']}/ADMISSION.json"
1411
+ )
1412
+ receipt, snapshot, error, _missing = stable_read_repo_json(
1413
+ packet["repo_root"],
1414
+ admission_repo_path,
1415
+ "ADMISSION.json",
1416
+ )
1417
+ if error:
1418
+ return None, error
1419
+ error = validate_admission_receipt(receipt, packet)
1420
+ if error:
1421
+ return None, error
1422
+ return {"data": receipt, "digest": snapshot["sha256"]}, None
1423
+
1424
+
1425
+ def _foreign_review_references(value, own_session: str):
1426
+ """Collect every foreign review-session path carried anywhere in a value."""
1427
+ found = []
1428
+ if isinstance(value, str):
1429
+ for match in FOREIGN_REVIEW_SESSION.finditer(value):
1430
+ if match.group(1) != own_session:
1431
+ found.append(match.group(0))
1432
+ elif isinstance(value, dict):
1433
+ for key, item in value.items():
1434
+ found.extend(_foreign_review_references(key, own_session))
1435
+ found.extend(_foreign_review_references(item, own_session))
1436
+ elif isinstance(value, list):
1437
+ for item in value:
1438
+ found.extend(_foreign_review_references(item, own_session))
1439
+ return found
1440
+
1441
+
1442
+ def validate_packet_review_history(packet, repo_root: Path) -> str | None:
1443
+ """Bound packet review history to one fully validated direct parent.
1444
+
1445
+ The only foreign review path allowed anywhere in a task delta packet is the
1446
+ exact `review_history.parent_summary_ref` field; an initial packet carries
1447
+ no foreign review reference at all. Admission then observes the parent
1448
+ SUMMARY as a bounded stable repository regular file, requires its exact
1449
+ recorded digest, requires the parent to bind the same mode, change, and
1450
+ tasks as the current packet, and accepts only still-unresolved current
1451
+ P0/P1 blocker ids that exist in that parent. Transitive summary chains and
1452
+ full-history replays stay out of new packets; the archive under
1453
+ `.ultra/reviews/**` is read only for a dedicated incident or owner audit.
1454
+ """
1455
+ own_session = packet.get("session")
1456
+ history = packet.get("review_history")
1457
+ if history is not None and (
1458
+ not isinstance(history, dict) or set(history) != REVIEW_HISTORY_FIELDS
1459
+ ):
1460
+ return (
1461
+ "WORKER-PACKET.json review_history must be exactly one direct parent "
1462
+ "{parent_summary_ref, parent_summary_digest, unresolved_blocking_ids}"
1463
+ )
1464
+ references: list[str] = []
1465
+ if isinstance(packet, dict):
1466
+ for key, item in packet.items():
1467
+ if key == "review_history":
1468
+ continue
1469
+ references.extend(_foreign_review_references(key, own_session))
1470
+ references.extend(_foreign_review_references(item, own_session))
1471
+ if history is not None:
1472
+ reference = history.get("parent_summary_ref")
1473
+ if (
1474
+ not isinstance(reference, str)
1475
+ or not re.fullmatch(
1476
+ r"\.ultra/reviews/[A-Za-z0-9][A-Za-z0-9._-]*/SUMMARY\.json",
1477
+ reference,
1478
+ )
1479
+ or reference.split("/")[2] == own_session
1480
+ ):
1481
+ return (
1482
+ "WORKER-PACKET.json review_history.parent_summary_ref must name "
1483
+ "exactly one prior session's SUMMARY.json"
1484
+ )
1485
+ if not digest_string(history.get("parent_summary_digest")):
1486
+ return (
1487
+ "WORKER-PACKET.json review_history.parent_summary_digest must be a "
1488
+ "lowercase SHA-256 digest"
1489
+ )
1490
+ blocking_ids = history.get("unresolved_blocking_ids")
1491
+ if (
1492
+ not isinstance(blocking_ids, list)
1493
+ or not all(nonempty_string(item) for item in blocking_ids)
1494
+ ):
1495
+ return (
1496
+ "WORKER-PACKET.json review_history.unresolved_blocking_ids must be "
1497
+ "an array of finding-id strings"
1498
+ )
1499
+ if references:
1500
+ return (
1501
+ "WORKER-PACKET.json may reference a foreign review session only through "
1502
+ "review_history.parent_summary_ref; observed outside that field: "
1503
+ + ", ".join(sorted(set(references)))
1504
+ )
1505
+ if history is None:
1506
+ return None
1507
+
1508
+ parent_snapshot, parent_error, _missing = stable_read_repo_bytes(
1509
+ repo_root,
1510
+ history["parent_summary_ref"],
1511
+ "review_history parent SUMMARY",
1512
+ )
1513
+ if parent_error:
1514
+ return (
1515
+ "WORKER-PACKET.json review_history parent summary could not be observed "
1516
+ f"as a stable repository regular file: {parent_error}"
1517
+ )
1518
+ if parent_snapshot["sha256"] != history["parent_summary_digest"]:
1519
+ return (
1520
+ "WORKER-PACKET.json review_history.parent_summary_digest does not match "
1521
+ "the observed parent SUMMARY bytes"
1522
+ )
1523
+ try:
1524
+ parent = json.loads(parent_snapshot["bytes"].decode("utf-8"))
1525
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
1526
+ return (
1527
+ "WORKER-PACKET.json review_history parent SUMMARY is invalid UTF-8 "
1528
+ f"JSON: {error}"
1529
+ )
1530
+ if not isinstance(parent, dict) or parent.get("$schema") != "ultra-review-summary-v4":
1531
+ return (
1532
+ "WORKER-PACKET.json review_history parent SUMMARY must be an "
1533
+ "ultra-review-summary-v4 object"
1534
+ )
1535
+ for field, current in (
1536
+ ("mode", packet.get("mode")),
1537
+ ("change_id", packet.get("change_id")),
1538
+ ("task_ids", packet.get("task_ids")),
1539
+ ):
1540
+ if parent.get(field) != current:
1541
+ return (
1542
+ "WORKER-PACKET.json review_history parent SUMMARY "
1543
+ f"{field} does not match the current packet"
1544
+ )
1545
+ findings = parent.get("findings")
1546
+ if not isinstance(findings, list):
1547
+ return (
1548
+ "WORKER-PACKET.json review_history parent SUMMARY carries no findings "
1549
+ "array to resolve blocking ids against"
1550
+ )
1551
+ severity_by_id: dict[str, str] = {}
1552
+ for finding in findings:
1553
+ if isinstance(finding, dict) and nonempty_string(finding.get("id")):
1554
+ severity_by_id[finding["id"]] = finding.get("severity")
1555
+ blocking_ids = history["unresolved_blocking_ids"]
1556
+ if len(blocking_ids) != len(set(blocking_ids)):
1557
+ return (
1558
+ "WORKER-PACKET.json review_history.unresolved_blocking_ids must not "
1559
+ "repeat a finding id"
1560
+ )
1561
+ for blocking_id in blocking_ids:
1562
+ severity = severity_by_id.get(blocking_id)
1563
+ if severity is None:
1564
+ return (
1565
+ "WORKER-PACKET.json review_history.unresolved_blocking_ids names "
1566
+ f"`{blocking_id}`, which is not a finding of the parent SUMMARY"
1567
+ )
1568
+ if severity not in {"P0", "P1"}:
1569
+ return (
1570
+ "WORKER-PACKET.json review_history.unresolved_blocking_ids names "
1571
+ f"`{blocking_id}`, whose parent severity `{severity}` is not a "
1572
+ "current P0/P1 blocker"
1573
+ )
1574
+ return None
1575
+
1576
+
1577
+ def load_worker_packet(
1578
+ session_path: Path,
1579
+ expected_packet_digest: str,
1580
+ admit_subjects=False,
1581
+ require_admission=False,
1582
+ legacy_v4=False,
1583
+ ):
1584
+ repo_root, root_error = canonical_repo_root(session_path)
1585
+ if root_error:
1586
+ return None, f"WORKER-PACKET.json {root_error}"
1587
+ session_name = session_path.resolve().name
1588
+ packet_repo_path = f".ultra/reviews/{session_name}/WORKER-PACKET.json"
1589
+ packet_snapshot, packet_error, _missing = stable_read_repo_bytes(
1590
+ repo_root,
1591
+ packet_repo_path,
1592
+ "WORKER-PACKET.json",
1593
+ )
1594
+ if packet_error:
1595
+ return None, packet_error
1596
+ packet_bytes = packet_snapshot["bytes"]
1597
+ actual_digest = packet_snapshot["sha256"]
1598
+ if actual_digest != expected_packet_digest:
1599
+ return None, (
1600
+ "WORKER-PACKET.json bytes do not match expected immutable packet digest "
1601
+ f"{expected_packet_digest}; observed {actual_digest}"
1602
+ )
1603
+ try:
1604
+ packet = json.loads(packet_bytes.decode("utf-8"))
1605
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
1606
+ return None, f"WORKER-PACKET.json is invalid UTF-8 JSON: {error}"
1607
+ if not isinstance(packet, dict):
1608
+ return None, "WORKER-PACKET.json top-level value must be an object"
1609
+ missing_fields = sorted(PACKET_REQUIRED_FIELDS - set(packet))
1610
+ if missing_fields:
1611
+ return None, (
1612
+ "WORKER-PACKET.json missing required fields: " + ", ".join(missing_fields)
1613
+ )
1614
+ if packet.get("$schema") != "ultra-review-worker-packet-v1":
1615
+ return None, "WORKER-PACKET.json must use ultra-review-worker-packet-v1"
1616
+ admission_contract = packet.get("admission_contract")
1617
+ if admit_subjects:
1618
+ if admission_contract != ADMISSION_CONTRACT_V2:
1619
+ return None, (
1620
+ "WORKER-PACKET.json admission_contract must be "
1621
+ f"{ADMISSION_CONTRACT_V2} for new strict v4 admission"
1622
+ )
1623
+ elif require_admission and admission_contract not in STRICT_ADMISSION_CONTRACTS:
1624
+ return None, (
1625
+ "WORKER-PACKET.json admission_contract must declare retained strict v1 "
1626
+ "or current strict v2 for strict v4 consumption"
1627
+ )
1628
+ elif legacy_v4 and admission_contract in STRICT_ADMISSION_CONTRACTS:
1629
+ return None, (
1630
+ "WORKER-PACKET.json declares a strict admission_contract and "
1631
+ "cannot be consumed with --legacy-v4"
1632
+ )
1633
+ for field in ["session", "head", "range", "change_id", "output_directory"]:
1634
+ if not nonempty_string(packet.get(field)):
1635
+ return None, f"WORKER-PACKET.json {field} must be a non-empty string"
1636
+ if (
1637
+ not ARTIFACT_STEM.fullmatch(packet["session"])
1638
+ or packet["session"] != session_path.resolve().name
1639
+ ):
1640
+ return None, (
1641
+ "WORKER-PACKET.json session must be a safe stem matching the canonical "
1642
+ "session directory"
1643
+ )
1644
+ if not timestamp_string(packet.get("created_at")):
1645
+ return None, "WORKER-PACKET.json created_at must be an RFC 3339 timestamp"
1646
+ if not git_blob_digest_string(packet.get("head")):
1647
+ return None, "WORKER-PACKET.json head must be a lowercase 40-hex Git commit digest"
1648
+ if packet.get("mode") not in REVIEW_MODES:
1649
+ return None, "WORKER-PACKET.json mode must be task, change, or plan"
1650
+ for field, require_nonempty in [
1651
+ ("task_ids", packet["mode"] == "task"),
1652
+ ("acceptance", True),
1653
+ ("public_seams", True),
1654
+ ]:
1655
+ array_error = validate_string_array(
1656
+ packet.get(field),
1657
+ f"WORKER-PACKET.json {field}",
1658
+ require_nonempty,
1659
+ )
1660
+ if array_error:
1661
+ return None, array_error
1662
+ for index, acceptance in enumerate(packet["acceptance"]):
1663
+ source, separator, literal = acceptance.partition(": ")
1664
+ if not separator or not literal:
1665
+ return None, (
1666
+ f"WORKER-PACKET.json acceptance[{index}] must be "
1667
+ "<repository-path>#<heading>: <captured owner-readable claim>"
1668
+ )
1669
+ _, path_error = validate_repo_path(
1670
+ source,
1671
+ repo_root,
1672
+ f"WORKER-PACKET.json acceptance[{index}] source",
1673
+ allow_fragment=True,
1674
+ )
1675
+ if path_error:
1676
+ return None, path_error
1677
+
1678
+ output_directory, path_error = validate_repo_path(
1679
+ packet["output_directory"],
1680
+ repo_root,
1681
+ "WORKER-PACKET.json output_directory",
1682
+ )
1683
+ if path_error:
1684
+ return None, path_error
1685
+ canonical_output = f".ultra/reviews/{packet['session']}"
1686
+ if output_directory != canonical_output:
1687
+ return None, (
1688
+ "WORKER-PACKET.json output_directory must equal the canonical session path "
1689
+ f"{canonical_output}"
1690
+ )
1691
+
1692
+ trace_data = packet.get("north_star_trace")
1693
+ if not isinstance(trace_data, dict) or set(trace_data) != TRACE_FIELDS:
1694
+ return None, (
1695
+ "WORKER-PACKET.json north_star_trace must contain exactly path, "
1696
+ "first_principles, serves, touches, north_star_revision, and "
1697
+ "north_star_digest"
1698
+ )
1699
+ trace = _trace_arrays(packet.get("north_star_trace"))
1700
+ if trace is None:
1701
+ return None, "WORKER-PACKET.json north_star_trace arrays are invalid"
1702
+ _, path_error = validate_repo_path(
1703
+ trace_data.get("path"),
1704
+ repo_root,
1705
+ "WORKER-PACKET.json north_star_trace.path",
1706
+ allow_fragment=True,
1707
+ )
1708
+ if path_error:
1709
+ return None, path_error
1710
+ trace_source, fragment = trace_data["path"].split("#", 1)
1711
+ parts = trace_source.split("/")
1712
+ if (
1713
+ fragment != "north-star-trace"
1714
+ or len(parts) != 5
1715
+ or parts[:2] != [".ultra", "changes"]
1716
+ or parts[2] not in {"active", "archive", "abandoned"}
1717
+ or parts[3] != packet["change_id"]
1718
+ or parts[4] != "intent.md"
1719
+ ):
1720
+ return None, (
1721
+ "north_star_trace.path must bind change_id to "
1722
+ ".ultra/changes/<active|archive|abandoned>/<change_id>/intent.md"
1723
+ "#north-star-trace"
1724
+ )
1725
+ if not nonempty_string(trace_data.get("north_star_revision")):
1726
+ return None, (
1727
+ "WORKER-PACKET.json north_star_trace.north_star_revision must be a "
1728
+ "non-empty string"
1729
+ )
1730
+ if not git_blob_digest_string(trace_data.get("north_star_digest")):
1731
+ return None, (
1732
+ "WORKER-PACKET.json north_star_trace.north_star_digest must be a "
1733
+ "lowercase 40-hex Git blob digest"
1734
+ )
1735
+ if admission_contract == ADMISSION_CONTRACT_V2:
1736
+ subject_observation_error = validate_packet_subject_observations(packet, repo_root)
1737
+ if subject_observation_error:
1738
+ return None, subject_observation_error
1739
+
1740
+ context_files = packet.get("context_files")
1741
+ if not isinstance(context_files, list):
1742
+ return None, "WORKER-PACKET.json context_files must be an array"
1743
+ seen_context_paths = set()
1744
+ for index, context_file in enumerate(context_files):
1745
+ if not isinstance(context_file, dict) or set(context_file) != CONTEXT_FIELDS:
1746
+ return None, (
1747
+ f"WORKER-PACKET.json context_files[{index}] must contain exactly "
1748
+ "path and sha256"
1749
+ )
1750
+ context_path, path_error = validate_repo_path(
1751
+ context_file.get("path"),
1752
+ repo_root,
1753
+ f"WORKER-PACKET.json context_files[{index}].path",
1754
+ )
1755
+ if path_error:
1756
+ return None, path_error
1757
+ if context_path in seen_context_paths:
1758
+ return None, "WORKER-PACKET.json context_files paths must be unique"
1759
+ seen_context_paths.add(context_path)
1760
+ if not digest_string(context_file.get("sha256")):
1761
+ return None, (
1762
+ f"WORKER-PACKET.json context_files[{index}].sha256 must be a "
1763
+ "lowercase SHA-256 digest"
1764
+ )
1765
+ if packet["mode"] == "task":
1766
+ expected_context_paths = [
1767
+ f".ultra/contexts/task-{task_id}.md" for task_id in packet["task_ids"]
1768
+ ]
1769
+ observed_context_paths = [item["path"] for item in context_files]
1770
+ if observed_context_paths != expected_context_paths:
1771
+ return None, (
1772
+ "task-mode context_files paths must exactly follow ordered task_ids: "
1773
+ + ", ".join(expected_context_paths)
1774
+ )
1775
+
1776
+ diff_files = packet.get("diff_files")
1777
+ if not isinstance(diff_files, list) or not diff_files:
1778
+ return None, "WORKER-PACKET.json diff_files must be a non-empty path array"
1779
+ normalized_diff_files = []
1780
+ for index, diff_file in enumerate(diff_files):
1781
+ normalized_path, path_error = validate_repo_path(
1782
+ diff_file,
1783
+ repo_root,
1784
+ f"WORKER-PACKET.json diff_files[{index}]",
1785
+ )
1786
+ if path_error:
1787
+ return None, path_error
1788
+ normalized_diff_files.append(normalized_path)
1789
+ if len(normalized_diff_files) != len(set(normalized_diff_files)):
1790
+ return None, "WORKER-PACKET.json diff_files must not contain duplicates"
1791
+
1792
+ history_error = validate_packet_review_history(packet, repo_root)
1793
+ if history_error:
1794
+ return None, history_error
1795
+
1796
+ workers = packet.get("workers")
1797
+ if not isinstance(workers, list) or not workers:
1798
+ return None, "WORKER-PACKET.json workers must be a non-empty array"
1799
+ by_stem = {}
1800
+ by_agent = {}
1801
+ ordered = []
1802
+ previous_order = -1
1803
+ for index, worker in enumerate(workers):
1804
+ if not isinstance(worker, dict) or set(worker) != WORKER_FIELDS:
1805
+ return None, (
1806
+ f"WORKER-PACKET.json workers[{index}] must contain exactly agent, "
1807
+ "axis, lens, and output"
1808
+ )
1809
+ agent = worker.get("agent")
1810
+ axis = worker.get("axis")
1811
+ lens = worker.get("lens")
1812
+ output = worker.get("output")
1813
+ if agent not in REVIEW_WORKERS or agent in by_agent:
1814
+ return None, f"WORKER-PACKET.json has invalid or duplicate worker agent: {agent}"
1815
+ canonical = CANONICAL_WORKER_BY_AGENT[agent]
1816
+ if axis != canonical["axis"]:
1817
+ return None, (
1818
+ f"WORKER-PACKET.json worker {agent} axis must use canonical value "
1819
+ f"{canonical['axis']}"
1820
+ )
1821
+ if lens != canonical["lens"]:
1822
+ return None, (
1823
+ f"WORKER-PACKET.json worker {agent} lens must use canonical path "
1824
+ f"{canonical['lens']}"
1825
+ )
1826
+ _, path_error = validate_repo_path(
1827
+ lens,
1828
+ repo_root,
1829
+ f"WORKER-PACKET.json worker {agent} lens",
1830
+ )
1831
+ if path_error:
1832
+ return None, path_error
1833
+ output_path, path_error = validate_repo_path(
1834
+ output,
1835
+ repo_root,
1836
+ f"WORKER-PACKET.json worker {agent} output",
1837
+ )
1838
+ if path_error:
1839
+ return None, path_error
1840
+ canonical_worker_output = f"{output_directory}/{agent}.json"
1841
+ if output_path != canonical_worker_output:
1842
+ return None, (
1843
+ f"WORKER-PACKET.json worker {agent} output must equal canonical path "
1844
+ f"{canonical_worker_output}"
1845
+ )
1846
+ if canonical["order"] <= previous_order:
1847
+ return None, "WORKER-PACKET.json workers must use canonical roster order"
1848
+ previous_order = canonical["order"]
1849
+ output_name = PurePosixPath(output_path).name
1850
+ output_stem = PurePosixPath(output_path).stem
1851
+ if output_stem in by_stem:
1852
+ return None, f"WORKER-PACKET.json repeats output stem: {output_stem}"
1853
+ normalized = {
1854
+ "agent": agent,
1855
+ "axis": axis,
1856
+ "lens": lens,
1857
+ "output": output_path,
1858
+ "stem": output_stem,
1859
+ "filename": output_name,
1860
+ }
1861
+ by_stem[output_stem] = normalized
1862
+ by_agent[agent] = normalized
1863
+ ordered.append(normalized)
1864
+ if "review-spec" not in by_agent:
1865
+ return None, (
1866
+ "WORKER-PACKET.json workers must include canonical review-spec for the "
1867
+ "permanent specification axis"
1868
+ )
1869
+ admission = None
1870
+ if admit_subjects:
1871
+ admission, subject_error = validate_packet_admission(
1872
+ packet,
1873
+ repo_root,
1874
+ actual_digest,
1875
+ )
1876
+ if subject_error:
1877
+ return None, f"WORKER-PACKET.json subject validation failed: {subject_error}"
1878
+ loaded = {
1879
+ "data": packet,
1880
+ "digest": actual_digest,
1881
+ "trace": {field: set(values) for field, values in trace.items()},
1882
+ "repo_root": repo_root,
1883
+ "diff_scope": set(normalized_diff_files),
1884
+ "context_digest": canonical_context_digest(context_files),
1885
+ "admission": admission,
1886
+ "workers": ordered,
1887
+ "by_stem": by_stem,
1888
+ "by_agent": by_agent,
1889
+ }
1890
+ if require_admission:
1891
+ receipt, receipt_error = load_admission_receipt(session_path, loaded)
1892
+ if receipt_error:
1893
+ return None, (
1894
+ "strict v4 packet consumption requires a valid ADMISSION.json receipt "
1895
+ f"from packet mode: {receipt_error}"
1896
+ )
1897
+ loaded["admission_receipt"] = receipt
1898
+ return loaded, None
1899
+
1900
+
1901
+ def reload_worker_packet(
1902
+ session_path: Path,
1903
+ expected_packet_digest: str,
1904
+ admit_subjects=False,
1905
+ require_admission=False,
1906
+ legacy_v4=False,
1907
+ ):
1908
+ return load_worker_packet(
1909
+ session_path,
1910
+ expected_packet_digest,
1911
+ admit_subjects,
1912
+ require_admission,
1913
+ legacy_v4,
1914
+ )
1915
+
1916
+
1917
+ def packet_admission_binding(packet):
1918
+ receipt = packet.get("admission_receipt") if packet is not None else None
1919
+ if receipt is None:
1920
+ return None
1921
+ return {
1922
+ "admission_digest": receipt["digest"],
1923
+ "subject_digest": receipt["data"]["subject_digest"],
1924
+ }
1925
+
1926
+
1927
+ def validate_artifact_admission_binding(data, packet, label: str):
1928
+ expected = packet_admission_binding(packet)
1929
+ if expected is None:
1930
+ return None
1931
+ for field in ("admission_digest", "subject_digest"):
1932
+ if not digest_string(data.get(field)):
1933
+ return f"{label} {field} must be the lowercase SHA-256 from ADMISSION.json"
1934
+ if data[field] != expected[field]:
1935
+ return f"{label} {field} must exactly match ADMISSION.json"
1936
+ return None
1937
+
1938
+
1939
+ def validate_pinned_admission(packet, expected):
1940
+ if expected is None:
1941
+ return None
1942
+ if packet_admission_binding(packet) != expected:
1943
+ return (
1944
+ "ADMISSION.json receipt changed during polling; the strict review session "
1945
+ "is INCOMPLETE and must be restarted as a fresh session"
1946
+ )
1947
+ return None
1948
+
1949
+
1950
+ def validate_north_star_trace(value, allowed=None):
1951
+ if not isinstance(value, dict) or set(value) != {"first_principles", "serves", "touches"}:
1952
+ return "finding north_star_trace must contain exactly first_principles, serves, and touches"
1953
+ patterns = {
1954
+ "first_principles": r"FP-[A-Za-z0-9][A-Za-z0-9._-]*",
1955
+ "serves": r"NS-[A-Za-z0-9][A-Za-z0-9._-]*",
1956
+ "touches": r"HC-[A-Za-z0-9][A-Za-z0-9._-]*",
1957
+ }
1958
+ for field, pattern in patterns.items():
1959
+ values = value.get(field)
1960
+ if not isinstance(values, list) or not all(
1961
+ isinstance(item, str) and re.fullmatch(pattern, item) for item in values
1962
+ ):
1963
+ return f"finding north_star_trace.{field} must be an array of resolving {pattern[:2]} IDs"
1964
+ if len(values) != len(set(values)):
1965
+ return f"finding north_star_trace.{field} must not contain duplicates"
1966
+ if allowed is not None:
1967
+ outside = [item for item in values if item not in allowed[field]]
1968
+ if outside:
1969
+ return (
1970
+ f"finding north_star_trace.{field} contains IDs outside the immutable "
1971
+ f"Worker Packet: {', '.join(outside)}"
1972
+ )
1973
+ return None
1974
+
1975
+
1976
+ def validate_finding(
1977
+ finding,
1978
+ artifact_axis: str,
1979
+ seen_ids,
1980
+ findings_schema: str,
1981
+ allowed_trace=None,
1982
+ allowed_files=None,
1983
+ repo_root=None,
1984
+ ):
74
1985
  if not isinstance(finding, dict):
75
1986
  return "finding must be an object"
76
1987
  missing = sorted(FINDING_FIELDS - set(finding))
@@ -88,40 +1999,107 @@ def validate_finding(finding, artifact_axis: str, seen_ids):
88
1999
  for field in ["category", "title", "file", "trigger", "impact", "evidence", "suggestion"]:
89
2000
  if not nonempty_string(finding.get(field)):
90
2001
  return f"finding {field} must be a non-empty string"
2002
+ if repo_root is not None:
2003
+ finding_file, path_error = validate_repo_path(
2004
+ finding["file"],
2005
+ repo_root,
2006
+ "finding file",
2007
+ )
2008
+ if path_error:
2009
+ return path_error
2010
+ if finding_file not in allowed_files:
2011
+ return "finding file must be within the immutable Worker Packet diff scope"
91
2012
  if not isinstance(finding.get("line"), int) or finding["line"] < 1:
92
2013
  return "finding line must be a positive integer"
93
2014
  line_end = finding.get("line_end")
94
2015
  if line_end is not None and (not isinstance(line_end, int) or line_end < finding["line"]):
95
2016
  return "finding line_end must be at or after line"
2017
+ if findings_schema == FINDINGS_SCHEMA:
2018
+ if "north_star_trace" not in finding:
2019
+ return "finding missing fields: north_star_trace"
2020
+ trace_error = validate_north_star_trace(
2021
+ finding.get("north_star_trace"),
2022
+ allowed_trace,
2023
+ )
2024
+ if trace_error:
2025
+ return trace_error
96
2026
  return None
97
2027
 
98
2028
 
99
- def validate_specialist(data, expected_packet_digest: str):
100
- if data.get("$schema") != FINDINGS_SCHEMA:
101
- return f"$schema must be {FINDINGS_SCHEMA}"
2029
+ def validate_specialist(
2030
+ data,
2031
+ expected_packet_digest: str,
2032
+ findings_schema: str,
2033
+ expected_worker=None,
2034
+ packet=None,
2035
+ ):
2036
+ if data.get("$schema") != findings_schema:
2037
+ return f"$schema must be {findings_schema}"
102
2038
  if not nonempty_string(data.get("agent")):
103
2039
  return "agent must be a non-empty string"
2040
+ if expected_worker is not None and data.get("agent") != expected_worker["agent"]:
2041
+ return f"agent must match packet roster worker {expected_worker['agent']}"
104
2042
  if not digest_string(data.get("packet_digest")):
105
2043
  return "packet_digest must be a lowercase SHA-256 digest"
106
2044
  if data["packet_digest"] != expected_packet_digest:
107
2045
  return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
2046
+ admission_error = validate_artifact_admission_binding(
2047
+ data,
2048
+ packet,
2049
+ "specialist artifact",
2050
+ )
2051
+ if admission_error:
2052
+ return admission_error
108
2053
  axis = data.get("axis")
109
2054
  if axis not in AXES:
110
2055
  return "axis must be spec_fidelity or engineering_standards"
111
- for field in ["session", "timestamp"]:
112
- if not nonempty_string(data.get(field)):
113
- return f"{field} must be a non-empty string"
2056
+ if expected_worker is not None and axis != expected_worker["axis"]:
2057
+ return f"axis must match packet roster axis {expected_worker['axis']}"
2058
+ if not nonempty_string(data.get("session")):
2059
+ return "session must be a non-empty string"
2060
+ if findings_schema == FINDINGS_SCHEMA:
2061
+ if not timestamp_string(data.get("timestamp")):
2062
+ return "timestamp must be an RFC 3339 timestamp"
2063
+ elif not nonempty_string(data.get("timestamp")):
2064
+ return "timestamp must be a non-empty string"
2065
+ if packet is not None and data.get("session") != packet["data"]["session"]:
2066
+ return "session must match WORKER-PACKET.json"
114
2067
  scope = data.get("scope")
115
2068
  if not isinstance(scope, dict):
116
2069
  return "scope must be an object"
117
2070
  for field in ["head", "range"]:
118
2071
  if not nonempty_string(scope.get(field)):
119
2072
  return f"scope.{field} must be a non-empty string"
2073
+ if packet is not None and scope.get(field) != packet["data"][field]:
2074
+ return f"scope.{field} must match WORKER-PACKET.json"
120
2075
  if not isinstance(scope.get("files_analyzed"), list) or not all(
121
2076
  nonempty_string(item) for item in scope["files_analyzed"]
122
2077
  ):
123
2078
  return "scope.files_analyzed must be a string array"
124
- if not isinstance(scope.get("diff_only"), bool):
2079
+ if packet is not None:
2080
+ normalized_analyzed = []
2081
+ for index, analyzed_file in enumerate(scope["files_analyzed"]):
2082
+ normalized_path, path_error = validate_repo_path(
2083
+ analyzed_file,
2084
+ packet["repo_root"],
2085
+ f"scope.files_analyzed[{index}]",
2086
+ )
2087
+ if path_error:
2088
+ return path_error
2089
+ normalized_analyzed.append(normalized_path)
2090
+ if len(normalized_analyzed) != len(set(normalized_analyzed)):
2091
+ return "scope.files_analyzed must not contain duplicates"
2092
+ outside_scope = [
2093
+ item for item in normalized_analyzed if item not in packet["diff_scope"]
2094
+ ]
2095
+ if outside_scope:
2096
+ return (
2097
+ "scope.files_analyzed must stay within the immutable Worker Packet "
2098
+ "diff scope: " + ", ".join(outside_scope)
2099
+ )
2100
+ if scope.get("diff_only") is not True:
2101
+ return "scope.diff_only must be true for packet-bound v4 artifacts"
2102
+ elif not isinstance(scope.get("diff_only"), bool):
125
2103
  return "scope.diff_only must be boolean"
126
2104
  if data.get("status") != "complete":
127
2105
  return "status must be complete"
@@ -130,51 +2108,197 @@ def validate_specialist(data, expected_packet_digest: str):
130
2108
  return "findings must be an array"
131
2109
  seen_ids = set()
132
2110
  for finding in findings:
133
- error = validate_finding(finding, axis, seen_ids)
2111
+ error = validate_finding(
2112
+ finding,
2113
+ axis,
2114
+ seen_ids,
2115
+ findings_schema,
2116
+ packet["trace"] if packet is not None else None,
2117
+ packet["diff_scope"] if packet is not None else None,
2118
+ packet["repo_root"] if packet is not None else None,
2119
+ )
134
2120
  if error:
135
2121
  return error
136
2122
  for field in ["positive_observations", "limitations"]:
137
2123
  if not isinstance(data.get(field), list):
138
2124
  return f"{field} must be an array"
2125
+ coverage_refs = data.get("coverage_refs")
2126
+ if not isinstance(coverage_refs, list) or not all(
2127
+ nonempty_string(item) for item in coverage_refs
2128
+ ):
2129
+ return "coverage_refs must be a string array"
2130
+ if not findings and not coverage_refs and not data["limitations"]:
2131
+ return "zero findings require coverage_refs or an explicit limitation"
139
2132
  return None
140
2133
 
141
2134
 
142
- def evaluate_artifacts(session_path: Path, expected, expected_packet_digest: str):
2135
+ def evaluate_artifacts(
2136
+ session_path: Path,
2137
+ expected,
2138
+ expected_packet_digest: str,
2139
+ findings_schema: str,
2140
+ packet=None,
2141
+ ):
143
2142
  done = []
144
2143
  missing = []
145
2144
  invalid = []
146
2145
  errors = {}
2146
+ digests = {}
2147
+ if packet is None:
2148
+ repo_root, root_error = canonical_repo_root(session_path)
2149
+ if root_error:
2150
+ return [], [], list(expected), {"session": root_error}, {}
2151
+ else:
2152
+ repo_root = packet["repo_root"]
147
2153
  for stem in expected:
148
- path = session_path / f"{stem}.json"
149
- if not path.exists():
2154
+ expected_worker = packet["by_stem"].get(stem) if packet is not None else None
2155
+ if packet is not None and expected_worker is None:
2156
+ invalid.append(stem)
2157
+ errors[stem] = "requested artifact stem is absent from WORKER-PACKET.json roster"
2158
+ continue
2159
+ repo_path = (
2160
+ expected_worker["output"]
2161
+ if expected_worker is not None
2162
+ else f".ultra/reviews/{session_path.resolve().name}/{stem}.json"
2163
+ )
2164
+ data, snapshot, error, is_missing = stable_read_repo_json(
2165
+ repo_root,
2166
+ repo_path,
2167
+ f"specialist artifact {stem}",
2168
+ )
2169
+ if is_missing:
150
2170
  missing.append(stem)
151
2171
  continue
152
- data, error = read_json(path)
153
2172
  if error is None:
154
- error = validate_specialist(data, expected_packet_digest)
2173
+ error = validate_specialist(
2174
+ data,
2175
+ expected_packet_digest,
2176
+ findings_schema,
2177
+ expected_worker,
2178
+ packet,
2179
+ )
155
2180
  if error:
156
2181
  invalid.append(stem)
157
2182
  errors[stem] = error
158
2183
  else:
159
2184
  done.append(stem)
160
- return done, missing, invalid, errors
2185
+ digests[stem] = snapshot["sha256"]
2186
+ return done, missing, invalid, errors, digests
161
2187
 
162
2188
 
163
- def wait_for_agents(session_path: Path, expected, expected_packet_digest: str, timeout: float) -> bool:
2189
+ def wait_for_agents(
2190
+ session_path: Path,
2191
+ expected,
2192
+ expected_packet_digest: str,
2193
+ findings_schema: str,
2194
+ timeout: float,
2195
+ require_admission: bool,
2196
+ legacy_v4: bool = False,
2197
+ ) -> bool:
2198
+ packet = None
2199
+ if findings_schema == FINDINGS_SCHEMA:
2200
+ packet, packet_error = reload_worker_packet(
2201
+ session_path,
2202
+ expected_packet_digest,
2203
+ require_admission=require_admission,
2204
+ legacy_v4=legacy_v4,
2205
+ )
2206
+ if packet_error:
2207
+ print(json.dumps({
2208
+ "status": "incomplete",
2209
+ "artifacts_done": [],
2210
+ "artifacts_missing": [],
2211
+ "artifacts_invalid": list(expected),
2212
+ "errors": {"WORKER-PACKET": packet_error},
2213
+ "count": 0,
2214
+ }, sort_keys=True))
2215
+ return False
2216
+ pinned_admission = packet_admission_binding(packet)
164
2217
  deadline = time.monotonic() + timeout
165
2218
  while True:
166
- done, missing, invalid, errors = evaluate_artifacts(
167
- session_path, expected, expected_packet_digest
2219
+ current_packet = packet
2220
+ if packet is not None:
2221
+ current_packet, packet_error = reload_worker_packet(
2222
+ session_path,
2223
+ expected_packet_digest,
2224
+ require_admission=require_admission,
2225
+ legacy_v4=legacy_v4,
2226
+ )
2227
+ if packet_error:
2228
+ print(json.dumps({
2229
+ "status": "incomplete",
2230
+ "artifacts_done": [],
2231
+ "artifacts_missing": [],
2232
+ "artifacts_invalid": list(expected),
2233
+ "errors": {"WORKER-PACKET": packet_error},
2234
+ "count": 0,
2235
+ }, sort_keys=True))
2236
+ return False
2237
+ admission_error = validate_pinned_admission(
2238
+ current_packet,
2239
+ pinned_admission,
2240
+ )
2241
+ if admission_error:
2242
+ print(json.dumps({
2243
+ "status": "incomplete",
2244
+ "artifacts_done": [],
2245
+ "artifacts_missing": [],
2246
+ "artifacts_invalid": list(expected),
2247
+ "errors": {"ADMISSION": admission_error},
2248
+ "count": 0,
2249
+ }, sort_keys=True))
2250
+ return False
2251
+ done, missing, invalid, errors, digests = evaluate_artifacts(
2252
+ session_path,
2253
+ expected,
2254
+ expected_packet_digest,
2255
+ findings_schema,
2256
+ current_packet,
168
2257
  )
169
2258
  if len(done) == len(expected):
170
- print(json.dumps({
2259
+ if packet is not None:
2260
+ final_packet, packet_error = reload_worker_packet(
2261
+ session_path,
2262
+ expected_packet_digest,
2263
+ require_admission=require_admission,
2264
+ legacy_v4=legacy_v4,
2265
+ )
2266
+ if packet_error:
2267
+ print(json.dumps({
2268
+ "status": "incomplete",
2269
+ "artifacts_done": [],
2270
+ "artifacts_missing": [],
2271
+ "artifacts_invalid": list(expected),
2272
+ "errors": {"WORKER-PACKET": packet_error},
2273
+ "count": 0,
2274
+ }, sort_keys=True))
2275
+ return False
2276
+ admission_error = validate_pinned_admission(
2277
+ final_packet,
2278
+ pinned_admission,
2279
+ )
2280
+ if admission_error:
2281
+ print(json.dumps({
2282
+ "status": "incomplete",
2283
+ "artifacts_done": [],
2284
+ "artifacts_missing": [],
2285
+ "artifacts_invalid": list(expected),
2286
+ "errors": {"ADMISSION": admission_error},
2287
+ "count": 0,
2288
+ }, sort_keys=True))
2289
+ return False
2290
+ output = {
171
2291
  "status": "complete",
172
2292
  "artifacts_done": done,
2293
+ "artifact_digests": digests,
173
2294
  "artifacts_missing": [],
174
2295
  "artifacts_invalid": [],
175
2296
  "errors": {},
176
2297
  "count": len(done),
177
- }, sort_keys=True))
2298
+ }
2299
+ if pinned_admission is not None:
2300
+ output.update(pinned_admission)
2301
+ print(json.dumps(output, sort_keys=True))
178
2302
  return True
179
2303
  if time.monotonic() >= deadline:
180
2304
  print(json.dumps({
@@ -199,20 +2323,101 @@ def expected_overall_verdict(data) -> str:
199
2323
  return "APPROVE"
200
2324
 
201
2325
 
202
- def validate_summary(data, expected_packet_digest: str):
203
- if data.get("$schema") != SUMMARY_SCHEMA:
204
- return f"$schema must be {SUMMARY_SCHEMA}"
2326
+ def load_completed_specialists(
2327
+ session_path: Path,
2328
+ packet,
2329
+ completed,
2330
+ expected_packet_digest: str,
2331
+ ):
2332
+ union = []
2333
+ refs_by_axis = {axis: [] for axis in AXES}
2334
+ for worker in packet["workers"]:
2335
+ if worker["agent"] not in completed:
2336
+ continue
2337
+ data, _snapshot, error, is_missing = stable_read_repo_json(
2338
+ packet["repo_root"],
2339
+ worker["output"],
2340
+ f"completed specialist {worker['agent']}",
2341
+ )
2342
+ if is_missing:
2343
+ error = "artifact is missing"
2344
+ if error is None:
2345
+ error = validate_specialist(
2346
+ data,
2347
+ expected_packet_digest,
2348
+ FINDINGS_SCHEMA,
2349
+ worker,
2350
+ packet,
2351
+ )
2352
+ if error:
2353
+ return None, None, (
2354
+ f"completed specialist {worker['agent']} is invalid: {error}"
2355
+ )
2356
+ refs_by_axis[worker["axis"]].append(worker["filename"])
2357
+ union.extend(data["findings"])
2358
+ return union, refs_by_axis, None
2359
+
2360
+
2361
+ def expected_axis_verdict(axis, union, packet, failed) -> str:
2362
+ selected = [worker for worker in packet["workers"] if worker["axis"] == axis]
2363
+ if not selected or any(worker["agent"] in failed for worker in selected):
2364
+ return "INCOMPLETE"
2365
+ severities = {
2366
+ finding["severity"] for finding in union if finding.get("axis") == axis
2367
+ }
2368
+ return "FAIL" if severities.intersection({"P0", "P1"}) else "PASS"
2369
+
2370
+
2371
+ def validate_summary(
2372
+ data,
2373
+ expected_packet_digest: str,
2374
+ summary_schema: str,
2375
+ findings_schema: str,
2376
+ session_path=None,
2377
+ packet=None,
2378
+ ):
2379
+ if data.get("$schema") != summary_schema:
2380
+ return f"$schema must be {summary_schema}"
205
2381
  if data.get("mode") not in REVIEW_MODES:
206
2382
  return "mode must be task, change, or plan"
207
- for field in ["session", "change_id", "head", "context_digest"]:
2383
+ if data.get("execution_mode") not in EXECUTION_MODES:
2384
+ return "execution_mode must be isolated or sequential-shared-context"
2385
+ for field in ["session", "change_id", "head"]:
208
2386
  if not nonempty_string(data.get(field)):
209
2387
  return f"{field} must be a non-empty string"
210
2388
  if not digest_string(data.get("packet_digest")):
211
2389
  return "packet_digest must be a lowercase SHA-256 digest"
212
2390
  if data["packet_digest"] != expected_packet_digest:
213
2391
  return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
214
- if data.get("worktree_digest") is not None and not nonempty_string(data.get("worktree_digest")):
215
- return "worktree_digest must be null or a non-empty string"
2392
+ admission_error = validate_artifact_admission_binding(data, packet, "SUMMARY.json")
2393
+ if admission_error:
2394
+ return admission_error
2395
+ if packet is not None:
2396
+ packet_data = packet["data"]
2397
+ exact_fields = ["mode", "session", "change_id", "task_ids", "head"]
2398
+ for field in exact_fields:
2399
+ if data.get(field) != packet_data.get(field):
2400
+ return f"summary {field} must match WORKER-PACKET.json"
2401
+ if summary_schema == SUMMARY_SCHEMA:
2402
+ if "worktree_digest" not in data or data.get("worktree_digest") is not None:
2403
+ return (
2404
+ "worktree_digest must be present and null until an exact worktree "
2405
+ "observation is bound by the immutable Worker Packet"
2406
+ )
2407
+ if not digest_string(data.get("context_digest")):
2408
+ return "context_digest must be a lowercase SHA-256 digest"
2409
+ if packet is not None and data["context_digest"] != packet["context_digest"]:
2410
+ return (
2411
+ "context_digest must equal the immutable Worker Packet context digest "
2412
+ f"{packet['context_digest']}"
2413
+ )
2414
+ else:
2415
+ if not nonempty_string(data.get("context_digest")):
2416
+ return "context_digest must be a non-empty string"
2417
+ if data.get("worktree_digest") is not None and not nonempty_string(
2418
+ data.get("worktree_digest")
2419
+ ):
2420
+ return "worktree_digest must be null or a non-empty string"
216
2421
  task_ids = data.get("task_ids")
217
2422
  if not isinstance(task_ids, list) or not all(nonempty_string(item) for item in task_ids):
218
2423
  return "task_ids must be a string array"
@@ -228,8 +2433,10 @@ def validate_summary(data, expected_packet_digest: str):
228
2433
  if not isinstance(item, dict) or item.get("verdict") not in AXIS_VERDICTS:
229
2434
  return f"invalid {axis} verdict"
230
2435
  refs = item.get("evidence_refs")
231
- if not isinstance(refs, list) or not refs or not all(nonempty_string(ref) for ref in refs):
232
- return f"{axis}.evidence_refs must be a non-empty string array"
2436
+ if not isinstance(refs, list) or not all(nonempty_string(ref) for ref in refs):
2437
+ return f"{axis}.evidence_refs must be a string array"
2438
+ if packet is None and not refs:
2439
+ return f"{axis}.evidence_refs must be non-empty for legacy summaries"
233
2440
  workers = data.get("workers")
234
2441
  if not isinstance(workers, dict):
235
2442
  return "workers must be an object"
@@ -239,6 +2446,11 @@ def validate_summary(data, expected_packet_digest: str):
239
2446
  return f"workers.{field} must be a string array"
240
2447
  if len(values) != len(set(values)):
241
2448
  return f"workers.{field} must not contain duplicates"
2449
+ completed_set = set(workers["completed"])
2450
+ failed_set = set(workers["failed"])
2451
+ skipped_set = set(workers["skipped"])
2452
+ if completed_set & failed_set or completed_set & skipped_set or failed_set & skipped_set:
2453
+ return "workers completed, failed, and skipped sets must be disjoint"
242
2454
  worker_selection = data.get("worker_selection")
243
2455
  if not isinstance(worker_selection, list) or not worker_selection:
244
2456
  return "worker_selection must be a non-empty array"
@@ -258,16 +2470,22 @@ def validate_summary(data, expected_packet_digest: str):
258
2470
  return f"worker selection rationale is required: {worker}"
259
2471
  seen_workers.add(worker)
260
2472
  (selected if status == "selected" else skipped).add(worker)
261
- if selected != set(workers["completed"]) | set(workers["failed"]):
2473
+ if selected != completed_set | failed_set:
262
2474
  return "selected workers must equal completed and failed workers"
263
- if skipped != set(workers["skipped"]):
2475
+ if skipped != skipped_set:
264
2476
  return "skipped worker selection must match workers.skipped"
265
2477
  if seen_workers != REVIEW_WORKERS:
266
2478
  missing = sorted(REVIEW_WORKERS - seen_workers)
267
2479
  extra = sorted(seen_workers - REVIEW_WORKERS)
268
2480
  return f"worker_selection must disposition the complete roster; missing={missing}, extra={extra}"
269
- if "review-spec" not in selected or "review-spec" not in set(workers["completed"]):
270
- return "review-spec must be selected and completed"
2481
+ if "review-spec" not in selected:
2482
+ return "review-spec must be selected"
2483
+ if packet is not None:
2484
+ packet_selected = {worker["agent"] for worker in packet["workers"]}
2485
+ if selected != packet_selected:
2486
+ return "selected worker roster must equal WORKER-PACKET.json workers"
2487
+ if skipped != REVIEW_WORKERS - packet_selected:
2488
+ return "skipped worker roster must equal workers absent from WORKER-PACKET.json"
271
2489
  findings = data.get("findings")
272
2490
  if not isinstance(findings, list):
273
2491
  return "findings must be an array"
@@ -276,44 +2494,351 @@ def validate_summary(data, expected_packet_digest: str):
276
2494
  axis = finding.get("axis") if isinstance(finding, dict) else None
277
2495
  if axis not in AXES:
278
2496
  return "summary finding has invalid axis"
279
- error = validate_finding(finding, axis, seen_ids)
2497
+ error = validate_finding(
2498
+ finding,
2499
+ axis,
2500
+ seen_ids,
2501
+ findings_schema,
2502
+ packet["trace"] if packet is not None else None,
2503
+ packet["diff_scope"] if packet is not None else None,
2504
+ packet["repo_root"] if packet is not None else None,
2505
+ )
280
2506
  if error:
281
2507
  return error
282
2508
  for field in ["positive_observations", "limitations"]:
283
2509
  if not isinstance(data.get(field), list):
284
2510
  return f"{field} must be an array"
2511
+ coverage_refs = data.get("coverage_refs")
2512
+ if not isinstance(coverage_refs, list) or not all(
2513
+ nonempty_string(item) for item in coverage_refs
2514
+ ):
2515
+ return "coverage_refs must be a string array"
2516
+ if not findings and not coverage_refs and not data["limitations"]:
2517
+ return "zero findings require coverage_refs or an explicit limitation"
2518
+ if packet is not None:
2519
+ if session_path is None:
2520
+ return "v4 summary validation requires the session path"
2521
+ union, refs_by_axis, specialist_error = load_completed_specialists(
2522
+ session_path,
2523
+ packet,
2524
+ set(workers["completed"]),
2525
+ expected_packet_digest,
2526
+ )
2527
+ if specialist_error:
2528
+ return specialist_error
2529
+ if findings != union:
2530
+ return (
2531
+ "SUMMARY findings must equal the complete ordered specialist finding "
2532
+ "union unchanged"
2533
+ )
2534
+ failed = set(workers["failed"])
2535
+ for axis in AXES:
2536
+ if axes[axis]["evidence_refs"] != refs_by_axis[axis]:
2537
+ return f"{axis}.evidence_refs must equal packet-ordered completed specialist outputs"
2538
+ axis_expected = expected_axis_verdict(axis, union, packet, failed)
2539
+ if axes[axis]["verdict"] != axis_expected:
2540
+ return (
2541
+ f"{axis} verdict {axes[axis]['verdict']} conflicts with completed "
2542
+ f"specialist evidence; expected {axis_expected}"
2543
+ )
285
2544
  expected = expected_overall_verdict(data)
286
2545
  if data["verdict"] != expected:
287
2546
  return f"verdict {data['verdict']} conflicts with evidence; expected {expected}"
288
2547
  return None
289
2548
 
290
2549
 
291
- def wait_for_summary(session_path: Path, expected_packet_digest: str, timeout: float) -> bool:
292
- summary_path = session_path / "SUMMARY.json"
2550
+ def wait_for_summary(
2551
+ session_path: Path,
2552
+ expected_packet_digest: str,
2553
+ summary_schema: str,
2554
+ findings_schema: str,
2555
+ timeout: float,
2556
+ summary_snapshot_bytes=None,
2557
+ expected_summary_digest=None,
2558
+ require_admission=True,
2559
+ legacy_v4=False,
2560
+ ) -> bool:
2561
+ packet = None
2562
+ if summary_schema == SUMMARY_SCHEMA:
2563
+ packet, packet_error = reload_worker_packet(
2564
+ session_path,
2565
+ expected_packet_digest,
2566
+ require_admission=require_admission,
2567
+ legacy_v4=legacy_v4,
2568
+ )
2569
+ if packet_error:
2570
+ print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
2571
+ return False
2572
+ pinned_admission = packet_admission_binding(packet)
2573
+ repo_root = packet["repo_root"] if packet is not None else canonical_repo_root(session_path)[0]
2574
+ summary_repo_path = f".ultra/reviews/{session_path.resolve().name}/SUMMARY.json"
293
2575
  deadline = time.monotonic() + timeout
294
2576
  last_error = "SUMMARY.json is missing"
295
2577
  while True:
296
- if summary_path.exists():
297
- data, error = read_json(summary_path)
298
- if error is None:
299
- error = validate_summary(data, expected_packet_digest)
300
- if error is None:
301
- counts = {severity: 0 for severity in sorted(SEVERITIES)}
302
- for finding in data["findings"]:
303
- counts[finding["severity"]] += 1
304
- print(
2578
+ current_packet = packet
2579
+ if packet is not None:
2580
+ current_packet, packet_error = reload_worker_packet(
2581
+ session_path,
2582
+ expected_packet_digest,
2583
+ require_admission=require_admission,
2584
+ legacy_v4=legacy_v4,
2585
+ )
2586
+ if packet_error:
2587
+ print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
2588
+ return False
2589
+ admission_error = validate_pinned_admission(
2590
+ current_packet,
2591
+ pinned_admission,
2592
+ )
2593
+ if admission_error:
2594
+ print(json.dumps({"status": "incomplete", "error": admission_error}, sort_keys=True))
2595
+ return False
2596
+
2597
+ if summary_snapshot_bytes is not None:
2598
+ summary_digest = hashlib.sha256(summary_snapshot_bytes).hexdigest()
2599
+ if summary_digest != expected_summary_digest:
2600
+ error = (
2601
+ "caller-owned SUMMARY snapshot bytes do not match "
2602
+ f"--summary-snapshot-digest {expected_summary_digest}; observed "
2603
+ f"{summary_digest}"
2604
+ )
2605
+ data = None
2606
+ else:
2607
+ data, error = decode_json_bytes(
2608
+ summary_snapshot_bytes,
2609
+ "caller-owned SUMMARY snapshot",
2610
+ )
2611
+ is_missing = False
2612
+ else:
2613
+ data, snapshot, error, is_missing = stable_read_repo_json(
2614
+ repo_root,
2615
+ summary_repo_path,
2616
+ "SUMMARY.json",
2617
+ )
2618
+ summary_digest = snapshot["sha256"] if snapshot is not None else None
2619
+
2620
+ if error is None:
2621
+ error = validate_summary(
2622
+ data,
2623
+ expected_packet_digest,
2624
+ summary_schema,
2625
+ findings_schema,
2626
+ session_path,
2627
+ current_packet,
2628
+ )
2629
+ if error is None:
2630
+ if packet is not None:
2631
+ final_packet, packet_error = reload_worker_packet(
2632
+ session_path,
2633
+ expected_packet_digest,
2634
+ require_admission=require_admission,
2635
+ legacy_v4=legacy_v4,
2636
+ )
2637
+ if packet_error:
2638
+ print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
2639
+ return False
2640
+ admission_error = validate_pinned_admission(
2641
+ final_packet,
2642
+ pinned_admission,
2643
+ )
2644
+ if admission_error:
2645
+ print(json.dumps({"status": "incomplete", "error": admission_error}, sort_keys=True))
2646
+ return False
2647
+ counts = {severity: 0 for severity in sorted(SEVERITIES)}
2648
+ for finding in data["findings"]:
2649
+ counts[finding["severity"]] += 1
2650
+ output = {
2651
+ "status": "complete",
2652
+ "verdict": data["verdict"],
2653
+ "message": (
305
2654
  f"Review complete: {data['verdict']} "
306
2655
  f"(P0:{counts['P0']} P1:{counts['P1']} P2:{counts['P2']} "
307
2656
  f"P3:{counts['P3']} total:{len(data['findings'])})"
308
- )
309
- return True
2657
+ ),
2658
+ "counts": {
2659
+ **counts,
2660
+ "total": len(data["findings"]),
2661
+ },
2662
+ "summary_digest": summary_digest,
2663
+ }
2664
+ if pinned_admission is not None:
2665
+ output.update(pinned_admission)
2666
+ print(json.dumps(output, sort_keys=True))
2667
+ return True
2668
+ if not is_missing:
310
2669
  last_error = error
2670
+ if summary_snapshot_bytes is not None:
2671
+ print(json.dumps({"status": "incomplete", "error": last_error}, sort_keys=True))
2672
+ return False
311
2673
  if time.monotonic() >= deadline:
312
2674
  print(json.dumps({"status": "incomplete", "error": last_error}, sort_keys=True))
313
2675
  return False
314
2676
  time.sleep(min(POLL_INTERVAL, max(0.0, deadline - time.monotonic())))
315
2677
 
316
2678
 
2679
+ def validate_packet_preflight(session_path: Path, expected_packet_digest: str) -> bool:
2680
+ packet, error = reload_worker_packet(
2681
+ session_path,
2682
+ expected_packet_digest,
2683
+ )
2684
+ if error:
2685
+ print(json.dumps({"status": "incomplete", "error": error}, sort_keys=True))
2686
+ return False
2687
+ admission_contract = packet["data"].get("admission_contract")
2688
+ if admission_contract == ADMISSION_CONTRACT_V1:
2689
+ retained, receipt_error = reload_worker_packet(
2690
+ session_path,
2691
+ expected_packet_digest,
2692
+ require_admission=True,
2693
+ )
2694
+ if receipt_error:
2695
+ print(json.dumps({
2696
+ "status": "incomplete",
2697
+ "error_code": "admission_v1_receipt_required",
2698
+ "error": (
2699
+ "retained strict v1 is read-only and requires its exact existing "
2700
+ "ADMISSION.json receipt; start a fresh strict v2 review session "
2701
+ f"instead of creating or recreating it: {receipt_error}"
2702
+ ),
2703
+ }, sort_keys=True))
2704
+ return False
2705
+ binding = packet_admission_binding(retained)
2706
+ print(json.dumps({
2707
+ "status": "complete",
2708
+ "packet_digest": retained["digest"],
2709
+ "context_digest": retained["context_digest"],
2710
+ "subject_digest": binding["subject_digest"],
2711
+ "admission_digest": binding["admission_digest"],
2712
+ "publication": "existing",
2713
+ }, sort_keys=True))
2714
+ return True
2715
+ if admission_contract != ADMISSION_CONTRACT_V2:
2716
+ print(json.dumps({
2717
+ "status": "incomplete",
2718
+ "error_code": "admission_contract_required",
2719
+ "error": (
2720
+ "WORKER-PACKET.json admission_contract must be "
2721
+ f"{ADMISSION_CONTRACT_V2} for new strict v4 admission"
2722
+ ),
2723
+ }, sort_keys=True))
2724
+ return False
2725
+
2726
+ admission_path = session_path / "ADMISSION.json"
2727
+ try:
2728
+ os.lstat(admission_path)
2729
+ admission_missing = False
2730
+ except FileNotFoundError:
2731
+ admission_missing = True
2732
+ except OSError as observation_error:
2733
+ print(json.dumps({
2734
+ "status": "incomplete",
2735
+ "error_code": "admission_receipt_observation_failed",
2736
+ "error": f"ADMISSION.json presence could not be observed safely: {observation_error}",
2737
+ }, sort_keys=True))
2738
+ return False
2739
+ if admission_missing:
2740
+ existing_outputs = []
2741
+ output_paths = [
2742
+ *(worker["output"] for worker in packet["workers"]),
2743
+ f".ultra/reviews/{packet['data']['session']}/SUMMARY.json",
2744
+ ]
2745
+ for output_path in output_paths:
2746
+ try:
2747
+ os.lstat(packet["repo_root"] / output_path)
2748
+ existing_outputs.append(output_path)
2749
+ except FileNotFoundError:
2750
+ continue
2751
+ except OSError as observation_error:
2752
+ print(json.dumps({
2753
+ "status": "incomplete",
2754
+ "error_code": "admission_output_observation_failed",
2755
+ "error": (
2756
+ f"review output presence could not be observed safely for "
2757
+ f"{output_path}: {observation_error}"
2758
+ ),
2759
+ }, sort_keys=True))
2760
+ return False
2761
+ if existing_outputs:
2762
+ print(json.dumps({
2763
+ "status": "incomplete",
2764
+ "error_code": "admission_receipt_missing_after_outputs",
2765
+ "error": (
2766
+ "ADMISSION.json is missing after review output exists; the current "
2767
+ "strict session is INCOMPLETE and requires a fresh review session: "
2768
+ + ", ".join(existing_outputs)
2769
+ ),
2770
+ }, sort_keys=True))
2771
+ return False
2772
+
2773
+ packet, error = reload_worker_packet(
2774
+ session_path,
2775
+ expected_packet_digest,
2776
+ admit_subjects=True,
2777
+ )
2778
+ if error:
2779
+ print(json.dumps({"status": "incomplete", "error": error}, sort_keys=True))
2780
+ return False
2781
+ publication, error = atomic_write_admission(
2782
+ session_path,
2783
+ packet["admission"],
2784
+ )
2785
+ if error:
2786
+ print(json.dumps({
2787
+ "status": "incomplete",
2788
+ "error_code": error["code"],
2789
+ "error": error["message"],
2790
+ }, sort_keys=True))
2791
+ return False
2792
+ output = {
2793
+ "status": "complete",
2794
+ "packet_digest": packet["digest"],
2795
+ "context_digest": packet["context_digest"],
2796
+ "subject_digest": packet["admission"]["subject_digest"],
2797
+ "admission_digest": publication["digest"],
2798
+ "publication": publication["publication"],
2799
+ }
2800
+ if "durability_warning" in publication:
2801
+ output["durability_warning"] = publication["durability_warning"]
2802
+ print(json.dumps(output, sort_keys=True))
2803
+ return True
2804
+
2805
+
2806
+ def validate_legacy_packet_boundary(
2807
+ session_path: Path,
2808
+ expected_packet_digest: str,
2809
+ legacy_flag: str,
2810
+ ):
2811
+ repo_root, root_error = canonical_repo_root(session_path)
2812
+ if root_error:
2813
+ return f"WORKER-PACKET.json {root_error}"
2814
+ packet_repo_path = (
2815
+ f".ultra/reviews/{session_path.resolve().name}/WORKER-PACKET.json"
2816
+ )
2817
+ snapshot, error, missing = stable_read_repo_bytes(
2818
+ repo_root,
2819
+ packet_repo_path,
2820
+ "WORKER-PACKET.json",
2821
+ )
2822
+ if missing:
2823
+ return None
2824
+ if error:
2825
+ return error
2826
+ if snapshot["sha256"] != expected_packet_digest:
2827
+ return (
2828
+ "WORKER-PACKET.json bytes do not match expected immutable packet digest "
2829
+ f"{expected_packet_digest}; observed {snapshot['sha256']}"
2830
+ )
2831
+ packet, decode_error = decode_json_bytes(snapshot["bytes"], "WORKER-PACKET.json")
2832
+ if decode_error:
2833
+ return decode_error
2834
+ if "admission_contract" in packet:
2835
+ return (
2836
+ "WORKER-PACKET.json declares a strict admission_contract and cannot be "
2837
+ f"consumed with {legacy_flag}; retain and use its exact ADMISSION.json receipt"
2838
+ )
2839
+ return None
2840
+
2841
+
317
2842
  def main():
318
2843
  if len(sys.argv) < 3:
319
2844
  print(__doc__, file=sys.stderr)
@@ -329,22 +2854,87 @@ def main():
329
2854
  print("Error: --packet-digest requires the expected lowercase SHA-256 digest", file=sys.stderr)
330
2855
  sys.exit(2)
331
2856
  expected_packet_digest = sys.argv[4]
2857
+ arguments = sys.argv[5:]
2858
+ legacy_v3 = bool(arguments and arguments[0] == "--legacy-v3")
2859
+ legacy_v4 = bool(arguments and arguments[0] == "--legacy-v4")
2860
+ if legacy_v3 or legacy_v4:
2861
+ arguments = arguments[1:]
2862
+ findings_schema = LEGACY_FINDINGS_SCHEMA if legacy_v3 else FINDINGS_SCHEMA
2863
+ summary_schema = LEGACY_SUMMARY_SCHEMA if legacy_v3 else SUMMARY_SCHEMA
2864
+ require_admission = not legacy_v3 and not legacy_v4
2865
+
2866
+ if mode in {"agents", "summary"} and (legacy_v3 or legacy_v4):
2867
+ legacy_flag = "--legacy-v3" if legacy_v3 else "--legacy-v4"
2868
+ legacy_error = validate_legacy_packet_boundary(
2869
+ session_path,
2870
+ expected_packet_digest,
2871
+ legacy_flag,
2872
+ )
2873
+ if legacy_error:
2874
+ print(json.dumps({"status": "incomplete", "error": legacy_error}, sort_keys=True))
2875
+ sys.exit(1)
332
2876
 
333
- if mode == "agents":
334
- expected = sys.argv[5:]
2877
+ if mode == "packet":
2878
+ if legacy_v3 or legacy_v4 or arguments:
2879
+ print("Error: packet mode accepts only --packet-digest", file=sys.stderr)
2880
+ sys.exit(2)
2881
+ ok = validate_packet_preflight(session_path, expected_packet_digest)
2882
+ elif mode == "agents":
2883
+ expected = arguments
335
2884
  if not expected or len(expected) != len(set(expected)) or not all(
336
2885
  ARTIFACT_STEM.fullmatch(stem) for stem in expected
337
2886
  ):
338
2887
  print("Error: agents mode requires unique safe artifact stems", file=sys.stderr)
339
2888
  sys.exit(2)
340
- ok = wait_for_agents(session_path, expected, expected_packet_digest, DEFAULT_TIMEOUT)
2889
+ ok = wait_for_agents(
2890
+ session_path,
2891
+ expected,
2892
+ expected_packet_digest,
2893
+ findings_schema,
2894
+ DEFAULT_TIMEOUT,
2895
+ require_admission,
2896
+ legacy_v4,
2897
+ )
341
2898
  elif mode == "summary":
342
- if len(sys.argv) != 5:
343
- print("Error: summary mode accepts only --packet-digest", file=sys.stderr)
344
- sys.exit(2)
345
- ok = wait_for_summary(session_path, expected_packet_digest, DEFAULT_TIMEOUT)
2899
+ summary_snapshot_bytes = None
2900
+ expected_summary_digest = None
2901
+ if arguments:
2902
+ if (
2903
+ legacy_v3
2904
+ or len(arguments) != 2
2905
+ or arguments[0] != "--summary-snapshot-digest"
2906
+ or not digest_string(arguments[1])
2907
+ ):
2908
+ print(
2909
+ "Error: summary mode accepts optional --legacy-v3, --legacy-v4, or "
2910
+ "--summary-snapshot-digest <sha256>",
2911
+ file=sys.stderr,
2912
+ )
2913
+ sys.exit(2)
2914
+ expected_summary_digest = arguments[1]
2915
+ summary_snapshot_bytes = sys.stdin.buffer.read(MAX_JSON_BYTES + 1)
2916
+ if len(summary_snapshot_bytes) > MAX_JSON_BYTES:
2917
+ print(json.dumps({
2918
+ "status": "incomplete",
2919
+ "error": (
2920
+ "caller-owned SUMMARY snapshot exceeds the "
2921
+ f"{MAX_JSON_BYTES}-byte artifact limit"
2922
+ ),
2923
+ }, sort_keys=True))
2924
+ sys.exit(1)
2925
+ ok = wait_for_summary(
2926
+ session_path,
2927
+ expected_packet_digest,
2928
+ summary_schema,
2929
+ findings_schema,
2930
+ DEFAULT_TIMEOUT,
2931
+ summary_snapshot_bytes,
2932
+ expected_summary_digest,
2933
+ require_admission,
2934
+ legacy_v4,
2935
+ )
346
2936
  else:
347
- print(f"Error: unknown mode '{mode}'. Use 'agents' or 'summary'.", file=sys.stderr)
2937
+ print(f"Error: unknown mode '{mode}'. Use 'packet', 'agents', or 'summary'.", file=sys.stderr)
348
2938
  sys.exit(2)
349
2939
  sys.exit(0 if ok else 1)
350
2940