From c1926ec664dc3b3d9347b32b818aa5ebdefd33f0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 13 Sep 2026 08:42:39 -0700 Subject: [PATCH 1/3] fix(ci): validate guidance for the selected CLI binary --- .github/workflows/macos-ci.yml | 1 + CHANGELOG.md | 2 ++ skills/peekaboo/SKILL.md | 3 +++ tests/background-capability-guidance.test.mjs | 14 +++++++++----- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 2fd69da9a..44f99ed6e 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -38,6 +38,7 @@ jobs: run: | node scripts/docs-lint.mjs node --test tests/docs-site-toc.test.mjs + node --test tests/background-capability-guidance.test.mjs - name: Select Xcode 26.6 (if present) or fallback to default run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 666a5b023..09fef404f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Keep skill guidance checks effective for the selected CLI binary, restore the read-only clipboard example, and run the checks in regular macOS CI. + - Keep caller screenshot destinations intact when remote evidence is rejected or raw output was not requested, staging ordinary captures before file publication as well as ROI captures. #710. - Fix host-routed screen observations with Accessibility elements by validating their semantic owner separately from the screen raster target. #715, #710. diff --git a/skills/peekaboo/SKILL.md b/skills/peekaboo/SKILL.md index db3f7dfd6..a5e40f02b 100644 --- a/skills/peekaboo/SKILL.md +++ b/skills/peekaboo/SKILL.md @@ -46,6 +46,9 @@ GUI_SOCKET="$HOME/Library/Application Support/Peekaboo/bridge.sock" "$PB" app list --include-hidden --include-background --json "$PB" window list --app Safari --json +# Read clipboard contents only when relevant to the authorized task. +"$PB" clipboard get --json + # Pixels only; an exact window also publishes a coordinate receipt. "$PB" see --window-id "$WINDOW_ID" --no-elements --path /tmp/peekaboo-window.png --json diff --git a/tests/background-capability-guidance.test.mjs b/tests/background-capability-guidance.test.mjs index af2e74651..88531c8d9 100644 --- a/tests/background-capability-guidance.test.mjs +++ b/tests/background-capability-guidance.test.mjs @@ -33,6 +33,7 @@ test('press guidance preserves the snapshot-pinned background route', () => { const isTargetedRawPress = (line) => /^peekaboo press\b.*--(?:app|pid)\b/.test(line); const hasSafeRawPressRoute = (line) => /--(?:foreground|snapshot|window-(?:id|title|index))\b/.test(line); +const normalizeSkillCommands = (source) => source.replace(/^"\$PB"(?=\s)/gm, 'peekaboo'); test('primary app automation examples stay exact-window and background-only', () => { for (const path of ['README.md', 'docs/quickstart.md']) { @@ -53,7 +54,7 @@ test('primary app automation examples stay exact-window and background-only', () }); test('bundled skill never advertises app/PID-only background press', () => { - const skill = read('skills/peekaboo/SKILL.md'); + const skill = normalizeSkillCommands(read('skills/peekaboo/SKILL.md')); const targetedPressExamples = skill .split('\n') .filter(isTargetedRawPress); @@ -67,13 +68,16 @@ test('bundled skill never advertises app/PID-only background press', () => { ); } - assert.equal(isTargetedRawPress('peekaboo press return --pid 1234'), true); - assert.equal(hasSafeRawPressRoute('peekaboo press return --pid 1234'), false); - assert.equal(hasSafeRawPressRoute('peekaboo press return --pid 1234 --window-id 42'), true); + for (const binary of ['peekaboo', '"$PB"']) { + const unsafe = normalizeSkillCommands(`${binary} press return --pid 1234`); + assert.equal(isTargetedRawPress(unsafe), true); + assert.equal(hasSafeRawPressRoute(unsafe), false); + assert.equal(hasSafeRawPressRoute(`${unsafe} --window-id 42`), true); + } }); test('bundled skill keeps routine management examples read-only', () => { - const skill = read('skills/peekaboo/SKILL.md'); + const skill = normalizeSkillCommands(read('skills/peekaboo/SKILL.md')); assert.doesNotMatch(skill, /^peekaboo clipboard (?:set|clear|restore)\b/m); assert.doesNotMatch(skill, /^peekaboo permissions request\b/m); From aa1c1c975227f983520533212c1edd5fce729620 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 13 Sep 2026 08:49:32 -0700 Subject: [PATCH 2/3] fix(docs): repair agent guide links and complete press coverage --- CHANGELOG.md | 2 +- docs/agent-skill.md | 5 +++-- tests/background-capability-guidance.test.mjs | 10 ++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09fef404f..b7b90d957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Keep skill guidance checks effective for the selected CLI binary, restore the read-only clipboard example, and run the checks in regular macOS CI. +- Keep skill guidance checks effective for the selected CLI binary, restore the read-only clipboard example, repair the guide's published links, and run the checks in regular macOS CI. - Keep caller screenshot destinations intact when remote evidence is rejected or raw output was not requested, staging ordinary captures before file publication as well as ROI captures. #710. diff --git a/docs/agent-skill.md b/docs/agent-skill.md index b9999ef0d..11511c1fc 100644 --- a/docs/agent-skill.md +++ b/docs/agent-skill.md @@ -7,7 +7,7 @@ read_when: # Agent Skill for Peekaboo -The [Peekaboo skill](../skills/peekaboo/SKILL.md) teaches agents to observe macOS UI, select an exact target, act in the background where supported, and verify the result. It covers native apps and browser chrome, routes page content to browser tooling, and explains execution-host permissions, snapshot ownership, coordinates, and unverified input outcomes. Ordinary automation uses the installed CLI; source builds belong to Peekaboo development. +The [Peekaboo skill](https://github.com/openclaw/Peekaboo/blob/main/skills/peekaboo/SKILL.md) teaches agents to observe macOS UI, select an exact target, act in the background where supported, and verify the result. It covers native apps and browser chrome, routes page content to browser tooling, and explains execution-host permissions, snapshot ownership, coordinates, and unverified input outcomes. Ordinary automation uses the installed CLI; source builds belong to Peekaboo development. ## Prerequisites @@ -58,11 +58,12 @@ For skill/documentation edits, run from the repository root: ```bash node scripts/docs-lint.mjs +node --test tests/background-capability-guidance.test.mjs ruby -e 'h=File.read("skills/peekaboo/SKILL.md").split(/^---\s*$/,3)[1]; keys=h.lines.grep(/^[A-Za-z0-9_-]+:/).map { |line| line.split(":",2).first }; abort("unexpected skill frontmatter") unless keys.sort == ["description","name"]' git diff --check ``` -Source-code changes follow [AGENTS.md](../AGENTS.md) and the [building guide](building.md). Live checks should use a controlled target, inspect the actual screen or state readback, and preserve retry-unsafe outcomes. Image dimensions and command success alone are insufficient proof that the intended UI changed. +Source-code changes follow [AGENTS.md](https://github.com/openclaw/Peekaboo/blob/main/AGENTS.md) and the [building guide](building.md). Live checks should use a controlled target, inspect the actual screen or state readback, and preserve retry-unsafe outcomes. Image dimensions and command success alone are insufficient proof that the intended UI changed. ## Canonical references diff --git a/tests/background-capability-guidance.test.mjs b/tests/background-capability-guidance.test.mjs index 88531c8d9..ecd0f3875 100644 --- a/tests/background-capability-guidance.test.mjs +++ b/tests/background-capability-guidance.test.mjs @@ -69,10 +69,12 @@ test('bundled skill never advertises app/PID-only background press', () => { } for (const binary of ['peekaboo', '"$PB"']) { - const unsafe = normalizeSkillCommands(`${binary} press return --pid 1234`); - assert.equal(isTargetedRawPress(unsafe), true); - assert.equal(hasSafeRawPressRoute(unsafe), false); - assert.equal(hasSafeRawPressRoute(`${unsafe} --window-id 42`), true); + for (const target of ['--app TextEdit', '--pid 1234']) { + const unsafe = normalizeSkillCommands(`${binary} press return ${target}`); + assert.equal(isTargetedRawPress(unsafe), true); + assert.equal(hasSafeRawPressRoute(unsafe), false); + assert.equal(hasSafeRawPressRoute(`${unsafe} --window-id 42`), true); + } } }); From 0ab5c6ef2e1f0e51491bd6f02e25eed8e0cebfa8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 13 Sep 2026 09:32:03 -0700 Subject: [PATCH 3/3] test: gate noncooperative detection timeout completion --- CHANGELOG.md | 2 +- .../ElementDetectionServiceTests.swift | 85 ++++++++++++++++--- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b90d957..7706d9211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Keep skill guidance checks effective for the selected CLI binary, restore the read-only clipboard example, repair the guide's published links, and run the checks in regular macOS CI. +- Repair selected-CLI skill checks and published guide links, restore the read-only clipboard example, run guidance checks in regular macOS CI, and make noncooperative detection timeout proof independent of scheduler timing. - Keep caller screenshot destinations intact when remote evidence is rejected or raw output was not requested, staging ordinary captures before file publication as well as ROI captures. #710. diff --git a/Core/PeekabooCore/Tests/PeekabooTests/ElementDetectionServiceTests.swift b/Core/PeekabooCore/Tests/PeekabooTests/ElementDetectionServiceTests.swift index c188d9051..8f5102797 100644 --- a/Core/PeekabooCore/Tests/PeekabooTests/ElementDetectionServiceTests.swift +++ b/Core/PeekabooCore/Tests/PeekabooTests/ElementDetectionServiceTests.swift @@ -292,24 +292,42 @@ struct ElementDetectionTimeoutRunnerTests { } } - @Test - func `Detection timeout wins over noncooperative work`() async throws { - let startedAt = Date() - - do { - _ = try await ElementDetectionTimeoutRunner.run(seconds: 0.02) { - let stopAt = Date().addingTimeInterval(0.5) - while Date() < stopAt { - try? await Task.sleep(nanoseconds: 50_000_000) - } + @Test(.timeLimit(.minutes(1))) + func `Detection timeout wins over noncooperative work`() async { + let gate = ElementDetectionTimeoutWorkGate() + let task = Task { + try await ElementDetectionTimeoutRunner.run(seconds: 1.0) { + await gate.wait() + await gate.finish() return [DetectedElement]() } + } + let watchdog = Task { + do { + try await Task.sleep(for: .seconds(10)) + await gate.release() + } catch {} + } + await gate.waitUntilBlocked() + #expect(await gate.hasEntered, "The timed operation must enter before its deadline") + + do { + _ = try await task.value Issue.record("Expected detection timeout") } catch let CaptureError.detectionTimedOut(duration) { - #expect(duration == 0.02) + #expect(duration == 1.0) + } catch { + Issue.record("Expected detection timeout, got \(error)") } - #expect(Date().timeIntervalSince(startedAt) < 0.25) + // Work remains blocked regardless of cancellation until the caller has returned. + #expect(await !gate.isReleased, "Detection timeout must not join noncooperative work") + watchdog.cancel() + await gate.release() + if await gate.hasEntered { + await gate.waitUntilFinished() + } + await watchdog.value } @Test @@ -338,6 +356,49 @@ struct ElementDetectionTimeoutRunnerTests { } } +private actor ElementDetectionTimeoutWorkGate { + private var waiting: CheckedContinuation? + private var entered: CheckedContinuation? + private var finished: CheckedContinuation? + private var hasFinished = false + private(set) var hasEntered = false + private(set) var isReleased = false + + func wait() async { + guard !self.isReleased else { return } + await withCheckedContinuation { continuation in + self.hasEntered = true + self.waiting = continuation + self.entered?.resume() + self.entered = nil + } + } + + func waitUntilBlocked() async { + guard self.waiting == nil, !self.isReleased else { return } + await withCheckedContinuation { self.entered = $0 } + } + + func release() { + self.isReleased = true + self.waiting?.resume() + self.waiting = nil + self.entered?.resume() + self.entered = nil + } + + func finish() { + self.hasFinished = true + self.finished?.resume() + self.finished = nil + } + + func waitUntilFinished() async { + guard !self.hasFinished else { return } + await withCheckedContinuation { self.finished = $0 } + } +} + @Suite(.tags(.fast)) struct ElementDetectionCacheTests { @Test