Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
106 lines
5.9 KiB
Markdown
106 lines
5.9 KiB
Markdown
# The live-DOM UAT capability
|
|
|
|
Why MSD can drive a browser during execution, and why it does that from a purpose-built agent
|
|
instead of from the plan executor.
|
|
|
|
## The problem
|
|
|
|
A phase with a live-UI acceptance criterion could not be finished by the agent that executed
|
|
it. `msd-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 shape that was not taken
|
|
|
|
The obvious fix is to add browser globs to `agents/msd-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](../adr/1244-capability-ecosystem.md)
|
|
D2 rejects an overlay whose `id` collides with a first-party id or that claims an agent stem
|
|
already owned. A `contribution` hook ([ADR-857](../adr/857-capability-system.md) 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`, and `prompt`. 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. `msd-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.
|
|
|
|
## The shape that was taken
|
|
|
|
One default-off capability owns everything:
|
|
|
|
- **The key.** `workflow.live_dom_uat`, `boolean`, default `false`, declared as the
|
|
capability's `activationKey`. With it off the capability resolves **inactive**, and
|
|
`resolveLoopHooks` is fail-closed on `state.active === true` — the hook does not render at
|
|
all. The step's own `when` guard is a second, independent gate.
|
|
- **The agent.** `msd-dom-verifier` carries `mcp__chrome-devtools__*` and
|
|
`mcp__claude-in-chrome__*` in **its own** `tools:` line, and carries no `Bash`. Browser reach
|
|
is confined to one agent that only exists to look at a DOM.
|
|
- **The step.** Registered at `execute:wave:post` as a `step` hook with `onError: skip`. A step
|
|
is additive by construction — it never halts the host. Blocking preconditions are `gate`s,
|
|
and this capability declares none.
|
|
|
|
`msd-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.
|
|
|
|
## Why the existing Playwright path was left alone
|
|
|
|
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.
|
|
|
|
## Why there is no browser-lock coordination
|
|
|
|
`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.
|
|
|
|
MSD cannot enforce one. `--isolated` is a flag on the **user's** MCP server registration; MSD
|
|
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.
|
|
|
|
## The distinction the artifact must preserve
|
|
|
|
`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.
|
|
|
|
## Known limits
|
|
|
|
- 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-devtools` and `claude-in-chrome` are detected but not feature-normalized; the
|
|
verifier uses whichever answers and does not paper over differences between them.
|