Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
- id: react-npm-vite7
framework: react
package-manager: npm
version-args: '--vite-version=7.3.7'
version-args: '--vite-version=7.3.7 --schema-peer'
- id: react-pnpm-vite5
framework: react
package-manager: pnpm
Expand All @@ -159,15 +159,19 @@ jobs:
- id: vue-pnpm-vite5
framework: vue
package-manager: pnpm
version-args: '--vite-version=5.4.21 --scope=list'
version-args: '--vite-version=5.4.21 --scope=list --schema-peer --adapter-only --raw-runtime'
- id: react-pnpm-vite8
framework: react
package-manager: pnpm
version-args: '--vite-version=8.3.5 --scope=table'
version-args: '--vite-version=8.3.5 --scope=table --schema-peer --adapter-only'
- id: vue-npm-vite8
framework: vue
package-manager: npm
version-args: '--vite-version=8.0.10 --scope=list'
- id: vue-pnpm-vite8-no-overlay
framework: vue
package-manager: pnpm
version-args: '--vite-version=8.3.5 --schema-peer --adapter-only --no-overlay'
- id: next15-npm-webpack
framework: next
package-manager: npm
Expand Down
74 changes: 69 additions & 5 deletions docs/agent-targets-and-replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Use MCP `domscribe.targets.query` or HTTP `POST /api/v1/targets/query`:
{ "mode": "list", "entryId": "aB3dEf7h", "limit": 50 }
```

The response contains `status`, `targets`, `total`, and `browserConnected`. Each target includes `instanceId`, `entryId` when mapped, `selector`, `locator`, `disabled`, and the effective `inputType` for inputs. Resolve an entry ID with the existing source-resolution tools for its source file and position.
The response contains `status`, optional `reason`, `targets`, `total`, and `browserConnected`. Each target includes `instanceId`, `entryId` when mapped, `selector`, `locator`, `disabled`, and the effective `inputType` for inputs. Resolve an entry ID with the existing source-resolution tools for its source file and position.

To inspect an instance, send `{"instanceId":"<returned instance ID>"}`. To locate a capture in a new session, send `{"locator":<saved locator>}`. `domscribe.query.bySource` also accepts an optional `instanceId` or `locator` and returns `selectionStatus`, `targets` and `total` alongside its existing fields.
To inspect an instance, send `{"instanceId":"<returned instance ID>"}`. To locate a capture in a new session, send `{"locator":<saved locator>}`. `domscribe.query.bySource` also accepts an optional `instanceId` or `locator` and returns `selectionStatus`, optional `selectionReason`, `targets` and `total` alongside its existing fields.

Only `resolved` identifies one exact target and returns its available runtime context. `listed` is discovery, not a selection. `ambiguous`, `not_found`, `stale`, `wrong_page`, `not_ready`, `unsupported`, `ambiguous_browser`, and `unavailable` are explicit stopping conditions. Reconnect/retry readiness failures or refine ambiguous targets; do not choose the first candidate. Keep one browser session connected to the relay for these queries. After HMR, the manifest can briefly lag newly rendered source IDs; retry source resolution while updates settle instead of substituting an old mapping.

Expand All @@ -24,6 +24,59 @@ Semantic table rows and list items (`tr`, `li`, `[role="row"]`, and `[role="list

PII is checked before segment encoding, including email/card/phone values split across text nodes; sensitive row identity is redacted as a whole and cannot be replayed. Local fingerprints retain complete raw ancestor constraints privately. If more than four identified ancestor scopes are needed, the portable locator is explicitly unsupported rather than silently dropping an outer business identity. The same applies when raw identity exceeds size limits even if redaction would shorten it.

## Dialog business identity

A reusable dialog can render identical controls for different business records.
A static dialog label, source ID or button text cannot prove which record is
selected. Portable capture/replay therefore requires an explicit, stable,
**non-secret** `data-ds-context` key on the dialog or an ancestor of the selected
control inside that dialog:

```tsx
<section role="dialog" data-ds-context={`customer:${customer.id}`}>
<button onClick={() => save(customer.id)}>Save</button>
</section>
```

In Vue, use `:data-ds-context="'customer:' + customer.id"`. Use an opaque public
record identifier, never a token, personal information, form value or secret.
Each nested dialog needs its own key; sibling keys and keys outside that dialog
do not establish its identity. The full ordered dialog-context chain must match.
Changing an inner or outer key invalidates old local handles and portable matches.

Unscoped dialogs are exported with `unsupportedReason: "context_required"`.
Portable resolution returns `status: "unsupported", reason: "context_required"`,
and recording refuses with an actionable error. Legacy locators without a saved
context chain also refuse inside dialogs, even if the current app now supplies a
key. Recapture after integration. Live instance inspection and source mapping
remain available, but a DOM handle alone does not prove business identity across
sessions. For an unmodified application without a key, safe refusal is expected.
Dialog detection covers native `dialog`, `role="dialog"`, `role="alertdialog"`
and `aria-modal="true"`; inline editors without these semantics still require
sufficient ordinary locator identity and are not automatically business-scoped.

## Source-owning runtime context

Relay target/source queries and overlay selection resolve manifest metadata before
capturing context, pinning the original instance across that asynchronous lookup.
Vue can follow component ancestry through shared controls and Teleport to the
component whose `__file` exactly matches that source. `componentIdentity` labels
whose props/state were captured and reports `sourceMatch: "matched"`, `"nearest"`
(no requested source), or `"unavailable"` (ownership could not be proved).
An unavailable match preserves nearest-component context without presenting it as
the application owner. Relative/unknown namespaces and duplicate basenames are
not guessed. Other adapters currently use that explicit nearest/unavailable
fallback; source-owner matching here is Vue-specific and depends on dev metadata.

Capture remains bounded and honors props/state opt-outs and configured redaction.
OAuth `clientSecret`/`client_secret` fields and entire records marked `secret: true`
are redacted before field/byte truncation, including nested environment values.
Credential-field descriptors identified by a known sensitive `id`, `key` or `name`
are redacted as records. With redaction enabled, native browser storage reachable
through an injected service is never enumerated into capture. Redaction cannot identify every
application-specific secret; configure blocked selectors and avoid capturing
private application state in issue evidence.

## Explicit recording

In the instrumented page's JavaScript context:
Expand All @@ -40,7 +93,9 @@ const trace = { version: 1, steps: [step] };

Actions are `click`, `fill` with `value`, `check` with `checked`, and `select` with an option `value`. Recording is explicit and does not scrape inputs or install an event recorder. Password/file input recording, blocked targets, and values requiring configured PII redaction are refused. Do not include credentials in manually authored traces.

New overlay annotations store `interaction.selectedElement.target`. An explicit trace can be stored in `context.replay`; creating an annotation alone does not record the preceding interactions. Annotation schema v4 adds these optional fields, and old annotations remain readable. The live target is captured at selection time; submission refuses a stale/recycled selection. Locating/refreshing an annotation uses its portable target; old annotations only use a unique legacy match.
New overlay annotations store `interaction.selectedElement.target`. An explicit trace can be stored in `context.replay`; creating an annotation alone does not record the preceding interactions. Annotation schema v4 adds these optional fields, and old annotations remain readable. The live target is captured at selection time; submission refuses a stale/recycled selection. Locating/refreshing an annotation uses its portable target; old annotations only use an eligible unique legacy match outside dialogs. Legacy
dialog annotations must be recaptured before relocation or metadata refresh; a
current dialog key cannot supply missing historical business identity.

## Replay through a real browser driver

Expand Down Expand Up @@ -83,7 +138,7 @@ node scripts/verify-instance-replay.mjs --framework=nuxt --package-manager=npm \

The harness installs local tarballs into a temporary consumer, uses the fixture project's existing Playwright dependency and installed Chromium, persists an annotation, closes the browser context, reorders rows in a fresh context, and replays the same four UI steps through the relay. Assertions cover exact mapped source/props, committed editor state after typing/checking, stale handles after unmount and across sessions, ambiguity refusal, application console/page/network failures, production instrumentation removal (including conditional editors), and correct production UI updates.

CI runs ten representative combinations, with at most two replay jobs simultaneously:
CI runs ten complete replay combinations plus one initialization-only case, with at most two replay jobs simultaneously:

| Integration | Consumer versions | Package manager |
| --------------------------- | ------------------------- | --------------- |
Expand All @@ -100,7 +155,7 @@ CI runs ten representative combinations, with at most two replay jobs simultaneo

The default remains React18/npm/Vite7. Use `--framework-version` for exact Next15/16 or Nuxt3/4 releases; use `--vite-version` for React/Vue consumers. Mixed or unsupported flags are rejected. Native SSR consumers use their framework integrations and normal hydration, not the Vite SPA integration. Nuxt excludes the runtime from Vite prebundling so its native plugin and consumer queries share one runtime on cold startup. Each run checks actual installed versions, expected framework source, and local runtime resolution.

`--scope=article` keeps the original labeled-card fixture. `--scope=table` and `--scope=list` use sibling business text with no added row label or test ID. They deliberately retain position-keyed action nodes during a customer replacement and verify that old handles/selectors retire, old portable identity does not resolve, the new runtime props belong to the replacement, and only that customer's UI data changes. CI runs Vue/pnpm/Vite5 and Vue/npm/Vite8 as lists, and Next16/pnpm and React/pnpm/Vite8 as tables. The other six cases retain article coverage.
`--scope=article` keeps the original labeled-card fixture. `--scope=table` and `--scope=list` use sibling business text with no added row label or test ID. They deliberately retain position-keyed action nodes during a customer replacement and verify that old handles/selectors retire, old portable identity does not resolve, the new runtime props belong to the replacement, and only that customer's UI data changes. CI runs Vue/pnpm/Vite5 and Vue/npm/Vite8 as lists, and Next16/pnpm and React/pnpm/Vite8 as tables. The other complete replay cases retain article coverage. The additional Vue/pnpm/Vite8 case disables the overlay and checks initialization/runtime capture only.

Vite8 consumers use the compatible official React6.1.2/Vue6.0.6 plugins. Older Vite5–7 fixture paths keep their existing plugins. [Vite8](https://vite.dev/blog/announcing-vite8) requires Node20.19+ or22.12+; qualification here uses Node24.22.1. The installed-package checks separately install only the adapter, framework and Vite through an isolated local registry, without adding transitive packages or dependency overrides. They typecheck a normal Vite configuration and check that virtual browser modules serve one shared runtime. Vite8 peer support and these APIs remain unpublished until the next package release; npm0.6.1 still declares Vite5–7 support.

Expand All @@ -109,3 +164,12 @@ The [real-application acceptance matrix](./real-app-acceptance.md) records separ
These are representative synthetic regression cases, not the full version/manager/router matrix or an adoption benchmark. Additional local testing on the Domscribe website (Next16.4.3 / React19.2.4) verified exact installation-tab source/state among six repeated instances, persisted replay after a fresh browser session, safe page-navigation failure, mobile navigation state/replay after reload, and controlled HMR identity changes without resurrecting retired handles. Its production build and UI also worked without instrumentation. This is one maintained app, not evidence for every router, virtualized list, framework version, or agent workflow advantage. Package installs use local tarballs, with explicit pnpm local-artifact overrides so unpublished artifacts cannot be replaced by registry copies. This is not evidence that the new APIs are in the published npm release.

Logs and result JSON are retained in the printed temporary directory on success and failure. `--report-dir` copies a compact evidence bundle. Traces and screenshots are retained when available, with best-effort failure screenshots after browser startup. Server startup and builds are bounded, and the harness stops its owned server process group and relay.

`--schema-peer` adds an independent application Zod3 parser and checks cold
initialization plus a forced optimizer restart without consumer optimizer includes.
`--adapter-only` (pnpm with schema-peer) installs only the documented Domscribe
adapter at the consumer root; local-artifact overrides supply its transitive
packages without hoisting runtime/overlay. `--raw-runtime` additionally preserves
an explicit runtime exclusion. `--no-overlay` checks initialization and direct
runtime capture without a WebSocket overlay; its result is explicitly scoped to
initialization, not full replay or production verification.
81 changes: 81 additions & 0 deletions docs/superpowers/plans/2026-10-26-stateful-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Stateful handoff implementation plan

> **For agentic workers:** Use superpowers:executing-plans; implement sequentially
> in the isolated worktree, with independent review before delivery.

**Goal:** Fix the three observed installation/context/replay gaps.
**Architecture:** Plugin-owned dependency initialization; optional source-aware
adapter capture; explicit dialog context guards in portable target identity.
**Tech stack:** Existing TypeScript/Nx/Vitest, Vite, Vue/React, browser consumers.
**Spec:** `docs/superpowers/specs/2026-10-26-stateful-handoff-design.md`.

## Global constraints

Preserve primary checkouts/lockfiles. Schema-first public types. No global Zod
alias or arbitrary state fingerprint. Bounded/redacted capture. Legacy locator
requests cannot bypass new guards. No publish/deploy/credential changes.

## Review focus

- Isolated pnpm dependency resolution and overlay=false must both initialize.
- Same-basename components and metadata missing in supported frameworks must
not masquerade as a source match.
- DOM replacement during async source lookup must refuse stale capture.
- Legacy/nested/redacted dialog locators must not act on the wrong record.
- Context keys stay explicit; regular row identity and production builds survive.

### Task 1 — Mixed-schema Vite initialization

Files: Vite plugins in transform/react/vue and focused browser consumer regression
under scripts/fixtures; affected plugin specs only where a contract needs it.

- [x] Reproduce normal app Zod3 + local-artifact Domscribe4 browser failure with
no optimizer override; assert schema works and runtime/relay connect.
- [x] Establish clean affected baseline through configured Nx tasks.
- [x] Fix browser-shared schema imports to explicitly select Zod4, per the
reproduced namespace collision; preserve consumer exclusions and dependency ownership while predeclaring
injected adapter dependencies; verify disabled-overlay behavior.
- [x] Verify npm/pnpm and Vite boundaries; production stripping. Commit slice.

### Task 2 — Source-owning component capture

Files: core runtime-context schema, runtime adapter/capture/bridge, Vue adapter,
relay/overlay capture consumers and their regression specs.

Interface: optional adapter `resolveSourceComponent(element, sourceFile)` returns
an instance and proven source file; capture options accept requested source file;
runtime context metadata labels matched/nearest/unavailable ownership.

- [x] Add failing tests for owner props through wrappers, exact path vs basename,
metadata unavailable, cycles and existing privacy/capture options.
- [x] Implement bounded Vue parent resolution and core/runtime metadata.
- [x] Resolve manifest before selected-instance capture through overlay/relay;
revalidate the instance after the async lookup. Add failing race coverage.
- [x] Real wrapped Vue/Hoppscotch owner capture; representative React compatibility,
fresh-session source context, redaction and production stripping. Commit slice.

### Task 3 — Dialog context guards

Files: core target schema/replay validation, runtime target registry/specs,
consumer fixtures and API documentation.

Interface: `data-ds-context` is an explicit non-secret locator attribute;
`unsupportedReason` also accepts `context_required`.

- [x] Failing wrong-record, legacy, retained-node, nested-dialog, empty/redacted
context and existing-row regression cases.
- [x] Enforce context coverage when capturing AND resolving portable locators;
preserve raw overflow checks and local inspection behavior.
- [x] Verify unmodified Hoppscotch refuses portable dialog replay; explicit-key
fixture replays only the intended record and preserves neighbors. Document
main-only API behavior and required context integration. Commit slice.

### Task 4 — Review and delivery

- [x] Run all affected configured checks and uncached broader unit suite, fresh
browser/npm/pnpm validation and original Hoppscotch flows without overrides.
- [x] Independent review; fix material findings with regression tests.
- [ ] Push focused PR(s), attach them, follow latest-head CI and merge with
standing admin-bypass authority only after required checks pass.
- [ ] Update continuity ledger with commit/commands/results/limitations and next
meaningful experiment; no commercial advantage claims from setup fixes.
Loading
Loading