motionloom 2.1.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 +27 -0
- package/README.md +6 -0
- package/ROADMAP.md +9 -5
- package/SECURITY.md +3 -2
- package/SKILL.md +27 -4
- package/agent-card.json +23 -2
- package/agent-surfaces.json +79 -0
- package/bin/motionloom.mjs +11 -1
- package/docs/AGENT-INTEGRATION.md +47 -0
- package/docs/CHECKLIST.md +2 -1
- package/docs/STATUS.md +1 -1
- package/docs/audits/ci-replay-remediation-2026-08-13.md +33 -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 +22 -7
- package/references/agent-interoperability.md +29 -0
- package/references/intelligence-core.md +4 -0
- package/schemas/agent-surfaces.schema.json +78 -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/devlab.py +1 -1
- package/scripts/discovery.py +257 -0
- package/scripts/docs-audit.py +17 -1
- package/scripts/pr.py +1 -0
- package/scripts/quality-gate.py +43 -3
- package/scripts/remediation-learning.py +326 -0
- package/scripts/report.py +16 -0
- package/scripts/visual-truth.py +310 -0
- package/src/output/browser-review-smoke/manifest.json +1 -0
- package/src/output/browser-review-smoke/visual-truth.json +68 -0
- package/tests/scripts/run_tests.py +59 -0
|
@@ -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)
|
package/scripts/docs-audit.py
CHANGED
|
@@ -23,7 +23,7 @@ for markdown in sorted(ROOT.rglob("*.md")):
|
|
|
23
23
|
if not (markdown.parent / target).resolve().exists():
|
|
24
24
|
errors.append(f"{markdown.relative_to(ROOT)} -> missing {target}")
|
|
25
25
|
|
|
26
|
-
for relative in ["package.json", "agent-card.json", "project-context.example.json", "tests/evals/project-corpus.json"]:
|
|
26
|
+
for relative in ["package.json", "agent-card.json", "agent-surfaces.json", "schemas/agent-surfaces.schema.json", "schemas/visual-truth.schema.json", "schemas/remediation-history.schema.json", "project-context.example.json", "tests/evals/project-corpus.json"]:
|
|
27
27
|
path = ROOT / relative
|
|
28
28
|
try:
|
|
29
29
|
json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -36,6 +36,22 @@ for required in ["author", "repository", "homepage", "bugs", "license", "engines
|
|
|
36
36
|
errors.append(f"package.json: missing public metadata {required}")
|
|
37
37
|
if package.get("packageManager") != "pnpm@11.20.0":
|
|
38
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}")
|
|
39
55
|
|
|
40
56
|
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
41
57
|
for heading in ["Why MotionLoom", "Quick start", "Durable Project Memory", "Evidence, trust and review", "Documentation map"]:
|
package/scripts/pr.py
CHANGED
|
@@ -94,6 +94,7 @@ def main() -> int:
|
|
|
94
94
|
"--context", args.context or str(repo / "project-context.json"),
|
|
95
95
|
"--task-dir", str(task_dir),
|
|
96
96
|
"--require-browser-review",
|
|
97
|
+
"--require-visual-truth",
|
|
97
98
|
]
|
|
98
99
|
print("== running context-bound quality gate ==")
|
|
99
100
|
run(repo, [python, *quality_args])
|
package/scripts/quality-gate.py
CHANGED
|
@@ -47,6 +47,14 @@ def _load_attestation_verifier():
|
|
|
47
47
|
return module
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _load_visual_truth():
|
|
51
|
+
path = ROOT / "scripts" / "visual-truth.py"
|
|
52
|
+
loader = importlib.util.spec_from_file_location("visual_truth", path)
|
|
53
|
+
module = importlib.util.module_from_spec(loader)
|
|
54
|
+
loader.loader.exec_module(module)
|
|
55
|
+
return module
|
|
56
|
+
|
|
57
|
+
|
|
50
58
|
def _json(path: Path):
|
|
51
59
|
try:
|
|
52
60
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -66,7 +74,7 @@ def _telemetry_bundle_sha256(task_dir: Path) -> str:
|
|
|
66
74
|
return hashlib.sha256(json.dumps(entries, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
67
75
|
|
|
68
76
|
|
|
69
|
-
def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = False, task_dir: Path | None = None, require_intelligence: bool = False, require_p1: bool = False, require_benchmark: bool = False, require_telemetry: bool = False, require_attestation: bool = False, attestation_path: Path | None = None, trust_policy_path: Path | None = None) -> list[str]:
|
|
77
|
+
def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = False, task_dir: Path | None = None, require_intelligence: bool = False, require_p1: bool = False, require_benchmark: bool = False, require_telemetry: bool = False, require_attestation: bool = False, attestation_path: Path | None = None, trust_policy_path: Path | None = None, require_visual_truth: bool = False) -> list[str]:
|
|
70
78
|
issues = []
|
|
71
79
|
manifest_path = scene_dir / "manifest.json"
|
|
72
80
|
spec_path = scene_dir / "motion-spec.json"
|
|
@@ -326,6 +334,36 @@ def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = F
|
|
|
326
334
|
issues.append(f"signed attestation required binding source is missing: {field}")
|
|
327
335
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
328
336
|
issues.append(f"signed attestation contract: {exc}")
|
|
337
|
+
if require_visual_truth:
|
|
338
|
+
visual_name = manifest.get("visual_truth")
|
|
339
|
+
if not isinstance(visual_name, str) or not visual_name.strip():
|
|
340
|
+
issues.append("visual truth gate requires manifest.visual_truth")
|
|
341
|
+
else:
|
|
342
|
+
visual_path = (scene_dir / visual_name).resolve()
|
|
343
|
+
if not visual_path.is_file() or scene_dir.resolve() not in visual_path.parents:
|
|
344
|
+
issues.append("manifest.visual_truth must point to an existing file inside the scene directory")
|
|
345
|
+
else:
|
|
346
|
+
try:
|
|
347
|
+
task_id = None
|
|
348
|
+
motion_ir_hash = None
|
|
349
|
+
if task_dir:
|
|
350
|
+
task = _json(task_dir / "task.json")
|
|
351
|
+
task_id = task.get("task_id")
|
|
352
|
+
motion_ir = task_dir / "motion-ir.json"
|
|
353
|
+
motion_ir_hash = _sha256_file(motion_ir) if motion_ir.is_file() else None
|
|
354
|
+
visual_truth = _load_visual_truth()
|
|
355
|
+
visual_issues = visual_truth.validate_report(
|
|
356
|
+
visual_path,
|
|
357
|
+
ROOT,
|
|
358
|
+
scene_dir.name,
|
|
359
|
+
task_id,
|
|
360
|
+
source_sha_for_evidence,
|
|
361
|
+
manifest_sha_for_evidence,
|
|
362
|
+
motion_ir_hash,
|
|
363
|
+
)
|
|
364
|
+
issues.extend(f"visual truth: {issue}" for issue in visual_issues)
|
|
365
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
366
|
+
issues.append(f"visual truth contract: {exc}")
|
|
329
367
|
return issues
|
|
330
368
|
|
|
331
369
|
|
|
@@ -343,6 +381,7 @@ def main() -> int:
|
|
|
343
381
|
parser.add_argument("--require-benchmark", action="store_true")
|
|
344
382
|
parser.add_argument("--require-telemetry", action="store_true")
|
|
345
383
|
parser.add_argument("--require-attestation", action="store_true")
|
|
384
|
+
parser.add_argument("--require-visual-truth", action="store_true")
|
|
346
385
|
parser.add_argument("--attestation")
|
|
347
386
|
parser.add_argument("--trust-policy")
|
|
348
387
|
args = parser.parse_args()
|
|
@@ -363,14 +402,15 @@ def main() -> int:
|
|
|
363
402
|
return 0
|
|
364
403
|
failed = False
|
|
365
404
|
for scene_dir in scenes:
|
|
366
|
-
issues = validate_scene(scene_dir, context, args.require_browser_review, task_dir, args.require_intelligence, args.require_p1, args.require_benchmark, args.require_telemetry, args.require_attestation, attestation_path, trust_policy_path)
|
|
405
|
+
issues = validate_scene(scene_dir, context, args.require_browser_review, task_dir, args.require_intelligence, args.require_p1, args.require_benchmark, args.require_telemetry, args.require_attestation, attestation_path, trust_policy_path, args.require_visual_truth)
|
|
367
406
|
if issues:
|
|
368
407
|
failed = True
|
|
369
408
|
print(f"REJECTED {scene_dir.name}:")
|
|
370
409
|
for issue in issues:
|
|
371
410
|
print(f" - {issue}")
|
|
372
411
|
else:
|
|
373
|
-
|
|
412
|
+
suffix = " + visual-truth contract" if args.require_visual_truth else ""
|
|
413
|
+
print(f"ACCEPTED {scene_dir.name}: context + spec + runtime snapshots + browser-review candidate + checklist{suffix}")
|
|
374
414
|
return 1 if failed else 0
|
|
375
415
|
|
|
376
416
|
|