Why GSD can drive a browser during execution, and why it does that from a purpose-built agent instead of from the plan executor.
A phase with a live-UI acceptance criterion could not be finished by the agent that executed
it. gsd-executor carries no browser tools, so it did the correct thing — returned a
checkpoint:human-action at the gate — even though the work was not actually human-only. An
orchestrator with a browser MCP server could do it unattended.
The practical result was that every phase with a DOM-level acceptance criterion quietly
degraded from executed by the executor to executed by the executor, then finished by hand in
the orchestrator. On a UI-heavy project that is routine, not an edge case. Worse, the plan's
own autonomous: false marker could not distinguish "a human must judge this" from
"the executor lacks the tool" — so the run notes had to explain the deviation every time.
The obvious fix is to add browser globs to agents/gsd-executor.md's tools: line. It is one
line, and an absent MCP server simply means the tool is not offered, so it is inert for anyone
without one.
That reasoning is correct and it is not the concern. The concern is the user who does have
a browser MCP configured for unrelated work. For a first-party agent, the static tools: list
is the only control that exists:
- A capability cannot grant tools to a first-party agent. ADR-1244
D2 rejects an overlay whose
idcollides with a first-party id or that claims an agent stem already owned. Acontributionhook (ADR-857 D4) injects prose into a step's prompt; no hook kind grants tool permissions. - There is no per-dispatch tool override. The executor is spawned with
subagent_type,description,model, andprompt. The agent definition file is the sole authority on its tool surface. - Nothing else contains it. ADR-1244 D5 is explicit: "there is no sandbox… Consent + integrity + reversibility are the barrier." There is no domain allowlist and no gate that inspects what a browser call fetched.
The codebase already reflected this instinct. gsd-ui-auditor — the one subagent that produces
UI screenshots — captures via CLI rather than taking an MCP tool grant. The executor carrying
only mcp__context7__* while the researcher agents carry the full web-reaching set is a
deliberate separation, not an oversight.
So widening the executor would trade a narrow, auditable surface for a permanent broad one, to solve a problem that a narrower surface also solves.
One default-off capability owns everything:
- The key.
workflow.live_dom_uat,boolean, defaultfalse, declared as the capability'sactivationKey. With it off the capability resolves inactive, andresolveLoopHooksis fail-closed onstate.active === true— the hook does not render at all. The step's ownwhenguard is a second, independent gate. - The agent.
gsd-dom-verifiercarriesmcp__chrome-devtools__*andmcp__claude-in-chrome__*in its owntools:line, and carries noBash. Browser reach is confined to one agent that only exists to look at a DOM. - The step. Registered at
execute:wave:postas astephook withonError: skip. A step is additive by construction — it never halts the host. Blocking preconditions aregates, and this capability declares none.
gsd-executor's tool surface is unchanged in every configuration. That is a tested invariant,
asserted as an absence, because that is the only way it is observable.
The orchestrator's automated_ui_verification step already used mcp__playwright__*, gated on
tool presence plus an active UI phase. Pulling that branch behind a new default-off key
would have silently removed working behaviour from every current Playwright-MCP user on
upgrade — a regression wearing an enhancement's clothes.
So the key gates only the newly added families. Playwright keeps the gating it already had. The instruction to gate on "presence AND the config key, not presence alone" is what gives the new families the same two-condition shape Playwright already possessed.
chrome-devtools-mcp holds an exclusive lock on its browser profile, so parallel execution
waves will collide. The tempting design is a lease or queue around the profile.
GSD cannot enforce one. --isolated is a flag on the user's MCP server registration; GSD
neither launches that server nor passes its arguments. Coordination over a resource you do not
own is theatre — it adds machinery, and the lock still happens.
So the verifier tolerates the lock instead: it reports could_not_look / profile_locked,
names --isolated so the operator knows the remedy, and stops. No retry, no wait, no held-up
wave. The docs carry the flag; the code does not pretend to.
DOM-VERIFY.md separates nothing_to_report (there were no UI criteria) from
could_not_look (there were criteria, and the check did not happen — with a reason code
saying which). Collapsing those two is what produced the ambiguous run notes that opened the
original report. A report claiming no issues when it never opened a browser is worse than no
report.
- No sandbox. Once the key is on, nothing constrains which origins a browser call reaches. This capability narrows who can reach a browser, not where it may go.
- Concurrent waves still collide on a shared profile unless the operator passes
--isolated. - DOM observation only — no screenshot diffing, accessibility audit, or performance tracing.
chrome-devtoolsandclaude-in-chromeare detected but not feature-normalized; the verifier uses whichever answers and does not paper over differences between them.