Skip to content

Commit c645045

Browse files
committed
ci(experiment): tighten readiness gate; fix gha cache; sync skill
1 parent a5363f5 commit c645045

4 files changed

Lines changed: 134 additions & 9 deletions

File tree

.claude/skills/devcontainer-dev/SKILL.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ The readiness banner is what you care about. It gates on **three** signals simul
7272

7373
Only once all three are true does the banner fire and the host's browser auto-open.
7474

75+
> **Process running ≠ UI rendered.** The three gates above tell you the
76+
> backing processes are alive, not that Electron has actually painted.
77+
> A devcontainer-in-CI proof captured a blank screen because all three
78+
> gates passed while the window was still mid-first-paint. For
79+
> agent-grade readiness (e.g. before driving the app via `xdotool` or
80+
> taking a screenshot to feed to a vision model), add this gate:
81+
>
82+
> ```bash
83+
> docker exec -u node "$CONTAINER" bash -c \
84+
> 'DISPLAY=:99 xdotool search --class ToolHive >/dev/null 2>&1'
85+
> ```
86+
>
87+
> `xdotool search --class` only succeeds once the main window is mapped
88+
> on Xvfb. A short settling sleep (~2s) after that first match is cheap
89+
> insurance against catching a partially rendered frame.
90+
7591
---
7692
7793
## Per-worktree isolation
@@ -146,13 +162,33 @@ All commands run via `docker exec` against the container with `DISPLAY=:99` set
146162

147163
### See the screen (screenshots)
148164

165+
Use the project's helper script — it auto-finds the container, captures the
166+
root window, and streams the PNG out to the host. Prints the absolute host
167+
path on stdout so it composes:
168+
149169
```bash
150-
# Take a PNG of the whole virtual framebuffer
170+
SHOT=$(scripts/devcontainer-screenshot.sh)
171+
# or with an explicit path:
172+
scripts/devcontainer-screenshot.sh /tmp/shot.png
173+
```
174+
175+
**Why a helper script and not just `docker cp`?** `/tmp` (and possibly
176+
other paths) inside the devcontainer is mounted as `tmpfs`. Docker's
177+
`docker cp` cannot read from tmpfs mounts — it only traverses the
178+
container's overlay layers — so the obvious one-liner
179+
180+
```bash
181+
# DON'T — silently broken: import succeeds, ls confirms the file,
182+
# but docker cp says "Could not find the file in container".
151183
docker exec "$CONTAINER" bash -c 'DISPLAY=:99 import -window root /tmp/shot.png'
152-
# Copy it to the host for viewing / feeding to a vision model
153184
docker cp "$CONTAINER:/tmp/shot.png" /tmp/shot.png
154185
```
155186

187+
**fails everywhere** (CI and local). The helper bypasses `docker cp` by
188+
streaming the file via `docker exec cat` (see `scripts/devcontainer-steal.sh`
189+
for the generic version — use that one to extract any tmpfs file, e.g. logs
190+
or generated artifacts, not just screenshots).
191+
156192
`import` is from ImageMagick. For a specific window only, use `xwininfo` to get the WID then `import -window <WID>`.
157193

158194
### See the window tree (what's there, where, which is focused)

.codex/skills/devcontainer-dev/SKILL.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ The readiness banner is what you care about. It gates on **three** signals simul
7272

7373
Only once all three are true does the banner fire and the host's browser auto-open.
7474

75+
> **Process running ≠ UI rendered.** The three gates above tell you the
76+
> backing processes are alive, not that Electron has actually painted.
77+
> A devcontainer-in-CI proof captured a blank screen because all three
78+
> gates passed while the window was still mid-first-paint. For
79+
> agent-grade readiness (e.g. before driving the app via `xdotool` or
80+
> taking a screenshot to feed to a vision model), add this gate:
81+
>
82+
> ```bash
83+
> docker exec -u node "$CONTAINER" bash -c \
84+
> 'DISPLAY=:99 xdotool search --class ToolHive >/dev/null 2>&1'
85+
> ```
86+
>
87+
> `xdotool search --class` only succeeds once the main window is mapped
88+
> on Xvfb. A short settling sleep (~2s) after that first match is cheap
89+
> insurance against catching a partially rendered frame.
90+
7591
---
7692
7793
## Per-worktree isolation
@@ -146,13 +162,33 @@ All commands run via `docker exec` against the container with `DISPLAY=:99` set
146162

147163
### See the screen (screenshots)
148164

165+
Use the project's helper script — it auto-finds the container, captures the
166+
root window, and streams the PNG out to the host. Prints the absolute host
167+
path on stdout so it composes:
168+
149169
```bash
150-
# Take a PNG of the whole virtual framebuffer
170+
SHOT=$(scripts/devcontainer-screenshot.sh)
171+
# or with an explicit path:
172+
scripts/devcontainer-screenshot.sh /tmp/shot.png
173+
```
174+
175+
**Why a helper script and not just `docker cp`?** `/tmp` (and possibly
176+
other paths) inside the devcontainer is mounted as `tmpfs`. Docker's
177+
`docker cp` cannot read from tmpfs mounts — it only traverses the
178+
container's overlay layers — so the obvious one-liner
179+
180+
```bash
181+
# DON'T — silently broken: import succeeds, ls confirms the file,
182+
# but docker cp says "Could not find the file in container".
151183
docker exec "$CONTAINER" bash -c 'DISPLAY=:99 import -window root /tmp/shot.png'
152-
# Copy it to the host for viewing / feeding to a vision model
153184
docker cp "$CONTAINER:/tmp/shot.png" /tmp/shot.png
154185
```
155186

