devops-bot-sdk 1.4.99__tar.gz → 1.4.101__tar.gz
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.
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/client.py +22 -1
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/design_verify/__init__.py +5 -0
- devops_bot_sdk-1.4.101/sdk/design_verify/assets.py +59 -0
- devops_bot_sdk-1.4.101/sdk/design_verify/bootstrap.py +170 -0
- devops_bot_sdk-1.4.101/sdk/design_verify/loop.py +204 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/ipc/handlers.py +65 -5
- devops_bot_sdk-1.4.99/sdk/design_verify/loop.py +0 -169
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/README.md +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/setup.cfg +0 -0
|
@@ -25,6 +25,8 @@ sdk/collectors/files.py
|
|
|
25
25
|
sdk/collectors/process.py
|
|
26
26
|
sdk/collectors/screenshot.py
|
|
27
27
|
sdk/design_verify/__init__.py
|
|
28
|
+
sdk/design_verify/assets.py
|
|
29
|
+
sdk/design_verify/bootstrap.py
|
|
28
30
|
sdk/design_verify/browser.py
|
|
29
31
|
sdk/design_verify/compare.py
|
|
30
32
|
sdk/design_verify/loop.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.101"
|
|
8
8
|
description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.101
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -30,7 +30,7 @@ Rules:
|
|
|
30
30
|
- All data egress through submit_webhook only
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.4.
|
|
33
|
+
__version__ = "1.4.101"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -58,7 +58,7 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
58
58
|
pass
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
SDK_VERSION = "1.4.
|
|
61
|
+
SDK_VERSION = "1.4.101"
|
|
62
62
|
_POLL_INTERVAL = 3.0
|
|
63
63
|
_POLL_TIMEOUT = 600.0
|
|
64
64
|
_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
|
|
@@ -195,6 +195,27 @@ class BackendClient:
|
|
|
195
195
|
_check_status(resp.status_code, self._base_url)
|
|
196
196
|
return [ThreadSummary(**t) for t in resp.json().get("threads", [])]
|
|
197
197
|
|
|
198
|
+
# ── Design image (for local visual verification) ───────────────────
|
|
199
|
+
|
|
200
|
+
async def fetch_design_image(
|
|
201
|
+
self, jira_task_id: str,
|
|
202
|
+
) -> tuple[bytes, str] | None:
|
|
203
|
+
"""GET /api/v1/orchestrate/design-image/{jira_task_id}.
|
|
204
|
+
|
|
205
|
+
Returns the task's design screenshot as ``(bytes, mime_type)``, or
|
|
206
|
+
``None`` when the task has no image attachment (backend 404). The backend
|
|
207
|
+
proxies the bytes from Jira (Atlassian auth lives there, not here). The
|
|
208
|
+
caller is responsible for treating the bytes as EPHEMERAL — see
|
|
209
|
+
``sdk.design_verify.ephemeral_design_image``.
|
|
210
|
+
"""
|
|
211
|
+
url = self._url(f"/api/v1/orchestrate/design-image/{jira_task_id}")
|
|
212
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
213
|
+
resp = await client.get(url, headers=self._headers)
|
|
214
|
+
if resp.status_code == 404:
|
|
215
|
+
return None
|
|
216
|
+
_check_status(resp.status_code, self._base_url)
|
|
217
|
+
return resp.content, resp.headers.get("content-type", "image/png")
|
|
218
|
+
|
|
198
219
|
# ── Orchestrator ──────────────────────────────────────────────────
|
|
199
220
|
|
|
200
221
|
async def orchestrator_run(
|
|
@@ -34,6 +34,8 @@ argument.
|
|
|
34
34
|
"""
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
|
+
from .assets import ephemeral_design_image
|
|
38
|
+
from .bootstrap import BootstrapResult, ensure_agent_browser
|
|
37
39
|
from .browser import BrowserError, DiffResult, diff_baseline, render_screenshot, snapshot
|
|
38
40
|
from .compare import ComparisonReport, compare_design
|
|
39
41
|
from .loop import RefinementResult, verify_and_refine
|
|
@@ -48,4 +50,7 @@ __all__ = [
|
|
|
48
50
|
"ComparisonReport",
|
|
49
51
|
"verify_and_refine",
|
|
50
52
|
"RefinementResult",
|
|
53
|
+
"ensure_agent_browser",
|
|
54
|
+
"BootstrapResult",
|
|
55
|
+
"ephemeral_design_image",
|
|
51
56
|
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Ephemeral handling of the design image fetched from the backend.
|
|
2
|
+
|
|
3
|
+
The JIRA design screenshot is downloaded from the backend proxy
|
|
4
|
+
(``BackendClient.fetch_design_image``) only for the duration of a verification
|
|
5
|
+
run and MUST NOT be persisted — same rule as the render/diff screenshots. This
|
|
6
|
+
module writes it to an OS temp file and deletes it on exit.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import AsyncIterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _ext_for_mime(mime: str) -> str:
|
|
17
|
+
m = (mime or "").lower()
|
|
18
|
+
if "png" in m:
|
|
19
|
+
return ".png"
|
|
20
|
+
if "jpeg" in m or "jpg" in m:
|
|
21
|
+
return ".jpg"
|
|
22
|
+
if "webp" in m:
|
|
23
|
+
return ".webp"
|
|
24
|
+
return ".img"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextlib.asynccontextmanager
|
|
28
|
+
async def ephemeral_design_image(client, jira_task_id: str) -> AsyncIterator[str | None]:
|
|
29
|
+
"""Download a task's design image to a temp file for the duration of a `with`.
|
|
30
|
+
|
|
31
|
+
Yields the local path, or ``None`` when the task has no design image. The
|
|
32
|
+
file is deleted on exit no matter what — the design image is never persisted.
|
|
33
|
+
|
|
34
|
+
``client`` is a ``BackendClient`` (anything exposing
|
|
35
|
+
``async fetch_design_image(jira_task_id) -> (bytes, mime) | None``).
|
|
36
|
+
|
|
37
|
+
Example::
|
|
38
|
+
|
|
39
|
+
async with ephemeral_design_image(client, task_id) as design_path:
|
|
40
|
+
if design_path is None:
|
|
41
|
+
... # no design attached — skip visual verification
|
|
42
|
+
else:
|
|
43
|
+
await verify_and_refine(design_path=design_path, ...)
|
|
44
|
+
"""
|
|
45
|
+
img = await client.fetch_design_image(jira_task_id)
|
|
46
|
+
if not img:
|
|
47
|
+
yield None
|
|
48
|
+
return
|
|
49
|
+
data, mime = img
|
|
50
|
+
fd, path = tempfile.mkstemp(prefix="agentos_design_", suffix=_ext_for_mime(mime))
|
|
51
|
+
try:
|
|
52
|
+
with os.fdopen(fd, "wb") as f:
|
|
53
|
+
f.write(data)
|
|
54
|
+
yield path
|
|
55
|
+
finally:
|
|
56
|
+
try:
|
|
57
|
+
os.remove(path)
|
|
58
|
+
except OSError:
|
|
59
|
+
pass
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Per-user, on-demand installation of the ``agent-browser`` CLI.
|
|
2
|
+
|
|
3
|
+
Why this lives in the SDK
|
|
4
|
+
-------------------------
|
|
5
|
+
The visual verification loop renders and screenshots the generated frontend, and
|
|
6
|
+
that generation runs on the USER'S machine (the default `claude_code` engine runs
|
|
7
|
+
the local `claude` CLI locally — the backend only plans). So `agent-browser` must
|
|
8
|
+
be present on every user's system, not on the backend server.
|
|
9
|
+
|
|
10
|
+
`agent-browser` is a native Rust binary, not a pip package, so it cannot ride
|
|
11
|
+
along with ``pip install devops-bot-sdk``. Instead we bootstrap it lazily on
|
|
12
|
+
first use: check if it's already there, otherwise install it via the best channel
|
|
13
|
+
available on this machine, then fetch Chrome for Testing once. The whole thing is
|
|
14
|
+
idempotent and cached, and it NEVER hard-fails the SDK — if install isn't
|
|
15
|
+
possible (locked-down box, no package manager), it returns a reason and the
|
|
16
|
+
caller degrades to the text-only path.
|
|
17
|
+
|
|
18
|
+
Design choices
|
|
19
|
+
--------------
|
|
20
|
+
* **Lazy, not install-time.** A hard dependency at ``pip install`` time would
|
|
21
|
+
break every user on a machine without a Rust/Node toolchain or network. Lazy +
|
|
22
|
+
graceful-degrade keeps the SDK installable everywhere.
|
|
23
|
+
* **Cached.** A marker file under ``~/.agentos`` means the (subprocess-heavy)
|
|
24
|
+
checks run once, then it's a fast no-op on every subsequent run.
|
|
25
|
+
* **Opt-outable.** ``AGENTOS_ENABLE_DESIGN_VERIFY=0`` disables all of this.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
_BINARY = "agent-browser"
|
|
37
|
+
_MARKER = Path.home() / ".agentos" / ".agent_browser_ready"
|
|
38
|
+
# Installs pull binaries / Chrome over the network — give them real headroom.
|
|
39
|
+
_INSTALL_TIMEOUT_S = 600.0
|
|
40
|
+
_QUICK_TIMEOUT_S = 60.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BootstrapResult:
|
|
45
|
+
ok: bool
|
|
46
|
+
path: str | None # resolved agent-browser path when ok
|
|
47
|
+
reason: str # human-readable outcome (why it failed, if not ok)
|
|
48
|
+
installed: bool = False # True if we actually installed something this call
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run(cmd: list[str], timeout: float) -> tuple[bool, str]:
|
|
52
|
+
"""Run a setup command; return (success, combined-output). Never raises."""
|
|
53
|
+
try:
|
|
54
|
+
proc = subprocess.run(
|
|
55
|
+
cmd, capture_output=True, text=True, timeout=timeout,
|
|
56
|
+
)
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
return False, f"{cmd[0]} not found"
|
|
59
|
+
except subprocess.TimeoutExpired:
|
|
60
|
+
return False, f"{' '.join(cmd)} timed out after {timeout:.0f}s"
|
|
61
|
+
except Exception as exc: # noqa: BLE001 — setup must never crash the SDK
|
|
62
|
+
return False, str(exc)
|
|
63
|
+
out = (proc.stdout or "") + (proc.stderr or "")
|
|
64
|
+
return proc.returncode == 0, out.strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _disabled() -> bool:
|
|
68
|
+
return os.getenv("AGENTOS_ENABLE_DESIGN_VERIFY", "1").lower() in ("0", "false", "no")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _install_binary() -> tuple[bool, str]:
|
|
72
|
+
"""Install the agent-browser binary via the best available channel.
|
|
73
|
+
|
|
74
|
+
Order: npm (Node is almost always present — it's how the `claude` CLI itself
|
|
75
|
+
is installed), then Homebrew on macOS, then Cargo. Each is tried only if its
|
|
76
|
+
package manager exists. Prebuilt-binary download is intentionally omitted here
|
|
77
|
+
(GitHub release asset plumbing) — document it as the manual fallback.
|
|
78
|
+
"""
|
|
79
|
+
attempts: list[str] = []
|
|
80
|
+
|
|
81
|
+
if shutil.which("npm"):
|
|
82
|
+
ok, out = _run(["npm", "install", "-g", _BINARY], _INSTALL_TIMEOUT_S)
|
|
83
|
+
if ok and shutil.which(_BINARY):
|
|
84
|
+
return True, "installed via npm"
|
|
85
|
+
attempts.append(f"npm: {out[-200:]}")
|
|
86
|
+
|
|
87
|
+
if sys.platform == "darwin" and shutil.which("brew"):
|
|
88
|
+
ok, out = _run(["brew", "install", _BINARY], _INSTALL_TIMEOUT_S)
|
|
89
|
+
if ok and shutil.which(_BINARY):
|
|
90
|
+
return True, "installed via brew"
|
|
91
|
+
attempts.append(f"brew: {out[-200:]}")
|
|
92
|
+
|
|
93
|
+
if shutil.which("cargo"):
|
|
94
|
+
ok, out = _run(["cargo", "install", _BINARY], _INSTALL_TIMEOUT_S)
|
|
95
|
+
if ok and shutil.which(_BINARY):
|
|
96
|
+
return True, "installed via cargo"
|
|
97
|
+
attempts.append(f"cargo: {out[-200:]}")
|
|
98
|
+
|
|
99
|
+
if not attempts:
|
|
100
|
+
return False, (
|
|
101
|
+
"no supported installer found (need npm, brew, or cargo). Install "
|
|
102
|
+
f"`{_BINARY}` manually — see sdk/design_verify/README.md."
|
|
103
|
+
)
|
|
104
|
+
return False, "install attempts failed -> " + " | ".join(attempts)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _ensure_chrome() -> tuple[bool, str]:
|
|
108
|
+
"""Ensure Chrome for Testing is present (`agent-browser install`)."""
|
|
109
|
+
# `doctor` reports daemon/browser state; if Chrome is already there this is fast.
|
|
110
|
+
ok, out = _run([_BINARY, "doctor"], _QUICK_TIMEOUT_S)
|
|
111
|
+
if ok and "chrome" in out.lower() and "not" not in out.lower():
|
|
112
|
+
return True, "chrome already present"
|
|
113
|
+
cmd = [_BINARY, "install"]
|
|
114
|
+
if sys.platform.startswith("linux"):
|
|
115
|
+
cmd.append("--with-deps") # pull system libs Chrome needs on Linux
|
|
116
|
+
ok, out = _run(cmd, _INSTALL_TIMEOUT_S)
|
|
117
|
+
return ok, out[-200:] if out else ("chrome installed" if ok else "chrome install failed")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def ensure_agent_browser(*, force: bool = False) -> BootstrapResult:
|
|
121
|
+
"""Make ``agent-browser`` usable on this machine; idempotent and cached.
|
|
122
|
+
|
|
123
|
+
Returns a :class:`BootstrapResult`. On success ``path`` is the binary location.
|
|
124
|
+
Never raises — callers gate the verification loop on ``result.ok``.
|
|
125
|
+
"""
|
|
126
|
+
if _disabled():
|
|
127
|
+
return BootstrapResult(False, None, "design verification disabled "
|
|
128
|
+
"(AGENTOS_ENABLE_DESIGN_VERIFY=0)")
|
|
129
|
+
|
|
130
|
+
existing = shutil.which(_BINARY)
|
|
131
|
+
if existing and _MARKER.exists() and not force:
|
|
132
|
+
return BootstrapResult(True, existing, "already bootstrapped (cached)")
|
|
133
|
+
|
|
134
|
+
installed = False
|
|
135
|
+
if not existing:
|
|
136
|
+
ok, reason = _install_binary()
|
|
137
|
+
if not ok:
|
|
138
|
+
return BootstrapResult(False, None, reason)
|
|
139
|
+
installed = True
|
|
140
|
+
existing = shutil.which(_BINARY)
|
|
141
|
+
|
|
142
|
+
chrome_ok, chrome_reason = _ensure_chrome()
|
|
143
|
+
if not chrome_ok:
|
|
144
|
+
return BootstrapResult(False, existing,
|
|
145
|
+
f"agent-browser installed but Chrome setup failed: {chrome_reason}",
|
|
146
|
+
installed=installed)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
_MARKER.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
_MARKER.write_text("ok")
|
|
151
|
+
except OSError:
|
|
152
|
+
pass # marker is an optimization; not being able to write it is harmless
|
|
153
|
+
|
|
154
|
+
return BootstrapResult(True, existing, "ready", installed=installed)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> int:
|
|
158
|
+
"""CLI entry point: ``python -m sdk.design_verify.bootstrap``.
|
|
159
|
+
|
|
160
|
+
Lets ops/onboarding provision agent-browser for a user ahead of time instead
|
|
161
|
+
of waiting for the first design ticket.
|
|
162
|
+
"""
|
|
163
|
+
res = ensure_agent_browser(force="--force" in sys.argv)
|
|
164
|
+
print(f"agent-browser: {'OK' if res.ok else 'UNAVAILABLE'} — {res.reason}"
|
|
165
|
+
+ (f" ({res.path})" if res.path else ""))
|
|
166
|
+
return 0 if res.ok else 1
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""The closed refinement loop: render -> compare -> correct -> repeat.
|
|
2
|
+
|
|
3
|
+
This is the piece that turns a one-shot design-to-code generation into a
|
|
4
|
+
self-correcting one. It assumes the frontend has already been generated once and
|
|
5
|
+
is being served at ``preview_url`` (a local dev server). Each iteration:
|
|
6
|
+
|
|
7
|
+
1. screenshot the live preview with agent-browser
|
|
8
|
+
2. compare it against the target design (local claude vision diff)
|
|
9
|
+
3. stop if fidelity >= threshold or we've run out of iterations
|
|
10
|
+
4. otherwise feed the correction prompt back to the coding agent, which edits
|
|
11
|
+
the real files; then loop (the caller's dev server hot-reloads)
|
|
12
|
+
|
|
13
|
+
The coding step is injectable (``coder``) so the real pipeline can plug in its
|
|
14
|
+
session-resuming ``run_claude_local`` call (keeping context across iterations)
|
|
15
|
+
instead of the default fresh-run coder used here.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import shutil
|
|
21
|
+
import tempfile
|
|
22
|
+
import time
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Awaitable, Callable
|
|
26
|
+
|
|
27
|
+
from sdk.local_exec import DEFAULT_ALLOWED_TOOLS, run_claude_local
|
|
28
|
+
|
|
29
|
+
from .bootstrap import ensure_agent_browser
|
|
30
|
+
from .browser import BrowserError, diff_baseline, render_screenshot
|
|
31
|
+
from .compare import ComparisonReport, compare_design
|
|
32
|
+
|
|
33
|
+
# A coder takes (project_path, correction_prompt) and applies fixes in place.
|
|
34
|
+
Coder = Callable[[str, str], Awaitable[None]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Iteration:
|
|
39
|
+
index: int
|
|
40
|
+
screenshot: str
|
|
41
|
+
fidelity_score: int
|
|
42
|
+
discrepancy_count: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class RefinementResult:
|
|
47
|
+
passed: bool
|
|
48
|
+
iterations: list[Iteration] = field(default_factory=list)
|
|
49
|
+
final_report: ComparisonReport | None = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def best_score(self) -> int:
|
|
53
|
+
return max((it.fidelity_score for it in self.iterations), default=0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _default_coder(project_path: str, correction_prompt: str) -> None:
|
|
57
|
+
"""Fallback coder: a fresh local `claude` run that applies the corrections.
|
|
58
|
+
|
|
59
|
+
The real pipeline should pass a ``coder`` that RESUMES the ticket's session
|
|
60
|
+
(``resume_session_id=...``) so the agent keeps the design context across
|
|
61
|
+
iterations instead of starting cold each time.
|
|
62
|
+
"""
|
|
63
|
+
prompt = (
|
|
64
|
+
"A visual review compared the current frontend against the target design "
|
|
65
|
+
"and found the differences below. Edit the code in this repository to "
|
|
66
|
+
"close them. Make the minimal, correct changes; do not restructure "
|
|
67
|
+
"unrelated code.\n\n"
|
|
68
|
+
f"{correction_prompt}"
|
|
69
|
+
)
|
|
70
|
+
result = await run_claude_local(
|
|
71
|
+
prompt, project_path, allowed_tools=DEFAULT_ALLOWED_TOOLS
|
|
72
|
+
)
|
|
73
|
+
if not result.get("ok"):
|
|
74
|
+
raise RuntimeError(f"correction run failed: {result.get('error')}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def verify_and_refine(
|
|
78
|
+
*,
|
|
79
|
+
project_path: str | Path,
|
|
80
|
+
design_path: str | Path,
|
|
81
|
+
preview_url: str,
|
|
82
|
+
artifact_dir: str | Path | None = None,
|
|
83
|
+
cleanup: bool = True,
|
|
84
|
+
threshold: int = 90,
|
|
85
|
+
max_iterations: int = 3,
|
|
86
|
+
full_page: bool = False,
|
|
87
|
+
coder: Coder | None = None,
|
|
88
|
+
on_event: Callable[[str], None] | None = None,
|
|
89
|
+
) -> RefinementResult:
|
|
90
|
+
"""Iteratively refine the generated UI until it matches ``design_path``.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
project_path: repo whose frontend files get edited on each correction.
|
|
94
|
+
design_path: the original design image (e.g. the JIRA screenshot).
|
|
95
|
+
preview_url: URL where the generated UI is being served (dev server).
|
|
96
|
+
artifact_dir: scratch dir for per-iteration screenshots. **Optional** — if
|
|
97
|
+
omitted, an OS temp dir is created and (by default) deleted
|
|
98
|
+
when the run ends. Screenshots are working artifacts, NOT
|
|
99
|
+
permanent storage.
|
|
100
|
+
cleanup: delete the screenshots/diffs when the run ends (default
|
|
101
|
+
True). Set False only for debugging. Note: when True, the
|
|
102
|
+
screenshot paths in the returned result no longer exist.
|
|
103
|
+
threshold: stop once fidelity_score >= this (0-100).
|
|
104
|
+
max_iterations: hard cap on correction rounds.
|
|
105
|
+
coder: applies corrections; defaults to a fresh local claude run.
|
|
106
|
+
on_event: optional progress sink (receives human-readable lines).
|
|
107
|
+
|
|
108
|
+
Returns a :class:`RefinementResult` with per-iteration scores and the final
|
|
109
|
+
comparison report.
|
|
110
|
+
"""
|
|
111
|
+
project_path = str(project_path)
|
|
112
|
+
# Screenshots are ephemeral working files. Default to a private OS temp dir
|
|
113
|
+
# that we remove in the `finally` below — nothing is persisted unless the
|
|
114
|
+
# caller both passes an artifact_dir AND sets cleanup=False.
|
|
115
|
+
if artifact_dir is None:
|
|
116
|
+
art = Path(tempfile.mkdtemp(prefix="agentos_design_verify_"))
|
|
117
|
+
else:
|
|
118
|
+
art = Path(artifact_dir).expanduser().resolve()
|
|
119
|
+
art.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
coder = coder or _default_coder
|
|
121
|
+
|
|
122
|
+
def emit(msg: str) -> None:
|
|
123
|
+
if on_event:
|
|
124
|
+
on_event(msg)
|
|
125
|
+
|
|
126
|
+
result = RefinementResult(passed=False)
|
|
127
|
+
report: ComparisonReport | None = None
|
|
128
|
+
|
|
129
|
+
# try/finally spans everything after the scratch dir exists, so the dir is
|
|
130
|
+
# always removed — even if bootstrap or the first render raises.
|
|
131
|
+
try:
|
|
132
|
+
# Provision agent-browser for THIS user on first use (idempotent, cached).
|
|
133
|
+
# The loop can't render without it, so this is a hard prerequisite — but
|
|
134
|
+
# the bootstrap never raises; we surface a clear, actionable error.
|
|
135
|
+
emit("ensuring agent-browser is installed")
|
|
136
|
+
boot = await asyncio.to_thread(ensure_agent_browser)
|
|
137
|
+
if not boot.ok:
|
|
138
|
+
raise BrowserError(f"agent-browser unavailable: {boot.reason}")
|
|
139
|
+
if boot.installed:
|
|
140
|
+
emit(f"agent-browser provisioned: {boot.reason}")
|
|
141
|
+
|
|
142
|
+
for i in range(1, max_iterations + 1):
|
|
143
|
+
shot = art / f"render_iter_{i}.png"
|
|
144
|
+
emit(f"[iter {i}/{max_iterations}] rendering {preview_url}")
|
|
145
|
+
await render_screenshot(preview_url, shot, full_page=full_page)
|
|
146
|
+
|
|
147
|
+
# Native pixel diff first — a fast, localizing signal + a highlight
|
|
148
|
+
# image the vision step can reason over. Best-effort: design mocks
|
|
149
|
+
# often differ in dimensions (CLI refuses the pixel compare), so never
|
|
150
|
+
# fail the loop on it — fall back to a pure vision comparison.
|
|
151
|
+
diff_path: str | None = None
|
|
152
|
+
diff_note = ""
|
|
153
|
+
try:
|
|
154
|
+
dr = await diff_baseline(
|
|
155
|
+
design_path, out_path=art / f"diff_iter_{i}.png"
|
|
156
|
+
)
|
|
157
|
+
if dr.dimension_mismatch:
|
|
158
|
+
diff_note = (
|
|
159
|
+
"the render and design differ in dimensions, so no pixel "
|
|
160
|
+
"diff was produced."
|
|
161
|
+
)
|
|
162
|
+
elif dr.mismatch_pct is not None:
|
|
163
|
+
diff_path = dr.diff_image
|
|
164
|
+
diff_note = f"the raw pixel mismatch is {dr.mismatch_pct:.1f}%."
|
|
165
|
+
except BrowserError as exc:
|
|
166
|
+
emit(f"[iter {i}/{max_iterations}] pixel diff skipped: {exc}")
|
|
167
|
+
|
|
168
|
+
emit(f"[iter {i}/{max_iterations}] comparing against design")
|
|
169
|
+
report = await compare_design(
|
|
170
|
+
design_path, shot, work_dir=art,
|
|
171
|
+
diff_path=diff_path, diff_note=diff_note,
|
|
172
|
+
)
|
|
173
|
+
result.iterations.append(
|
|
174
|
+
Iteration(
|
|
175
|
+
index=i,
|
|
176
|
+
screenshot=str(shot),
|
|
177
|
+
fidelity_score=report.fidelity_score,
|
|
178
|
+
discrepancy_count=len(report.discrepancies),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
emit(
|
|
182
|
+
f"[iter {i}/{max_iterations}] fidelity={report.fidelity_score} "
|
|
183
|
+
f"discrepancies={len(report.discrepancies)}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if report.fidelity_score >= threshold or not report.correction_prompt:
|
|
187
|
+
result.passed = report.fidelity_score >= threshold
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if i == max_iterations:
|
|
191
|
+
break # scored but out of correction budget
|
|
192
|
+
|
|
193
|
+
emit(f"[iter {i}/{max_iterations}] applying corrections")
|
|
194
|
+
await coder(project_path, report.correction_prompt)
|
|
195
|
+
# Give a hot-reloading dev server a moment to rebuild before re-shooting.
|
|
196
|
+
time.sleep(2)
|
|
197
|
+
finally:
|
|
198
|
+
# Screenshots/diffs are working artifacts, never permanent storage.
|
|
199
|
+
if cleanup:
|
|
200
|
+
shutil.rmtree(art, ignore_errors=True)
|
|
201
|
+
emit("cleaned up screenshots")
|
|
202
|
+
|
|
203
|
+
result.final_report = report
|
|
204
|
+
return result
|
|
@@ -162,6 +162,7 @@ def _build_local_prompt(
|
|
|
162
162
|
repo_name: str | None = None,
|
|
163
163
|
grounded: bool = False,
|
|
164
164
|
branch_convention: str | None = None,
|
|
165
|
+
design_image_path: str | None = None,
|
|
165
166
|
) -> str:
|
|
166
167
|
"""Compose the coding prompt for the local claude CLI from the server plan.
|
|
167
168
|
|
|
@@ -200,6 +201,21 @@ def _build_local_prompt(
|
|
|
200
201
|
"code actually is, trust the code and the operator instructions."
|
|
201
202
|
)
|
|
202
203
|
parts.append(scope_line)
|
|
204
|
+
# 0.5 Design reference — when the Jira ticket has a design screenshot it is the
|
|
205
|
+
# authoritative VISUAL spec. Placed near the top so the model attends to it;
|
|
206
|
+
# the local claude agent's Read tool views images natively, so it can code
|
|
207
|
+
# against the real design instead of blind from the text description.
|
|
208
|
+
if design_image_path:
|
|
209
|
+
parts.append(
|
|
210
|
+
"=== Design reference (AUTHORITATIVE visual spec) ===\n"
|
|
211
|
+
f"A design screenshot for this task is attached at: {design_image_path}\n"
|
|
212
|
+
"Use the Read tool to VIEW this image BEFORE writing any UI code, and "
|
|
213
|
+
"match it precisely: layout & structure, spacing & alignment, colors, "
|
|
214
|
+
"typography (family/size/weight), component choices, and iconography. "
|
|
215
|
+
"When the image and the text description conflict, the image wins for "
|
|
216
|
+
"anything visual. Treat visual fidelity to this design as a required "
|
|
217
|
+
"acceptance criterion."
|
|
218
|
+
)
|
|
203
219
|
# 1. Operator instructions — FIRST, verbatim, authoritative.
|
|
204
220
|
if human_instructions and human_instructions.strip():
|
|
205
221
|
parts.append(
|
|
@@ -1648,6 +1664,35 @@ def _tool_detail(tool_name: str, inp: dict) -> str:
|
|
|
1648
1664
|
return ""
|
|
1649
1665
|
|
|
1650
1666
|
|
|
1667
|
+
async def _download_design_image(client: "BackendClient", task_id: str) -> str | None:
|
|
1668
|
+
"""Best-effort: download the ticket's design screenshot to a temp file.
|
|
1669
|
+
|
|
1670
|
+
The local claude agent Reads this path to code against the real design (not
|
|
1671
|
+
blind from text). Returns the temp path or None (no image / any failure).
|
|
1672
|
+
EPHEMERAL — the caller MUST delete it after the run; it is never persisted.
|
|
1673
|
+
"""
|
|
1674
|
+
if not task_id:
|
|
1675
|
+
return None
|
|
1676
|
+
try:
|
|
1677
|
+
img = await client.fetch_design_image(task_id)
|
|
1678
|
+
except Exception: # noqa: BLE001 — visual grounding is best-effort
|
|
1679
|
+
return None
|
|
1680
|
+
if not img:
|
|
1681
|
+
return None
|
|
1682
|
+
data, mime = img
|
|
1683
|
+
m = (mime or "").lower()
|
|
1684
|
+
ext = ".png" if "png" in m else (".jpg" if ("jpeg" in m or "jpg" in m) else ".img")
|
|
1685
|
+
try:
|
|
1686
|
+
import os
|
|
1687
|
+
import tempfile
|
|
1688
|
+
fd, path = tempfile.mkstemp(prefix="agentos_design_", suffix=ext)
|
|
1689
|
+
with os.fdopen(fd, "wb") as f:
|
|
1690
|
+
f.write(data)
|
|
1691
|
+
return path
|
|
1692
|
+
except OSError:
|
|
1693
|
+
return None
|
|
1694
|
+
|
|
1695
|
+
|
|
1651
1696
|
async def _code_one_repo(
|
|
1652
1697
|
client: BackendClient,
|
|
1653
1698
|
*,
|
|
@@ -1874,21 +1919,36 @@ async def _code_one_repo(
|
|
|
1874
1919
|
# When recon ran, the implementation resumes that session so its grounded plan
|
|
1875
1920
|
# (and the technical PRD, if generated) stays in context; if a PRD is present the
|
|
1876
1921
|
# prompt makes it the authoritative spec.
|
|
1922
|
+
# Design-aware generation: if the ticket has a design screenshot, download it
|
|
1923
|
+
# (ephemeral) so the local agent can Read it while coding. Consumed by the
|
|
1924
|
+
# FIRST run below — after that it's in the session context, so recovery/repair
|
|
1925
|
+
# resumes don't need the file. Deleted in `finally` regardless of outcome.
|
|
1926
|
+
design_path = await _download_design_image(client, task_id)
|
|
1927
|
+
|
|
1877
1928
|
prompt = _build_local_prompt(
|
|
1878
1929
|
pending["user_input"], pending.get("structured_plan"),
|
|
1879
1930
|
pending.get("human_instructions"),
|
|
1880
1931
|
agent_mode=agent_mode, git_managed=bool(branch_info),
|
|
1881
1932
|
repo_name=name, grounded=bool(recon_session), branch_convention=branch_name,
|
|
1933
|
+
design_image_path=design_path,
|
|
1882
1934
|
)
|
|
1883
1935
|
|
|
1884
1936
|
# 3b. initial claude run. resume_session_id (rate-limit/context retry) wins;
|
|
1885
1937
|
# otherwise resume the recon session so implementation builds on the recon
|
|
1886
1938
|
# exploration instead of starting cold.
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1939
|
+
try:
|
|
1940
|
+
result = await local_exec.run_claude_local(
|
|
1941
|
+
prompt, repo_path, on_event=_on_event,
|
|
1942
|
+
github_token=gh_token, approval_dir=approval_dir,
|
|
1943
|
+
resume_session_id=resume_session_id or recon_session or None,
|
|
1944
|
+
)
|
|
1945
|
+
finally:
|
|
1946
|
+
if design_path:
|
|
1947
|
+
import os
|
|
1948
|
+
try:
|
|
1949
|
+
os.remove(design_path)
|
|
1950
|
+
except OSError:
|
|
1951
|
+
pass
|
|
1892
1952
|
|
|
1893
1953
|
# 4. context-window recovery
|
|
1894
1954
|
import time as _time
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
"""The closed refinement loop: render -> compare -> correct -> repeat.
|
|
2
|
-
|
|
3
|
-
This is the piece that turns a one-shot design-to-code generation into a
|
|
4
|
-
self-correcting one. It assumes the frontend has already been generated once and
|
|
5
|
-
is being served at ``preview_url`` (a local dev server). Each iteration:
|
|
6
|
-
|
|
7
|
-
1. screenshot the live preview with agent-browser
|
|
8
|
-
2. compare it against the target design (local claude vision diff)
|
|
9
|
-
3. stop if fidelity >= threshold or we've run out of iterations
|
|
10
|
-
4. otherwise feed the correction prompt back to the coding agent, which edits
|
|
11
|
-
the real files; then loop (the caller's dev server hot-reloads)
|
|
12
|
-
|
|
13
|
-
The coding step is injectable (``coder``) so the real pipeline can plug in its
|
|
14
|
-
session-resuming ``run_claude_local`` call (keeping context across iterations)
|
|
15
|
-
instead of the default fresh-run coder used here.
|
|
16
|
-
"""
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import time
|
|
20
|
-
from dataclasses import dataclass, field
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import Awaitable, Callable
|
|
23
|
-
|
|
24
|
-
from sdk.local_exec import DEFAULT_ALLOWED_TOOLS, run_claude_local
|
|
25
|
-
|
|
26
|
-
from .browser import BrowserError, diff_baseline, render_screenshot
|
|
27
|
-
from .compare import ComparisonReport, compare_design
|
|
28
|
-
|
|
29
|
-
# A coder takes (project_path, correction_prompt) and applies fixes in place.
|
|
30
|
-
Coder = Callable[[str, str], Awaitable[None]]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass
|
|
34
|
-
class Iteration:
|
|
35
|
-
index: int
|
|
36
|
-
screenshot: str
|
|
37
|
-
fidelity_score: int
|
|
38
|
-
discrepancy_count: int
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@dataclass
|
|
42
|
-
class RefinementResult:
|
|
43
|
-
passed: bool
|
|
44
|
-
iterations: list[Iteration] = field(default_factory=list)
|
|
45
|
-
final_report: ComparisonReport | None = None
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def best_score(self) -> int:
|
|
49
|
-
return max((it.fidelity_score for it in self.iterations), default=0)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async def _default_coder(project_path: str, correction_prompt: str) -> None:
|
|
53
|
-
"""Fallback coder: a fresh local `claude` run that applies the corrections.
|
|
54
|
-
|
|
55
|
-
The real pipeline should pass a ``coder`` that RESUMES the ticket's session
|
|
56
|
-
(``resume_session_id=...``) so the agent keeps the design context across
|
|
57
|
-
iterations instead of starting cold each time.
|
|
58
|
-
"""
|
|
59
|
-
prompt = (
|
|
60
|
-
"A visual review compared the current frontend against the target design "
|
|
61
|
-
"and found the differences below. Edit the code in this repository to "
|
|
62
|
-
"close them. Make the minimal, correct changes; do not restructure "
|
|
63
|
-
"unrelated code.\n\n"
|
|
64
|
-
f"{correction_prompt}"
|
|
65
|
-
)
|
|
66
|
-
result = await run_claude_local(
|
|
67
|
-
prompt, project_path, allowed_tools=DEFAULT_ALLOWED_TOOLS
|
|
68
|
-
)
|
|
69
|
-
if not result.get("ok"):
|
|
70
|
-
raise RuntimeError(f"correction run failed: {result.get('error')}")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
async def verify_and_refine(
|
|
74
|
-
*,
|
|
75
|
-
project_path: str | Path,
|
|
76
|
-
design_path: str | Path,
|
|
77
|
-
preview_url: str,
|
|
78
|
-
artifact_dir: str | Path,
|
|
79
|
-
threshold: int = 90,
|
|
80
|
-
max_iterations: int = 3,
|
|
81
|
-
full_page: bool = False,
|
|
82
|
-
coder: Coder | None = None,
|
|
83
|
-
on_event: Callable[[str], None] | None = None,
|
|
84
|
-
) -> RefinementResult:
|
|
85
|
-
"""Iteratively refine the generated UI until it matches ``design_path``.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
project_path: repo whose frontend files get edited on each correction.
|
|
89
|
-
design_path: the original design image (e.g. the JIRA screenshot).
|
|
90
|
-
preview_url: URL where the generated UI is being served (dev server).
|
|
91
|
-
artifact_dir: where per-iteration screenshots are written.
|
|
92
|
-
threshold: stop once fidelity_score >= this (0-100).
|
|
93
|
-
max_iterations: hard cap on correction rounds.
|
|
94
|
-
coder: applies corrections; defaults to a fresh local claude run.
|
|
95
|
-
on_event: optional progress sink (receives human-readable lines).
|
|
96
|
-
|
|
97
|
-
Returns a :class:`RefinementResult` with per-iteration scores and the final
|
|
98
|
-
comparison report.
|
|
99
|
-
"""
|
|
100
|
-
project_path = str(project_path)
|
|
101
|
-
art = Path(artifact_dir).expanduser().resolve()
|
|
102
|
-
art.mkdir(parents=True, exist_ok=True)
|
|
103
|
-
coder = coder or _default_coder
|
|
104
|
-
|
|
105
|
-
def emit(msg: str) -> None:
|
|
106
|
-
if on_event:
|
|
107
|
-
on_event(msg)
|
|
108
|
-
|
|
109
|
-
result = RefinementResult(passed=False)
|
|
110
|
-
report: ComparisonReport | None = None
|
|
111
|
-
|
|
112
|
-
for i in range(1, max_iterations + 1):
|
|
113
|
-
shot = art / f"render_iter_{i}.png"
|
|
114
|
-
emit(f"[iter {i}/{max_iterations}] rendering {preview_url}")
|
|
115
|
-
await render_screenshot(preview_url, shot, full_page=full_page)
|
|
116
|
-
|
|
117
|
-
# Native pixel diff first — a fast, localizing signal + a highlight image
|
|
118
|
-
# the vision step can reason over. Best-effort: design mocks often differ
|
|
119
|
-
# in dimensions (CLI refuses the pixel compare), so never fail the loop
|
|
120
|
-
# on it — fall back to a pure vision comparison.
|
|
121
|
-
diff_path: str | None = None
|
|
122
|
-
diff_note = ""
|
|
123
|
-
try:
|
|
124
|
-
dr = await diff_baseline(
|
|
125
|
-
design_path, out_path=art / f"diff_iter_{i}.png"
|
|
126
|
-
)
|
|
127
|
-
if dr.dimension_mismatch:
|
|
128
|
-
diff_note = (
|
|
129
|
-
"the render and design differ in dimensions, so no pixel "
|
|
130
|
-
"diff was produced."
|
|
131
|
-
)
|
|
132
|
-
elif dr.mismatch_pct is not None:
|
|
133
|
-
diff_path = dr.diff_image
|
|
134
|
-
diff_note = f"the raw pixel mismatch is {dr.mismatch_pct:.1f}%."
|
|
135
|
-
except BrowserError as exc:
|
|
136
|
-
emit(f"[iter {i}/{max_iterations}] pixel diff skipped: {exc}")
|
|
137
|
-
|
|
138
|
-
emit(f"[iter {i}/{max_iterations}] comparing against design")
|
|
139
|
-
report = await compare_design(
|
|
140
|
-
design_path, shot, work_dir=art,
|
|
141
|
-
diff_path=diff_path, diff_note=diff_note,
|
|
142
|
-
)
|
|
143
|
-
result.iterations.append(
|
|
144
|
-
Iteration(
|
|
145
|
-
index=i,
|
|
146
|
-
screenshot=str(shot),
|
|
147
|
-
fidelity_score=report.fidelity_score,
|
|
148
|
-
discrepancy_count=len(report.discrepancies),
|
|
149
|
-
)
|
|
150
|
-
)
|
|
151
|
-
emit(
|
|
152
|
-
f"[iter {i}/{max_iterations}] fidelity={report.fidelity_score} "
|
|
153
|
-
f"discrepancies={len(report.discrepancies)}"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
if report.fidelity_score >= threshold or not report.correction_prompt:
|
|
157
|
-
result.passed = report.fidelity_score >= threshold
|
|
158
|
-
break
|
|
159
|
-
|
|
160
|
-
if i == max_iterations:
|
|
161
|
-
break # scored but out of correction budget
|
|
162
|
-
|
|
163
|
-
emit(f"[iter {i}/{max_iterations}] applying corrections")
|
|
164
|
-
await coder(project_path, report.correction_prompt)
|
|
165
|
-
# Give a hot-reloading dev server a moment to rebuild before re-shooting.
|
|
166
|
-
time.sleep(2)
|
|
167
|
-
|
|
168
|
-
result.final_report = report
|
|
169
|
-
return result
|
|
File without changes
|
{devops_bot_sdk-1.4.99 → devops_bot_sdk-1.4.101}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|