Targeting Real Apps
Two recipes for pointing Jevitate at something less tidy than a static demo page: a dev server whose SPA calls its own API directly, and an app that carries real state between runs.
Dev server / SPA with a direct API origin
A Vite (or similar) dev server serves module requests from the same origin as the page — Vite's
own /@vite/*, /src/** and /node_modules/** — while the
app's own XHR/fetch calls hit an API, often under a path prefix like /api/, and the
page also pulls third-party requests (Stripe.js, web fonts, analytics). Without telling Jevitate
which is which, all of it lands in one undifferentiated timing bucket and can dominate
the "slowest requests" summary with module loads and third-party noise instead of your API.
Two flags fix this:
-
--api-prefix <path>(repeatable) — marks a path prefix as the app's own API for thetimingsummary. Any XHR/fetch under it is classifiedapiand shows up inslowestEndpoints; everything else that returns non-HTML data is also inferred asapiautomatically, and scripts/styles/fonts/images/media — including a dev server's own modules — are classifiedassetand ranked separately inslowestAssets. -
--settle-ignore <pattern>(repeatable,*wildcard) — marks a request as background so it never blocks "the page has settled." A pattern containing://matches the full URL (use this for a third-party origin likehttps://js.stripe.com/*); any other pattern matches the path and query on the target's own origin.
jevitate explore --url http://localhost:5173 \
--goal "check out with a saved card" --success urlIncludes:/order-confirmed \
--api-prefix /api/ \
--settle-ignore "https://js.stripe.com/*" --settle-ignore "https://fonts.gstatic.com/*" \
--real
You do not need --allow for the API calls themselves — --allow
authorizes origins Jevitate may navigate to, not origins a page's own fetch/XHR talks to.
A same-origin dev-server page calling a different-origin API (say, the SPA on
:5173 and the API on :8000) works without any extra flag; you'd only add
--allow if a link click or redirect actually navigates the browser to that other
origin. The exceptions: fixtures and invariant probes only call --allow origins, and a
read-only find-out goal treats writes to an origin outside --allow as third-party
unless they carry credentials, so add your API origin there when you rely on those.
Persist the same settings per-origin instead of repeating flags on every run, in ~/.jevitate/targets.json:
{ "http://localhost:5173": {
"settle": {
"ignoreRequests": ["/@vite/*", "/src/**", "https://js.stripe.com/*", "https://fonts.gstatic.com/*"],
"longPollMs": 5000
},
"timing": { "apiPrefixes": ["/api/"] }
} }
Since 0.4.0, a page that settles before any interactive control has rendered gets up to 3 s for
one to appear (never past the render ceiling) before it counts as blank. Lazy and code-split
routes no longer end a run blocked; a page with genuinely no controls is judged blank
up to 3 s later. A very short --stall-timeout can fire on such a step.
The full classification rules are in docs/exploration.md ("When is a page settled?" and "Page timing").
Running against a stateful app
Most real apps are not idempotent fixtures. Three things bite when a target carries real, persistent state across runs.
1. Bound the run — and remember what doesn't bound it
--max-actions caps executed mutations/navigations, and --max-decisions
caps model round-trips. They are not the same cap: a wait (or
scroll_up/scroll_down) decision spends a decision but never an action, so
a run stuck deciding to wait on a stateful page that never finishes an async operation can exhaust
--max-decisions long before --max-actions would ever stop it — and with a
generous decision budget, it can wait far longer than you'd expect from
--max-actions alone. A built-in stall detector (three quiet waits in a row that
change nothing) provides a floor, but for a run against a target with real latency or async work,
set --max-decisions deliberately rather than relying on --max-actions to
bound wall-clock time.
jevitate explore --url https://staging.acme.test/inbox \
--goal "reply to the newest thread and confirm it sent" \
--success 'requestMade:POST /api/threads/*/messages' \
--max-actions 25 --max-decisions 60 \
--storage-state ./auth-run-1.json \
--real 2. Run conversational/stateful journeys sequentially, never concurrently
A conversational or otherwise stateful journey (the goal loop, or any run that reads back its own
writes — an inbox, a sidebar list, an inquiry thread) mutates real state under the identity your
--storage-state carries. Two runs sharing one --storage-state or tenant
race on the same account: the app's own UI is not scoped per Jevitate run, so a second run can see
— and act on — state a first run just created. Run these one at a time:
# one at a time, never &-backgrounded or xargs -P > 1, against the SAME --storage-state
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
Sequential execution against a shared identity is the only safe pattern for these runs today.
--repeat, --persona and multi-actor runs already run their sessions one
after another.
3. Re-seed one-shot server state between runs
A pending approval, a one-time modal, a single-use invite link, an unread badge — anything the
server only shows or allows once — is consumed by the first run that encounters it. A second run
against the same account will not see it, and a goal written to expect it will read as
blocked or exhausted rather than a Jevitate defect. A goal run can reset
it itself: --fixtures <file> declares HTTP setup and
restore steps to an --allow origin, and --before /
--after shell hooks (with --allow-shell-hooks) run your own reseed script.
Both run before the mission and around every verify-fix and hang replay, so a
replay starts from the same state. For other strategies, run the same reset yourself between
runs. An environment in .jevitate/environments.json can carry
fixtures and hooks for Journey runs
(environments).
Some flows have two parties: an owner mints an invite, a recipient opens it. A fixture step's
auth can name an identity ("identity": "owner"), bound with
--fixture-identity <name>=<storageState> (or the origin's
targets.json persona), while the mission runs as its own session or cold. An unbound
identity is refused before any request; replays re-mint as the same identities.
--url accepts a root-relative ${setup.*} path right after the origin.
# the fixture mints the invite as the owner; the mission opens it cold (no --storage-state)
jevitate explore --url 'http://localhost:3000${setup.link}' \
--goal "accept the invite" --success 'textIncludes:css=h1|Welcome' \
--fixtures invite.fixtures.json --fixture-identity owner=owner.json --real 4. Tie backend-log lines to the request that caused them
With --log-source, a log line carrying a request's trace or correlation id
(traceparent, x-request-id, …) is attached to that exact request and
the step that sent it, not matched by time, and a blocked reason names it:
caused by: … on POST /x (409). --log-correlation-header adds a header,
--log-id-pattern says how ids are written in your lines, and
--log-scope attributes only matching lines to the run when several runs share one
log.
jevitate explore --url https://staging.acme.test/orders --strategy adversarial --fake-ai \
--log-source file:./api.log --log-correlation-header x-trace --log-scope tenant-42 Signal triage: only the lines that relate to a defect
A whole log in an issue is noise. --log-triage (every strategy; it needs at least one
--log-source, else exit 64) records the run's signal timeline and attaches to each
defect only the lines that relate to it.
jevitate explore --url https://staging.acme.test/orders --strategy adversarial --real \
--log-source file:./api.log --log-triage
jevitate logs triage --result .jevitate/logs/2026-10-02/run.result.json --real --threshold 0.6 - The timeline.
<run>.signals.jsonl: backend lines at every level, the browser console (every type), page errors and failed requests. Redacted, and bounded to 20,000 backend lines, 5,000 browser signals and 2,000 characters each;signals.truncatedsays when a cap was hit. - Code first. Lines correlated to the defect's request by id are kept (
keptBy: "request-id"). Code prefilters the defect step's window and the step before it (deduped, by severity, at most 150 candidates). - Jev second. With
--real, Jev answers yes or no for each candidate against the defect, its step's action and its correlated lines, 25 per call and at most 200 calls per run. A score of 0.5 or more is kept (keptBy: "jev"). With--fake-ai, the window's error and warning lines are kept (keptBy: "window"). - Where it goes.
defects[].relatedLogs, a## Related logssection in each issue draft, the result'ssignalssummary, andrelatedLogson each defect inreport, MCPget_reportandreport.md.
Why Jev: a Jev call costs a small fraction of what a generative model spends reading the same lines, so Jev filters and the expensive model reads only what Jev kept. Jev only selects evidence. It never adds or drops a defect, and a defect with no related lines is still reported.
jevitate logs triage --result <run>.result.json [--real|--fake-ai] [--threshold <p>] [--secret …]
re-triages a finished run from its saved timeline. For queued and MCP runs, set
"logTriage": true on the target in ~/.jevitate/targets.json; a
check suite item or target takes logTriage too. Sending log text to the
judgment model is the operator's decision, so --log-triage and logs triage
are not MCP arguments or tools. MCP callers get relatedLogs in every result.
Codes, dialogs and location
A one-time code emailed or texted during the run does not exist when it starts. Bind the field to
a command with a cmd: secret source and pass --allow-secret-cmd. The
command runs when the field is about to be typed, and its trimmed stdout is typed. The model sees
only «secret:CMD_…», the value becomes a run secret when read, and the command has
60 s. It is operator-only: never an MCP argument or a suite option.
jevitate explore --url https://app.example.test/verify --goal "verify your email" \
--secret-field 'label=Verification code=cmd:./read-code.sh --to ada@example.test' \
--allow-secret-cmd --success 'visible:testId=verified' --real --geolocation <lat>,<lng>[,<accuracy m>]places the browser at a fixed position for "near me" pages. The permission is granted to the allowed origins only.--dialogs acceptconfirms awindow.confirmorpromptan action raises. The default isdismiss; a dialog naming a destructive or paid action is still dismissed.--reply-quiet-ms <ms>sets how long a chat reply must hold still before it counts as complete (default 1000). Raise it for an assistant that answers in several parts.- A job behind an in-progress status is waited out within
--job-wait-ms. If the same status is still shown after that budget and no request is in flight, the run endsno-progress.
Browser extensions
--extension <dir> (repeatable) loads an unpacked extension on every command
that drives a browser; MCP tools take an extension array. Only the loaded extensions'
chrome-extension://<id> origins are allowed. Side panels, popups and options
pages open as normal tabs, and the agent acts on their DOM.
# the extension's side panel, opened as a page
jevitate explore --extension ./my-extension \
--url chrome-extension://<id>/sidepanel.html \
--goal "give consent and start a task" --success "textIncludes:css=#status|consent given" --real
# a finding replays only with the same extension build
jevitate verify-fix --result <run>.result.json --fingerprint <fp> --extension ./my-extension - The directory must hold a valid
manifest.json; anything else is refused (exit 64) before a browser opens. Add akeyto the manifest to keep the id stable across checkouts. - Headless runs use Playwright's full Chromium in new-headless mode (
npx playwright install chromium). Branded Chrome 137+ no longer loads unpacked extensions; use the bundled Chromium. - Recordings record each extension's
{id, name, version};verify-fixrefuses a different build (exit 64).
Shared machines: resource governance
Runs share the machine with builds, test suites and other agents, so jevitate governs itself:
- A machine-wide browser cap,
--max-browsers(defaultJEVITATE_MAX_BROWSERS, else cores/4 within 2..6), shared by every jevitate on the machine. - A memory ceiling per run,
--max-browser-memory <MiB>(defaultJEVITATE_MAX_BROWSER_MEMORY_MB, else 4 GiB or half the RAM). Over it, the run endsinconclusivewithfailure.kind: "resource-limit", never a crash or a finding about the app. - Throttling: under load or memory pressure a new run takes half the cap and settle windows double; on a starved host a new run refuses with
E_HOST_STARVEDunless--ignore-host-load. - Orphan cleanup: browsers left by a killed jevitate are found by an owner marker and closed before the next browser run. Only processes jevitate launched are ever signalled.
jevitate explore ... --max-browsers 3 --max-browser-memory 2048
jevitate doctor # host load, browser slots and holders, jevitate browsers and orphans
jevitate doctor --cleanup # close orphans left by a killed jevitate; clear stale slots
Every result records it in hostHealth.resources.
JEVITATE_RESOURCE_GOVERNANCE=off turns off the automatic parts; an explicit
--max-browsers or --max-browser-memory still applies.
See also Flags Reference for the full bounds/settle flag set, and Usability Review for the same sequential-run guidance as it applies to a live usability review.