187+
**fails everywhere** (CI and local). The helper bypasses `docker cp` by
188+
streaming the file via `docker exec cat` (see `scripts/devcontainer-steal.sh`
189+
for the generic version — use that one to extract any tmpfs file, e.g. logs
190+
or generated artifacts, not just screenshots).
191+
156192
`import` is from ImageMagick. For a specific window only, use `xwininfo` to get the WID then `import -window <WID>`.
157193

158194
### See the window tree (what's there, where, which is focused)

.cursor/skills/devcontainer-dev/SKILL.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ The readiness banner is what you care about. It gates on **three** signals simul
7272

7373
Only once all three are true does the banner fire and the host's browser auto-open.
7474

75+
> **Process running ≠ UI rendered.** The three gates above tell you the
76+
> backing processes are alive, not that Electron has actually painted.
77+
> A devcontainer-in-CI proof captured a blank screen because all three
78+
> gates passed while the window was still mid-first-paint. For
79+
> agent-grade readiness (e.g. before driving the app via `xdotool` or
80+
> taking a screenshot to feed to a vision model), add this gate:
81+
>
82+
> ```bash
83+
> docker exec -u node "$CONTAINER" bash -c \
84+
> 'DISPLAY=:99 xdotool search --class ToolHive >/dev/null 2>&1'
85+
> ```
86+
>
87+
> `xdotool search --class` only succeeds once the main window is mapped
88+
> on Xvfb. A short settling sleep (~2s) after that first match is cheap
89+
> insurance against catching a partially rendered frame.
90+
7591
---
7692
7793
## Per-worktree isolation
@@ -146,13 +162,33 @@ All commands run via `docker exec` against the container with `DISPLAY=:99` set
146162

147163
### See the screen (screenshots)
148164

165+
Use the project's helper script — it auto-finds the container, captures the
166+
root window, and streams the PNG out to the host. Prints the absolute host
167+
path on stdout so it composes:
168+
149169
```bash
150-
# Take a PNG of the whole virtual framebuffer
170+
SHOT=$(scripts/devcontainer-screenshot.sh)
171+
# or with an explicit path:
172+
scripts/devcontainer-screenshot.sh /tmp/shot.png
173+
```
174+
175+
**Why a helper script and not just `docker cp`?** `/tmp` (and possibly
176+
other paths) inside the devcontainer is mounted as `tmpfs`. Docker's
177+
`docker cp` cannot read from tmpfs mounts — it only traverses the
178+
container's overlay layers — so the obvious one-liner
179+
180+
```bash
181+
# DON'T — silently broken: import succeeds, ls confirms the file,
182+
# but docker cp says "Could not find the file in container".
151183
docker exec "$CONTAINER" bash -c 'DISPLAY=:99 import -window root /tmp/shot.png'
152-
# Copy it to the host for viewing / feeding to a vision model
153184
docker cp "$CONTAINER:/tmp/shot.png" /tmp/shot.png
154185
```
155186

187+
**fails everywhere** (CI and local). The helper bypasses `docker cp` by
188+
streaming the file via `docker exec cat` (see `scripts/devcontainer-steal.sh`
189+
for the generic version — use that one to extract any tmpfs file, e.g. logs
190+
or generated artifacts, not just screenshots).
191+
156192
`import` is from ImageMagick. For a specific window only, use `xwininfo` to get the WID then `import -window <WID>`.
157193

158194
### See the window tree (what's there, where, which is focused)

.github/workflows/experiment-devcontainer-proof.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ jobs:
4646
- name: Checkout
4747
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
4848

49+
- name: Set up Docker Buildx
50+
# Required so BuildKit's `gha` cache backend is available below.
51+
# Without this, `cacheFrom: type=gha` fails with
52+
# "unknown cache importer: gha" and the build goes fully cold.
53+
uses: docker/setup-buildx-action@v3
54+
4955
- name: Build & start devcontainer (cached via gha)
5056
uses: devcontainers/ci@v0.3
5157
with:
@@ -95,12 +101,23 @@ jobs:
95101
C=${{ steps.container.outputs.id }}
96102
T_START=$(date +%s)
97103
for i in $(seq 1 120); do
98-
# See devcontainer-dev skill — bracket trick avoids pgrep self-match.
99-
if docker exec "$C" bash -c '
104+
# Process gates AND a window-mapped check via xdotool. The previous
105+
# version only checked process presence, which is not the same as
106+
# "UI has rendered" — the first proof run captured a blank screen
107+
# because Electron was alive but had not painted yet. The
108+
# `xdotool search --class ToolHive` call only succeeds once the
109+
# app's main window is mapped on Xvfb. Bracket trick on pgrep
110+
# avoids self-match (see devcontainer-dev skill).
111+
if docker exec -u node "$C" bash -c '
100112
curl -fsS http://localhost:6080/ >/dev/null 2>&1 \
101113
&& pgrep -f "[e]lectron/dist/electron" >/dev/null \
102-
&& pgrep -f "[t]hv serve" >/dev/null
114+
&& pgrep -f "[t]hv serve" >/dev/null \
115+
&& DISPLAY=:99 xdotool search --class ToolHive >/dev/null 2>&1
103116
'; then
117+
# Settling pause — the window may be mid-first-paint when
118+
# xdotool first finds it. Cheap insurance against a partially
119+
# rendered screenshot.
120+
sleep 2
104121
NOW=$(date +%s)
105122
ELAPSED=$(( NOW - T_START ))
106123
SINCE_T1=$(( NOW - ${{ steps.t1.outputs.epoch }} ))

0 commit comments

Comments
 (0)