Repository navigation
2026 10 28 headless
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: open_session(headless=true) opens a browser that draws nothing on the node's screen. The workspace remembers it as it remembers insecure. record=true on it is a 400 saying where headless came from, its live view is refused, and a headless flag smuggled through raw capabilities is refused in the call (400) and in config (boot).
Architecture: headless is one more SETTINGS row, so the cascade, the record and the reap carry it with no new code. Actions.open_session turns it into session_caps.build(option_args=…), which #82 left for this. session_caps.check gains the guard, so every place that already checks (the call, the reap, the config at load) refuses the flag. open_browser refuses record beside RECORDING_OFF. The live proxy's look() and the UI's liveUnavailable read settings.headless off the record.
Tech Stack: Python 3.11+, Selenium 4.48+, FastMCP 4, pytest; Svelte 5 runes and Vitest with jsdom in ui/.
Spec: docs/superpowers/specs/2026-10-28-headless-design.md, drafted on #86 and finished here rather than replaced. Rulings 1–10 are settled. This round changed three of the agent's calls, none of Dr K's: the guard covers both vendor blocks (ruling 6), the admin row gains headless, and "the headless default" leaves the prose (ruling 10).
The spec is the design. What this plan settles where the spec left the call to it:
-
Names in
core/session_caps.py.-
HEADLESS_ARGS = {CHROME: "--headless=new", FIREFOX: "-headless"}. -
HEADLESS_OWNED = "use headless=true or headless=false instead". -
_headless(arg: str) -> bool, which isarg.lstrip("-").split("=", 1)[0].lower() == "headless".
-
-
The guard's order inside
check. Owned top-level keys are refused first, as today. Then, per vendor block, the shape checks. Then every headless flag across both blocks is refused in oneValueError, joined by"; ", eachf"{block}.args {arg} cannot be sent in {where}: {HEADLESS_OWNED}"orf"moz:firefoxOptions.env.MOZ_HEADLESS cannot be sent in {where}: {HEADLESS_OWNED}", plusREMEMBEREDwhenremembered.envis looked at only when it is a mapping; its shape is geckodriver's to refuse. -
Names in
workspace/workspaces.py, besideRECORDING_OFF:-
HEADLESS_RECORD = "a headless browser draws nothing to film: record=true cannot be asked with headless=true"; -
HEADLESS_RECORD_REMEMBERED = "this workspace's browser is headless, remembered from an earlier open: pass headless=false to record".
-
-
Actions.open_session(…, capabilities=None, headless=None, *, …). It goes aftercapabilities, so no positional caller shifts._set_upgainsheadless: bool, andapplied["headless"] = Truefollowsinsecure. -
The tool's parameter
headless: bool | None = Nonegoes afterinsecure, the option it is remembered like. Adding an optional argument is safe (CONTRIBUTING, Changing an argument's shape). -
The admin row gains
"headless": bool(settings.get("headless"))afterinsecure.WorkspaceRowgainsheadless?: boolean. The WORKSPACE card's row is['headless', row.headless ? 'on' : null, sources('headless')], after INSECURE.
- Python 3.11 is the floor: nothing 3.12+.
-
core/session_caps.pystays plain: no Selenium, no protocol import (tests/test_boundaries.py). - No query parameter, header or config setting for
headless(ruling 2).FROM_CONFIGdoes not change. - No log line formats a Grid id, a capability value or a setting's value.
-
ran.live_viewstaysse:vncEnabled(ruling 5). - Every published shape that moves is a golden: regenerate with
GOLDEN_UPDATE=1 pytest tests/test_golden.py, read the diff, and commit goldens on their own (CONTRIBUTING.md). - Wiki pages are regenerated with
python scripts/generate_wiki.py, andtests/test_wiki.pypasses. The wiki is thedocs/submodule: a commit inside it, then the pointer here. - Changelog: only
## [Unreleased], one short line. - No new integration flow (AGENTS.md: less is more).
- The UI copy rules: no helper text in a value.
- Never commit
kubed/selenium_flow/http/static/oropenapi.yaml. - The checks, as CI runs them:
- Python (
test.yml):ruff check .andpytest. - UI (
ui.yml):npm --prefix ui ci,npm --prefix ui run lint,npm --prefix ui run check,npm --prefix ui test,npm --prefix ui run build. - OpenAPI (
quality.yml):python scripts/generate_openapi.pysucceeds.
- Python (
-
A workspace that remembers headless, and a caller who now asks
open_session(record=true). A person expects a 400 that says the headless came from an earlier open and how to clear it, and the browser they hold still running. Task 4 tests the wording andactions.calls/grid.aliveuntouched. -
The same caller then passes
open_session(record=true, headless=false). A person expects a recorded, headful browser, and the nextopen_session()to stay headful. Task 4 tests both opens. -
A Chrome workspace whose remembered
capabilitieshold{"moz:firefoxOptions": {"args": ["-headless"]}}, from before the upgrade, thenopen_session(browser="firefox"). A person expects a refusal saying to clear it withcapabilities={}, not a headless Firefox the rules never saw. Task 2 tests the block for the other browser, and Task 4 tests the explicit open and the reap. -
An HTTP caller sending
{"headless": "false"}as a string, or?headless=trueon its URL. A person expects the string read as false and beating a remembered true, and the URL ignored. Task 4 tests both throughopen_browser. -
The admin opening the Live tab of a headless browser on a node that reports VNC. A person expects the tab greyed
no live view · headless, and a forced mint refused, not an empty desktop. Task 5 tests the mint and the socket, and Task 6 the tab's order.
Files:
- Modify:
docs/superpowers/specs/2026-10-28-headless-design.md, Research (the two measured bullets)
Interfaces: none. Nothing in the repository changes but the spec.
- Step 1: A node with no screen (ruling 9). For each image,
docker run -d --rm -p 4444:4444 --shm-size 2g -e SE_START_XVFB=false selenium/standalone-chrome:4.48.0-20260905(thenselenium/standalone-firefox:4.48.0-20260905), wait for/opt/bin/check-grid.sh, and open a headful browser with a scratch script outside the repository:
from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException
try:
webdriver.Remote("http://localhost:4444", options=webdriver.ChromeOptions()) # FirefoxOptions
except SessionNotCreatedException as e:
print(e.msg)Record the message with its hosts and build lines cut, as session_caps._reason would cut them. If it opens instead, record that.
-
Step 2: The window cap (ruling 8). On each image with its default screen, open headful and then with
--headless=new/-headless, callset_window_size(3000, 4000), and record whatget_window_size()returns. -
Step 3: Write both into Research, replacing the "What the plan's Task 1 measures" paragraph with the results and the date. If Docker is not available where this runs, write
not measured: <why>for each, keep ruling 8's sentence as the issue's research states it, and say so in the pull request. If Step 2 finds the headful window not capped, stop and tell Dr K: ruling 8's docstring sentences in Task 3 would be false. -
Step 4: Commit inside
docs/:docs: what a headful browser gets with no screen, and the window cap.
No check applies: nothing outside the spec changes.
Files:
- Modify:
kubed/selenium_flow/core/session_caps.py: new names afterOWNED(53–67);check(175–207);build's docstring (276–278, "nothing passes it yet") - Test:
tests/test_session_caps.py,tests/test_config_load.py
Interfaces:
-
Produces:
HEADLESS_ARGS,HEADLESS_OWNED,_headless(Design).check(raw, *, where, remembered)keeps its signature.buildis unchanged:option_argsalready exists. -
Step 1: Write the failing tests. In
tests/test_session_caps.py:
@pytest.mark.parametrize("block", ["goog:chromeOptions", "moz:firefoxOptions"])
@pytest.mark.parametrize("arg", ["--headless", "--headless=new", "--headless=old", "-headless", "-HEADLESS", "headless"])
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
def test_a_headless_arg_is_refused_in_either_block_on_either_browser(block, arg, browser): # ruling 6
said = f"{block}.args {arg} cannot be sent in capabilities: use headless=true or headless=false instead"
with pytest.raises(ValueError, match=re.escape(said)):
build(browser, raw={block: {"args": ["--lang=fr", arg]}})
def test_moz_headless_in_env_is_refused_whatever_its_value():
for value in ("1", "0", ""):
with pytest.raises(ValueError, match=re.escape(
"moz:firefoxOptions.env.MOZ_HEADLESS cannot be sent in capabilities: use headless=true")):
check({"moz:firefoxOptions": {"env": {"MOZ_HEADLESS": value}}})
def test_args_that_only_look_like_headless_pass():
check({"goog:chromeOptions": {"args": ["--headless-mode-x", "--no-headless-thing", "--lang=headless"]}})
def test_a_headless_flag_in_config_names_session_capabilities():
with pytest.raises(ValueError, match=re.escape(
"goog:chromeOptions.args --headless cannot be sent in session.capabilities: use headless=")):
build("chrome", config={"goog:chromeOptions": {"args": ["--headless"]}})
def test_a_remembered_headless_flag_says_how_to_clear_it():
with pytest.raises(ValueError, match=re.escape("open_session(capabilities={})")):
check({"moz:firefoxOptions": {"args": ["-headless"]}}, remembered=True)
def test_two_headless_flags_are_named_in_one_refusal():
with pytest.raises(ValueError) as refused:
check({"goog:chromeOptions": {"args": ["--headless"]}, "moz:firefoxOptions": {"env": {"MOZ_HEADLESS": "1"}}})
assert "goog:chromeOptions.args --headless" in str(refused.value)
assert "moz:firefoxOptions.env.MOZ_HEADLESS" in str(refused.value)
def test_the_headless_args_are_each_browsers_own(): # ruling 7
assert HEADLESS_ARGS == {"chrome": "--headless=new", "firefox": "-headless"}test_an_option_arg_joins_the_browsers_own_block (126) already pins the arg's place and its option source; leave it. In tests/test_config_load.py, beside test_an_owned_key_in_session_capabilities_stops_the_boot:
def test_a_headless_flag_in_session_capabilities_stops_the_boot(): # ruling 6
with pytest.raises(ConfigError) as refused:
load([], {"SESSION_CAPABILITIES": '{"moz:firefoxOptions": {"args": ["-headless"]}}'})
assert "moz:firefoxOptions.args -headless cannot be sent in session.capabilities: " \
"use headless=true or headless=false instead" in str(refused.value)- Step 2: Run them to see them fail
Run: pytest tests/test_session_caps.py tests/test_config_load.py -v
Expected: FAIL, HEADLESS_ARGS not defined, and the refusals not raised.
-
Step 3: Implement the names and the guard as Design orders them.
build's docstring saysoption_argsis whatheadlessadds. -
Step 4: Run them
Run: pytest tests/test_session_caps.py tests/test_config_load.py tests/test_boundaries.py -v && ruff check .
Expected: PASS.
- Step 5: Commit
feat: a headless flag in raw or config capabilities is refused, naming headless=.
Files:
- Modify:
kubed/selenium_flow/workspace/settings.py:SETTINGS(130–152), aheadlessrow afterinsecure; the module docstring's list of settings (3–6) - Modify:
kubed/selenium_flow/core/actions.py:open_session(273–387),_set_up(389–518) - Modify:
kubed/selenium_flow/mcp/tools.py:open_session's parameter and docstring (377–448),resize's docstring (602),screenshot's docstring (778–) - Test:
tests/test_session_open.py,tests/test_surfaces.py - Regenerate:
tests/golden/tools-on.json,tests/golden/tools-off.json,tests/golden/openapi.json; wiki pages
Interfaces:
-
Consumes:
HEADLESS_ARGS(Task 2). -
Produces:
SETTINGS["headless"] == (None, None, _as_flag);Actions.open_session(…, headless=None); the result'ssettings["headless"] is Truewhen set, absent otherwise. -
Step 1: Write the failing tests. In
tests/test_session_open.py, with itsGridandDriver:
@pytest.mark.parametrize("browser, arg, block", [
("chrome", "--headless=new", "goog:chromeOptions"), ("firefox", "-headless", "moz:firefoxOptions")])
def test_headless_sends_the_browsers_own_flag_and_says_so(browser, arg, block): # ruling 7
grid = Grid(RAN)
result = Actions(grid).open_session(browser=browser, headless=True)
assert arg in grid.built.send[block]["args"]
assert result["settings"]["headless"] is True
@pytest.mark.parametrize("given", [False, None, "false"])
def test_headful_sends_no_flag(given):
grid = Grid(RAN)
result = Actions(grid).open_session(headless=given)
assert not any("headless" in a for a in grid.built.send["goog:chromeOptions"]["args"])
assert "headless" not in result["settings"]
def test_headless_over_http_reaches_the_open(...): # POST /browser {"headless": true} on a SeleniumMCP
# whose actions.grid is this file's Grid: the endpoint passes it because it is a SETTINGS key
assert "--headless=new" in grid.built.send["goog:chromeOptions"]["args"]In tests/test_surfaces.py, beside the existing schema checks: open_session's input schema has headless as an optional boolean (anyOf boolean / null), and it is not required.
- Step 2: Run them to see them fail
Run: pytest tests/test_session_open.py tests/test_surfaces.py -v
Expected: FAIL, open_session takes no headless.
-
Step 3: Implement the row, the action's keyword and
_set_up's report, and the tool's parameter, passed through toworkspaces.open_browser. Then the prose:-
open_session's docstring. In the window bullet, "the headless default is narrow" becomes "the default window is narrow" (ruling 10). A new bullet goes after theinsecureone and beforerecord:headless=true opens a browser that draws nothing on the node's screen: quicker, and its window is not limited to that screen, so it is the way to a very large window or full-page screenshot. It cannot be recorded and nobody can watch it live, and sites' bot checks spot it more easily, so leave it off for real sites. Remembered like the options above until you pass headless=false.
-
resize's docstring: "the headless default is small" becomes "the default window is small". -
screenshot's docstring, one sentence after thefull_pageone: full_page grows the window to the page, and a window cannot outgrow the node's screen unless the browser was opened with headless=true. Drop it if Task 1 measured no cap.
-
-
Step 4: Run them, regenerate, and read the diffs
Run: pytest tests/test_session_open.py tests/test_surfaces.py tests/test_browser_choice.py -v && GOLDEN_UPDATE=1 pytest tests/test_golden.py && python scripts/generate_wiki.py && pytest && ruff check . && python scripts/generate_openapi.py
Expected: PASS. The golden diff shows headless in open_session's schema and the three descriptions, in tools-on, tools-off and openapi, and nothing else.
- Step 5: Commit the code, then the goldens, then the wiki pages:
feat: open_session(headless=true).
Files:
- Modify:
kubed/selenium_flow/workspace/workspaces.py: the two refusals besideRECORDING_OFF(78–83);open_browser(1044–1047) - Test:
tests/test_workspaces.py(besidetest_version_and_capabilities_are_remembered_and_carried, 1818),tests/test_recording_open.py
Interfaces:
- Consumes:
SETTINGS["headless"](Task 3); the guard (Task 2). - Produces:
HEADLESS_RECORD,HEADLESS_RECORD_REMEMBERED.
In open_browser, right after the RECORDING_OFF check:
if settings.get("record") and settings.get("headless"):
passed = settings_module.SETTINGS["headless"][2](wanted.get("headless")) is not None
raise ValueError(HEADLESS_RECORD if passed else HEADLESS_RECORD_REMEMBERED)The cascade itself needs no code: the tests prove it carries the new row.
- Step 1: Write the failing tests. In
tests/test_workspaces.py, withmanagerandRecordingActions:
def test_headless_is_remembered_and_carried(): # ruling 2
workspaces.open_browser(Caller(NAMED), headless=True)
workspaces.open_browser(Caller(NAMED))
assert actions.opened_settings[-1]["headless"] is True
assert workspaces.store.get(NAMED).set_by["headless"] == "caller"
def test_an_explicit_false_beats_a_remembered_true_and_is_remembered(): # Review Focus 2, 4
workspaces.open_browser(Caller(NAMED), headless=True)
workspaces.open_browser(Caller(NAMED), headless="false")
workspaces.open_browser(Caller(NAMED))
assert actions.opened_settings[-1]["headless"] is False
def test_a_reap_replays_headless():
workspaces.store.set(NAMED, Workspace(session_id="dead",
settings={"browser": "chrome", "headless": True}, set_by={"browser": "caller", "headless": "caller"}))
workspaces.resolve(NAMED)
assert actions.opened_settings[-1]["headless"] is True
def test_headless_is_read_from_no_parameter_and_no_header(): # Review Focus 4
caller = Caller.from_request({"workspace": [NAMED], "headless": ["true"]}, {"x-headless": ["true"]})
workspaces.open_browser(caller)
assert "headless" not in actions.opened_settings[-1]
def test_a_remembered_raw_headless_flag_is_refused_on_an_explicit_open(): # Review Focus 3
# stored, attached and alive: settings {"capabilities": {"moz:firefoxOptions": {"args": ["-headless"]}}}, set_by caller
with pytest.raises(ValueError, match=re.escape("open_session(capabilities={})")):
workspaces.open_browser(Caller(NAMED), browser="firefox")
assert actions.closed == []
def test_a_reap_refuses_a_remembered_raw_headless_flag():
# the same settings on a dead session: resolve raises with "open_session(capabilities={})", actions.opened == 0In tests/test_recording_open.py, with its manager(Recorder()):
def test_record_with_headless_in_the_same_call_is_a_400_and_opens_nothing(): # ruling 4
m = manager(Recorder())
with pytest.raises(ValueError) as exc:
m.open_browser(caller(), record=True, headless=True)
assert str(exc.value) == workspaces_module.HEADLESS_RECORD
assert m.actions.calls == []
def test_record_on_a_remembered_headless_says_where_it_came_from_and_keeps_the_browser(): # Review Focus 1
m = manager(Recorder())
m.open_browser(caller(), headless=True)
with pytest.raises(ValueError) as exc:
m.open_browser(caller(), record=True)
assert str(exc.value) == workspaces_module.HEADLESS_RECORD_REMEMBERED
assert m.actions.grid.alive == {"grid0001"} and len(m.actions.calls) == 1
def test_headless_false_lets_a_remembered_headless_workspace_record(): # Review Focus 2
m = manager(Recorder())
m.open_browser(caller(), headless=True)
assert m.open_browser(caller(), record=True, headless=False)["recording"] is True
assert m.actions.calls[-1]["headless"] is False
def test_recording_off_is_said_before_headless():
with pytest.raises(ValueError) as exc:
manager(None).open_browser(caller(), record=True, headless=True)
assert str(exc.value) == workspaces_module.RECORDING_OFF- Step 2: Run them to see them fail
Run: pytest tests/test_workspaces.py tests/test_recording_open.py -v
Expected: the record tests FAIL, HEADLESS_RECORD not defined. The cascade tests may already pass after Task 3; that is the point of them.
-
Step 3: Implement the two constants and the check.
-
Step 4: Run the suite
Run: pytest && ruff check .
Expected: PASS.
- Step 5: Commit
feat: a headless workspace refuses record=true, saying where headless came from.
Files:
- Modify:
kubed/selenium_flow/http/admin/live.py:HEADLESSbesideNO_LIVE_VIEW(55);look(140–149) - Modify:
kubed/selenium_flow/http/admin/workspaces.py: the row (522) - Test:
tests/test_admin_live.py(build, 47–59; the two parametrised refusals, 103–115 and 200–218),tests/test_files_and_admin.py - Regenerate:
tests/golden/admin-workspaces.json
Interfaces:
- Consumes: the stored
settings["headless"](Tasks 3, 4). - Produces:
live.HEADLESS = "this browser is headless: it has no live view"; the row'sheadless: bool.
look, after the NO_BROWSER return: if (record.settings or {}).get("headless"): return None, HEADLESS.
- Step 1: Write the failing tests. In
tests/test_admin_live.py,buildstores one more record:
store.set("headless", Workspace(session_id="g-headless", ran={"live_view": True},
settings={"headless": True}, set_by={"headless": "caller"}))and both parametrised lists gain ("headless", True, "this browser is headless: it has no live view"), so the mint answers 404 and the socket closes 4004 with it, with the audit line. In tests/test_files_and_admin.py, beside test_the_row_carries_insecure_and_each_settings_source (805) and built the same way:
def test_the_row_says_whether_the_browser_is_headless():
# one record with settings {"headless": True}, set_by caller; one with none
assert [row["headless"] for row in rows] == [True, False]- Step 2: Run them to see them fail
Run: pytest tests/test_admin_live.py tests/test_files_and_admin.py -v
Expected: FAIL: the headless record mints a link, and the row has no headless.
-
Step 3: Implement
HEADLESS, the check, and the field. -
Step 4: Run them and regenerate the admin golden
Run: pytest tests/test_admin_live.py tests/test_files_and_admin.py -v && GOLDEN_UPDATE=1 pytest tests/test_golden.py && pytest && ruff check .
Expected: PASS. The golden diff adds headless to each row and nothing else.
- Step 5: Commit the code, then the golden:
feat: a headless browser has no live view, and the admin row says so.
Files:
- Modify:
ui/src/lib/types.ts(WorkspaceRow, 46),ui/src/admin/liveview.svelte.ts(liveUnavailable, 24–30),ui/src/admin/SessionPane.svelte(27) - Test:
ui/src/admin/liveview.test.ts(236–253),ui/src/admin/SessionPane.test.ts(beside the INSECURE test, 150–159)
Interfaces:
-
Consumes: the row's
headless(Task 5). -
Produces:
WorkspaceRow.headless?: boolean;liveUnavailablereturns'no live view · headless'afterno browser, beforethis Grid has no VNC. -
Step 1: Write the failing tests. In
liveview.test.ts:
test('a headless browser has no live view, said after no browser and before the Grid', () => {
const headless = { key: 'k', live: true, headless: true, ran: { version: null, node: null, live_view: true } }
expect(liveUnavailable(headless, true)).toBe('no live view · headless')
expect(liveUnavailable({ ...headless, ran: { version: null, node: null, live_view: false } }, true)).toBe('no live view · headless')
expect(liveUnavailable({ ...headless, live: false }, true)).toBe('no live view · no browser')
expect(liveUnavailable(headless, false)).toBe('no live view · switched off')
})In SessionPane.test.ts:
test('HEADLESS shows only when on, idle too, with its source', () => {
const on = render(SessionPane, { row: { ...LIVE, live: false, attached: false, headless: true, set_by: { headless: 'caller' } }, hidden: false })
expect(fact(on.container, 'workspaceCard', 'headless')).toBe('on')
expect(pillsOf(on.container, 'headless').map((p) => p.textContent)).toEqual(['caller'])
const off = render(SessionPane, { row: { ...LIVE, headless: false }, hidden: false })
expect(fact(off.container, 'workspaceCard', 'headless')).toBeUndefined()
})- Step 2: Run them to see them fail
Run: npm --prefix ui ci && npm --prefix ui test
Expected: FAIL.
-
Step 3: Implement the type, the branch and the row.
-
Step 4: Run every UI check
Run: npm --prefix ui run lint && npm --prefix ui run check && npm --prefix ui test && npm --prefix ui run build
Expected: PASS. Then look at it: npm --prefix ui run dev against docker compose up, open a workspace with headless=true, and compare the Session and Live tabs with the Penpot pages Workspace · Session and Workspace · Live (live-unavailable). Say so if that could not be done.
- Step 5: Commit
feat: the Session tab shows HEADLESS, and the Live tab says why it is greyed.
Files:
- Modify:
skills/selenium-flow/references/WORKSPACES.md, Choosing the browser (76–109) - Modify:
skills/selenium-flow/references/TROUBLESHOOTING.md, Sending a capability no option covers (207–231) - Modify:
skills/selenium-flow/references/CONFIGURATION.md(142–145) - Modify:
skills/selenium-flow/references/READING_PAGES.md(153–154),skills/selenium-flow/references/INTERACTION.md(249–250) - Modify:
docs/notes/open_session.notes.md(37–58) - Modify:
AGENTS.md: The live view (the bullet list after "A person in Control holds the browser") and Recordings - Modify:
CHANGELOG.md(## [Unreleased]) - Regenerate: wiki pages
- Test:
tests/test_skill.py,tests/test_wiki.py
Interfaces: none produced.
Content:
-
WORKSPACES.md. A table row after
insecure:| `headless` | draws nothing on the node's screen: quicker, and the window is not limited to it; no recording, no live view | keeps it if you passed it |. Under the paragraph after the table, a short one: choose it for speed, a very large window or full-page screenshot, or when nobody will watch; leave it off for real sites, whose bot checks spot it more easily;record=trueon a headless browser is refused, so passheadless=falseto record. -
TROUBLESHOOTING.md. The owned list gains a headless flag in either browser's
args, orMOZ_HEADLESSin Firefox'senv(headless=). The node-class example{"kubed:pool": "headless"}becomes{"kubed:pool": "large"}, here and indocs/notes/open_session.notes.md, since a pool called headless now reads as the option. -
CONFIGURATION.md, after the
insecureparagraph:headlesshas no default either: onlyopen_session(headless=true)sets it, and a headless flag insession.capabilitiesstops the boot. -
READING_PAGES.md, INTERACTION.md: "headless defaults" / "The headless default" become "the default window" (ruling 10).
-
open_session.notes.md: the owned-key list gains the headless flag.
-
AGENTS.md, one bullet in The live view: A headless browser has no live view:
look()refuses it on the record'ssettings.headless, never onran.live_view, which stays what the Grid said (a headless browser on a node with a screen still reports VNC). One in Recordings:record=truewithheadlessis a 400 before anything ends, in words that say whether headless was passed or remembered; a headless flag in raw or config capabilities is refused, so the named option is the only way to it. -
CHANGELOG, one line:
- **`open_session(headless=true)`** opens a browser that draws nothing on screen, remembered until `headless=false`; it cannot be recorded or watched live. -
Step 1: Write the content.
-
Step 2: Regenerate and run the published-shape tests
Run: python scripts/generate_wiki.py && pytest tests/test_skill.py tests/test_wiki.py tests/test_readme.py -v
Expected: PASS.
- Step 3: Run every check once more
Run: ruff check . && pytest && npm --prefix ui ci && npm --prefix ui run lint && npm --prefix ui run check && npm --prefix ui test && npm --prefix ui run build && python scripts/generate_openapi.py
Expected: PASS.
- Step 4: Commit
docs: when to open a browser headless, the wiki pages insidedocs/with the pointer, andchore: changelog for headless.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out
Site data
Console and network