motionloom 2.0.0 → 2.2.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/.agents/skills/motionloom/SKILL.md +14 -0
- package/.claude/skills/motionloom.md +5 -0
- package/.codex/skills/motionloom.md +11 -0
- package/AGENTS.md +17 -0
- package/CHANGELOG.md +68 -0
- package/CODE_OF_CONDUCT.md +19 -0
- package/CONTRIBUTING.md +65 -0
- package/README.md +193 -134
- package/ROADMAP.md +36 -0
- package/SECURITY.md +28 -0
- package/SKILL.md +57 -9
- package/SUPPORT.md +23 -0
- package/agent-card.json +42 -6
- package/agent-surfaces.json +79 -0
- package/bin/motionloom.mjs +33 -5
- package/docs/AGENT-INTEGRATION.md +47 -0
- package/docs/CHECKLIST.md +2 -1
- package/docs/STATUS.md +33 -0
- package/docs/audits/2.1.0-deep-stress-evaluation.md +97 -0
- package/docs/audits/ci-replay-remediation-2026-08-13.md +33 -0
- package/docs/audits/data/2.1.0-deep-stress-6900.json +329 -0
- package/docs/audits/data/deep-stress-latest.json +329 -0
- package/docs/audits/external-project-corpus-2026-08-13.md +26 -0
- package/docs/releases/2.1.0.md +23 -0
- package/docs/releases/2.2.0.md +35 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/examples/agent-consumer/README.md +18 -0
- package/examples/agent-consumer/fixture-manifest.json +82 -0
- package/package.json +69 -28
- package/references/agent-interoperability.md +29 -0
- package/references/intelligence-core.md +5 -1
- package/schemas/agent-surfaces.schema.json +78 -0
- package/schemas/project-memory.schema.json +180 -0
- package/schemas/remediation-history.schema.json +23 -0
- package/schemas/scene-manifest.schema.json +1 -0
- package/schemas/visual-truth.schema.json +80 -0
- package/scripts/analyze.py +56 -0
- package/scripts/capture-runtime-telemetry.py +119 -0
- package/scripts/devlab.py +126 -0
- package/scripts/discovery.py +257 -0
- package/scripts/docs-audit.py +112 -0
- package/scripts/eval-intelligence.py +23 -0
- package/scripts/eval-projects.py +156 -0
- package/scripts/intelligence.py +106 -6
- package/scripts/pr.py +151 -0
- package/scripts/prepack-clean.mjs +37 -0
- package/scripts/project-memory.py +483 -0
- package/scripts/project_memory_loader.py +31 -0
- package/scripts/quality-gate.py +43 -3
- package/scripts/release-verify.py +52 -0
- package/scripts/remediation-learning.py +326 -0
- package/scripts/render.py +65 -0
- package/scripts/report.py +60 -2
- package/scripts/review-hook.py +13 -2
- package/scripts/skill-doctor.py +12 -2
- package/scripts/to-dotlottie.mjs +26 -20
- package/scripts/visual-truth.py +310 -0
- package/src/core/analyzer.py +174 -25
- package/src/output/browser-review-smoke/manifest.json +1 -0
- package/src/output/browser-review-smoke/visual-truth.json +68 -0
- package/tests/evals/intelligence-cases.json +10 -0
- package/tests/evals/project-corpus.json +51 -0
- package/tests/scripts/run_tests.py +111 -1
- package/tests/scripts/test_project_memory.py +129 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Cross-platform runtime telemetry capture for MotionLoom.
|
|
2
|
+
|
|
3
|
+
This is the platform-neutral replacement for capture-runtime-telemetry.sh.
|
|
4
|
+
It intentionally shells out only to the repository's npm runtime:test script,
|
|
5
|
+
using pathlib and subprocess APIs that work on Ubuntu, macOS and Windows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SCENE_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_args() -> argparse.Namespace:
|
|
24
|
+
parser = argparse.ArgumentParser(description="Capture and verify runtime telemetry")
|
|
25
|
+
parser.add_argument("scene")
|
|
26
|
+
parser.add_argument("task_dir")
|
|
27
|
+
parser.add_argument("--max-age-days", type=int, default=1)
|
|
28
|
+
return parser.parse_args()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_json(path: Path) -> dict:
|
|
32
|
+
try:
|
|
33
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
34
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
35
|
+
raise SystemExit(f"capture-runtime-telemetry: invalid JSON {path}: {exc}") from exc
|
|
36
|
+
if not isinstance(value, dict):
|
|
37
|
+
raise SystemExit(f"capture-runtime-telemetry: expected object in {path}")
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
args = parse_args()
|
|
43
|
+
if not SCENE_RE.fullmatch(args.scene):
|
|
44
|
+
print(f"capture-runtime-telemetry: unsafe scene identifier: {args.scene}", file=sys.stderr)
|
|
45
|
+
return 2
|
|
46
|
+
if args.max_age_days < 0:
|
|
47
|
+
print("capture-runtime-telemetry: --max-age-days must be non-negative", file=sys.stderr)
|
|
48
|
+
return 2
|
|
49
|
+
|
|
50
|
+
root = Path(__file__).resolve().parents[1]
|
|
51
|
+
task_dir = Path(args.task_dir).expanduser()
|
|
52
|
+
task_dir = task_dir.resolve() if task_dir.is_absolute() else (root / task_dir).resolve()
|
|
53
|
+
scene_dir = (root / "src" / "output" / args.scene).resolve()
|
|
54
|
+
manifest_path = scene_dir / "manifest.json"
|
|
55
|
+
task_path = task_dir / "task.json"
|
|
56
|
+
if not manifest_path.is_file() or not task_path.is_file():
|
|
57
|
+
print("capture-runtime-telemetry: missing scene manifest or task.json", file=sys.stderr)
|
|
58
|
+
return 2
|
|
59
|
+
|
|
60
|
+
manifest = load_json(manifest_path)
|
|
61
|
+
task = load_json(task_path)
|
|
62
|
+
source = manifest.get("file")
|
|
63
|
+
task_id = task.get("task_id")
|
|
64
|
+
if not isinstance(source, str) or not source or Path(source).is_absolute() or ".." in Path(source).parts:
|
|
65
|
+
print("capture-runtime-telemetry: manifest.file must be a safe relative path", file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
if not isinstance(task_id, str) or not task_id:
|
|
68
|
+
print("capture-runtime-telemetry: task.json.task_id is required", file=sys.stderr)
|
|
69
|
+
return 2
|
|
70
|
+
|
|
71
|
+
source_path = (scene_dir / source).resolve()
|
|
72
|
+
if scene_dir not in source_path.parents or not source_path.is_file():
|
|
73
|
+
print("capture-runtime-telemetry: manifest source must remain inside scene directory", file=sys.stderr)
|
|
74
|
+
return 2
|
|
75
|
+
|
|
76
|
+
output_dir = task_dir / "runtime-adapters"
|
|
77
|
+
if output_dir.exists():
|
|
78
|
+
shutil.rmtree(output_dir)
|
|
79
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
env = os.environ.copy()
|
|
82
|
+
env.update(
|
|
83
|
+
{
|
|
84
|
+
"RUNTIME_EVIDENCE_DIR": str(output_dir),
|
|
85
|
+
"RUNTIME_SCENE": args.scene,
|
|
86
|
+
"RUNTIME_TASK_ID": task_id,
|
|
87
|
+
"RUNTIME_SOURCE_PATH": str(source_path),
|
|
88
|
+
"RUNTIME_MANIFEST_PATH": str(manifest_path),
|
|
89
|
+
"RUNTIME_MOTION_IR_PATH": str(task_dir / "motion-ir.json"),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
npm = "npm.cmd" if os.name == "nt" else "npm"
|
|
93
|
+
runtime = subprocess.run([npm, "run", "runtime:test"], cwd=root, env=env, check=False)
|
|
94
|
+
if runtime.returncode != 0:
|
|
95
|
+
return runtime.returncode
|
|
96
|
+
|
|
97
|
+
verifier = subprocess.run(
|
|
98
|
+
[
|
|
99
|
+
sys.executable,
|
|
100
|
+
str(root / "scripts" / "evidence-verifier.py"),
|
|
101
|
+
"--scene-dir",
|
|
102
|
+
str(scene_dir),
|
|
103
|
+
"--task-dir",
|
|
104
|
+
str(task_dir),
|
|
105
|
+
"--runtime-evidence",
|
|
106
|
+
"runtime-adapters/runtime-evidence.json",
|
|
107
|
+
"--max-age-days",
|
|
108
|
+
str(args.max_age_days),
|
|
109
|
+
"--output",
|
|
110
|
+
str(task_dir / "evidence-verifier-report.json"),
|
|
111
|
+
],
|
|
112
|
+
cwd=root,
|
|
113
|
+
check=False,
|
|
114
|
+
)
|
|
115
|
+
return verifier.returncode
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Cross-platform Dev Lab scene/task preparation and local serving helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
16
|
+
TASK_ARTIFACTS = (
|
|
17
|
+
"task.json", "browser-review.json", "review.json", "execution-report.json",
|
|
18
|
+
"handoff.json", "quality-report.json", "artifact-manifest.json",
|
|
19
|
+
"issue-register.json", "decision-log.jsonl", "project-memory.json",
|
|
20
|
+
"project-graph.json", "provenance.json", "capability-registry.json",
|
|
21
|
+
"motion-ir.json", "replay-bundle.json", "semantic-lint-report.json",
|
|
22
|
+
"continuity-report.json", "fix-plan.json", "visual-truth.json", "browser-observation.md",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fail(message: str) -> "NoReturn":
|
|
27
|
+
print(f"MotionLoom Dev Lab error: {message}", file=sys.stderr)
|
|
28
|
+
raise SystemExit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_json(path: Path) -> dict:
|
|
32
|
+
try:
|
|
33
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
34
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
35
|
+
fail(f"cannot read JSON {path}: {exc}")
|
|
36
|
+
return value if isinstance(value, dict) else {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def safe_name(value: str, label: str) -> None:
|
|
40
|
+
if not value or value in {".", ".."} or not SAFE_NAME.fullmatch(value):
|
|
41
|
+
fail(f"{label} contains unsafe path characters: {value!r}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def inside(child: Path, parent: Path) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
child.relative_to(parent)
|
|
47
|
+
return True
|
|
48
|
+
except ValueError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def prepare_scene(root: Path, lab: Path, scene: str, task_dir: Path | None) -> tuple[Path, Path | None, str | None]:
|
|
53
|
+
safe_name(scene, "scene")
|
|
54
|
+
scene_dir = (root / "src" / "output" / scene).resolve()
|
|
55
|
+
if not scene_dir.is_dir():
|
|
56
|
+
fail(f"scene directory not found: {scene_dir}; render the scene first")
|
|
57
|
+
if not (scene_dir / "browser-review.json").is_file():
|
|
58
|
+
fail("browser-review.json is required; run review-hook prepare first")
|
|
59
|
+
lab_root = lab.resolve()
|
|
60
|
+
destination = (lab_root / "public" / "scenes" / scene).resolve()
|
|
61
|
+
if not inside(destination, (lab_root / "public" / "scenes").resolve()):
|
|
62
|
+
fail("scene destination escaped Dev Lab public directory")
|
|
63
|
+
if destination.exists():
|
|
64
|
+
shutil.rmtree(destination)
|
|
65
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
shutil.copytree(scene_dir, destination)
|
|
67
|
+
|
|
68
|
+
task_destination: Path | None = None
|
|
69
|
+
task_id: str | None = None
|
|
70
|
+
if task_dir is not None:
|
|
71
|
+
task_dir = task_dir.resolve()
|
|
72
|
+
if not inside(task_dir, root.resolve()):
|
|
73
|
+
fail("task bundle must be inside the MotionLoom repository")
|
|
74
|
+
task = read_json(task_dir / "task.json")
|
|
75
|
+
task_id = str(task.get("task_id") or "")
|
|
76
|
+
safe_name(task_id, "task_id")
|
|
77
|
+
task_destination = (lab_root / "public" / "tasks" / task_id).resolve()
|
|
78
|
+
if not inside(task_destination, (lab_root / "public" / "tasks").resolve()):
|
|
79
|
+
fail("task destination escaped Dev Lab public directory")
|
|
80
|
+
if task_destination.exists():
|
|
81
|
+
shutil.rmtree(task_destination)
|
|
82
|
+
task_destination.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
for name in TASK_ARTIFACTS:
|
|
84
|
+
source = task_dir / name
|
|
85
|
+
if source.is_file():
|
|
86
|
+
shutil.copy2(source, task_destination / name)
|
|
87
|
+
return destination, task_destination, task_id
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pnpm_executable() -> str:
|
|
91
|
+
candidate = "pnpm.cmd" if os.name == "nt" else "pnpm"
|
|
92
|
+
return shutil.which(candidate) or candidate
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def serve(lab: Path, port: int) -> int:
|
|
96
|
+
if not (lab / "public").is_dir():
|
|
97
|
+
fail(f"Dev Lab public directory not found: {lab / 'public'}")
|
|
98
|
+
if not (lab / "node_modules").is_dir():
|
|
99
|
+
print("== installing Dev Lab dependencies (first run) ==")
|
|
100
|
+
subprocess.run([pnpm_executable(), "install", "--silent"], cwd=lab, check=True)
|
|
101
|
+
print(f"== Dev Lab ready: http://localhost:{port}/ ==")
|
|
102
|
+
return subprocess.run([sys.executable, "-m", "http.server", str(port), "--directory", str(lab / "public")], check=False).returncode
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
107
|
+
parser.add_argument("scene")
|
|
108
|
+
parser.add_argument("--task-dir")
|
|
109
|
+
parser.add_argument("--lab-dir")
|
|
110
|
+
parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "3300")))
|
|
111
|
+
parser.add_argument("--prepare-only", action="store_true")
|
|
112
|
+
args = parser.parse_args()
|
|
113
|
+
root = Path(__file__).resolve().parents[1]
|
|
114
|
+
lab = Path(args.lab_dir or os.environ.get("MOTIONLOOM_DEV_LAB") or root / "dev-lab").expanduser().resolve()
|
|
115
|
+
task_dir = Path(args.task_dir).expanduser().resolve() if args.task_dir else None
|
|
116
|
+
destination, task_destination, task_id = prepare_scene(root, lab, args.scene, task_dir)
|
|
117
|
+
print(f"== Dev Lab scene prepared: {destination} ==")
|
|
118
|
+
if task_destination:
|
|
119
|
+
print(f"== Dev Lab task bundle prepared: {task_destination} (task_id={task_id}) ==")
|
|
120
|
+
if args.prepare_only:
|
|
121
|
+
return 0
|
|
122
|
+
return serve(lab, args.port)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate and expose MotionLoom's cross-agent discovery contract.
|
|
3
|
+
|
|
4
|
+
The command is deliberately offline and read-only. It verifies that every
|
|
5
|
+
Agent-facing surface points back to the canonical root SKILL.md, that install
|
|
6
|
+
recipes name a deterministic verification command, and that the package can be
|
|
7
|
+
discovered from a clean npm/Git/local checkout without inferring approval.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import platform
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SCHEMA_VERSION = "1.0"
|
|
22
|
+
EXIT_OK = 0
|
|
23
|
+
EXIT_USAGE = 2
|
|
24
|
+
EXIT_INVALID = 11
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def repo_root() -> Path:
|
|
28
|
+
return Path(__file__).resolve().parent.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_json(path: Path) -> Any:
|
|
32
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def relpath(path: Path, root: Path) -> str:
|
|
36
|
+
return path.relative_to(root).as_posix()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_package(root: Path) -> dict[str, Any]:
|
|
40
|
+
package = load_json(root / "package.json")
|
|
41
|
+
return package if isinstance(package, dict) else {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def git_remote(root: Path) -> str | None:
|
|
45
|
+
try:
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
["git", "-C", str(root), "config", "--get", "remote.origin.url"],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
check=False,
|
|
51
|
+
)
|
|
52
|
+
except OSError:
|
|
53
|
+
return None
|
|
54
|
+
value = result.stdout.strip()
|
|
55
|
+
return value or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def source_identity(root: Path) -> dict[str, Any]:
|
|
59
|
+
package = read_package(root)
|
|
60
|
+
return {
|
|
61
|
+
"name": package.get("name"),
|
|
62
|
+
"version": package.get("version"),
|
|
63
|
+
"root": str(root.resolve()),
|
|
64
|
+
"git_remote": git_remote(root),
|
|
65
|
+
"platform": platform.system().lower(),
|
|
66
|
+
"node_entrypoint": str((root / "bin" / "motionloom.mjs").resolve()),
|
|
67
|
+
"canonical_skill": str((root / "SKILL.md").resolve()),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate(root: Path) -> dict[str, Any]:
|
|
72
|
+
errors: list[str] = []
|
|
73
|
+
warnings: list[str] = []
|
|
74
|
+
root = root.resolve()
|
|
75
|
+
manifest_path = root / "agent-surfaces.json"
|
|
76
|
+
|
|
77
|
+
if not manifest_path.is_file():
|
|
78
|
+
return {"status": "fail", "errors": ["missing agent-surfaces.json"], "warnings": [], "root": str(root)}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
manifest = load_json(manifest_path)
|
|
82
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
83
|
+
return {"status": "fail", "errors": [f"invalid agent-surfaces.json: {exc}"], "warnings": [], "root": str(root)}
|
|
84
|
+
|
|
85
|
+
if manifest.get("schema_version") != SCHEMA_VERSION:
|
|
86
|
+
errors.append(f"unsupported schema_version: {manifest.get('schema_version')!r}")
|
|
87
|
+
package = read_package(root)
|
|
88
|
+
if manifest.get("name") != package.get("name"):
|
|
89
|
+
errors.append("manifest name does not match package.json")
|
|
90
|
+
if manifest.get("version") != package.get("version"):
|
|
91
|
+
errors.append("manifest version does not match package.json")
|
|
92
|
+
if manifest.get("canonical") != {
|
|
93
|
+
"skill": "SKILL.md",
|
|
94
|
+
"agent_card": "agent-card.json",
|
|
95
|
+
"cli": "bin/motionloom.mjs",
|
|
96
|
+
}:
|
|
97
|
+
errors.append("canonical paths do not match the package contract")
|
|
98
|
+
|
|
99
|
+
for required in ("SKILL.md", "agent-card.json", "bin/motionloom.mjs", "package.json"):
|
|
100
|
+
path = root / required
|
|
101
|
+
if not path.is_file():
|
|
102
|
+
errors.append(f"missing canonical file: {required}")
|
|
103
|
+
|
|
104
|
+
surfaces = manifest.get("surfaces")
|
|
105
|
+
if not isinstance(surfaces, list) or not surfaces:
|
|
106
|
+
errors.append("surfaces must be a non-empty array")
|
|
107
|
+
surfaces = []
|
|
108
|
+
ids: set[str] = set()
|
|
109
|
+
paths: set[str] = set()
|
|
110
|
+
for surface in surfaces:
|
|
111
|
+
if not isinstance(surface, dict):
|
|
112
|
+
errors.append("surface entry must be an object")
|
|
113
|
+
continue
|
|
114
|
+
surface_id = surface.get("id")
|
|
115
|
+
surface_path = surface.get("path")
|
|
116
|
+
if surface_id in ids:
|
|
117
|
+
errors.append(f"duplicate surface id: {surface_id}")
|
|
118
|
+
if isinstance(surface_id, str):
|
|
119
|
+
ids.add(surface_id)
|
|
120
|
+
if not isinstance(surface_path, str) or surface_path.startswith("/") or ".." in Path(surface_path).parts:
|
|
121
|
+
errors.append(f"surface path is not safe: {surface_path!r}")
|
|
122
|
+
continue
|
|
123
|
+
if surface_path in paths:
|
|
124
|
+
errors.append(f"duplicate surface path: {surface_path}")
|
|
125
|
+
paths.add(surface_path)
|
|
126
|
+
file_path = root / surface_path
|
|
127
|
+
if not file_path.is_file():
|
|
128
|
+
errors.append(f"missing surface file: {surface_path}")
|
|
129
|
+
if file_path.is_symlink():
|
|
130
|
+
errors.append(f"symlinked surface is not portable: {surface_path}")
|
|
131
|
+
if surface.get("canonical") != "SKILL.md":
|
|
132
|
+
errors.append(f"surface {surface_id!r} does not point to SKILL.md")
|
|
133
|
+
if surface.get("load_mode") not in {"alias", "router"}:
|
|
134
|
+
errors.append(f"surface {surface_id!r} has invalid load_mode")
|
|
135
|
+
if not isinstance(surface.get("agents"), list) or not surface.get("agents"):
|
|
136
|
+
errors.append(f"surface {surface_id!r} has no supported agents")
|
|
137
|
+
|
|
138
|
+
installations = manifest.get("installations")
|
|
139
|
+
if not isinstance(installations, list) or not installations:
|
|
140
|
+
errors.append("installations must be a non-empty array")
|
|
141
|
+
installations = []
|
|
142
|
+
installation_ids: set[str] = set()
|
|
143
|
+
for item in installations:
|
|
144
|
+
if not isinstance(item, dict):
|
|
145
|
+
errors.append("installation entry must be an object")
|
|
146
|
+
continue
|
|
147
|
+
item_id = item.get("id")
|
|
148
|
+
if item_id in installation_ids:
|
|
149
|
+
errors.append(f"duplicate installation id: {item_id}")
|
|
150
|
+
if isinstance(item_id, str):
|
|
151
|
+
installation_ids.add(item_id)
|
|
152
|
+
for key in ("source_kind", "command", "verification", "provenance"):
|
|
153
|
+
if not item.get(key):
|
|
154
|
+
errors.append(f"installation {item_id!r} missing {key}")
|
|
155
|
+
|
|
156
|
+
compatibility = manifest.get("compatibility", {})
|
|
157
|
+
for key in ("operating_systems", "node", "python", "agents"):
|
|
158
|
+
if not compatibility.get(key):
|
|
159
|
+
errors.append(f"compatibility missing {key}")
|
|
160
|
+
rules = manifest.get("rules", {})
|
|
161
|
+
if rules.get("canonical_instruction_source") != "SKILL.md":
|
|
162
|
+
errors.append("canonical_instruction_source must be SKILL.md")
|
|
163
|
+
for key in ("no_surface_copy", "no_network_required_for_check", "approval_is_never_inferred"):
|
|
164
|
+
if rules.get(key) is not True:
|
|
165
|
+
errors.append(f"rule {key} must remain true")
|
|
166
|
+
|
|
167
|
+
package_files = package.get("files", [])
|
|
168
|
+
for required_package_path in ("agent-surfaces.json", ".agents", ".claude", ".codex", "AGENTS.md"):
|
|
169
|
+
if required_package_path not in package_files:
|
|
170
|
+
warnings.append(f"package.json files does not explicitly include {required_package_path}")
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"status": "pass" if not errors else "fail",
|
|
174
|
+
"schema_version": SCHEMA_VERSION,
|
|
175
|
+
"root": str(root),
|
|
176
|
+
"source": source_identity(root),
|
|
177
|
+
"surface_count": len(surfaces),
|
|
178
|
+
"installation_count": len(installations),
|
|
179
|
+
"errors": errors,
|
|
180
|
+
"warnings": warnings,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def install_matrix(root: Path) -> dict[str, Any]:
|
|
185
|
+
result = validate(root)
|
|
186
|
+
manifest = load_json(root / "agent-surfaces.json") if (root / "agent-surfaces.json").is_file() else {}
|
|
187
|
+
rows = []
|
|
188
|
+
for item in manifest.get("installations", []):
|
|
189
|
+
rows.append({
|
|
190
|
+
"id": item.get("id"),
|
|
191
|
+
"source_kind": item.get("source_kind"),
|
|
192
|
+
"command": item.get("command"),
|
|
193
|
+
"verification": item.get("verification"),
|
|
194
|
+
"provenance": item.get("provenance"),
|
|
195
|
+
"status": "available" if result.get("status") == "pass" else "blocked_by_contract",
|
|
196
|
+
})
|
|
197
|
+
return {"status": result.get("status"), "matrix": rows, "compatibility": manifest.get("compatibility", {}), "errors": result.get("errors", [])}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def parser() -> argparse.ArgumentParser:
|
|
201
|
+
root_default = str(repo_root())
|
|
202
|
+
command = argparse.ArgumentParser(prog="motionloom discovery", description=__doc__)
|
|
203
|
+
sub = command.add_subparsers(dest="action", required=True)
|
|
204
|
+
for name, help_text in (
|
|
205
|
+
("check", "Validate Agent surfaces and installation contract"),
|
|
206
|
+
("show", "Print the canonical discovery manifest"),
|
|
207
|
+
("source", "Print source identity for this checkout"),
|
|
208
|
+
("install-matrix", "Print supported installation sources and verification commands"),
|
|
209
|
+
):
|
|
210
|
+
child = sub.add_parser(name, help=help_text)
|
|
211
|
+
child.add_argument("--root", default=root_default, help="MotionLoom checkout root")
|
|
212
|
+
child.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
|
213
|
+
return command
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main(argv: list[str] | None = None) -> int:
|
|
217
|
+
args = parser().parse_args(argv)
|
|
218
|
+
root = Path(args.root).expanduser().resolve()
|
|
219
|
+
if args.action == "check":
|
|
220
|
+
result = validate(root)
|
|
221
|
+
elif args.action == "show":
|
|
222
|
+
try:
|
|
223
|
+
result = load_json(root / "agent-surfaces.json")
|
|
224
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
225
|
+
result = {"status": "fail", "errors": [str(exc)]}
|
|
226
|
+
elif args.action == "source":
|
|
227
|
+
result = source_identity(root)
|
|
228
|
+
else:
|
|
229
|
+
result = install_matrix(root)
|
|
230
|
+
|
|
231
|
+
if args.json:
|
|
232
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
233
|
+
else:
|
|
234
|
+
if args.action == "show":
|
|
235
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
236
|
+
elif args.action == "source":
|
|
237
|
+
print(f"{result.get('name')}@{result.get('version')} — {result.get('platform')} — {result.get('root')}")
|
|
238
|
+
if result.get("git_remote"):
|
|
239
|
+
print(f"remote: {result['git_remote']}")
|
|
240
|
+
elif args.action == "install-matrix":
|
|
241
|
+
print(f"installation matrix: {result.get('status')}")
|
|
242
|
+
for row in result.get("matrix", []):
|
|
243
|
+
print(f"- {row['id']}: {row['command']} -> {row['verification']}")
|
|
244
|
+
else:
|
|
245
|
+
print(f"discovery contract: {result.get('status')}")
|
|
246
|
+
for error in result.get("errors", []):
|
|
247
|
+
print(f"error: {error}")
|
|
248
|
+
for warning in result.get("warnings", []):
|
|
249
|
+
print(f"warning: {warning}")
|
|
250
|
+
return EXIT_OK if result.get("status") in {None, "pass"} else EXIT_INVALID
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
try:
|
|
255
|
+
raise SystemExit(main())
|
|
256
|
+
except KeyboardInterrupt:
|
|
257
|
+
raise SystemExit(EXIT_USAGE)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate public documentation, links and workflow safety without network access."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).resolve().parents[1])
|
|
13
|
+
errors: list[str] = []
|
|
14
|
+
|
|
15
|
+
for markdown in sorted(ROOT.rglob("*.md")):
|
|
16
|
+
if any(part in {".git", "node_modules"} for part in markdown.parts):
|
|
17
|
+
continue
|
|
18
|
+
text = markdown.read_text(encoding="utf-8")
|
|
19
|
+
for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", text):
|
|
20
|
+
target = target.strip().split("#", 1)[0].split("?", 1)[0]
|
|
21
|
+
if not target or target.startswith(("http://", "https://", "mailto:", "#")):
|
|
22
|
+
continue
|
|
23
|
+
if not (markdown.parent / target).resolve().exists():
|
|
24
|
+
errors.append(f"{markdown.relative_to(ROOT)} -> missing {target}")
|
|
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"]:
|
|
27
|
+
path = ROOT / relative
|
|
28
|
+
try:
|
|
29
|
+
json.loads(path.read_text(encoding="utf-8"))
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
errors.append(f"{relative}: invalid JSON: {exc}")
|
|
32
|
+
|
|
33
|
+
package = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
34
|
+
for required in ["author", "repository", "homepage", "bugs", "license", "engines", "files"]:
|
|
35
|
+
if not package.get(required):
|
|
36
|
+
errors.append(f"package.json: missing public metadata {required}")
|
|
37
|
+
if package.get("packageManager") != "pnpm@11.20.0":
|
|
38
|
+
errors.append("package.json: packageManager must pin pnpm@11.20.0")
|
|
39
|
+
for required_surface in [".agents", ".claude", ".codex", "AGENTS.md", "agent-surfaces.json"]:
|
|
40
|
+
if required_surface not in package.get("files", []):
|
|
41
|
+
errors.append(f"package.json: files must include Agent surface {required_surface}")
|
|
42
|
+
|
|
43
|
+
sys.path.insert(0, str(ROOT))
|
|
44
|
+
try:
|
|
45
|
+
from scripts.discovery import validate as validate_discovery
|
|
46
|
+
discovery = validate_discovery(ROOT)
|
|
47
|
+
for discovery_error in discovery.get("errors", []):
|
|
48
|
+
errors.append(f"agent discovery: {discovery_error}")
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
errors.append(f"agent discovery: validator could not load: {exc}")
|
|
51
|
+
|
|
52
|
+
for required_doc in ["docs/AGENT-INTEGRATION.md", "references/agent-interoperability.md"]:
|
|
53
|
+
if not (ROOT / required_doc).is_file():
|
|
54
|
+
errors.append(f"missing Agent interoperability document: {required_doc}")
|
|
55
|
+
|
|
56
|
+
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"]:
|
|
58
|
+
if f"## {heading}" not in readme:
|
|
59
|
+
errors.append(f"README.md: missing heading {heading}")
|
|
60
|
+
|
|
61
|
+
workflow_dir = ROOT / ".github" / "workflows"
|
|
62
|
+
for workflow in sorted(workflow_dir.glob("*.yml")):
|
|
63
|
+
text = workflow.read_text(encoding="utf-8")
|
|
64
|
+
top_level_keys: dict[str, int] = {}
|
|
65
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
66
|
+
match = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):(?:\s|$)", line)
|
|
67
|
+
if not match:
|
|
68
|
+
continue
|
|
69
|
+
key = match.group(1)
|
|
70
|
+
if key in top_level_keys:
|
|
71
|
+
errors.append(
|
|
72
|
+
f"{workflow.relative_to(ROOT)}: duplicate top-level key {key!r} "
|
|
73
|
+
f"at lines {top_level_keys[key]} and {line_number}"
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
top_level_keys[key] = line_number
|
|
77
|
+
for required in ["name:", "on:", "jobs:", "permissions:"]:
|
|
78
|
+
if required not in text:
|
|
79
|
+
errors.append(f"{workflow.relative_to(ROOT)}: missing {required}")
|
|
80
|
+
if "pull_request:" in text and "secrets." in text:
|
|
81
|
+
errors.append(f"{workflow.relative_to(ROOT)}: secrets referenced in pull_request workflow")
|
|
82
|
+
|
|
83
|
+
release = (workflow_dir / "release.yml").read_text(encoding="utf-8")
|
|
84
|
+
for required in ["workflow_dispatch:", "environment: npm-release", "id-token: write", "release_version:", "scripts/release-verify.py"]:
|
|
85
|
+
if required not in release:
|
|
86
|
+
errors.append(f"release.yml: missing release safety control {required}")
|
|
87
|
+
|
|
88
|
+
devlab = (workflow_dir / "devlab.yml").read_text(encoding="utf-8")
|
|
89
|
+
for required in [
|
|
90
|
+
"cp -R src/output/browser-review-smoke dev-lab/public/scenes/browser-review-smoke",
|
|
91
|
+
"scenes/browser-review-smoke/manifest.json",
|
|
92
|
+
"scenes/browser-review-smoke/motion-spec.json",
|
|
93
|
+
"--diagnostics /tmp/motionloom-devlab-diagnostics",
|
|
94
|
+
"id: fixture",
|
|
95
|
+
"steps.fixture.outputs.task_id",
|
|
96
|
+
"steps.fixture.outputs.candidate_id",
|
|
97
|
+
'payload["expires_at"] = "2099-01-01T00:00:00Z"',
|
|
98
|
+
]:
|
|
99
|
+
if required not in devlab:
|
|
100
|
+
errors.append(f"devlab.yml: missing fixture/readiness control {required}")
|
|
101
|
+
|
|
102
|
+
if errors:
|
|
103
|
+
print("Documentation/workflow audit: FAIL")
|
|
104
|
+
print("\n".join(f"- {error}" for error in errors))
|
|
105
|
+
raise SystemExit(1)
|
|
106
|
+
|
|
107
|
+
print("Documentation/workflow audit: PASS")
|
|
108
|
+
print(f"markdown_files={len(list(ROOT.rglob('*.md')))}")
|
|
109
|
+
print(f"workflow_files={len(list(workflow_dir.glob('*.yml')))}")
|
|
110
|
+
print("internal_links=PASS")
|
|
111
|
+
print("json_metadata=PASS")
|
|
112
|
+
print("workflow_safety=PASS")
|
|
@@ -23,6 +23,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
23
23
|
|
|
24
24
|
ROOT = Path(__file__).resolve().parents[1]
|
|
25
25
|
INTELLIGENCE = ROOT / "scripts/intelligence.py"
|
|
26
|
+
PROJECT_EVAL = ROOT / "scripts/eval-projects.py"
|
|
26
27
|
REPORT = ROOT / "scripts/report.py"
|
|
27
28
|
VERIFIER = ROOT / "scripts/evidence-verifier.py"
|
|
28
29
|
ATTESTATION = ROOT / "scripts/attestation.py"
|
|
@@ -134,6 +135,27 @@ def run_attestation_cases(root: Path, results: list[dict[str, object]]) -> None:
|
|
|
134
135
|
record(results, "p2-attestation-unknown-signer", unknown_result.returncode == 13, unknown_result.stdout + unknown_result.stderr)
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
def run_project_corpus_cases(root: Path, results: list[dict[str, object]]) -> None:
|
|
139
|
+
"""Keep first-party analyzer evidence separate from unavailable external evidence."""
|
|
140
|
+
first_party = invoke([
|
|
141
|
+
str(PROJECT_EVAL), "--workspace", str(ROOT), "--require-external", "0", "--allow-insufficient"
|
|
142
|
+
])
|
|
143
|
+
first_doc = json.loads(first_party.stdout) if first_party.stdout.strip().startswith("{") else {}
|
|
144
|
+
first_pass = any(item.get("id") == "motionloom-first-party" and item.get("status") == "pass" for item in first_doc.get("results", []))
|
|
145
|
+
record(results, "project-corpus-first-party-pass", first_party.returncode == 0 and first_doc.get("status") == "pass" and first_pass, first_party.stdout + first_party.stderr)
|
|
146
|
+
|
|
147
|
+
insufficient = invoke([
|
|
148
|
+
str(PROJECT_EVAL), "--workspace", str(ROOT), "--allow-insufficient"
|
|
149
|
+
])
|
|
150
|
+
insufficient_doc = json.loads(insufficient.stdout) if insufficient.stdout.strip().startswith("{") else {}
|
|
151
|
+
record(
|
|
152
|
+
results,
|
|
153
|
+
"project-corpus-insufficient-external-explicit",
|
|
154
|
+
insufficient.returncode == 0 and insufficient_doc.get("status") == "insufficient_evidence" and insufficient_doc.get("unavailable_external_projects") == 3,
|
|
155
|
+
insufficient.stdout + insufficient.stderr,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
137
159
|
def run_p1_cases(root: Path, task_dir: Path, results: list[dict[str, object]]) -> None:
|
|
138
160
|
"""Exercise P1 semantic, continuity and feedback contracts in isolated copies."""
|
|
139
161
|
lint_task = root / "p1-human-review"
|
|
@@ -364,6 +386,7 @@ def main() -> int:
|
|
|
364
386
|
run_performance_perceptual_cases(root, task_dir, results)
|
|
365
387
|
run_runtime_verifier_cases(root, results)
|
|
366
388
|
run_attestation_cases(root, results)
|
|
389
|
+
run_project_corpus_cases(root, results)
|
|
367
390
|
|
|
368
391
|
missing = expected - {str(item["id"]) for item in results}
|
|
369
392
|
for case_id in sorted(missing):
|