Two pathways — never mix in the same spool dispatch.
exec:browser runs JS against page. Globals available: page, snapshot, screenshotWithAccessibilityLabels, state. 15s live window, then backgrounds; output drains automatically on every subsequent plugkit call.
browser: prefix is playwriter session management. One command per block.
Core
Write to .gm/exec-spool/in/browser/<N>.txt:
await page.goto('https://example.com')
await snapshot({ page })
browser:
playwriter session new --direct
browser:
playwriter -s 1 -e 'await page.goto("http://example.com")'
Session state persists across browser: calls. -e arg uses single quotes outside, double inside JS strings.
Timing
Never await setTimeout(N) with N > 10000. Poll instead.
Write to .gm/exec-spool/in/browser/<N>.txt:
const start = Date.now()
while (!state.done && Date.now() - start < 12000) {
await new Promise(r => setTimeout(r, 500))
}
console.log(state.result)
Assertion failed: UV_HANDLE_CLOSING is normal background-on-exit noise; ignore it.
Patterns
Data extraction — write to .gm/exec-spool/in/browser/<N>.txt:
const items = await page.$$eval('.title', els => els.map(e => e.textContent))
console.log(JSON.stringify(items))
Console monitoring — set listeners first, then poll. Write to .gm/exec-spool/in/browser/<N>.txt:
state.logs = []
page.on('console', msg => state.logs.push({ type: msg.type(), text: msg.text() }))
Then write to .gm/exec-spool/in/browser/<N+1>.txt:
console.log(JSON.stringify(state.logs.slice(-20)))
Constraints
- One playwriter command per
browser:block exec:browserbody is plain JS, no shell quoting- Browser tasks drain automatically on every plugkit interaction
- Sessions reap after 5–15 min idle; cleaned up on session end
- Never write standalone
.mjs/.jsPlaywright scripts as a fallback —exec:browsererrors must be debugged throughexec:browserretries, not by creating test files on disk