motionloom 2.2.0 → 2.3.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.
- package/CHANGELOG.md +25 -1
- package/README.md +54 -15
- package/ROADMAP.md +5 -3
- package/SKILL.md +15 -3
- package/agent-card.json +21 -4
- package/agent-surfaces.json +8 -1
- package/bin/motionloom.mjs +15 -2
- package/docs/AGENT-INTEGRATION.md +15 -2
- package/docs/CHECKLIST.md +5 -0
- package/docs/STATUS.md +2 -2
- package/docs/releases/2.3.0.md +33 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/examples/agent-consumer/ai-generated-pilot/hero-male.json +10 -0
- package/examples/agent-consumer/ai-generated-pilot-provenance.json +55 -0
- package/package.json +11 -2
- package/references/agent-interoperability.md +11 -0
- package/references/intelligence-core.md +8 -2
- package/schemas/agent-surfaces.schema.json +1 -1
- package/schemas/asset-provenance.schema.json +183 -0
- package/schemas/scene-manifest.schema.json +1 -0
- package/scripts/asset-provenance.py +390 -0
- package/scripts/docs-audit.py +14 -2
- package/scripts/pr.py +2 -0
- package/scripts/quality-gate.py +41 -3
- package/scripts/report.py +50 -0
- package/scripts/setup.mjs +472 -0
- package/scripts/skill-doctor.py +2 -1
- package/src/output/browser-review-smoke/asset-provenance.json +77 -0
- package/src/output/browser-review-smoke/manifest.json +1 -0
- package/src/output/browser-review-smoke/visual-truth.json +3 -3
- package/tests/scripts/run_tests.py +24 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MotionLoom onboarding wizard.
|
|
3
|
+
* Style: Timeline Desk — one clear path, explicit states, no hidden side effects.
|
|
4
|
+
* The wizard is intentionally Node-only at the entrypoint so npx can explain
|
|
5
|
+
* missing Python before delegating to the canonical Python contracts.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const PACKAGE_JSON_PATH = join(PACKAGE_ROOT, "package.json");
|
|
15
|
+
const PACKAGE = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf8"));
|
|
16
|
+
const VERSION = PACKAGE.version;
|
|
17
|
+
const PACKAGE_NAME = PACKAGE.name;
|
|
18
|
+
const MARKER_START = "<!-- MOTIONLOOM:START -->";
|
|
19
|
+
const MARKER_END = "<!-- MOTIONLOOM:END -->";
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.log(`MotionLoom ${VERSION} — onboarding and project readiness
|
|
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
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--project-root <path> Host project (default: current directory)
|
|
31
|
+
--package-manager <name> auto, npm, pnpm or yarn (default: auto)
|
|
32
|
+
--motionloom-root <path> Use a local checkout instead of node_modules
|
|
33
|
+
--dry-run Preview commands and file changes only
|
|
34
|
+
--skip-install Do not install the npm package
|
|
35
|
+
--skip-memory Do not analyze or initialize Project Memory
|
|
36
|
+
--no-router Do not create or update AGENTS.md
|
|
37
|
+
--yes Accept safe defaults without prompting
|
|
38
|
+
--json Emit machine-readable JSON
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
|
|
41
|
+
Safe defaults:
|
|
42
|
+
- local devDependency, never a global install
|
|
43
|
+
- idempotent AGENTS.md merge, never overwrite existing project guidance
|
|
44
|
+
- Project Memory stays bound to the current project root
|
|
45
|
+
- no commit, push, PR, approval or production promotion
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = [...argv];
|
|
51
|
+
let action = "setup";
|
|
52
|
+
if (["setup", "status", "repair"].includes(args[0])) action = args.shift();
|
|
53
|
+
const options = {
|
|
54
|
+
action,
|
|
55
|
+
projectRoot: process.cwd(),
|
|
56
|
+
packageManager: "auto",
|
|
57
|
+
motionloomRoot: null,
|
|
58
|
+
dryRun: false,
|
|
59
|
+
skipInstall: false,
|
|
60
|
+
skipMemory: false,
|
|
61
|
+
noRouter: false,
|
|
62
|
+
yes: false,
|
|
63
|
+
json: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
67
|
+
const arg = args[index];
|
|
68
|
+
if (arg === "-h" || arg === "--help") {
|
|
69
|
+
options.help = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--dry-run") {
|
|
73
|
+
options.dryRun = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--skip-install") {
|
|
77
|
+
options.skipInstall = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--skip-memory") {
|
|
81
|
+
options.skipMemory = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--no-router") {
|
|
85
|
+
options.noRouter = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--yes") {
|
|
89
|
+
options.yes = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === "--json") {
|
|
93
|
+
options.json = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const next = args[index + 1];
|
|
97
|
+
if (["--project-root", "--package-manager", "--motionloom-root"].includes(arg)) {
|
|
98
|
+
if (!next || next.startsWith("--")) throw new Error(`${arg} requires a value`);
|
|
99
|
+
index += 1;
|
|
100
|
+
if (arg === "--project-root") options.projectRoot = next;
|
|
101
|
+
if (arg === "--package-manager") options.packageManager = next;
|
|
102
|
+
if (arg === "--motionloom-root") options.motionloomRoot = next;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`unknown option: ${arg}`);
|
|
106
|
+
}
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readJson(path) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function projectPackage(root) {
|
|
119
|
+
return readJson(join(root, "package.json"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasDependency(packageJson) {
|
|
123
|
+
if (!packageJson) return false;
|
|
124
|
+
return Boolean(
|
|
125
|
+
packageJson.dependencies?.[PACKAGE_NAME] ||
|
|
126
|
+
packageJson.devDependencies?.[PACKAGE_NAME] ||
|
|
127
|
+
packageJson.optionalDependencies?.[PACKAGE_NAME],
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function detectPackageManager(root, requested) {
|
|
132
|
+
if (requested !== "auto") return requested;
|
|
133
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
134
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
135
|
+
if (existsSync(join(root, "package-lock.json"))) return "npm";
|
|
136
|
+
const packageJson = projectPackage(root);
|
|
137
|
+
const declared = String(packageJson?.packageManager || "").split("@")[0];
|
|
138
|
+
return ["npm", "pnpm", "yarn"].includes(declared) ? declared : "npm";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installCommand(manager) {
|
|
142
|
+
if (manager === "pnpm") return ["pnpm", ["add", "--save-dev", `${PACKAGE_NAME}@${VERSION}`]];
|
|
143
|
+
if (manager === "yarn") return ["yarn", ["add", "--dev", `${PACKAGE_NAME}@${VERSION}`]];
|
|
144
|
+
return ["npm", ["install", "--save-dev", `${PACKAGE_NAME}@${VERSION}`]];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function run(program, args, cwd, capture = false) {
|
|
148
|
+
const result = spawnSync(program, args, {
|
|
149
|
+
cwd,
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
152
|
+
env: process.env,
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
ok: !result.error && result.status === 0,
|
|
156
|
+
status: result.status ?? 1,
|
|
157
|
+
error: result.error?.message || null,
|
|
158
|
+
stdout: result.stdout || "",
|
|
159
|
+
stderr: result.stderr || "",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pythonRuntime() {
|
|
164
|
+
const candidates = process.platform === "win32" ? ["python", "python3"] : ["python3", "python"];
|
|
165
|
+
for (const executable of candidates) {
|
|
166
|
+
const result = run(executable, ["--version"], process.cwd(), true);
|
|
167
|
+
if (!result.error && result.status === 0) {
|
|
168
|
+
const version = `${result.stdout}\n${result.stderr}`.match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
169
|
+
if (version) {
|
|
170
|
+
const major = Number(version[1]);
|
|
171
|
+
const minor = Number(version[2]);
|
|
172
|
+
return { executable, version: `${major}.${minor}.${version[3]}`, supported: major > 3 || (major === 3 && minor >= 11) };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { executable: null, version: null, supported: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runtimeChecks() {
|
|
180
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
181
|
+
const python = pythonRuntime();
|
|
182
|
+
return {
|
|
183
|
+
node: { version: process.versions.node, supported: nodeMajor >= 18 },
|
|
184
|
+
python,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveMotionLoomRoot(projectRoot, explicitRoot) {
|
|
189
|
+
if (explicitRoot) {
|
|
190
|
+
const root = resolve(explicitRoot);
|
|
191
|
+
return existsSync(join(root, "bin", "motionloom.mjs")) ? root : null;
|
|
192
|
+
}
|
|
193
|
+
const hostPackage = projectPackage(projectRoot);
|
|
194
|
+
if (hostPackage?.name === PACKAGE_NAME && existsSync(join(projectRoot, "bin", "motionloom.mjs"))) {
|
|
195
|
+
return projectRoot;
|
|
196
|
+
}
|
|
197
|
+
const local = join(projectRoot, "node_modules", PACKAGE_NAME);
|
|
198
|
+
if (existsSync(join(local, "bin", "motionloom.mjs"))) return resolve(local);
|
|
199
|
+
try {
|
|
200
|
+
const require = createRequire(import.meta.url);
|
|
201
|
+
const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`, { paths: [projectRoot] });
|
|
202
|
+
const root = dirname(packageJson);
|
|
203
|
+
return existsSync(join(root, "bin", "motionloom.mjs")) ? root : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function localCli(motionloomRoot, args, projectRoot, capture = false) {
|
|
210
|
+
return run(process.execPath, [join(motionloomRoot, "bin", "motionloom.mjs"), ...args], projectRoot, capture);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function routerBlock() {
|
|
214
|
+
return `${MARKER_START}
|
|
215
|
+
## MotionLoom
|
|
216
|
+
|
|
217
|
+
When a task involves animation, motion, assets, runtime rendering or Dev Lab review:
|
|
218
|
+
|
|
219
|
+
- Read the installed MotionLoom package's canonical \`SKILL.md\` and \`agent-card.json\` first; do not copy them into the host project.
|
|
220
|
+
- Run \`npx --no-install motionloom status --json\` before planning animation work.
|
|
221
|
+
- 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.
|
|
224
|
+
- Keep Git side effects local-only until the user explicitly confirms commit, push or PR.
|
|
225
|
+
|
|
226
|
+
${MARKER_END}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function routerState(projectRoot) {
|
|
230
|
+
const path = join(projectRoot, "AGENTS.md");
|
|
231
|
+
if (!existsSync(path)) return { status: "missing", path };
|
|
232
|
+
const text = readFileSync(path, "utf8");
|
|
233
|
+
if (text.includes(MARKER_START) && text.includes(MARKER_END)) return { status: "managed", path };
|
|
234
|
+
if (/^##\s+MotionLoom\s*$/im.test(text)) return { status: "unmarked", path };
|
|
235
|
+
return { status: "absent", path };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ensureRouter(projectRoot, dryRun) {
|
|
239
|
+
const state = routerState(projectRoot);
|
|
240
|
+
const path = state.path;
|
|
241
|
+
if (state.status === "managed") {
|
|
242
|
+
const text = readFileSync(path, "utf8");
|
|
243
|
+
const pattern = new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}`, "m");
|
|
244
|
+
const updated = text.replace(pattern, routerBlock());
|
|
245
|
+
if (updated !== text && !dryRun) writeFileSync(path, updated, "utf8");
|
|
246
|
+
return { ...state, action: updated !== text ? (dryRun ? "would_update" : "updated") : "unchanged" };
|
|
247
|
+
}
|
|
248
|
+
if (state.status === "unmarked") return { ...state, action: "manual_review_required" };
|
|
249
|
+
const prefix = existsSync(path) ? `\n\n${routerBlock()}\n` : `${routerBlock()}\n`;
|
|
250
|
+
if (!dryRun) writeFileSync(path, prefix, { encoding: "utf8", flag: existsSync(path) ? "a" : "w" });
|
|
251
|
+
return { ...state, action: dryRun ? "would_create" : "created" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function checkMemory(motionloomRoot, projectRoot) {
|
|
255
|
+
const result = localCli(motionloomRoot, ["memory", "validate", "--project-root", projectRoot, "--json"], projectRoot, true);
|
|
256
|
+
let payload = null;
|
|
257
|
+
try {
|
|
258
|
+
payload = JSON.parse(result.stdout);
|
|
259
|
+
} catch {
|
|
260
|
+
payload = { raw: result.stdout.trim() };
|
|
261
|
+
}
|
|
262
|
+
return { ok: result.ok, payload, stderr: result.stderr.trim() };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function checkDiscovery(motionloomRoot) {
|
|
266
|
+
const result = localCli(motionloomRoot, ["discovery", "check", "--root", motionloomRoot, "--json"], motionloomRoot, true);
|
|
267
|
+
let payload = null;
|
|
268
|
+
try {
|
|
269
|
+
payload = JSON.parse(result.stdout);
|
|
270
|
+
} catch {
|
|
271
|
+
payload = { raw: result.stdout.trim() };
|
|
272
|
+
}
|
|
273
|
+
return { ok: result.ok, payload, stderr: result.stderr.trim() };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function statusReport(options) {
|
|
277
|
+
const projectRoot = resolve(options.projectRoot);
|
|
278
|
+
const packageJson = projectPackage(projectRoot);
|
|
279
|
+
const motionloomRoot = resolveMotionLoomRoot(projectRoot, options.motionloomRoot);
|
|
280
|
+
const runtime = runtimeChecks();
|
|
281
|
+
const router = routerState(projectRoot);
|
|
282
|
+
const context = existsSync(join(projectRoot, "project-context.json"));
|
|
283
|
+
const memory = existsSync(join(projectRoot, ".motionloom", "project-memory.json"));
|
|
284
|
+
const memoryCheck = motionloomRoot && memory ? checkMemory(motionloomRoot, projectRoot) : { ok: false, payload: null };
|
|
285
|
+
const discovery = motionloomRoot ? checkDiscovery(motionloomRoot) : { ok: false, payload: null };
|
|
286
|
+
const packageInstalled = Boolean(motionloomRoot && existsSync(join(motionloomRoot, "package.json")));
|
|
287
|
+
const needsReview = Boolean(memory && !memoryCheck.ok);
|
|
288
|
+
const ready = Boolean(
|
|
289
|
+
packageInstalled &&
|
|
290
|
+
runtime.node.supported &&
|
|
291
|
+
runtime.python.supported &&
|
|
292
|
+
discovery.ok &&
|
|
293
|
+
["managed"].includes(router.status) &&
|
|
294
|
+
context &&
|
|
295
|
+
memory &&
|
|
296
|
+
memoryCheck.ok,
|
|
297
|
+
);
|
|
298
|
+
const status = ready ? "ready" : needsReview ? "needs_review" : "needs_setup";
|
|
299
|
+
return {
|
|
300
|
+
status,
|
|
301
|
+
project_root: projectRoot,
|
|
302
|
+
package: {
|
|
303
|
+
declared: hasDependency(packageJson),
|
|
304
|
+
installed: packageInstalled,
|
|
305
|
+
version: motionloomRoot ? readJson(join(motionloomRoot, "package.json"))?.version || null : null,
|
|
306
|
+
root: motionloomRoot,
|
|
307
|
+
},
|
|
308
|
+
runtime,
|
|
309
|
+
router,
|
|
310
|
+
context: { path: join(projectRoot, "project-context.json"), exists: context },
|
|
311
|
+
memory: { path: join(projectRoot, ".motionloom", "project-memory.json"), exists: memory, validation: memoryCheck },
|
|
312
|
+
discovery,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function printStatus(report, json) {
|
|
317
|
+
if (json) {
|
|
318
|
+
console.log(JSON.stringify(report, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
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");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function setup(options) {
|
|
334
|
+
const projectRoot = resolve(options.projectRoot);
|
|
335
|
+
const packageJson = projectPackage(projectRoot);
|
|
336
|
+
const result = {
|
|
337
|
+
command: options.action,
|
|
338
|
+
status: "blocked",
|
|
339
|
+
project_root: projectRoot,
|
|
340
|
+
dry_run: options.dryRun,
|
|
341
|
+
changed: [],
|
|
342
|
+
planned: [],
|
|
343
|
+
warnings: [],
|
|
344
|
+
errors: [],
|
|
345
|
+
};
|
|
346
|
+
if (!packageJson) {
|
|
347
|
+
result.errors.push("package.json is missing; run this command from the root of a Node project or pass --project-root");
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
const runtime = runtimeChecks();
|
|
351
|
+
if (!runtime.node.supported) result.errors.push(`Node.js ${runtime.node.version} is unsupported; MotionLoom requires Node.js 18+`);
|
|
352
|
+
if (!runtime.python.supported) result.errors.push("Python 3.11+ was not found; install Python and ensure it is on PATH");
|
|
353
|
+
if (result.errors.length) {
|
|
354
|
+
result.runtime = runtime;
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const manager = detectPackageManager(projectRoot, options.packageManager);
|
|
359
|
+
if (!["npm", "pnpm", "yarn"].includes(manager)) {
|
|
360
|
+
result.errors.push(`unsupported package manager: ${manager}`);
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
result.package_manager = manager;
|
|
364
|
+
let motionloomRoot = resolveMotionLoomRoot(projectRoot, options.motionloomRoot);
|
|
365
|
+
const needsInstall = !motionloomRoot || !hasDependency(packageJson);
|
|
366
|
+
if (needsInstall && options.skipInstall) {
|
|
367
|
+
result.errors.push("MotionLoom is not installed in this project; remove --skip-install or install the local devDependency first");
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
if (needsInstall) {
|
|
371
|
+
const [program, args] = installCommand(manager);
|
|
372
|
+
result.planned.push({ step: "install", command: [program, ...args] });
|
|
373
|
+
if (!options.dryRun) {
|
|
374
|
+
const install = run(program, args, projectRoot, options.json);
|
|
375
|
+
if (!install.ok) {
|
|
376
|
+
const detail = install.stderr.trim() || install.stdout.trim();
|
|
377
|
+
result.errors.push(detail || install.error || `package installation failed with exit code ${install.status}`);
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
result.changed.push("installed local MotionLoom devDependency");
|
|
381
|
+
motionloomRoot = resolveMotionLoomRoot(projectRoot, options.motionloomRoot);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
result.planned.push({ step: "install", action: "already_installed" });
|
|
385
|
+
}
|
|
386
|
+
if (!motionloomRoot && !options.dryRun) {
|
|
387
|
+
result.errors.push("MotionLoom package root could not be resolved after installation");
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!options.noRouter) {
|
|
392
|
+
const router = ensureRouter(projectRoot, options.dryRun);
|
|
393
|
+
if (router.action === "manual_review_required") {
|
|
394
|
+
result.warnings.push("AGENTS.md already contains an unmarked MotionLoom section; no automatic merge was attempted");
|
|
395
|
+
} else if (router.action.startsWith("would_") || ["created", "updated"].includes(router.action)) {
|
|
396
|
+
result[options.dryRun ? "planned" : "changed"].push({ step: "agent_router", action: router.action, path: relative(projectRoot, router.path) });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!options.skipMemory && !options.dryRun && motionloomRoot) {
|
|
401
|
+
const contextPath = join(projectRoot, "project-context.json");
|
|
402
|
+
const memoryPath = join(projectRoot, ".motionloom", "project-memory.json");
|
|
403
|
+
let bootstrap;
|
|
404
|
+
if (!existsSync(contextPath)) {
|
|
405
|
+
bootstrap = localCli(motionloomRoot, ["analyze", projectRoot, "--init-memory"], projectRoot, options.json);
|
|
406
|
+
if (bootstrap.ok) result.changed.push("analyzed project and initialized Project Memory");
|
|
407
|
+
} else if (!existsSync(memoryPath)) {
|
|
408
|
+
bootstrap = localCli(motionloomRoot, ["memory", "init", "--project-root", projectRoot, "--context-path", contextPath, "--json"], projectRoot, options.json);
|
|
409
|
+
if (bootstrap.ok) result.changed.push("initialized Project Memory from existing project context");
|
|
410
|
+
}
|
|
411
|
+
if (bootstrap && !bootstrap.ok) result.errors.push("Project Memory bootstrap failed; run `npx motionloom analyze . --init-memory` and inspect the reported project path");
|
|
412
|
+
} else if (!options.skipMemory) {
|
|
413
|
+
result.planned.push({ step: "project_memory", action: "analyze project and initialize .motionloom/project-memory.json" });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!options.dryRun && motionloomRoot) {
|
|
417
|
+
const report = statusReport({ ...options, projectRoot, motionloomRoot });
|
|
418
|
+
result.status = report.status;
|
|
419
|
+
result.readiness = report;
|
|
420
|
+
if (report.status === "needs_review") result.warnings.push("Project Memory exists but needs review; no destructive repair was attempted");
|
|
421
|
+
if (report.status === "needs_setup") result.warnings.push("Setup completed partially; run `npx motionloom status --json` for the exact missing item");
|
|
422
|
+
} else {
|
|
423
|
+
result.status = "planned";
|
|
424
|
+
result.planned.push({ step: "verification", action: "run discovery check, validate memory and print readiness" });
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function printSetup(result, json) {
|
|
430
|
+
if (json) {
|
|
431
|
+
console.log(JSON.stringify(result, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
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}`);
|
|
440
|
+
for (const error of result.errors) console.log(` BLOCK ${error}`);
|
|
441
|
+
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.");
|
|
444
|
+
} else if (result.status === "planned") {
|
|
445
|
+
console.log("Dry run only: rerun without --dry-run to apply the safe local setup.");
|
|
446
|
+
} else {
|
|
447
|
+
console.log("Next: run `npx --no-install motionloom status --json` and follow the reported missing or review-required item.");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function main() {
|
|
452
|
+
const options = parseArgs(process.argv.slice(2));
|
|
453
|
+
if (options.help) {
|
|
454
|
+
usage();
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
if (options.action === "status") {
|
|
458
|
+
const report = statusReport(options);
|
|
459
|
+
printStatus(report, options.json);
|
|
460
|
+
return report.status === "ready" ? 0 : report.status === "needs_review" ? 10 : 11;
|
|
461
|
+
}
|
|
462
|
+
const result = setup(options);
|
|
463
|
+
printSetup(result, options.json);
|
|
464
|
+
return ["ready", "planned"].includes(result.status) ? 0 : result.status === "needs_review" ? 10 : 11;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
process.exitCode = main();
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error(`MotionLoom onboarding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
471
|
+
process.exitCode = 2;
|
|
472
|
+
}
|
package/scripts/skill-doctor.py
CHANGED
|
@@ -22,6 +22,7 @@ REQUIRED_SCRIPT_FILES = [
|
|
|
22
22
|
"scripts/project_memory_loader.py",
|
|
23
23
|
"scripts/analyze.py",
|
|
24
24
|
"scripts/devlab.py",
|
|
25
|
+
"scripts/setup.mjs",
|
|
25
26
|
]
|
|
26
27
|
REQUIRED_SCHEMAS = [
|
|
27
28
|
"task.schema.json",
|
|
@@ -131,7 +132,7 @@ def run() -> int:
|
|
|
131
132
|
package_path = ROOT / "package.json"
|
|
132
133
|
try:
|
|
133
134
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
134
|
-
for script in ("test", "validate", "doctor", "report", "report:check", "review", "memory:bootstrap", "memory:recover", "memory:validate", "devlab", "pack:dotlottie"):
|
|
135
|
+
for script in ("test", "validate", "doctor", "setup", "setup:dry", "status", "repair", "report", "report:check", "review", "memory:bootstrap", "memory:recover", "memory:validate", "devlab", "pack:dotlottie"):
|
|
135
136
|
if script not in package.get("scripts", {}):
|
|
136
137
|
warnings.append({"code": "missing_package_script", "message": f"package.json has no {script} script."})
|
|
137
138
|
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"provenance_id": "browser-review-smoke-asset-provenance",
|
|
4
|
+
"task_id": "browser-review-smoke-task",
|
|
5
|
+
"scene": "browser-review-smoke",
|
|
6
|
+
"created_at": "2026-08-14T00:00:00Z",
|
|
7
|
+
"asset": {
|
|
8
|
+
"id": "browser-review-smoke-animation",
|
|
9
|
+
"path": "animation.json",
|
|
10
|
+
"type": "deterministic-fixture",
|
|
11
|
+
"framework": "lottie",
|
|
12
|
+
"version": "fixture-2.2.0"
|
|
13
|
+
},
|
|
14
|
+
"authority": "ai_assisted_human_reviewed",
|
|
15
|
+
"readiness": "production_eligible",
|
|
16
|
+
"generator": {
|
|
17
|
+
"model": "motionloom-deterministic-fixture-builder",
|
|
18
|
+
"task_id": "browser-review-smoke-task",
|
|
19
|
+
"source": "repository-fixture",
|
|
20
|
+
"generated_at": "2026-08-14T00:00:00Z",
|
|
21
|
+
"agent": "motionloom-ci"
|
|
22
|
+
},
|
|
23
|
+
"human_review": {
|
|
24
|
+
"reviewer": "MotionLoom fixture maintainer",
|
|
25
|
+
"decision": "approved",
|
|
26
|
+
"scope": "deterministic runtime and contract fixture, not production art approval",
|
|
27
|
+
"reviewed_at": "2026-08-14T00:00:00Z",
|
|
28
|
+
"user_confirmed": true,
|
|
29
|
+
"notes": "The fixture is eligible for repository CI and PR evidence; final product approval remains outside this artifact."
|
|
30
|
+
},
|
|
31
|
+
"license": {
|
|
32
|
+
"spdx": "MIT",
|
|
33
|
+
"source": "MotionLoom repository fixture",
|
|
34
|
+
"attribution": "MotionLoom deterministic browser-review smoke fixture."
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
{
|
|
38
|
+
"path": "animation.json",
|
|
39
|
+
"role": "runtime-scene",
|
|
40
|
+
"sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"provenance_chain": [
|
|
44
|
+
{
|
|
45
|
+
"step": "generate",
|
|
46
|
+
"actor": "agent:motionloom-ci",
|
|
47
|
+
"source": "repository-fixture",
|
|
48
|
+
"timestamp": "2026-08-14T00:00:00Z"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"step": "runtime-test",
|
|
52
|
+
"actor": "runtime:lottie",
|
|
53
|
+
"source": "browser-review-smoke runtime evidence",
|
|
54
|
+
"timestamp": "2026-08-14T00:00:00Z"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"step": "human-review",
|
|
58
|
+
"actor": "user:fixture-maintainer",
|
|
59
|
+
"source": "deterministic fixture review boundary",
|
|
60
|
+
"timestamp": "2026-08-14T00:00:00Z"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"runtime_evidence": {
|
|
64
|
+
"status": "pass",
|
|
65
|
+
"runtime": "lottie-runtime",
|
|
66
|
+
"tested_at": "2026-08-14T00:00:00Z",
|
|
67
|
+
"evidence_path": "snapshot/.render-meta.json"
|
|
68
|
+
},
|
|
69
|
+
"full_gate": {
|
|
70
|
+
"status": "pass",
|
|
71
|
+
"quality_gate": "pass",
|
|
72
|
+
"visual_truth": "pass",
|
|
73
|
+
"license": "pass",
|
|
74
|
+
"checked_at": "2026-08-14T00:00:00Z",
|
|
75
|
+
"report_path": "artifacts/browser-review-smoke-task/quality-report.json"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.0",
|
|
3
3
|
"contract": "motionloom-visual-truth",
|
|
4
|
-
"generated_at": "2026-08-
|
|
4
|
+
"generated_at": "2026-08-13T17:17:31.327320Z",
|
|
5
5
|
"status": "review_required",
|
|
6
6
|
"scene": "browser-review-smoke",
|
|
7
7
|
"task_id": "browser-review-smoke-task",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"source_path": "src/output/browser-review-smoke/animation.json",
|
|
52
52
|
"source_sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a",
|
|
53
53
|
"manifest_path": "src/output/browser-review-smoke/manifest.json",
|
|
54
|
-
"manifest_sha256": "
|
|
54
|
+
"manifest_sha256": "8d1e07c81dbbcbcc060415ac71dfcae7ce88f2e7aa91347b226dbf4f554e5623",
|
|
55
55
|
"runtime_evidence_path": "artifacts/browser-review-smoke-task/runtime-adapters/runtime-evidence.json",
|
|
56
|
-
"runtime_evidence_sha256": "
|
|
56
|
+
"runtime_evidence_sha256": "88194b8ed8e8eb9780a80bc55114b9cf52886c911b70a7f4331c18fa39a9b090",
|
|
57
57
|
"motion_ir_path": "artifacts/browser-review-smoke-task/motion-ir.json",
|
|
58
58
|
"motion_ir_sha256": "bdb2305737e5dee9b9ee078dd5657a4a5717bf4184f112a1d01ac82361c5a23f",
|
|
59
59
|
"runtime_status": "pass",
|
|
@@ -256,6 +256,10 @@ def test_runtime_evidence_binding():
|
|
|
256
256
|
root = Path(td)
|
|
257
257
|
scene = root / "src/output/browser-review-smoke"
|
|
258
258
|
shutil.copytree(ROOT / "src/output/browser-review-smoke", scene)
|
|
259
|
+
candidate_path = scene / "browser-review.json"
|
|
260
|
+
candidate = json.loads(candidate_path.read_text())
|
|
261
|
+
candidate["expires_at"] = "2099-01-01T00:00:00Z"
|
|
262
|
+
candidate_path.write_text(json.dumps(candidate, indent=2) + "\n")
|
|
259
263
|
context = root / "project-context.json"
|
|
260
264
|
shutil.copy(ROOT / "artifacts/browser-review-smoke-task/project-context.json", context)
|
|
261
265
|
|
|
@@ -902,6 +906,16 @@ def test_quality_workflow_rebuilds_replay_after_generated_artifacts():
|
|
|
902
906
|
"cross-platform installation matrix contract passes",
|
|
903
907
|
matrix_tests.returncode == 0 and "installation matrix tests: PASS" in matrix_tests.stdout,
|
|
904
908
|
)
|
|
909
|
+
setup_tests = subprocess.run(
|
|
910
|
+
[sys.executable, str(ROOT / "tests/scripts/test_setup.py")],
|
|
911
|
+
capture_output=True,
|
|
912
|
+
text=True,
|
|
913
|
+
)
|
|
914
|
+
check(
|
|
915
|
+
"one-command onboarding is idempotent and project-bound",
|
|
916
|
+
setup_tests.returncode == 0 and "setup onboarding tests: PASS" in setup_tests.stdout,
|
|
917
|
+
setup_tests.stdout.strip() or setup_tests.stderr.strip(),
|
|
918
|
+
)
|
|
905
919
|
visual_tests = subprocess.run(
|
|
906
920
|
[sys.executable, str(ROOT / "tests/scripts/test_visual_truth.py")],
|
|
907
921
|
capture_output=True,
|
|
@@ -921,6 +935,16 @@ def test_quality_workflow_rebuilds_replay_after_generated_artifacts():
|
|
|
921
935
|
remediation_tests.returncode == 0 and "remediation learning tests: PASS" in remediation_tests.stdout,
|
|
922
936
|
remediation_tests.stdout.strip() or remediation_tests.stderr.strip(),
|
|
923
937
|
)
|
|
938
|
+
asset_provenance_tests = subprocess.run(
|
|
939
|
+
[sys.executable, str(ROOT / "tests/scripts/test_asset_provenance.py")],
|
|
940
|
+
capture_output=True,
|
|
941
|
+
text=True,
|
|
942
|
+
)
|
|
943
|
+
check(
|
|
944
|
+
"Asset provenance preserves runtime-ready and human-governed production boundaries",
|
|
945
|
+
asset_provenance_tests.returncode == 0 and "asset provenance contract tests: PASS" in asset_provenance_tests.stdout,
|
|
946
|
+
asset_provenance_tests.stdout.strip() or asset_provenance_tests.stderr.strip(),
|
|
947
|
+
)
|
|
924
948
|
|
|
925
949
|
|
|
926
950
|
if __name__ == "__main__":
|