motionloom 2.2.0 → 2.4.0

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 (114) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +123 -17
  3. package/ROADMAP.md +8 -3
  4. package/SKILL.md +34 -3
  5. package/agent-card.json +48 -5
  6. package/agent-surfaces.json +8 -1
  7. package/artifact-adapter-registry.json +53 -0
  8. package/bin/motionloom.mjs +41 -4
  9. package/docs/AGENT-INTEGRATION.md +15 -2
  10. package/docs/CHECKLIST.md +21 -0
  11. package/docs/STATUS.md +2 -2
  12. package/docs/audits/data/deep-stress-latest.json +65 -65
  13. package/docs/releases/2.3.0.md +33 -0
  14. package/docs/releases/2.4.0.md +31 -0
  15. package/docs/releases/npm-publish-from-workstation.md +19 -8
  16. package/docs/research/agent-skill-ecosystem-notes.md +47 -0
  17. package/docs/research/ai-animation-tools-2026-notes.md +81 -0
  18. package/docs/research/ai-animation-tools-2026-report.md +168 -0
  19. package/docs/research/ai-pilot-ingest-notes.md +29 -0
  20. package/examples/agent-consumer/ai-generated-pilot/hero-male.json +10 -0
  21. package/examples/agent-consumer/ai-generated-pilot-provenance.json +55 -0
  22. package/examples/agent-consumer/ai-pilot-scout/CHATGPT-HANDOFF.md +70 -0
  23. package/examples/agent-consumer/ai-pilot-scout/CODEX-HANDOFF.md +80 -0
  24. package/examples/agent-consumer/ai-pilot-scout/README.md +39 -0
  25. package/examples/agent-consumer/ai-pilot-scout/chatgpt-geometry-correction-prompt.md +55 -0
  26. package/examples/agent-consumer/ai-pilot-scout/chatgpt-pose-generation-prompt.md +78 -0
  27. package/examples/agent-consumer/ai-pilot-scout/partial-handoff.json +73 -0
  28. package/examples/agent-consumer/artifact-intake/hero-motion-controls.json +15 -0
  29. package/examples/agent-consumer/artifact-intake/hero-motion-export.json +14 -0
  30. package/examples/agent-consumer/artifact-intake/hero-motion-provenance.json +20 -0
  31. package/examples/agent-consumer/artifact-intake/hero-motion-receipt.json +16 -0
  32. package/examples/agent-consumer/asset-consistency/assets/forest-back.png +0 -0
  33. package/examples/agent-consumer/asset-consistency/assets/forest-front.png +0 -0
  34. package/examples/agent-consumer/asset-consistency/assets/forest-mid.png +0 -0
  35. package/examples/agent-consumer/asset-consistency/assets/hero-atlas.png +0 -0
  36. package/examples/agent-consumer/asset-consistency/assets/hero-frame-00.png +0 -0
  37. package/examples/agent-consumer/asset-consistency/assets/hero-frame-01.png +0 -0
  38. package/examples/agent-consumer/asset-consistency/assets/hero-frame-02.png +0 -0
  39. package/examples/agent-consumer/asset-consistency/assets/hero-frame-03.png +0 -0
  40. package/examples/agent-consumer/asset-consistency/forest-layered-map.json +53 -0
  41. package/examples/agent-consumer/asset-consistency/hero-atlas-contract.json +24 -0
  42. package/examples/agent-consumer/asset-consistency/hero-identity.json +51 -0
  43. package/examples/agent-consumer/asset-consistency/hero-walk-action-set.json +27 -0
  44. package/examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json +63 -0
  45. package/examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json +13 -0
  46. package/examples/agent-consumer/rive-package-gate/README.md +20 -0
  47. package/examples/agent-consumer/runtime-candidate/hero-walk-candidate.json +18 -0
  48. package/examples/agent-consumer/runtime-pilot/action-set.json +19 -0
  49. package/examples/agent-consumer/runtime-pilot/asset-identity.json +44 -0
  50. package/examples/agent-consumer/runtime-pilot/candidate.json +21 -0
  51. package/examples/agent-consumer/runtime-pilot/controls.json +35 -0
  52. package/examples/agent-consumer/runtime-pilot/export.json +17 -0
  53. package/examples/agent-consumer/runtime-pilot/provenance.json +38 -0
  54. package/examples/agent-consumer/runtime-pilot/receipt.json +30 -0
  55. package/package.json +65 -4
  56. package/references/agent-interoperability.md +11 -0
  57. package/references/browser-review-contract.md +7 -1
  58. package/references/intelligence-core.md +29 -2
  59. package/rig-adapter-registry.json +39 -0
  60. package/schemas/action-set.schema.json +42 -0
  61. package/schemas/agent-surfaces.schema.json +1 -1
  62. package/schemas/artifact-adapter-registry.schema.json +16 -0
  63. package/schemas/asset-identity.schema.json +117 -0
  64. package/schemas/asset-provenance.schema.json +183 -0
  65. package/schemas/atlas-contract.schema.json +48 -0
  66. package/schemas/control-track.schema.json +18 -0
  67. package/schemas/export-manifest.schema.json +19 -0
  68. package/schemas/frame-geometry.schema.json +79 -0
  69. package/schemas/generation-receipt.schema.json +20 -0
  70. package/schemas/layered-map.schema.json +75 -0
  71. package/schemas/rig-adapter-registry.schema.json +14 -0
  72. package/schemas/rig-compatibility.schema.json +53 -0
  73. package/schemas/rive-package-manifest.schema.json +43 -0
  74. package/schemas/runtime-candidate.schema.json +48 -0
  75. package/schemas/scene-manifest.schema.json +19 -0
  76. package/scripts/artifact-intake.py +408 -0
  77. package/scripts/asset-consistency.py +517 -0
  78. package/scripts/asset-provenance.py +395 -0
  79. package/scripts/build-ai-pilot.py +504 -0
  80. package/scripts/docs-audit.py +14 -2
  81. package/scripts/isolate-alpha-background.py +147 -0
  82. package/scripts/pr.py +2 -0
  83. package/scripts/quality-gate.py +127 -3
  84. package/scripts/report.py +135 -0
  85. package/scripts/resolve-task-bundle.py +84 -0
  86. package/scripts/review-hook.py +26 -4
  87. package/scripts/rig-compatibility.py +251 -0
  88. package/scripts/rive-package-gate.py +245 -0
  89. package/scripts/runtime-candidate.py +301 -0
  90. package/scripts/setup.mjs +500 -0
  91. package/scripts/skill-doctor.py +2 -1
  92. package/src/output/browser-review-smoke/asset-provenance.json +77 -0
  93. package/src/output/browser-review-smoke/manifest.json +1 -0
  94. package/src/output/browser-review-smoke/visual-truth.json +3 -3
  95. package/src/output/runtime-pilot-framer/asset-provenance.json +38 -0
  96. package/src/output/runtime-pilot-framer/browser-review.json +20 -0
  97. package/src/output/runtime-pilot-framer/framer-motion/runtime-telemetry.json +103 -0
  98. package/src/output/runtime-pilot-framer/manifest.json +31 -0
  99. package/src/output/runtime-pilot-framer/motion-spec.json +25 -0
  100. package/src/output/runtime-pilot-framer/runtime-evidence.json +96 -0
  101. package/src/output/runtime-pilot-framer/runtime-telemetry.json +100 -0
  102. package/src/output/runtime-pilot-framer/scene.jsx +52 -0
  103. package/src/output/runtime-pilot-framer/snapshot/.render-meta.json +9 -0
  104. package/src/output/runtime-pilot-framer/snapshot/frame-00.png +0 -0
  105. package/src/output/runtime-pilot-framer/snapshot/frame-100.png +0 -0
  106. package/src/output/runtime-pilot-framer/snapshot/frame-50.png +0 -0
  107. package/tests/scripts/run_tests.py +131 -0
  108. package/tests/scripts/test_ai_pilot_builder.py +128 -0
  109. package/tests/scripts/test_alpha_isolation.py +60 -0
  110. package/tests/scripts/test_artifact_intake.py +146 -0
  111. package/tests/scripts/test_asset_consistency.py +199 -0
  112. package/tests/scripts/test_rig_compatibility.py +105 -0
  113. package/tests/scripts/test_rive_package_gate.py +101 -0
  114. package/tests/scripts/test_runtime_candidate.py +95 -0
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env python3
2
+ """Build a hash-bound, review-only MotionLoom AI scout pilot from four PNG frames.
3
+
4
+ The builder is intentionally pilot-specific. It copies source bytes into a local
5
+ ``.motionloom`` workspace, measures their actual alpha geometry using the same
6
+ deterministic reader as Asset Consistency, and writes the contracts that feed
7
+ Artifact Intake and Runtime Candidate. It never manufactures a human approval,
8
+ artist authority, or runtime-pass claim.
9
+
10
+ It uses Python's standard library only and runs on Ubuntu, macOS, and Windows.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import hashlib
17
+ import importlib.util
18
+ import json
19
+ import shutil
20
+ import sys
21
+ from datetime import UTC, datetime
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+
26
+ REPO_ROOT = Path(__file__).resolve().parents[1]
27
+ ASSET_ID = "character.scout-robot.ai-pilot"
28
+ PILOT_ID = "ai-pilot-scout-walk"
29
+ FRAME_ORDER = (
30
+ ("idle", "idle", "preview"),
31
+ ("contact-right", "contact_right", "runtime_candidate"),
32
+ ("passing", "passing", "runtime_candidate"),
33
+ ("contact-left", "contact_left", "runtime_candidate"),
34
+ )
35
+ PROVIDER_PROFILES = {
36
+ "internal-imagegen": {
37
+ "adapter_id": "internal.imagegen",
38
+ "kind": "internal_skill",
39
+ "invocation_mode": "agent-mediated",
40
+ "cost_class": "included",
41
+ "generator_source": "internal-imagegen",
42
+ "default_model": "default",
43
+ "default_task_id": "motionloom-scout-ai-pilot-ingest-2026",
44
+ "license_source": "Internal ImageGen generation; provider-native receipt was not exported",
45
+ "reconstruction_source": "agent-mediated ImageGen source",
46
+ "lab_limit": "Internal ImageGen adapter is scaffold-only",
47
+ },
48
+ "chatgpt": {
49
+ "adapter_id": "openai.chatgpt",
50
+ "kind": "external_provider",
51
+ "invocation_mode": "manual",
52
+ "cost_class": "external",
53
+ "generator_source": "openai-chatgpt",
54
+ "default_model": "chatgpt-image-generation",
55
+ "default_task_id": "motionloom-scout-chatgpt-pilot-2026",
56
+ "license_source": "User-provided ChatGPT generation; provider-native receipt was not exported",
57
+ "reconstruction_source": "user-mediated ChatGPT source",
58
+ "lab_limit": "ChatGPT import adapter is scaffold-only",
59
+ },
60
+ }
61
+
62
+
63
+ def sha256_file(path: Path) -> str:
64
+ return hashlib.sha256(path.read_bytes()).hexdigest()
65
+
66
+
67
+ def sha256_text(value: str) -> str:
68
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
69
+
70
+
71
+ def write_json(path: Path, value: dict[str, Any]) -> None:
72
+ path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
73
+
74
+
75
+ def load_png_reader() -> Any:
76
+ source = REPO_ROOT / "scripts" / "asset-consistency.py"
77
+ spec = importlib.util.spec_from_file_location("motionloom_pilot_png", source)
78
+ if spec is None or spec.loader is None:
79
+ raise RuntimeError(f"could not load deterministic PNG reader: {source}")
80
+ module = importlib.util.module_from_spec(spec)
81
+ sys.modules[spec.name] = module
82
+ spec.loader.exec_module(module)
83
+ return module.PNGImage
84
+
85
+
86
+ def parse_urls(values: list[str]) -> dict[str, str]:
87
+ result: dict[str, str] = {}
88
+ for value in values:
89
+ frame_id, separator, url = value.partition("=")
90
+ if not separator or frame_id not in {item[0] for item in FRAME_ORDER} or not url.startswith(("/", "https://", "http://")):
91
+ raise ValueError("--frame-url must look like idle=/manus-storage/frame.png")
92
+ result[frame_id] = url
93
+ return result
94
+
95
+
96
+ def measured_frame(PNGImage: Any, path: Path) -> dict[str, Any]:
97
+ image = PNGImage(path)
98
+ supports_alpha = image.color_type in {4, 6} or (image.color_type == 3 and bool(image._transparency))
99
+ if not supports_alpha:
100
+ raise ValueError(
101
+ f"{path.name} is color type {image.color_type} and has no alpha channel; "
102
+ "do not treat a painted checkerboard as transparency"
103
+ )
104
+ total = image.width * image.height
105
+ alpha_pixels = image.alpha_count(1)
106
+ if alpha_pixels == total:
107
+ raise ValueError(f"{path.name} has no transparent padding; an isolated alpha frame is required")
108
+ bbox = image.alpha_bbox(1)
109
+ if bbox is None:
110
+ raise ValueError(f"{path.name} has no visible alpha pixels")
111
+ return {
112
+ "width": image.width,
113
+ "height": image.height,
114
+ "alpha_pixels": alpha_pixels,
115
+ "alpha_bbox": bbox,
116
+ "sha256": sha256_file(path),
117
+ "bytes": path.stat().st_size,
118
+ }
119
+
120
+
121
+ def require_clean_pilot_geometry(frame_id: str, data: dict[str, Any]) -> None:
122
+ """Reject canvas-spanning residue for this isolated, single-character pilot.
123
+
124
+ Alpha isolation can deliberately remove only edge-connected background. A
125
+ detached band may therefore remain alpha-opaque without touching an edge.
126
+ The pilot has a narrow, centred single-character contract, so its measured
127
+ alpha bounds must retain padding on every side and cannot occupy most of a
128
+ canvas width. This is a geometry guard, not a human-quality approval.
129
+ """
130
+ bbox = data["alpha_bbox"]
131
+ width, height = int(data["width"]), int(data["height"])
132
+ minimum_padding = max(1, round(min(width, height) * 0.05))
133
+ paddings = {
134
+ "left": int(bbox["x"]),
135
+ "top": int(bbox["y"]),
136
+ "right": width - (int(bbox["x"]) + int(bbox["width"])),
137
+ "bottom": height - (int(bbox["y"]) + int(bbox["height"])),
138
+ }
139
+ if min(paddings.values()) < minimum_padding:
140
+ raise ValueError(
141
+ f"{frame_id} does not retain {minimum_padding}px clean padding on every edge; "
142
+ f"measured {paddings}. Reject possible canvas-spanning contamination."
143
+ )
144
+ if int(bbox["width"]) / width > 0.6:
145
+ raise ValueError(
146
+ f"{frame_id} alpha subject spans {int(bbox['width'])}/{width}px of the canvas width; "
147
+ "reject possible detached horizontal contamination."
148
+ )
149
+
150
+
151
+ def partial_frame_record(frame_id: str, source: Path, data: dict[str, Any] | None, error: str | None) -> dict[str, Any]:
152
+ """Describe a rejected source without emitting an ingestable candidate."""
153
+ record: dict[str, Any] = {
154
+ "role": frame_id,
155
+ "source": str(source),
156
+ "sha256": sha256_file(source),
157
+ "bytes": source.stat().st_size,
158
+ }
159
+ if data is not None:
160
+ bbox = data["alpha_bbox"]
161
+ record["canvas"] = [data["width"], data["height"]]
162
+ record["alpha_bbox"] = [bbox["x"], bbox["y"], bbox["width"], bbox["height"]]
163
+ record["padding"] = {
164
+ "left": bbox["x"],
165
+ "top": bbox["y"],
166
+ "right": data["width"] - (bbox["x"] + bbox["width"]),
167
+ "bottom": data["height"] - (bbox["y"] + bbox["height"]),
168
+ }
169
+ if error is not None:
170
+ record["error"] = error
171
+ return record
172
+
173
+
174
+ def write_partial_handoff(
175
+ output: Path,
176
+ provider: dict[str, str],
177
+ provider_model: str,
178
+ provider_task_id: str,
179
+ timestamp: str,
180
+ rejected_frames: list[dict[str, Any]],
181
+ errors: list[str],
182
+ ) -> None:
183
+ """Write source-bound refusal evidence; never create controls or a candidate."""
184
+ handoff = {
185
+ "contract": "motionloom-ai-pilot-partial-handoff",
186
+ "version": 1,
187
+ "asset_id": ASSET_ID,
188
+ "state": "partial",
189
+ "authority": "ai_generated",
190
+ "production_approved": False,
191
+ "created_at": timestamp,
192
+ "blocked": {
193
+ "reason": "Source preflight failed. No canvas resampling, transparent padding, metadata edit, intake, candidate, render, or approval was emitted.",
194
+ "errors": errors,
195
+ "missing_frames": [entry["role"] for entry in rejected_frames if entry.get("error")],
196
+ "not_emitted": [
197
+ "controls.json",
198
+ "generation receipt",
199
+ "export manifest",
200
+ "asset consistency contract",
201
+ "Artifact Intake report",
202
+ "runtime candidate",
203
+ "runtime render evidence",
204
+ "Dev Lab review candidate",
205
+ ],
206
+ },
207
+ "rejected_source_attempts": [
208
+ {
209
+ "provider": provider["generator_source"],
210
+ "adapter_id": provider["adapter_id"],
211
+ "model": provider_model,
212
+ "task_id": provider_task_id,
213
+ "authority": "ai_generated",
214
+ "state": "rejected_pre_ingest",
215
+ "reason": "Measured source bytes failed the pilot preflight; no transform may bypass the gate.",
216
+ "frames": rejected_frames,
217
+ }
218
+ ],
219
+ "resume_requirements": [
220
+ "Provide independent source frames that satisfy the measured canvas and clean-padding requirements.",
221
+ "Do not resize, pad, crop, isolate, or relabel rejected bytes to bypass the source gate.",
222
+ "Re-run build-ai-pilot and downstream pipeline stages only after preflight succeeds.",
223
+ ],
224
+ }
225
+ write_json(output / "partial-handoff.json", handoff)
226
+
227
+
228
+ def frame_entry(frame_id: str, role: str, target: str, file_name: str, data: dict[str, Any], root_relative_output: str) -> dict[str, Any]:
229
+ return {
230
+ "path": f"{root_relative_output}/frames/{file_name}",
231
+ "role": f"walk_{role}",
232
+ "sha256": data["sha256"],
233
+ "bytes": data["bytes"],
234
+ "target": target,
235
+ }
236
+
237
+
238
+ def main(argv: list[str] | None = None) -> int:
239
+ parser = argparse.ArgumentParser(description="Build MotionLoom's review-only scout AI pilot workspace")
240
+ parser.add_argument("--idle", required=True, type=Path, help="Alpha-isolated idle PNG")
241
+ parser.add_argument("--contact-right", required=True, type=Path, help="Alpha-isolated contact-right PNG")
242
+ parser.add_argument("--passing", required=True, type=Path, help="Alpha-isolated passing PNG")
243
+ parser.add_argument("--contact-left", required=True, type=Path, help="Alpha-isolated contact-left PNG")
244
+ parser.add_argument("--root", type=Path, default=REPO_ROOT, help="MotionLoom repository root")
245
+ parser.add_argument(
246
+ "--output",
247
+ default=".motionloom/pilots/ai-pilot-scout",
248
+ help="Safe relative output directory inside --root",
249
+ )
250
+ parser.add_argument("--frame-url", action="append", default=[], help="Optional frame URL: idle=/manus-storage/frame.png")
251
+ parser.add_argument("--generated-at", default=None, help="ISO-8601 evidence timestamp; defaults to build time")
252
+ parser.add_argument("--provider", choices=sorted(PROVIDER_PROFILES), default="internal-imagegen", help="Truthful source provider profile")
253
+ parser.add_argument("--provider-model", default=None, help="Provider/model label for provenance; defaults to profile value")
254
+ parser.add_argument("--provider-task-id", default=None, help="Provider or user-mediated task identifier for provenance")
255
+ parser.add_argument("--overwrite", action="store_true", help="Replace an existing pilot workspace")
256
+ args = parser.parse_args(argv)
257
+
258
+ root = args.root.resolve()
259
+ output = (root / args.output).resolve()
260
+ try:
261
+ output.relative_to(root)
262
+ except ValueError as exc:
263
+ raise SystemExit("--output must stay inside --root") from exc
264
+ if output.exists():
265
+ if not args.overwrite:
266
+ raise SystemExit(f"pilot workspace already exists: {output}; pass --overwrite to replace it")
267
+ shutil.rmtree(output)
268
+ urls = parse_urls(args.frame_url)
269
+ timestamp = args.generated_at or datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
270
+ provider = PROVIDER_PROFILES[args.provider]
271
+ provider_model = args.provider_model or provider["default_model"]
272
+ provider_task_id = args.provider_task_id or provider["default_task_id"]
273
+
274
+ sources = {
275
+ "idle": args.idle,
276
+ "contact-right": args.contact_right,
277
+ "passing": args.passing,
278
+ "contact-left": args.contact_left,
279
+ }
280
+ for frame_id, source in sources.items():
281
+ if not source.is_file():
282
+ raise SystemExit(f"missing {frame_id} source image: {source}")
283
+
284
+ output.mkdir(parents=True)
285
+ frames_dir = output / "frames"
286
+ frames_dir.mkdir()
287
+ PNGImage = load_png_reader()
288
+ frames: dict[str, dict[str, Any]] = {}
289
+ rejected_frames: list[dict[str, Any]] = []
290
+ preflight_errors: list[str] = []
291
+ for frame_id, role, _ in FRAME_ORDER:
292
+ destination = frames_dir / f"scout-{frame_id}.png"
293
+ shutil.copy2(sources[frame_id], destination)
294
+ data: dict[str, Any] | None = None
295
+ try:
296
+ data = measured_frame(PNGImage, destination)
297
+ require_clean_pilot_geometry(frame_id, data)
298
+ except ValueError as exc:
299
+ error = str(exc)
300
+ preflight_errors.append(error)
301
+ rejected_frames.append(partial_frame_record(frame_id, destination, data, error))
302
+ continue
303
+ assert data is not None
304
+ frames[frame_id] = data
305
+ rejected_frames.append(partial_frame_record(frame_id, destination, data, None))
306
+
307
+ if not preflight_errors:
308
+ canvas_sizes = {(item["width"], item["height"]) for item in frames.values()}
309
+ if len(canvas_sizes) != 1:
310
+ preflight_errors.append(f"all frames must use one canvas; measured {sorted(canvas_sizes)}")
311
+
312
+ if preflight_errors:
313
+ write_partial_handoff(output, provider, provider_model, provider_task_id, timestamp, rejected_frames, preflight_errors)
314
+ print(json.dumps({"status": "partial", "root": str(root), "output": output.relative_to(root).as_posix(), "asset_id": ASSET_ID, "errors": preflight_errors}, indent=2))
315
+ return 2
316
+
317
+ canvas_sizes = {(item["width"], item["height"]) for item in frames.values()}
318
+ width, height = next(iter(canvas_sizes))
319
+ alpha_bboxes = [item["alpha_bbox"] for item in frames.values()]
320
+ footlines = [bbox["y"] + bbox["height"] - 1 for bbox in alpha_bboxes]
321
+ pivots = [(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] - 1) for bbox in alpha_bboxes]
322
+ reference_pivot = pivots[0]
323
+ pivot_tolerance = max(max(abs(x - reference_pivot[0]), abs(y - reference_pivot[1])) for x, y in pivots)
324
+ footline_tolerance = max(abs(value - footlines[0]) for value in footlines)
325
+ bbox_tolerance = max(
326
+ max(abs(item["width"] - alpha_bboxes[0]["width"]), abs(item["height"] - alpha_bboxes[0]["height"]))
327
+ for item in alpha_bboxes
328
+ )
329
+ min_alpha = min(int(item["alpha_pixels"]) for item in frames.values())
330
+ idle_hash = frames["idle"]["sha256"]
331
+ prompt_capsule = (
332
+ f"MotionLoom scout robot AI pilot imported from {provider['generator_source']}. "
333
+ "Post-hoc ingest capsule only; provider-native prompt and task receipt were not exported. "
334
+ "The capsule binds reviewed output bytes without asserting a human approval or artist authorship."
335
+ )
336
+ prompt_hash = sha256_text(prompt_capsule)
337
+ motion_hash = sha256_text("walk_pilot_4frame:idle,contact-right,passing,contact-left")
338
+ style_hash = sha256_text("cream-orange-black-cyan pixel-art scout robot style lock")
339
+
340
+ relative_output = output.relative_to(root).as_posix()
341
+ geometry_frames: list[dict[str, Any]] = []
342
+ receipt_outputs: list[dict[str, Any]] = []
343
+ export_outputs: list[dict[str, Any]] = []
344
+ provenance_files: list[dict[str, Any]] = []
345
+ for index, (frame_id, role, target) in enumerate(FRAME_ORDER):
346
+ data = frames[frame_id]
347
+ bbox = data["alpha_bbox"]
348
+ pivot = pivots[index]
349
+ filename = f"scout-{frame_id}.png"
350
+ geometry_frames.append(
351
+ {
352
+ "frame_id": f"scout-{frame_id}",
353
+ "image": f"frames/{filename}",
354
+ "rect": {"x": 0, "y": 0, "width": width, "height": height},
355
+ "alpha_bbox": bbox,
356
+ "pivot": {"x": pivot[0], "y": pivot[1], "space": "pixels"},
357
+ "footline_px": footlines[index],
358
+ "safe_rect": bbox,
359
+ "bleed_margin_px": 0,
360
+ "sha256": data["sha256"],
361
+ }
362
+ )
363
+ record = frame_entry(frame_id, role, target, filename, data, relative_output)
364
+ receipt_outputs.append({key: value for key, value in record.items() if key != "target"})
365
+ export_outputs.append(record)
366
+ provenance_files.append(
367
+ {
368
+ "path": record["path"],
369
+ "role": record["role"],
370
+ "sha256": record["sha256"],
371
+ "bytes": record["bytes"],
372
+ }
373
+ )
374
+
375
+ controls = {
376
+ "schema_version": "0.1",
377
+ "control_id": "scout-ai-pilot-controls-v1",
378
+ "asset_id": ASSET_ID,
379
+ "created_at": timestamp,
380
+ "references": [{"id": "scout-idle-master", "path": f"{relative_output}/frames/scout-idle.png", "sha256": idle_hash, "role": "identity"}],
381
+ "tracks": [
382
+ {"id": "identity-lock", "kind": "identity", "binding": "required", "value_hash": idle_hash, "samples": 1},
383
+ {"id": "style-lock", "kind": "style", "binding": "advisory", "value_hash": style_hash, "samples": 4},
384
+ {"id": "pose-motion", "kind": "pose", "binding": "required", "value_hash": motion_hash, "samples": 4},
385
+ ],
386
+ "output_profile": {"kind": "frame_sequence", "fps": 8, "expected_frame_count": 4, "loop": True},
387
+ }
388
+ provenance = {
389
+ "schema_version": "1.0",
390
+ "provenance_id": "scout-ai-pilot-provenance-v1",
391
+ "task_id": provider_task_id,
392
+ "scene": "scout-walk",
393
+ "created_at": timestamp,
394
+ "asset": {"id": ASSET_ID, "path": f"{relative_output}/frames/scout-idle.png", "type": "frame_sequence", "framework": "canvas", "version": "pilot-v1"},
395
+ "authority": "ai_generated",
396
+ "readiness": "runtime_ready",
397
+ "generator": {"model": provider_model, "task_id": provider_task_id, "source": provider["generator_source"], "generated_at": timestamp, "prompt_hash": prompt_hash, "agent": "MotionLoom"},
398
+ "license": {"spdx": "LicenseRef-Generated-Pilot", "source": provider["license_source"], "attribution": "AI-generated scout pilot; human review remains required"},
399
+ "files": provenance_files,
400
+ "provenance_chain": [{"step": "ingest", "actor": "MotionLoom", "source": f"post-hoc hash-bound local ingest from {provider['reconstruction_source']}; not a provider-native receipt", "timestamp": timestamp}],
401
+ "runtime_evidence": {"status": "not_run", "runtime": "canvas", "tested_at": timestamp},
402
+ }
403
+ receipt = {
404
+ "schema_version": "0.1",
405
+ "receipt_id": "scout-ai-pilot-receipt-v1",
406
+ "created_at": timestamp,
407
+ "asset": {"id": ASSET_ID, "kind": "frame_sequence", "intended_use": "runtime_candidate"},
408
+ "authority": "ai_generated",
409
+ "provider": {"adapter_id": provider["adapter_id"], "kind": provider["kind"], "invocation_mode": provider["invocation_mode"], "task_id": provider_task_id, "model": provider_model, "generated_at": timestamp, "cost_class": provider["cost_class"], "prompt_hash": prompt_hash},
410
+ "control_track_ref": f"{relative_output}/controls.json",
411
+ "provenance_ref": f"{relative_output}/provenance.json",
412
+ "outputs": receipt_outputs,
413
+ }
414
+ export = {
415
+ "schema_version": "0.1",
416
+ "manifest_id": "scout-ai-pilot-export-v1",
417
+ "asset_id": ASSET_ID,
418
+ "created_at": timestamp,
419
+ "receipt_ref": f"{relative_output}/receipt.json",
420
+ "control_track_ref": f"{relative_output}/controls.json",
421
+ "outputs": export_outputs,
422
+ }
423
+ identity = {
424
+ "schema_version": "0.1",
425
+ "asset_id": ASSET_ID,
426
+ "asset_kind": "character",
427
+ "identity": {
428
+ "subject_id": "scout-robot",
429
+ "reference_hashes": [idle_hash],
430
+ "camera": {"projection": "orthographic", "width": width, "height": height, "focal_length": 1},
431
+ "coordinate_system": "screen_y_down",
432
+ "scale": 1,
433
+ "pivot": {"x": reference_pivot[0] / width, "y": reference_pivot[1] / height, "space": "normalized_canvas"},
434
+ "palette_lock": {"colors": ["#f7e4c6", "#f47c15", "#121116", "#18d8f8"], "delta_e_tolerance": 28},
435
+ "style_profile": "pixel-art scout robot pilot",
436
+ "lighting_profile": "flat game sprite lighting",
437
+ },
438
+ "derivation": {"origin": "ai_generated", "generator": {"model": provider_model, "task_id": provider_task_id, "prompt_hash": prompt_hash}, "source_refs": ["provenance.json", "receipt-reconstruction.json"]},
439
+ "provenance_ref": "provenance.json",
440
+ "notes": "Identity controls are a post-hoc ingest representation. They bind output hashes but do not prove provider-native generation controls.",
441
+ }
442
+ action_set = {
443
+ "schema_version": "0.1",
444
+ "asset_identity": ASSET_ID,
445
+ "actions": [{"action_id": "walk-pilot", "fps": 8, "frames": [f"frames/scout-{frame_id}.png" for frame_id, _, _ in FRAME_ORDER], "loop": True, "events": ["foot_contact_right", "passing", "foot_contact_left"]}],
446
+ "invariants": {"canvas_width": width, "canvas_height": height, "pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance, "min_alpha_pixels": min_alpha},
447
+ }
448
+ frame_geometry = {
449
+ "schema_version": "0.1",
450
+ "asset_identity": ASSET_ID,
451
+ "canvas": {"width": width, "height": height, "color_space": "srgb", "alpha_mode": "straight"},
452
+ "invariants": {"pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance, "min_alpha_pixels": min_alpha, "allow_external_opaque_pixels": False},
453
+ "frames": geometry_frames,
454
+ }
455
+ candidate = {
456
+ "schema_version": "0.1",
457
+ "candidate_id": "scout-ai-pilot-candidate-v1",
458
+ "asset_id": ASSET_ID,
459
+ "created_at": timestamp,
460
+ "artifact_intake": {"registry": "artifact-adapter-registry.json", "receipt": f"{relative_output}/receipt.json", "controls": f"{relative_output}/controls.json", "export_manifest": f"{relative_output}/export.json"},
461
+ "consistency": {"asset_identity": f"{relative_output}/asset-identity.json", "action_set": f"{relative_output}/action-set.json", "frame_geometry": f"{relative_output}/frame-geometry.json"},
462
+ "runtime": {"target": "canvas", "review_required": True},
463
+ }
464
+ reconstruction = {
465
+ "artifact": ASSET_ID,
466
+ "status": "incomplete_provider_receipt",
467
+ "created_at": timestamp,
468
+ "known": ["output bytes", "output SHA-256", provider["reconstruction_source"], "post-hoc control capsule hash"],
469
+ "unknown": ["provider-native request ID", "provider-native prompt record", "provider-native seed", "provider-native output receipt"],
470
+ "governance": {"authority": "ai_generated", "human_review": "required", "production_approved": False, "attestation_approval": False},
471
+ }
472
+ lab_evidence = {
473
+ "pilot_id": PILOT_ID,
474
+ "asset_id": ASSET_ID,
475
+ "created_at": timestamp,
476
+ "state": "review_required",
477
+ "authority": "ai_generated",
478
+ "production_approved": False,
479
+ "runtime_verified": False,
480
+ "limits": [provider["lab_limit"], "Provider-native generation receipt was unavailable", "Runtime proof is not yet present", "No human approval exists"],
481
+ "frames": [
482
+ {"id": frame_id, "label": role.replace("_", " "), "path": f"frames/scout-{frame_id}.png", "url": urls.get(frame_id), "sha256": frames[frame_id]["sha256"], "alpha_bbox": frames[frame_id]["alpha_bbox"]}
483
+ for frame_id, role, _ in FRAME_ORDER
484
+ ],
485
+ }
486
+ for filename, document in (
487
+ ("controls.json", controls),
488
+ ("provenance.json", provenance),
489
+ ("receipt.json", receipt),
490
+ ("export.json", export),
491
+ ("asset-identity.json", identity),
492
+ ("action-set.json", action_set),
493
+ ("frame-geometry.json", frame_geometry),
494
+ ("candidate.json", candidate),
495
+ ("receipt-reconstruction.json", reconstruction),
496
+ ("devlab-pilot-evidence.json", lab_evidence),
497
+ ):
498
+ write_json(output / filename, document)
499
+ print(json.dumps({"status": "built", "root": str(root), "output": relative_output, "asset_id": ASSET_ID, "frame_count": len(frames), "alpha_geometry": {"footline_range": [min(footlines), max(footlines)], "pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance}}, indent=2))
500
+ return 0
501
+
502
+
503
+ if __name__ == "__main__":
504
+ raise SystemExit(main())
@@ -23,7 +23,7 @@ for markdown in sorted(ROOT.rglob("*.md")):
23
23
  if not (markdown.parent / target).resolve().exists():
24
24
  errors.append(f"{markdown.relative_to(ROOT)} -> missing {target}")
25
25
 
26
- for relative in ["package.json", "agent-card.json", "agent-surfaces.json", "schemas/agent-surfaces.schema.json", "schemas/visual-truth.schema.json", "schemas/remediation-history.schema.json", "project-context.example.json", "tests/evals/project-corpus.json"]:
26
+ for relative in ["package.json", "agent-card.json", "agent-surfaces.json", "schemas/agent-surfaces.schema.json", "schemas/provenance.schema.json", "schemas/asset-provenance.schema.json", "schemas/scene-manifest.schema.json", "schemas/visual-truth.schema.json", "schemas/remediation-history.schema.json", "project-context.example.json", "examples/agent-consumer/ai-generated-pilot-provenance.json", "tests/evals/project-corpus.json"]:
27
27
  path = ROOT / relative
28
28
  try:
29
29
  json.loads(path.read_text(encoding="utf-8"))
@@ -39,6 +39,12 @@ if package.get("packageManager") != "pnpm@11.20.0":
39
39
  for required_surface in [".agents", ".claude", ".codex", "AGENTS.md", "agent-surfaces.json"]:
40
40
  if required_surface not in package.get("files", []):
41
41
  errors.append(f"package.json: files must include Agent surface {required_surface}")
42
+ for required_path in ["scripts/asset-provenance.py", "schemas/asset-provenance.schema.json", "examples/agent-consumer/ai-generated-pilot-provenance.json"]:
43
+ if required_path not in package.get("files", []):
44
+ errors.append(f"package.json: files must include asset provenance contract {required_path}")
45
+ for onboarding_script in ["setup", "setup:dry", "status", "repair"]:
46
+ if onboarding_script not in package.get("scripts", {}):
47
+ errors.append(f"package.json: missing onboarding script {onboarding_script}")
42
48
 
43
49
  sys.path.insert(0, str(ROOT))
44
50
  try:
@@ -54,9 +60,13 @@ for required_doc in ["docs/AGENT-INTEGRATION.md", "references/agent-interoperabi
54
60
  errors.append(f"missing Agent interoperability document: {required_doc}")
55
61
 
56
62
  readme = (ROOT / "README.md").read_text(encoding="utf-8")
57
- for heading in ["Why MotionLoom", "Quick start", "Durable Project Memory", "Evidence, trust and review", "Documentation map"]:
63
+ for heading in ["Why MotionLoom", "Quick start", "Durable Project Memory", "Evidence, trust and review", "Asset provenance tiers", "Documentation map"]:
58
64
  if f"## {heading}" not in readme:
59
65
  errors.append(f"README.md: missing heading {heading}")
66
+ if "npx --yes motionloom setup" not in readme:
67
+ errors.append("README.md: missing one-command onboarding recipe")
68
+ if "npx --yes motionloom setup" not in (ROOT / "docs/AGENT-INTEGRATION.md").read_text(encoding="utf-8"):
69
+ errors.append("docs/AGENT-INTEGRATION.md: missing one-command onboarding recipe")
60
70
 
61
71
  workflow_dir = ROOT / ".github" / "workflows"
62
72
  for workflow in sorted(workflow_dir.glob("*.yml")):
@@ -77,6 +87,8 @@ for workflow in sorted(workflow_dir.glob("*.yml")):
77
87
  for required in ["name:", "on:", "jobs:", "permissions:"]:
78
88
  if required not in text:
79
89
  errors.append(f"{workflow.relative_to(ROOT)}: missing {required}")
90
+ if "--require-asset-provenance" not in (workflow_dir / "quality.yml").read_text(encoding="utf-8"):
91
+ errors.append("quality.yml: missing fail-closed asset provenance production gate")
80
92
  if "pull_request:" in text and "secrets." in text:
81
93
  errors.append(f"{workflow.relative_to(ROOT)}: secrets referenced in pull_request workflow")
82
94