Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions plugin-surfaces.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -856,27 +856,27 @@
"version": "3.2.0",
"canonical": {
"skills/screenote/SKILL.md": {
"sha256": "2944db045534e4d6f45f9637d26c0b117fb96e3595c584cdf4155a635c875782",
"semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209",
"sha256": "0dd17ee55e234d8e0e92db3c9d991a1f69a5837d6af785e07dcad19b1df23d8a",
"semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a",
"sections": {
"1:screenote — one-page visual review": "6860d0df6138cb5f476bd78c6be5a9986d11aa8022f56510b21c9876e3b66acb",
"2:parse the request": "07af83c395625a98b29917f770bfd2628eb303cec19c667f92ba8bdc6de46ce0",
"2:resolve a safe target": "329d16bfb9bfa3862ad531e0392ca12ead8ac4d58e175bbcf47abfb516d5be24",
"2:establish the cli and project": "3e721f75c2d0227fb1522d224de9d573d31ad57480f33cfbece0e6b8d5cf0e2b",
"2:existing-image upload mode": "f1b596ac83c23e33fc9fe64e1be0e81aa6dcf8229a1678cd638c01416c37f8d2",
"2:browser capture and upload mode": "d9887b3a12436b01bb61c4c14f78e1438502b29851eec0479d44cf55e5102696",
"2:build and publish one logical version": "f829329359547291e730570a7c9513752ea128b46fb035efd66d0bd790d046d6",
"2:build and publish one logical version": "9f43d88d0f4360bb829fa2fce5204baf6dd127750092d76f66cb088b11f82327",
"2:report and clean up": "6e2a6f1cb431fd682f1faad3d478baafd331075dc9c6e81e1b39fb5841f3f31c"
}
},
"skills/snapshot/SKILL.md": {
"sha256": "a07233be927a686d5d77b6b5276b44ee65c6b878b8513dcaa2f7ec78dea82e8b",
"semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b",
"sha256": "2117830d92dd06273ecd8d02afca4ab01722b4a345b8ad65f243f59315d76e6c",
"semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560",
"sections": {
"1:snapshot — multi-page visual review": "6be3486148325dcc124469e4eff8988f00b33f0e8778d2d4fe7fa13e40cc1026",
"2:preflight": "4a0dd546789198a62058792ec0866f08ac3e9c65d35fc3d856b44a42479c5629",
"2:discover and confirm routes": "5f514b67e742e1beac6d215cb8802e081f26eae46fd07c9698785f188bbfc257",
"2:capture and publish": "61216542ce17a118b8b6feaf46849fa2b8c20d1235270c7dc5fdb921cb4b81df"
"2:capture and publish": "cf02f92c1b7091fbcb3fbf527b424350266012cabeb1b038f1591b27df506a4f"
}
},
"skills/feedback/SKILL.md": {
Expand All @@ -893,7 +893,7 @@
"resources": {
"references": {
"exists": true,
"sha256": "4c65cfd4dbef0720cfc33cfc893e02acbfbc09f4d674b35755be1f352ba559d5",
"sha256": "bdc193fa70a54038a311bd3416d3e0c833d82117cc5fe7333314d93f2fd90343",
"files": [
"references/cli.md",
"references/workflows.json"
Expand All @@ -916,14 +916,14 @@
},
"scripts/screenote_flow.py": {
"exists": true,
"sha256": "e7183fe7a22ab633bad4bd7ea21655daf214f37c533614eee53c8894c38e6ae7"
"sha256": "7f017523318da1ae7da795b9296b15804a0ddb0e106508e2cd62d7f1a9a780f1"
}
},
"adapters": {
"pi/skills/screenote/SKILL.md": {
"sha256": "6acc53b130d691fe0fe3288e2079904d52bbb76065b6620524cdcbad7f03be3f",
"canonical": "skills/screenote/SKILL.md",
"canonical_semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209",
"canonical_semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a",
"overlays": [
"frontmatter",
"invocation",
Expand All @@ -933,7 +933,7 @@
"openclaw/skills/screenote/SKILL.md": {
"sha256": "a8e0378d09299bc70297800080e348ad928ea914327b3a761ec5804a6a12f018",
"canonical": "skills/screenote/SKILL.md",
"canonical_semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209",
"canonical_semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a",
"overlays": [
"frontmatter",
"invocation",
Expand All @@ -943,7 +943,7 @@
"pi/skills/snapshot/SKILL.md": {
"sha256": "c935637fe16765d450c68e1bcf60123d2f2af8e1cc362d39838286b2a52ce931",
"canonical": "skills/snapshot/SKILL.md",
"canonical_semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b",
"canonical_semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560",
"overlays": [
"frontmatter",
"invocation",
Expand All @@ -953,7 +953,7 @@
"openclaw/skills/snapshot/SKILL.md": {
"sha256": "adebbedd30e55d544a8600fd6b89b4a3aeb70a5549a9fb65f5e6f7c186aed94e",
"canonical": "skills/snapshot/SKILL.md",
"canonical_semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b",
"canonical_semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560",
"overlays": [
"frontmatter",
"invocation",
Expand Down
5 changes: 5 additions & 0 deletions plugins/screenote/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to the Screenote plugin are documented here.

## [Unreleased]

### Fixed

- Reject snapshot manifests that put several distinct screens under one Page,
preventing those screens from appearing as versions of each other.

## [3.2.0] - 2026-09-02

### Added
Expand Down
8 changes: 5 additions & 3 deletions plugins/screenote/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,11 @@ screenote_flow.py prepare-snapshot-manifest \
The helper requires 1-100 new or prepared mode-`0600` image files directly
beneath the private directory and writes a new mode-`0600` `snapshot.json`.
Viewport variants of one logical screen must repeat the exact same `page` and
`title`; only `viewport` and `file` differ. It rejects path escapes, symlinks,
missing files, duplicate `(page, title, viewport)` tuples, and invalid manifest
metadata.
`title`; only `viewport` and `file` differ. One case-insensitive Page
identity may name only that one screen group in a manifest because later
capture runs, not neighboring screens, become Page versions. It rejects path
escapes, symlinks, missing files, duplicate `(page, title, viewport)` tuples,
multiple screen groups under one Page, and invalid manifest metadata.

Publish the whole manifest once:

Expand Down
6 changes: 6 additions & 0 deletions plugins/screenote/scripts/screenote_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def create_snapshot_manifest(

normalized_entries: list[dict[str, str]] = []
seen: set[tuple[str, str, str]] = set()
page_groups: dict[str, tuple[str, str]] = {}
for entry in entries:
page = str(entry.get("page", "")).strip()
title = str(entry.get("title", "")).strip()
Expand All @@ -292,6 +293,11 @@ def create_snapshot_manifest(
if key in seen:
raise CaptureSafetyError("viewport must be unique within each page and title group")
seen.add(key)
page_key = page.lower()
group_key = (page, title)
if page_key in page_groups and page_groups[page_key] != group_key:
raise CaptureSafetyError("each snapshot page must identify one logical screen")
page_groups[page_key] = group_key
normalized_entries.append({"page": page, "title": title, "file": filename, "viewport": viewport})

body = json.dumps(
Expand Down
4 changes: 3 additions & 1 deletion plugins/screenote/skills/screenote/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ argv element and repeat `--entry` once per image:

Require exit zero and parse its complete JSON. The helper writes a new
mode-`0600` `snapshot.json`, rejects missing/private-path escapes, duplicate
`(page, title, viewport)` tuples, invalid metadata, and more than 100 images.
`(page, title, viewport)` tuples, multiple screen groups under one
case-insensitive Page identity, invalid metadata, and more than 100 images. A
Page identifies one stable screen across runs, not a category within a run.
For one screen, every viewport entry must repeat the exact same page and title.

Publish exactly once:
Expand Down
7 changes: 5 additions & 2 deletions plugins/screenote/skills/snapshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ entry arguments:
```

Require exit zero and inspect the returned manifest path. The helper rejects
invalid/private-path entries, duplicate `(page, title, viewport)` tuples, and
more than 100 images. Publish the complete route/viewport matrix exactly once:
invalid/private-path entries, duplicate `(page, title, viewport)` tuples,
multiple screen groups under one case-insensitive Page identity, and more than
100 images. A Page is one stable screen, normally the normalized route; it is
not a category for several screens. Publish the complete route/viewport matrix
exactly once:

```text
../../scripts/screenote-cli.sh --project PROJECT_ID snapshot --manifest PRIVATE_MANIFEST --wait 2m
Expand Down
58 changes: 58 additions & 0 deletions tests/test_screenote_cli_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,64 @@ def test_snapshot_manifest_helper_preserves_logical_viewport_groups(self):
name="other.json",
)

def test_snapshot_manifest_helper_rejects_multiple_screens_for_one_page(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
private = create_private_directory(Path(temporary.name))
create_private_file(private, "task-board.png")
create_private_file(private, "agent-status.png")

with self.assertRaisesRegex(CaptureSafetyError, "one logical screen"):
create_snapshot_manifest(
private,
git_commit="abc1234",
taken_at="2026-07-10T10:00:00Z",
entries=[
{
"page": "Hive Web",
"title": "Task board",
"file": "task-board.png",
"viewport": "desktop",
},
{
"page": "hive web",
"title": "Agent status",
"file": "agent-status.png",
"viewport": "desktop",
},
],
)

def test_snapshot_manifest_helper_uses_server_page_case_normalization(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
private = create_private_directory(Path(temporary.name))
create_private_file(private, "street.png")
create_private_file(private, "capital-street.png")

manifest_path = create_snapshot_manifest(
private,
git_commit="abc1234",
taken_at="2026-07-10T10:00:00Z",
entries=[
{
"page": "Straße",
"title": "German street",
"file": "street.png",
"viewport": "desktop",
},
{
"page": "STRASSE",
"title": "Capital street",
"file": "capital-street.png",
"viewport": "desktop",
},
],
)

manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
self.assertEqual(["Straße", "STRASSE"], [entry["page"] for entry in manifest["images"]])

def test_capture_targets_must_be_safe_http_urls(self):
self.assertEqual("https://example.test/login?q=one", validate_http_url("https://example.test/login?q=one"))
for unsafe in (
Expand Down
4 changes: 3 additions & 1 deletion wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ collections, pagination, identifiers, private image paths, and manifest-backed
viewport groups before canonical skills act on them. Browser captures and
explicit existing images publish through one resumable snapshot per logical
screen, with desktop, tablet, and mobile represented as child variants rather
than separate versions. Feedback retrieval exports annotation crops plus root
than separate versions. It also enforces one stable Page identity per logical
screen within a capture run, so neighboring screens cannot be published as
versions of one Page. Feedback retrieval exports annotation crops plus root
and reply attachments into one private tree. Image replies remain an optional
flag on the existing comment tuple, require explicit file intent, and fail
closed instead of silently creating a text-only comment.
Expand Down
3 changes: 3 additions & 0 deletions wiki/gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
knowledge remains authoritative here.
- Automatic Screenote annotation resolution remains deferred until the
mutation is explicitly approved in the CLI contract.
- The Screenote helper now rejects multiple logical screens under one Page.
Repairing already-published malformed snapshots remains a server/data
operation and is outside the plugin package.
- LLM Wiki's standalone and four-host marketplace packages intentionally use
separate version lines (`0.1.x` upstream runtime and `0.3.x` consent-gated
marketplace package). A marketplace release must preserve the consent and
Expand Down
Loading