motionloom 2.3.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 (102) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +78 -11
  3. package/ROADMAP.md +8 -5
  4. package/SKILL.md +24 -5
  5. package/agent-card.json +31 -5
  6. package/agent-surfaces.json +1 -1
  7. package/artifact-adapter-registry.json +53 -0
  8. package/bin/motionloom.mjs +29 -5
  9. package/docs/CHECKLIST.md +16 -0
  10. package/docs/STATUS.md +2 -2
  11. package/docs/audits/data/deep-stress-latest.json +65 -65
  12. package/docs/releases/2.4.0.md +31 -0
  13. package/docs/releases/npm-publish-from-workstation.md +19 -8
  14. package/docs/research/agent-skill-ecosystem-notes.md +47 -0
  15. package/docs/research/ai-animation-tools-2026-notes.md +81 -0
  16. package/docs/research/ai-animation-tools-2026-report.md +168 -0
  17. package/docs/research/ai-pilot-ingest-notes.md +29 -0
  18. package/examples/agent-consumer/ai-pilot-scout/CHATGPT-HANDOFF.md +70 -0
  19. package/examples/agent-consumer/ai-pilot-scout/CODEX-HANDOFF.md +80 -0
  20. package/examples/agent-consumer/ai-pilot-scout/README.md +39 -0
  21. package/examples/agent-consumer/ai-pilot-scout/chatgpt-geometry-correction-prompt.md +55 -0
  22. package/examples/agent-consumer/ai-pilot-scout/chatgpt-pose-generation-prompt.md +78 -0
  23. package/examples/agent-consumer/ai-pilot-scout/partial-handoff.json +73 -0
  24. package/examples/agent-consumer/artifact-intake/hero-motion-controls.json +15 -0
  25. package/examples/agent-consumer/artifact-intake/hero-motion-export.json +14 -0
  26. package/examples/agent-consumer/artifact-intake/hero-motion-provenance.json +20 -0
  27. package/examples/agent-consumer/artifact-intake/hero-motion-receipt.json +16 -0
  28. package/examples/agent-consumer/asset-consistency/assets/forest-back.png +0 -0
  29. package/examples/agent-consumer/asset-consistency/assets/forest-front.png +0 -0
  30. package/examples/agent-consumer/asset-consistency/assets/forest-mid.png +0 -0
  31. package/examples/agent-consumer/asset-consistency/assets/hero-atlas.png +0 -0
  32. package/examples/agent-consumer/asset-consistency/assets/hero-frame-00.png +0 -0
  33. package/examples/agent-consumer/asset-consistency/assets/hero-frame-01.png +0 -0
  34. package/examples/agent-consumer/asset-consistency/assets/hero-frame-02.png +0 -0
  35. package/examples/agent-consumer/asset-consistency/assets/hero-frame-03.png +0 -0
  36. package/examples/agent-consumer/asset-consistency/forest-layered-map.json +53 -0
  37. package/examples/agent-consumer/asset-consistency/hero-atlas-contract.json +24 -0
  38. package/examples/agent-consumer/asset-consistency/hero-identity.json +51 -0
  39. package/examples/agent-consumer/asset-consistency/hero-walk-action-set.json +27 -0
  40. package/examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json +63 -0
  41. package/examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json +13 -0
  42. package/examples/agent-consumer/rive-package-gate/README.md +20 -0
  43. package/examples/agent-consumer/runtime-candidate/hero-walk-candidate.json +18 -0
  44. package/examples/agent-consumer/runtime-pilot/action-set.json +19 -0
  45. package/examples/agent-consumer/runtime-pilot/asset-identity.json +44 -0
  46. package/examples/agent-consumer/runtime-pilot/candidate.json +21 -0
  47. package/examples/agent-consumer/runtime-pilot/controls.json +35 -0
  48. package/examples/agent-consumer/runtime-pilot/export.json +17 -0
  49. package/examples/agent-consumer/runtime-pilot/provenance.json +38 -0
  50. package/examples/agent-consumer/runtime-pilot/receipt.json +30 -0
  51. package/package.json +56 -4
  52. package/references/browser-review-contract.md +7 -1
  53. package/references/intelligence-core.md +22 -1
  54. package/rig-adapter-registry.json +39 -0
  55. package/schemas/action-set.schema.json +42 -0
  56. package/schemas/artifact-adapter-registry.schema.json +16 -0
  57. package/schemas/asset-identity.schema.json +117 -0
  58. package/schemas/asset-provenance.schema.json +1 -1
  59. package/schemas/atlas-contract.schema.json +48 -0
  60. package/schemas/control-track.schema.json +18 -0
  61. package/schemas/export-manifest.schema.json +19 -0
  62. package/schemas/frame-geometry.schema.json +79 -0
  63. package/schemas/generation-receipt.schema.json +20 -0
  64. package/schemas/layered-map.schema.json +75 -0
  65. package/schemas/rig-adapter-registry.schema.json +14 -0
  66. package/schemas/rig-compatibility.schema.json +53 -0
  67. package/schemas/rive-package-manifest.schema.json +43 -0
  68. package/schemas/runtime-candidate.schema.json +48 -0
  69. package/schemas/scene-manifest.schema.json +18 -0
  70. package/scripts/artifact-intake.py +408 -0
  71. package/scripts/asset-consistency.py +517 -0
  72. package/scripts/asset-provenance.py +8 -3
  73. package/scripts/build-ai-pilot.py +504 -0
  74. package/scripts/isolate-alpha-background.py +147 -0
  75. package/scripts/quality-gate.py +88 -2
  76. package/scripts/report.py +85 -0
  77. package/scripts/resolve-task-bundle.py +84 -0
  78. package/scripts/review-hook.py +26 -4
  79. package/scripts/rig-compatibility.py +251 -0
  80. package/scripts/rive-package-gate.py +245 -0
  81. package/scripts/runtime-candidate.py +301 -0
  82. package/scripts/setup.mjs +54 -26
  83. package/src/output/runtime-pilot-framer/asset-provenance.json +38 -0
  84. package/src/output/runtime-pilot-framer/browser-review.json +20 -0
  85. package/src/output/runtime-pilot-framer/framer-motion/runtime-telemetry.json +103 -0
  86. package/src/output/runtime-pilot-framer/manifest.json +31 -0
  87. package/src/output/runtime-pilot-framer/motion-spec.json +25 -0
  88. package/src/output/runtime-pilot-framer/runtime-evidence.json +96 -0
  89. package/src/output/runtime-pilot-framer/runtime-telemetry.json +100 -0
  90. package/src/output/runtime-pilot-framer/scene.jsx +52 -0
  91. package/src/output/runtime-pilot-framer/snapshot/.render-meta.json +9 -0
  92. package/src/output/runtime-pilot-framer/snapshot/frame-00.png +0 -0
  93. package/src/output/runtime-pilot-framer/snapshot/frame-100.png +0 -0
  94. package/src/output/runtime-pilot-framer/snapshot/frame-50.png +0 -0
  95. package/tests/scripts/run_tests.py +107 -0
  96. package/tests/scripts/test_ai_pilot_builder.py +128 -0
  97. package/tests/scripts/test_alpha_isolation.py +60 -0
  98. package/tests/scripts/test_artifact_intake.py +146 -0
  99. package/tests/scripts/test_asset_consistency.py +199 -0
  100. package/tests/scripts/test_rig_compatibility.py +105 -0
  101. package/tests/scripts/test_rive_package_gate.py +101 -0
  102. package/tests/scripts/test_runtime_candidate.py +95 -0
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python3
2
+ """MotionLoom control-to-runtime candidate bridge.
3
+
4
+ This contract links a provider-neutral Artifact Intake bundle to the existing
5
+ identity/action/frame/atlas/map contracts. It is deterministic, standard
6
+ library only, and intentionally reports runtime-test readiness rather than any
7
+ form of production approval or user review outcome.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import hashlib
14
+ import importlib.util
15
+ import json
16
+ import re
17
+ import sys
18
+ from dataclasses import asdict, dataclass
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+
24
+ ROOT = Path(__file__).resolve().parents[1]
25
+ SAFE = re.compile(r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$)).+")
26
+ SHA256 = re.compile(r"^[a-f0-9]{64}$")
27
+ KINDS = {"asset_identity": "identity", "action_set": "action-set", "frame_geometry": "frame-geometry", "atlas": "atlas", "layered_map": "layered-map"}
28
+ PROFILE_REQUIREMENTS = {
29
+ "image": {"asset_identity"},
30
+ "frame_sequence": {"asset_identity", "action_set", "frame_geometry"},
31
+ "sprite_atlas": {"asset_identity", "action_set", "frame_geometry", "atlas"},
32
+ "layered_map": {"asset_identity", "layered_map"},
33
+ "video": {"asset_identity", "action_set"},
34
+ "rigged_2d": {"asset_identity", "action_set"},
35
+ "rigged_3d": {"asset_identity", "action_set"},
36
+ "runtime_scene": {"asset_identity", "action_set"},
37
+ }
38
+
39
+
40
+ @dataclass
41
+ class Issue:
42
+ severity: str
43
+ code: str
44
+ message: str
45
+ path: str = ""
46
+
47
+
48
+ def issue(issues: list[Issue], code: str, message: str, path: str = "", severity: str = "error") -> None:
49
+ issues.append(Issue(severity, code, message, path))
50
+
51
+
52
+ def safe(value: Any) -> bool:
53
+ return isinstance(value, str) and "\\" not in value and bool(SAFE.fullmatch(value))
54
+
55
+
56
+ def resolve(root: Path, value: Any) -> Path | None:
57
+ if not safe(value):
58
+ return None
59
+ raw = root / str(value)
60
+ try:
61
+ path = raw.resolve()
62
+ path.relative_to(root.resolve())
63
+ except ValueError:
64
+ return None
65
+ return path if not raw.is_symlink() and path.is_file() else None
66
+
67
+
68
+ def load_json(path: Path, issues: list[Issue], label: str) -> dict[str, Any] | None:
69
+ try:
70
+ value = json.loads(path.read_text(encoding="utf-8"))
71
+ except (OSError, json.JSONDecodeError) as exc:
72
+ issue(issues, "invalid_json", f"{label}: {exc}", str(path))
73
+ return None
74
+ if not isinstance(value, dict):
75
+ issue(issues, "invalid_document", f"{label} root must be an object", str(path))
76
+ return None
77
+ return value
78
+
79
+
80
+ def digest(path: Path) -> str:
81
+ return hashlib.sha256(path.read_bytes()).hexdigest()
82
+
83
+
84
+ def load_module(name: str, path: Path) -> Any:
85
+ spec = importlib.util.spec_from_file_location(name, path)
86
+ if spec is None or spec.loader is None:
87
+ raise RuntimeError(f"cannot load {path}")
88
+ module = importlib.util.module_from_spec(spec)
89
+ sys.modules[name] = module
90
+ spec.loader.exec_module(module)
91
+ return module
92
+
93
+
94
+ def required(document: dict[str, Any], fields: tuple[str, ...], prefix: str, issues: list[Issue]) -> None:
95
+ for field in fields:
96
+ if field not in document:
97
+ issue(issues, "missing_field", f"{prefix}.{field} is required", f"{prefix}.{field}")
98
+
99
+
100
+ def valid_datetime(value: Any) -> bool:
101
+ try:
102
+ datetime.fromisoformat(str(value).replace("Z", "+00:00"))
103
+ return True
104
+ except ValueError:
105
+ return False
106
+
107
+
108
+ def resolve_group(root: Path, document: dict[str, Any], key: str, fields: tuple[str, ...], issues: list[Issue]) -> dict[str, Path]:
109
+ group = document.get(key) if isinstance(document.get(key), dict) else {}
110
+ required(group, fields, key, issues)
111
+ resolved: dict[str, Path] = {}
112
+ for field in fields:
113
+ path = resolve(root, group.get(field))
114
+ if path is None:
115
+ issue(issues, "missing_or_unsafe_ref", f"{key}.{field} must be an existing safe relative file", f"{key}.{field}")
116
+ else:
117
+ resolved[field] = path
118
+ return resolved
119
+
120
+
121
+ def validate_runtime_evidence(path: Path, issues: list[Issue]) -> bool:
122
+ evidence = load_json(path, issues, "runtime evidence")
123
+ if evidence is None:
124
+ return False
125
+ if evidence.get("mode") != "runtime" or evidence.get("status") != "pass":
126
+ issue(issues, "runtime_evidence_not_pass", "runtime evidence must record mode=runtime and status=pass", "runtime.runtime_evidence_ref")
127
+ return False
128
+ frameworks = evidence.get("frameworks")
129
+ if not isinstance(frameworks, list) or not frameworks or any(item.get("status") != "pass" or item.get("ready") is not True for item in frameworks if isinstance(item, dict)):
130
+ issue(issues, "runtime_framework_not_ready", "runtime evidence requires at least one ready passing framework", "runtime.runtime_evidence_ref")
131
+ return False
132
+ return True
133
+
134
+
135
+ def validate_candidate(args: argparse.Namespace) -> dict[str, Any]:
136
+ root = args.root.resolve()
137
+ issues: list[Issue] = []
138
+ candidate_path = args.input.resolve()
139
+ candidate = load_json(candidate_path, issues, "runtime candidate")
140
+ if candidate is None:
141
+ return result(None, issues, {}, None, False)
142
+ required(candidate, ("schema_version", "candidate_id", "asset_id", "created_at", "artifact_intake", "consistency", "runtime"), "candidate", issues)
143
+ if candidate.get("schema_version") != "0.1":
144
+ issue(issues, "schema_version", "candidate.schema_version must be 0.1", "candidate.schema_version")
145
+ if not isinstance(candidate.get("candidate_id"), str) or len(candidate["candidate_id"]) < 3:
146
+ issue(issues, "invalid_candidate_id", "candidate_id must be a non-empty stable ID", "candidate.candidate_id")
147
+ if not isinstance(candidate.get("asset_id"), str) or not candidate["asset_id"].strip():
148
+ issue(issues, "invalid_asset_id", "asset_id must be non-empty", "candidate.asset_id")
149
+ if not valid_datetime(candidate.get("created_at")):
150
+ issue(issues, "invalid_datetime", "created_at must be ISO-8601", "candidate.created_at")
151
+ intake_paths = resolve_group(root, candidate, "artifact_intake", ("registry", "receipt", "controls", "export_manifest"), issues)
152
+ consistency_paths = resolve_group(root, candidate, "consistency", ("asset_identity", "action_set"), issues)
153
+ consistency_group = candidate.get("consistency") if isinstance(candidate.get("consistency"), dict) else {}
154
+ for name in ("frame_geometry", "atlas", "layered_map"):
155
+ if name in consistency_group:
156
+ path = resolve(root, consistency_group.get(name))
157
+ if path is None:
158
+ issue(issues, "missing_or_unsafe_ref", f"consistency.{name} must be an existing safe relative file", f"consistency.{name}")
159
+ else:
160
+ consistency_paths[name] = path
161
+ runtime = candidate.get("runtime") if isinstance(candidate.get("runtime"), dict) else {}
162
+ if runtime.get("review_required") is not True:
163
+ issue(issues, "review_required", "runtime.review_required must be true; candidate evidence cannot replace human review", "runtime.review_required")
164
+ if runtime.get("target") not in {"sprite", "canvas", "lottie", "dotlottie", "rive", "gsap", "framer-motion", "spine", "three"}:
165
+ issue(issues, "invalid_runtime_target", "runtime.target is unsupported", "runtime.target")
166
+
167
+ intake_result: dict[str, Any] | None = None
168
+ controls: dict[str, Any] | None = None
169
+ export: dict[str, Any] | None = None
170
+ if len(intake_paths) == 4:
171
+ try:
172
+ intake = load_module("motionloom_artifact_intake_bridge", ROOT / "scripts" / "artifact-intake.py")
173
+ # Intake strictness evaluates provider adapter authority. Candidate strictness
174
+ # evaluates actual runtime evidence below; a static intake adapter must not be
175
+ # promoted, but it can feed a separately tested runtime candidate.
176
+ intake_result = intake.evaluate_bundle(argparse.Namespace(root=root, registry=intake_paths["registry"], receipt=intake_paths["receipt"], controls=intake_paths["controls"], export_manifest=intake_paths["export_manifest"], strict=False))
177
+ for entry in intake_result.get("errors", []):
178
+ issue(issues, f"intake_{entry.get('code', 'error')}", entry.get("message", "artifact intake error"), entry.get("path", "artifact_intake"))
179
+ controls = load_json(intake_paths["controls"], issues, "control track")
180
+ export = load_json(intake_paths["export_manifest"], issues, "export manifest")
181
+ receipt = load_json(intake_paths["receipt"], issues, "generation receipt")
182
+ for label, document in (("receipt", receipt), ("controls", controls), ("export", export)):
183
+ if document is not None and document.get("asset_id", document.get("asset", {}).get("id")) != candidate.get("asset_id"):
184
+ issue(issues, "asset_id_mismatch", f"candidate.asset_id must match {label} asset ID", f"{label}.asset_id")
185
+ except (OSError, ValueError, AttributeError, KeyError, TypeError, RuntimeError) as exc:
186
+ issue(issues, "artifact_intake_unavailable", str(exc), "artifact_intake")
187
+
188
+ consistency_results: dict[str, Any] = {}
189
+ try:
190
+ analyzer = load_module("motionloom_asset_consistency_bridge", ROOT / "scripts" / "asset-consistency.py")
191
+ for name, path in consistency_paths.items():
192
+ document = load_json(path, issues, f"consistency {name}")
193
+ if document is None:
194
+ continue
195
+ check = analyzer.run(KINDS[name], document, path.parent.resolve(), args.strict)
196
+ consistency_results[name] = check
197
+ for entry in check.get("errors", []):
198
+ issue(issues, f"consistency_{entry.get('code', 'error')}", entry.get("message", "consistency error"), f"consistency.{name}")
199
+ if name in {"asset_identity", "action_set", "frame_geometry"} and document.get("asset_id", document.get("asset_identity")) != candidate.get("asset_id"):
200
+ issue(issues, "consistency_asset_id_mismatch", f"candidate.asset_id must match consistency.{name}", f"consistency.{name}")
201
+ except (OSError, ValueError, AttributeError, KeyError, TypeError, RuntimeError) as exc:
202
+ issue(issues, "consistency_unavailable", str(exc), "consistency")
203
+
204
+ profile = ((controls or {}).get("output_profile") or {}).get("kind")
205
+ for name in PROFILE_REQUIREMENTS.get(str(profile), set()):
206
+ if name not in consistency_paths:
207
+ issue(issues, "missing_profile_contract", f"{profile} requires consistency.{name}", f"consistency.{name}")
208
+ if profile not in PROFILE_REQUIREMENTS:
209
+ issue(issues, "unknown_output_profile", "control output_profile.kind is unsupported for runtime bridging", "controls.output_profile.kind")
210
+
211
+ output_paths: dict[Path, dict[str, Any]] = {}
212
+ for entry in ((export or {}).get("outputs") or []):
213
+ if isinstance(entry, dict):
214
+ path = resolve(root, entry.get("path"))
215
+ if path is not None:
216
+ output_paths[path] = entry
217
+ action = consistency_results.get("action_set", {}).get("metrics", {}).get("actions", [])
218
+ action_doc = load_json(consistency_paths["action_set"], issues, "action set") if "action_set" in consistency_paths else None
219
+ action_frames: list[Path] = []
220
+ if isinstance(action_doc, dict):
221
+ for item in action_doc.get("actions", []):
222
+ if isinstance(item, dict):
223
+ action_frames.extend((consistency_paths["action_set"].parent / str(frame)).resolve() for frame in item.get("frames", []) if safe(frame))
224
+ profile_data = (controls or {}).get("output_profile") or {}
225
+ expected_count = profile_data.get("expected_frame_count")
226
+ if profile in {"frame_sequence", "sprite_atlas"} and isinstance(expected_count, int) and len(action_frames) != expected_count:
227
+ issue(issues, "frame_count_mismatch", "control expected_frame_count must match action-set frame count", "controls.output_profile.expected_frame_count")
228
+ if profile in {"frame_sequence", "sprite_atlas"}:
229
+ for frame in action_frames:
230
+ if frame not in output_paths:
231
+ issue(issues, "unexported_action_frame", f"action frame is not hash-bound in export manifest: {frame.name}", "export_manifest.outputs")
232
+ geometry_doc = load_json(consistency_paths["frame_geometry"], issues, "frame geometry") if "frame_geometry" in consistency_paths else None
233
+ if isinstance(geometry_doc, dict):
234
+ for frame in geometry_doc.get("frames", []):
235
+ if isinstance(frame, dict) and safe(frame.get("image")):
236
+ path = (consistency_paths["frame_geometry"].parent / str(frame["image"])).resolve()
237
+ export_entry = output_paths.get(path)
238
+ if export_entry is None:
239
+ issue(issues, "unexported_geometry_frame", f"geometry frame is not hash-bound in export manifest: {path.name}", "export_manifest.outputs")
240
+ elif not isinstance(frame.get("sha256"), str) or frame.get("sha256") != export_entry.get("sha256") or digest(path) != frame.get("sha256"):
241
+ issue(issues, "frame_hash_mismatch", f"geometry/export hash mismatch: {path.name}", "frame_geometry.frames")
242
+
243
+ runtime_verified = False
244
+ if "runtime_evidence_ref" in runtime:
245
+ evidence_path = resolve(root, runtime.get("runtime_evidence_ref"))
246
+ if evidence_path is None:
247
+ issue(issues, "missing_or_unsafe_runtime_evidence", "runtime.runtime_evidence_ref must be an existing safe relative file", "runtime.runtime_evidence_ref")
248
+ else:
249
+ runtime_verified = validate_runtime_evidence(evidence_path, issues)
250
+ if args.strict and not runtime_verified:
251
+ issue(issues, "runtime_evidence_required", "strict runtime candidate validation requires passing runtime evidence", "runtime.runtime_evidence_ref")
252
+ return result(candidate, issues, consistency_results, intake_result, runtime_verified)
253
+
254
+
255
+ def result(candidate: dict[str, Any] | None, issues: list[Issue], consistency: dict[str, Any], intake: dict[str, Any] | None, runtime_verified: bool) -> dict[str, Any]:
256
+ errors = [asdict(item) for item in issues if item.severity == "error"]
257
+ warnings = [asdict(item) for item in issues if item.severity == "warning"]
258
+ ready = not errors
259
+ return {
260
+ "contract": "runtime-candidate",
261
+ "status": "blocked" if errors else "review_required",
262
+ "runtime_test_ready": ready,
263
+ "runtime_verified": runtime_verified,
264
+ "review_required": True,
265
+ "production_eligible": False,
266
+ "production_approved": False,
267
+ "candidate_id": (candidate or {}).get("candidate_id"),
268
+ "asset_id": (candidate or {}).get("asset_id"),
269
+ "intake": {"status": (intake or {}).get("status"), "adapter": (intake or {}).get("adapter")},
270
+ "consistency": {name: {"status": value.get("status"), "ready": value.get("ready")} for name, value in consistency.items()},
271
+ "errors": errors,
272
+ "warnings": warnings,
273
+ }
274
+
275
+
276
+ def main(argv: list[str] | None = None) -> int:
277
+ parser = argparse.ArgumentParser(description="Bind MotionLoom artifact intake and consistency evidence into a runtime-test candidate")
278
+ sub = parser.add_subparsers(dest="command", required=True)
279
+ for command in ("validate", "report"):
280
+ item = sub.add_parser(command)
281
+ item.add_argument("--input", required=True, type=Path)
282
+ item.add_argument("--root", type=Path, default=Path("."))
283
+ item.add_argument("--strict", action="store_true")
284
+ item.add_argument("--output", type=Path)
285
+ item.add_argument("--json", action="store_true")
286
+ args = parser.parse_args(argv)
287
+ payload = validate_candidate(args)
288
+ if args.output:
289
+ args.output.parent.mkdir(parents=True, exist_ok=True)
290
+ args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
291
+ if args.command == "validate" or args.json:
292
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
293
+ else:
294
+ print(f"runtime-candidate: {payload['status']} ({len(payload['errors'])} errors, {len(payload['warnings'])} warnings)")
295
+ for entry in payload["errors"] + payload["warnings"]:
296
+ print(f"- {entry['severity']}: {entry['code']}: {entry['message']}")
297
+ return 0 if payload["runtime_test_ready"] else 1
298
+
299
+
300
+ if __name__ == "__main__":
301
+ raise SystemExit(main())
package/scripts/setup.mjs CHANGED
@@ -19,12 +19,16 @@ const MARKER_START = "<!-- MOTIONLOOM:START -->";
19
19
  const MARKER_END = "<!-- MOTIONLOOM:END -->";
20
20
 
21
21
  function usage() {
22
- console.log(`MotionLoom ${VERSION} — onboarding and project readiness
22
+ console.log(`MotionLoom ${VERSION} — quick start for project-aware animation
23
23
 
24
- Usage:
25
- motionloom setup [options] Install and bootstrap the current project
26
- motionloom status [options] Read-only readiness report
27
- motionloom repair [options] Re-apply safe missing setup pieces
24
+ Start here:
25
+ motionloom init [options] One safe setup for the current project
26
+ motionloom status [options] Short, read-only project readiness check
27
+ motionloom doctor Validate the installed package
28
+
29
+ Maintenance:
30
+ motionloom setup [options] Alias for init (kept for compatibility)
31
+ motionloom repair [options] Re-apply only missing managed pieces
28
32
 
29
33
  Options:
30
34
  --project-root <path> Host project (default: current directory)
@@ -43,13 +47,16 @@ Safe defaults:
43
47
  - idempotent AGENTS.md merge, never overwrite existing project guidance
44
48
  - Project Memory stays bound to the current project root
45
49
  - no commit, push, PR, approval or production promotion
50
+
51
+ No asset or runtime gate runs during init. Choose an animation task first;
52
+ the installed Agent router will load the relevant MotionLoom workflow then.
46
53
  `);
47
54
  }
48
55
 
49
56
  function parseArgs(argv) {
50
57
  const args = [...argv];
51
58
  let action = "setup";
52
- if (["setup", "status", "repair"].includes(args[0])) action = args.shift();
59
+ if (["setup", "init", "status", "repair"].includes(args[0])) action = args.shift();
53
60
  const options = {
54
61
  action,
55
62
  projectRoot: process.cwd(),
@@ -119,6 +126,23 @@ function projectPackage(root) {
119
126
  return readJson(join(root, "package.json"));
120
127
  }
121
128
 
129
+ function detectProjectKind(packageJson) {
130
+ const dependencies = {
131
+ ...(packageJson?.dependencies || {}),
132
+ ...(packageJson?.devDependencies || {}),
133
+ };
134
+ if (dependencies.next) return "Next.js";
135
+ if (dependencies["@angular/core"]) return "Angular";
136
+ if (dependencies.vue || dependencies.nuxt) return "Vue";
137
+ if (dependencies.svelte || dependencies["@sveltejs/kit"]) return "Svelte";
138
+ if (dependencies.phaser) return "Phaser";
139
+ if (dependencies["pixi.js"] || dependencies["@pixi/react"]) return "PixiJS";
140
+ if (dependencies["@babylonjs/core"]) return "Babylon.js";
141
+ if (dependencies.three || dependencies["@react-three/fiber"]) return "Three.js";
142
+ if (dependencies.react || dependencies["react-dom"]) return "React";
143
+ return "Node / unclassified";
144
+ }
145
+
122
146
  function hasDependency(packageJson) {
123
147
  if (!packageJson) return false;
124
148
  return Boolean(
@@ -219,8 +243,9 @@ When a task involves animation, motion, assets, runtime rendering or Dev Lab rev
219
243
  - Read the installed MotionLoom package's canonical \`SKILL.md\` and \`agent-card.json\` first; do not copy them into the host project.
220
244
  - Run \`npx --no-install motionloom status --json\` before planning animation work.
221
245
  - Keep project context and \`.motionloom/project-memory.json\` bound to this project; never copy them from another project.
222
- - After rendering, open the MotionLoom Dev Lab candidate for user review before any PR handoff.
223
- - Treat runtime evidence, quality gates, attestation and heuristics as evidence only; they never imply user approval.
246
+ - Continue normal product work without running MotionLoom gates until the task actually includes animation or an animation asset.
247
+ - For an animation task, follow the applicable canonical workflow; use ingest/runtime checks only when importing assets or testing a target runtime.
248
+ - After rendering, open the MotionLoom Dev Lab candidate for user review before any PR handoff. Evidence never implies user approval.
224
249
  - Keep Git side effects local-only until the user explicitly confirms commit, push or PR.
225
250
 
226
251
  ${MARKER_END}`;
@@ -310,6 +335,11 @@ function statusReport(options) {
310
335
  context: { path: join(projectRoot, "project-context.json"), exists: context },
311
336
  memory: { path: join(projectRoot, ".motionloom", "project-memory.json"), exists: memory, validation: memoryCheck },
312
337
  discovery,
338
+ quick_start: {
339
+ project_kind: detectProjectKind(packageJson),
340
+ gates_active: false,
341
+ next_action: "Continue normal work. Start an animation task when you are ready; MotionLoom will then load only the relevant workflow.",
342
+ },
313
343
  };
314
344
  }
315
345
 
@@ -319,15 +349,14 @@ function printStatus(report, json) {
319
349
  return;
320
350
  }
321
351
  const label = report.status === "ready" ? "READY" : report.status === "needs_review" ? "NEEDS REVIEW" : "NEEDS SETUP";
322
- console.log(`MotionLoom status: ${label}`);
323
- console.log(`Project: ${report.project_root}`);
324
- console.log(`Package: ${report.package.installed ? `installed ${report.package.version || "unknown"}` : "not installed locally"}`);
325
- console.log(`Runtime: Node ${report.runtime.node.version} ${report.runtime.node.supported ? "PASS" : "BLOCKED"}; Python ${report.runtime.python.version || "missing"} ${report.runtime.python.supported ? "PASS" : "BLOCKED"}`);
326
- console.log(`Agent router: ${report.router.status}`);
327
- console.log(`Project context: ${report.context.exists ? "present" : "missing"}`);
328
- console.log(`Project Memory: ${report.memory.exists ? (report.memory.validation.ok ? "valid" : "needs review") : "missing"}`);
329
- console.log(`Discovery: ${report.discovery.ok ? "PASS" : "BLOCKED"}`);
330
- if (report.status !== "ready") console.log("Next step: npx --yes motionloom setup");
352
+ console.log(`MotionLoom: ${label} · ${report.quick_start.project_kind}`);
353
+ if (report.status === "ready") {
354
+ console.log("Project memory and Agent routing are ready. No animation gate is active.");
355
+ console.log("Next: continue normal work. When you start animation, ask the Agent to use MotionLoom.");
356
+ return;
357
+ }
358
+ console.log(`Missing setup pieces: package ${report.package.installed ? "ready" : "missing"}, memory ${report.memory.exists ? "present" : "missing"}, router ${report.router.status}.`);
359
+ console.log("Next: npx --yes motionloom init");
331
360
  }
332
361
 
333
362
  function setup(options) {
@@ -335,8 +364,10 @@ function setup(options) {
335
364
  const packageJson = projectPackage(projectRoot);
336
365
  const result = {
337
366
  command: options.action,
367
+ onboarding: "quick_start",
338
368
  status: "blocked",
339
369
  project_root: projectRoot,
370
+ project_kind: detectProjectKind(packageJson),
340
371
  dry_run: options.dryRun,
341
372
  changed: [],
342
373
  planned: [],
@@ -432,19 +463,16 @@ function printSetup(result, json) {
432
463
  return;
433
464
  }
434
465
  const label = result.status === "ready" ? "READY" : result.status === "planned" ? "DRY RUN" : result.status === "needs_review" ? "NEEDS REVIEW" : "BLOCKED";
435
- console.log(`MotionLoom setup: ${label}`);
436
- console.log(`Project: ${result.project_root}`);
437
- for (const item of result.changed) console.log(` PASS ${typeof item === "string" ? item : `${item.step}: ${item.action}`}`);
438
- for (const item of result.planned) console.log(` PLAN ${typeof item === "string" ? item : item.command ? item.command.join(" ") : `${item.step}: ${item.action || "planned"}`}`);
439
- for (const warning of result.warnings) console.log(` WARN ${warning}`);
466
+ console.log(`MotionLoom quick start: ${label} · ${result.project_kind || "project"}`);
440
467
  for (const error of result.errors) console.log(` BLOCK ${error}`);
468
+ for (const warning of result.warnings) console.log(` NOTE ${warning}`);
441
469
  if (result.status === "ready") {
442
- console.log("Next: ask your Agent to read the MotionLoom router, inspect project memory, then create a small animation candidate.");
443
- console.log("Review: open the Dev Lab candidate before any PR or Git side effect.");
470
+ console.log("Ready. Project memory is set up and your normal work stays uninterrupted.");
471
+ console.log("When an animation task begins, ask your Agent to use MotionLoom. Asset and runtime checks appear only when relevant.");
444
472
  } else if (result.status === "planned") {
445
- console.log("Dry run only: rerun without --dry-run to apply the safe local setup.");
473
+ console.log("Dry run only: rerun without --dry-run to apply the same safe quick start.");
446
474
  } else {
447
- console.log("Next: run `npx --no-install motionloom status --json` and follow the reported missing or review-required item.");
475
+ console.log("Next: run `npx --no-install motionloom status --json` for details, then resolve the reported setup item.");
448
476
  }
449
477
  }
450
478
 
@@ -0,0 +1,38 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "provenance_id": "runtime-pilot-framer-scene-provenance",
4
+ "task_id": "runtime-pilot-001",
5
+ "scene": "runtime-pilot-framer",
6
+ "created_at": "2026-08-15T00:00:00Z",
7
+ "asset": {
8
+ "id": "runtime/framer-motion-pilot",
9
+ "path": "scene.jsx",
10
+ "type": "runtime_scene",
11
+ "framework": "framer-motion",
12
+ "version": "13.1.0"
13
+ },
14
+ "authority": "code_authored",
15
+ "readiness": "review_required",
16
+ "license": {
17
+ "spdx": "MIT",
18
+ "source": "MotionLoom repository source",
19
+ "attribution": "MotionLoom contributors"
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "scene.jsx",
24
+ "role": "runtime-source",
25
+ "sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
26
+ "bytes": 1769
27
+ }
28
+ ],
29
+ "provenance_chain": [
30
+ {
31
+ "step": "code-authoring",
32
+ "actor": "MotionLoom runtime-pilot author",
33
+ "source": "scene.jsx",
34
+ "timestamp": "2026-08-15T00:00:00Z",
35
+ "sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b"
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "candidate_id": "8cccc4ac5a7e493ffe40",
4
+ "task_id": "runtime-pilot-001",
5
+ "scene": "runtime-pilot-framer",
6
+ "url": "https://animdevlab-hcxnxr9c.manus.space/lab/?scene=runtime-pilot-framer&task_id=runtime-pilot-001&candidate_id=8cccc4ac5a7e493ffe40&artifact_base=https%3A%2F%2Fanimdevlab-hcxnxr9c.manus.space%2Fscenes%2Fruntime-pilot-framer&task_base=https%3A%2F%2Fanimdevlab-hcxnxr9c.manus.space%2Ftasks%2Fruntime-pilot-001",
7
+ "status": "prepared",
8
+ "context_sha256": "9e9bb3bf2e490d23e9f5d52dfc4779b69adf0cc63b32df66adfde7050845fc74",
9
+ "source_sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
10
+ "runtime": "framer-motion",
11
+ "checkpoints": [
12
+ 0,
13
+ 50,
14
+ 100
15
+ ],
16
+ "review_artifact": "review.json",
17
+ "requires_user_approval": true,
18
+ "prepared_at": "2026-08-15T15:44:47Z",
19
+ "expires_at": "2026-08-16T15:44:47Z"
20
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "run_id": "1786808870894-145958",
4
+ "generated_at": "2026-08-15T15:47:52.478Z",
5
+ "mode": "runtime-telemetry",
6
+ "task_id": "runtime-pilot-001",
7
+ "scene": "runtime-pilot-framer",
8
+ "framework": "framer-motion",
9
+ "runtime": "framer-motion@13.1.0",
10
+ "source_sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
11
+ "manifest_sha256": "2b53bf3b6fc8dfe16a271ecd52ec51c2acb5b612c0b77d9fd4e485696989011d",
12
+ "motion_ir_sha256": "1d339ca2ec9c510e54e53c37ef32c2ddf1485dbaec97c5e9aa178a8f6c9c3287",
13
+ "samples": [
14
+ {
15
+ "sequence": 0,
16
+ "percent": 0,
17
+ "captured_at_ms": 767.8999999761581,
18
+ "raf_intervals_ms": [
19
+ 16.699999999999932,
20
+ 16.600000000000023,
21
+ 16.799999999999955
22
+ ],
23
+ "state_sha256": "0769bb6867251ff8d951b01e7d817ab4644d1f7d4a38b0e91ad54ec5b0f81f6b",
24
+ "state": {
25
+ "progress": 0,
26
+ "transform": "matrix(0.82, 0, 0, 0.82, 0, 0)",
27
+ "opacity": "0.28",
28
+ "contract": {
29
+ "framework": "framer-motion",
30
+ "checkpoints": [
31
+ 0,
32
+ 50,
33
+ 100
34
+ ],
35
+ "durationSeconds": 1.2,
36
+ "loop": true,
37
+ "sourceAuthority": "code_authored"
38
+ }
39
+ }
40
+ },
41
+ {
42
+ "sequence": 1,
43
+ "percent": 50,
44
+ "captured_at_ms": 851.1000000238419,
45
+ "raf_intervals_ms": [
46
+ 16.700000000000045,
47
+ 16.699999999999932,
48
+ 16.600000000000023
49
+ ],
50
+ "state_sha256": "015ad2af517be79a3cf102b48b5f61282acd1843c7b5efe20472c2752d79c890",
51
+ "state": {
52
+ "progress": 0.5,
53
+ "transform": "matrix(1.02851, -0.256437, 0.256437, 1.02851, 176, -48)",
54
+ "opacity": "1",
55
+ "contract": {
56
+ "framework": "framer-motion",
57
+ "checkpoints": [
58
+ 0,
59
+ 50,
60
+ 100
61
+ ],
62
+ "durationSeconds": 1.2,
63
+ "loop": true,
64
+ "sourceAuthority": "code_authored"
65
+ }
66
+ }
67
+ },
68
+ {
69
+ "sequence": 2,
70
+ "percent": 100,
71
+ "captured_at_ms": 934.6000000238419,
72
+ "raf_intervals_ms": [
73
+ 16.700000000000045,
74
+ 16.600000000000023,
75
+ 16.699999999999932
76
+ ],
77
+ "state_sha256": "275a832573d3c660f6d820256f95412e253f0a29c02e8f535dfdec2c47e56de2",
78
+ "state": {
79
+ "progress": 1,
80
+ "transform": "matrix(0.882948, 0.469472, -0.469472, 0.882948, 240, -36)",
81
+ "opacity": "1",
82
+ "contract": {
83
+ "framework": "framer-motion",
84
+ "checkpoints": [
85
+ 0,
86
+ 50,
87
+ 100
88
+ ],
89
+ "durationSeconds": 1.2,
90
+ "loop": true,
91
+ "sourceAuthority": "code_authored"
92
+ }
93
+ }
94
+ }
95
+ ],
96
+ "metrics": {
97
+ "sample_count": 3,
98
+ "raf_interval_count": 9,
99
+ "max_raf_interval_ms": 16.799999999999955,
100
+ "p95_raf_interval_ms": 16.799999999999955
101
+ },
102
+ "status": "pass"
103
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "scene": "runtime-pilot-framer",
3
+ "task_id": "runtime-pilot-001",
4
+ "name": "Framer Motion Runtime Pilot",
5
+ "description": "Code-authored Framer Motion scene rendered at deterministic 0/50/100 checkpoints before human review.",
6
+ "framework": "framer-motion",
7
+ "category": "runtime-pilot",
8
+ "file": "scene.jsx",
9
+ "runtime_evidence": "runtime-evidence.json",
10
+ "asset_provenance": "asset-provenance.json",
11
+ "source_binding": {
12
+ "kind": "project",
13
+ "source_path": "scene.jsx",
14
+ "authority": "code_authored",
15
+ "license": "MIT",
16
+ "attribution": "MotionLoom contributors",
17
+ "sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b"
18
+ },
19
+ "checks": [
20
+ {
21
+ "id": "source-bound",
22
+ "label": "Runtime source is hash-bound before adapter execution",
23
+ "pass": true
24
+ },
25
+ {
26
+ "id": "review-first",
27
+ "label": "Runtime evidence does not approve a production release or pull request",
28
+ "pass": true
29
+ }
30
+ ]
31
+ }