Journeys
A Journey is Jevitate's deterministic, replayable browser flow: a Recording of typed steps plus parameters and metadata. Discovery can be nondeterministic, but a Journey replays the same way every time, and a replay never clicks a guess.
The artifact
- A name, parameters and a param-schema derived from its variables
- Typed steps with assertions that decide whether a step really succeeded
- Optional intent: a goal, preconditions, success criteria, and each step's objective and expected result
- Optional anchors: named steps worth exploring from (
metadata.anchors) - Provenance and promotion state
- Secret references, bound to the origin they were recorded on, never the value
Journeys live in the repo's .jevitate/journeys/ (outside a repo,
~/.jevitate/journeys). They keep app-relative paths, so one Journey runs on every
environment you name.
1. Author one from a goal
jevitate explore-author-journey --url http://localhost:3000 \
--goal "add a product to the cart and reach checkout" --success urlIncludes:/checkout \
--id checkout --name "Checkout" --real
jevitate journey promote checkout # a human approval gate: unpromoted Journeys never run
jevitate journey run checkout
Jev drives toward the goal; the authored Journey carries the --success check as its
final assertion, so a replay fails when the flow stops reaching its goal. It is written
unpromoted: promotion is always a deliberate human act. --takes <n>
corroborates the flow over several takes. Over MCP this is author_journey.
explore-author-journey takes every success check explore takes except
reloadThen, and --success is repeatable. A network check
(requestMade, responseStatus) is stored as
metadata.networkChecks, and journey run and source run
evaluate it over the replay's own requests.
Every take is the same goal run as explore --strategy goal, in a fresh browser, and
takes its run-shaping flags: --secret, --secret-field (with
--allow-secret-cmd and --secret-cmd-attempts), --totp,
--type-fixture, --fixture, --success-when,
--allow-vacuous-checks, --action-deltas, --dialogs,
--deny, --paid, --allow-destructive, --read-rpc,
the reply and job waits, --hang-replays, the settle options,
--save-storage-state, --viewport / --device /
--geolocation, --screenshots and --out.
When the goal is not reached, the result's reason is the discovery run's own
(discovery mission <outcome>: <reason>). It carries a discovery
block (stop reason, each check's verdict, decision and action counts, final URL, and the result,
transcript, Recording and screenshot paths) and takes counts, and the human summary
prints them.
A field typed from a --secret-field or --totp binding becomes a secret
Journey param (secret1, ...). Supply it with
journey run --param secret1=.... reloadThen is refused before a browser
opens, with a message naming the inner check to author instead: a Journey has no reload step.
Outcome assertions in a Journey
0.7.0 authoring keeps the evidence that the outcome happened, not only that the clicks did:
- End state (
metadata.endState): every--successcheck, of any kind (textIncludes,valueEquals,count,attr,flashed,requestMade,responseStatusand nowreloadThen), in the order given.journey runandsource runjudge it after the last step with the goal run's own evaluator: page checks on the final page, one reload forreloadThen, then network checks over the requests the replay sent. A replay whose steps all pass but whose outcome is absent fails and names each check that did not hold. - A step's request expectation (
expectRequests): a write the step sent during discovery becomesresponseStatus:<METHOD> <path>=2xx, judged over the requests that step sent, as the step ends (0.14.0). A later step's request to the same endpoint never counts for an earlier step. - A step's
expectnow comes from what the step changed: the navigation it caused, the value a fill typed, or, with--action-deltas, the text or element a changed page added. Authoring never writesvisibleof the step's own target: it holds before and after the click, so it proves nothing. -
expectedResult: with--action-deltas, a step with a recorded delta gets itsexpectedResultfilled by code from that delta, so the reviewer sees what each step is supposed to prove. An existingexpectedResultis never overwritten.
{
"metadata": {
"id": "publish",
"endState": [
{ "kind": "responseStatus", "method": "POST", "pathGlob": "/portal.v1.Sites/PublishSiteEdits", "status": { "class": 2 } },
{ "kind": "reloadThen", "assertion": { "kind": "textIncludes", "target": { "testId": "live" }, "text": "published" } }
]
},
"recording": { "pages": [{ "url": "/editor", "steps": [
{ "step": { "kind": "click", "target": { "testId": "publish" },
"expect": { "kind": "visible", "target": { "text": "Your site is live", "textMatch": "contains" } } },
"expectRequests": [
{ "kind": "responseStatus", "method": "POST", "pathGlob": "/portal.v1.Sites/PublishSiteEdits", "status": { "class": 2 } }
] }
] }] }
}
Both fields are optional: an older Journey (a trailing assert step, network checks in
metadata.networkChecks) loads and runs unchanged. load run judges each
iteration the same way, so an iteration whose end-state or expectRequests checks fail
is quarantined, not ok.
Parameters in a navigate URL
A Journey can use a single-use link such as an invitation accept, a magic-link sign-in or a
password reset. Declare the parameter, mark it secret if it is one, and put the
placeholder in the URL:
"url": "/accept?token=${inviteToken}"
"parameters": [{ "name": "inviteToken", "secret": true }]
jevitate journey run accept-invite --param inviteToken=...
A placeholder must name a declared parameter and must come after a literal origin. Its value is
percent-encoded as one URL component, the resolved URL must keep the template's origin, and a secret
value is redacted from output, errors, screenshots and evidence. Steps show it as
<param inviteToken>. Over MCP, pass it in run_journey's
params.
2. Or record it by demonstration
jevitate record --url http://localhost:3000 --intent "check out with a saved card"
jevitate recording diff takeA.json takeB.json # which values vary between takes
jevitate recording propose takeA.json takeB.json --id reply # an unpromoted binding draft (0.16.0)
jevitate recording postdoc take.json --decisions decisions.json --out recording.json
jevitate recording promote take.json --page 0 --step 2 --var email # one value into a variable
jevitate recording fit take.json # a timing policy from your pacing record opens a headed browser and records what you do; Enter or Ctrl+C ends the take
and saves it. A recorded take is a Recording, one step short of a Journey: there is no single
command yet that turns a post-processed take into a promoted Journey. Authoring from a goal is the
direct path today.
3. Promote, then run
jevitate journey list
jevitate journey run checkout --param sku=A1 --storage-state auth.json
jevitate journey run checkout --env staging --record-video --screenshots
jevitate journey run checkout --self-heal hybrid --changes origin/main...HEAD --real # propose a repair the change explains (exit 5 until accepted) - Promotion runs
journey lintfirst and refuses a Journey whose assertions cannot prove its outcome (below). - Only a promoted Journey runs, and only promoted ones are discoverable over MCP (
find_capabilities,run_journey). - Replay is deterministic: the same descriptors, the same steps, no model in the loop.
- A Journey that declares
metadata.requiresAuth: truerefuses to start without a session, before any browser opens. --env/--base-urlpick where it runs;--record-video,--screenshotsand--headedshow it (Demos & Environments).journey runnotes how many steps have noobjective. Informational; it never fails a run.--action-deltas(opt-in) compares each replayed step's page change with the Recording's, as evidence only;journey annotate --action-deltasdrafts expected results from those deltas.- A Journey recorded with a browser extension (
--extension) runs only with the same extension build loaded.
Review and promotion sign-off
jevitate journey review <id> (MCP review_journey) produces one sheet for the
reviewer, as text, Markdown or JSON: the goal and success criteria, each step, the writes it is
expected to fire, its inputs (parameters by name only, never a value), its proof (lint and the last
verify --mutate verdict) and what changed since the last approval. The sheet shows a content
hash. journey promote shows the sheet first and accepts --reviewed-hash /
--review-sheet, refusing with E_JOURNEY_REVIEW_STALE if the Journey changed after
the review. Promotion needs an interactive terminal and a typed confirmation, and records
metadata.approval with its provenance. A Journey can link a job and a persona; see
Catalog & Sign-off.
Since 0.10.0 the sheet also shows anchor-rule warnings with their fixes (anchors as job-step boundaries),
the brittle steps with the fix for each (locator health), and the pull request
on an approval that was verified from a review (pr-review).
When the only change since the approval is minted step ids, the change section says
ids added only (warn path): re-approving it only warns about the anchor rules instead of
enforcing them. In --json these are anchorWarnings and locatorHealth.
The review sheet since 0.12.0
The sheet (journey review, and the one journey promote shows first) is built for a person signing off.
It reads top to bottom as:
- NEEDS YOUR ATTENTION: what to decide first: weak steps, missing intent, lint, risky controls, findings, change since approval, no mutation proof, brittle locators.
- WHAT IT DOES: the recording's intent, sign-in, origins and uploaded files.
- STEPS: each step with its checks in plain English, its writes and its anchors. A step recorded with a visible label shows the exact locator in
‹›beside it. - PROVES AT THE END, then WRITES (one line per endpoint), a one-line CLEAR summary and APPROVE with the promote command.
Empty sections are left out. Lines wrap to the terminal width, and colour appears only on a terminal (NO_COLOR turns it off).
--full on journey review and journey promote brings back every detail section. --full is CLI only:
over MCP, review_journey returns the JSON sheet. The sheet still carries one Content hash: line, so
--review-sheet binding is unchanged. The typed journey promote confirmation repeats the attention items right above it, and a
self-heal proposal shows each step's exact locator, so a retarget is visible even when the label is unchanged.
The JSON only gains fields (also over MCP): attention, per-step checks, file, anchors and locator, summary.intent and proof.endStateText.
An illustrative sheet, built from the layout above (the wording and the journey are made up):
NEEDS YOUR ATTENTION
- No goal or success criteria written -> jevitate journey annotate import-contacts
- 1 step has a check that proves nothing
WHAT IT DOES
Imports a contacts file. Uploads fixtures/contacts.csv
STEPS
1. click "Import" <data-testid=import>
checks: the URL contains "/contacts"
PROVES AT THE END
...
WRITES
POST /api/contacts/import
CLEAR lint
APPROVE jevitate journey promote import-contacts --reviewed-hash <hash> Step display labels (0.12.0)
Recordings (the recorder and Jev-authored ones) store each step target's visible label and role as display
({ role?, name }). It is display metadata only and never used to find an element. A credential field's label, a label that holds a
secret, and the typed text of an editable field (a textarea, a contenteditable, a textbox) are never stored. Step descriptions read
click "Import"; a step with only a test id reads click [action import] (no recorded label).
To backfill labels on Journeys recorded before 0.12.0:
jevitate journey migrate --labels [--id <id>...] [--dry-run]
It replays each Journey, so the app must be running; --dry-run writes nothing. A promoted Journey it changes
needs re-approval (journey review --stale lists them); the migration never approves.
migrate --labels is CLI only: there is no MCP tool for it.
Stable step ids (0.10.0)
Every recorded step carries a stable stepId (s- and six characters, for example
s-7f3k2a; any 1 to 64 of [a-z0-9._:-]). It is assigned when a Journey is saved, never when
it is read. The id is kept across edits, splices, heals and proposals, and a revision that renames an id
is refused. Anchors, locator-health reports and review sheets refer to a step by its id as well as its
position, and journey promote stamps each anchor with the id of the step it follows.
jevitate journey migrate --step-ids --dry-run # list what would change; write nothing
jevitate journey migrate --step-ids # mint ids on every step that has none
jevitate journey review --stale # list the Journeys whose approval is now stale jevitate journey migrate --step-ids [--dry-run] adds a stable id to every step of every local
Journey and regression capture; a second run changes nothing. Adding ids changes each Journey's content
hash, so its approval becomes stale, and so does any later save of an older Journey.
jevitate journey review --stale (MCP review_journey with stale: true) lists
the affected Journeys, labelled ids added only (warn path), and prints the command to re-approve
each one. It never approves. Re-approving an ids-only change uses the warning path; the new anchor rules
are not enforced on it. Do the migration in one reviewed pull request.
Locator health (0.10.0)
jevitate locator-health (MCP locator_health) reports how each recorded step finds its
target and whether that survives a change to the app. It is read-only and advisory: it never changes a
Journey and exits 0 on a report.
jevitate locator-health # every promoted Journey
jevitate locator-health --journey checkout # one Journey
jevitate locator-health --run <result.json> # the rungs one run actually matched
jevitate locator-health --baseline last-week.json --json # with the trend against an earlier --json output
jevitate check --suite ci.json --max-brittle-steps 0 # opt in to gating on it - Per step: the rung that resolved it (
testId,anchor,role+name,role,label,textorcss), how stable that rung is, why it is brittle, and a concrete fix for the app, such asadd data-testid="save-contact" to the "Save" button on /contacts/new. A step is stable only when it resolves by a test id in the project's convention. - Fixes are de-duplicated across steps and runs, and
--baselinereports the trend. The list is a work list for the codebase, not a change to the Journey. - In
checkthe results appear as JUnit and SARIF warnings.check --max-brittle-steps <n>makes them a gate: a Journey item with more thannbrittle steps is a gating finding (exit 1). See Regression & CI. - The convention. The attributes that count as a test id come from
testIdAttributesin the repo's.jevitate/project.json, in preference order; the default isdata-testidanddata-test. There is no flag for it. Commitproject.jsonwith the app; it holds only project settings, never secrets. data-tflow-idis tracking-only. It is recorded astflowIdand exported with findings and the bundle, but never used to locate an element. Listing it intestIdAttributesis refused (E_PROJECT_CONFIG), and a target found only by it is brittle.
{ "testIdAttributes": ["data-testid", "data-test", "data-cy"] } Does it prove anything? Lint and mutation proof
A green replay only proves something when its assertions can fail for the wrong reason.
journey lint reports the assertions that cannot, and journey promote runs
the same lint first: a Journey with any error-level finding is refused unless the reviewer accepts
it with --accept-weak "<reason>", recorded as
metadata.acceptedWeak with the waived rules. Warnings never block promotion; approving
a demo (demo approve) is the reviewer's approval, so it records the waiver instead of
refusing. journey lint exits 1 when any error is found, so CI can gate on it.
jevitate journey lint checkout # one line per finding, then a summary
jevitate journey lint checkout --json # { id, findings, errors, warnings }
jevitate journey lint checkout --sarif lint.sarif
jevitate journey promote checkout --accept-weak "reviewed: the write is covered by a separate Journey" | Rule | Level | Flags |
|---|---|---|
own-target-visible | error | A step's expect only restates that its own target is visible: true before and after the step. |
write-without-effect | error | A state-changing step has no assertion on its effect. |
visibility-only | error | Every assertion is a bare visible, so the Journey cannot fail for the wrong reason. |
nothing-after-last-write | error | No effect assertion follows the last state-changing step. |
no-persistence-check | warning | A write has no reloadThen or http end-state check. |
http-check-off-allowlist | error | An http end-state check reads an origin that is neither the Journey's site nor in its metadata.allowedOrigins (0.15.0). |
http-check-without-reset | warning | The Journey has an http end-state check but records no fixtures, so nothing resets its source (0.15.0). |
intent-uncovered | warning | A step's documented expectedResult maps to no effect assertion. |
The lint reads a Journey; journey verify --mutate runs it. An assertion is evidence only
if it fails when the outcome is absent, so the proof replays the Journey once as recorded (it must
pass), then once per mutation, and checks that each mutation breaks the assertion paired with it:
jevitate journey verify checkout --mutate # one line per assertion, then a summary
jevitate journey verify checkout --mutate --json # the full report
jevitate journey verify checkout --mutate --env staging --param sku=A1 --fixtures reset.json skip:<n>: leaves step n's action out (each write step).block-write:<n>: aborts the write requests step n sends (each write step).stale-value:<n>: types an empty value into fill step n (each fill whose value an assertion checks).
Each assertion gets one verdict: sensitive (a paired mutation broke it),
insensitive (it still passed: the assertion is vacuous), cascade (the replay
failed somewhere else first; never counted as proof), not-applied, error or
unpaired. When a mutated replay first fails at the mutated step's own postcondition (a
save step that expects its redirect or status line), that step's assertion is sensitive, and the
proof replays the mutation once more with that postcondition left out, so the step's
expectRequests and the end-state checks are judged on that relaxed replay instead of as
cascades (0.15.1; since 0.16.0 that relaxed replay keeps a weak visible postcondition on the
step's own target, so it stays schema-valid and later checks no longer come out as errors). Exit 0: every paired assertion is sensitive. Exit 1:
any is insensitive. Exit 2: nothing could be proven. Exit 64: usage
(journey verify without --mutate, an unknown id, bad params). The
--json report carries the Journey's content hash (journeyHash), each
mutation's outcome, failedSites, blockedWrites and relaxed (the relaxed replay, when one ran), each assertion's verdict
and provedBy, and summary counts. Pin a pairing the derivation would not make with
metadata.mutationPairs; a pair naming a step or anchor the Journey does not have is
refused when it loads:
"mutationPairs": [{ "check": "end-state:0", "mustFailWhen": "skip:publish" }] Safe by construction: a mutation only leaves a step's action out, types an empty
value, or aborts the app's own write requests. It never sends, fabricates or answers a request.
Server-side state caveat: every replay starts fresh with the same fixtures and hooks
as journey run, but the replays share the app's server-side state. When the app keeps
state between sessions, reset it between replays with --fixtures or
--before, or the replay after a blocked save can still find the base replay's saved
data. Over MCP: lint_journey and verify_journey.
4. Give it intent
jevitate journey annotate checkout --env staging --real # draft objectives on playback
jevitate journey annotate checkout --approve # review the diff, then write it journey annotate replays the Journey, reads the redacted page before and after each
step, and drafts the missing goal, success criteria, objectives and expected results into
.jevitate/journeys/.drafts/. Nothing is written into the Journey until you approve,
and a draft made against a Journey that has since changed is refused. Intent is what
journey demo narrates. Details:
Demos & Environments.
5. Explore from inside it: anchors and campaigns
Real defects sit deep in a flow: a verification step mid-booking, a half-filled form. A journey-anchored mission gets there first. It replays a promoted Journey up to a step in the mission's own browser context (page, form contents and session kept), then hands the live page to the mission.
jevitate journey anchors checkout # named states, the step each follows, suggested probes
jevitate explore --from-journey checkout --at-step payment --param email=a@example.test \
--env staging --strategy adversarial --max-actions 60 --real
# every step (or --at-step anchors: every anchor), each in a fresh session restored by --fixtures
jevitate explore --from-journey checkout --at-step all --strategy adversarial \
--fixtures restore.json --max-actions 120 --real --at-steptakes an anchor name or a 1-based step number;all/anchorssweep every step or anchor, with--max-actionssplit evenly per stop and one deduped report.- Strategies:
goal,coverage,exploratory,adversarial,usability.--feature,--url,--repeat,--personaand--actorare refused with it (exit 64). - The prefix replays as
journey rundoes: fail-closed, never self-healed,--env/--base-urland the site policy applied. Only the prefix's own params are required. - A prefix that no longer replays ends the run
inconclusivewithfailure.kind: "journey-stale"(exit 2). It never restarts from a URL instead. Since 0.7.0 the failure carries the page's URL, title and a short redacted visible-text excerpt, so "the chat said it is unavailable" reads differently from "the click came too early". Failed steps are counted 1-based here and injourney runandcheck. --fixtures,--beforeand--afterwork on a single--at-step <anchor|n>mission too, with any strategy (hooks need--allow-shell-hooks). It runs as a one-stop campaign, so the result has the sweep's shape (sweep.mode: "step",sweep.atStep).- Every result and finding records
branch: {journeyId, step, anchor}, andreportlists each defect's branch points. A reset inside the mission re-replays the prefix (counted against--max-actions). verify-fixandregression capture|runreplay a branch-point finding through its prefix; a secret param is given again with--param.
Declare anchors in the Journey's metadata (validated: unique names, real steps):
"anchors": [
{ "name": "payment", "step": 6, "description": "card form, order total shown",
"probes": ["double submit", "swap the cart id"] }
] Naming and authoring anchors (0.11.0)
An anchor name follows catalog bundle v1: 1-64 characters of lowercase a-z, 0-9,
., _ and -, starting with a letter or digit. : is no longer allowed.
journey lint and journey promote report a name outside the rule, and existing Journeys still load.
You no longer need to edit metadata.anchors by hand:
jevitate journey anchor add <journeyId> --name <n> --step <n|stepId> [--description <text>] [--probe <text>]...
jevitate journey anchor remove <journeyId> --name <n>
jevitate journey lint <id> --fix
jevitate journey anchors <id> --suggest anchor addtakes a 1-based step number or a step id and writes the step's stable id at once. Duplicates and unknown steps are refused (exit64), and the remaining anchor lint issues are printed.anchor removeremoves one by name.- On a promoted Journey (0.13.0) the edit works too, but the approval goes stale because it binds to the content hash. See below. (Before 0.13.0 a promoted Journey was refused.)
journey lint --fixstamps the step id of each anchor that references a step by number only, writes the Journey and reports what it fixed. It works on a promoted Journey since 0.13.0 (the approval goes stale).journey anchors <id> --suggestproposes anchors where the recorded flow changes state (a new page, a form that became valid, a completed write), each with default probes and theanchor addcommand to accept it. Advisory: it writes nothing. Since 0.13.0 it also works on older recordings: writes are found from the recorded request timings and end-state checks, a new page goes on the click that navigated, and a job-linked Journey with nothing else to suggest gets its last step proposed as the job's end.journey anchorsalso lists each anchor'sstepId.- Over MCP:
journey_anchor_add(journeyId,name,step,description,probe),journey_anchor_remove(journeyId,name),lint_journeywithfix, andjourney_anchorswithsuggest.
Editing anchors on a promoted Journey (0.13.0)
journey anchor add, journey anchor remove and journey lint --fix work on a promoted Journey, from the CLI and over MCP.
The Journey's content hash covers its anchors, so the edit makes its approval stale:
- The Journey stays promoted, and its old approval is kept for the review diff.
journey review --stalelists it, andcheck --require-approvalsfails it until a person re-approves.- The result carries
needsReapproval: trueandreapproveCommand(jevitate journey promote <id> --reviewed-hash <new review hash>). Over MCP,journey_anchor_addandjourney_anchor_removereturn both fields; only a person runs the command.
Anchor region (0.14.0)
journey anchor add --region <descriptor> (MCP journey_anchor_add.region) stores metadata.anchors[].region, the page region missions at that anchor attack, in the --success descriptor syntax (testId=import-view).
A run from the anchor measures only the controls inside it and --scope-region cannot override it. Changing it makes a promoted Journey's approval stale, like any anchor edit. See scope regions.
Promotion-ready (0.14.0)
An agent hands a Journey to its owner only when the review sheet is clean:
jevitate journey review checkout --strict # exit 1 on any attention item
jevitate journey review checkout --strict --json # adds strict: { passed, warnings }
jevitate journey link checkout --job <jobId> --persona <personaId>
jevitate catalog draft job <id> --name "..." # a DRAFT entry, never approved journey review --strict(MCPreview_journeywithstrict) exits1on any attention item and runs the job and persona link checks without--readiness. Advisory items and Jev never fail it.--stale --strictis refused.journey link <id> --job|--persona(MCPjourney_link) sets the catalog links;--unlink-joband--unlink-personaclear them. An unknown id is refused (E_JOURNEY_LINK_ARGS, exit64, listing the catalog's ids). A draft entry can be linked and is flagged as still needing approval. On a promoted Journey the approval goes stale and the re-approve command is printed.journey lintandexplore-author-journeywarnmissing-job/missing-personawith the fix command. The exit code is unchanged.journey verify --mutaterecords its proof over executable content only (steps, targets, assertions, end state). Rewording a goal, success criterion, objective or expected result (whatjourney annotate --approvewrites) no longer stales the proof; an approval still goes stale.- The owner's order is persona approve, job approve,
journey annotate --approve, thenjourney promote --reviewed-hash. See thejevitate-promotion-readyskill and the catalog.
Split-origin apps, http checks and recorded fixtures (0.15.0)
Allowed origins (#542). explore-author-journey records the origins it was given as
metadata.allowedOrigins. journey run and journey verify take
--allow <origin> (repeatable). A replay's allowlist is the recorded site (or the --env origins),
plus metadata.allowedOrigins, plus --allow, so verify --mutate can block a write to another
origin and a split-origin app's Journey reaches a conclusive proof. The recorded origins are not remapped under
--env / --base-url: name the replacement origin with --allow or the environment's allow.
MCP run_journey and verify_journey take no allow argument; they use the recorded origins.
jevitate journey verify checkout --mutate --allow http://127.0.0.1:26110 # block a write to another origin http: end-state check (#543). A write whose effect is off the UI, such as a password-reset email in a dev outbox
(Mailpit, MailHog), is proved by a polled GET to an allowed origin:
jevitate explore-author-journey --url http://localhost:3000/forgot --goal "send me a password reset link" \
--allow http://localhost:3000 --allow http://localhost:8025 --fixtures reset-outbox.json \
--success 'http:GET http://localhost:8025/api/v1/search?query=to:${setup.email}|jsonPath=$.messages[*].Subject|includes=Reset your password' \
--id reset --name "Password reset" See the grammar and safety rules. It satisfies no-persistence-check, and journey run and verify --mutate judge it. A transport error is unevaluable, never mutation proof.
Recorded fixtures (#544). explore-author-journey --fixtures <file> (with --before, --after,
--fixture-identity) sets the fixtures up before, and restores them after, every take, and records metadata.fixtures
({ file, specHash }). journey run, journey verify --mutate and journey annotate then reuse it with no flag.
- Precedence:
--fixtures, then the Journey's recorded file, then the environment's.--fixture-identityalways comes from flags or personas, and hooks are never recorded. - Reach: a fixture step may reach the replay's allowlist (the site,
metadata.allowedOrigins,--allow), so a fixture can empty a dev outbox on another origin. - Missing file: refused with
E_FIXTURE_SPECbefore any browser opens. - Drift: a changed spec runs with a warning, and the JSON result carries
fixtureDrift{ file, recordedSpecHash, currentSpecHash, message }. - Author flags:
explore-author-journeyalso takesexplore's backend-log flags (--log-source,--log-defectand the rest) and--allow-writes. MCPauthor_journeytakesfixturesandfixtureIdentity, mapsallowWritesand omits the log flags, likerun_exploration.
Parameter-relative assertions, restore outputs and recording propose (0.16.0)
Parameter-relative assertions (#562, #565). An assertion's expected text can follow a run's input.
textIncludes.text, valueEquals.value and attr.value take
{ "var": "name" }, and any assertion string may carry ${name}
(urlIncludes too, percent-encoded the way a navigate URL is):
{ "kind": "textIncludes", "target": { "testId": "welcome" }, "text": "Welcome, ${name}" }
{ "kind": "valueEquals", "target": { "label": "Email" }, "value": { "var": "email" } }
{ "kind": "urlIncludes", "text": "/threads/${thread}" } - Where a name comes from: a declared param, the output of an
extractstep earlier in the Journey (in the end state, any extract output) or a fixturesetup.xoutput. Step expects and page end-state checks resolve it at run time, ashttp:checks already did. - Fails closed, at load and at run: an unbound name is
E_ASSERTION_REF_UNBOUND, a secret param (or an extract output named like a credential, such asapiKeyortoken) isE_ASSERTION_REF_SECRET, a non-string value isE_ASSERTION_REF_TYPE. A persisted result keeps the template, never the resolved value. - Behaviour change: a literal
${...}in existing assertion text,urlIncludesincluded, is now a reference. Declare that name or reword the text. - An authored Journey gives a non-secret
{var}fill an exactvalueEqualspostcondition, and lint andjourney reviewcount a check on a declared non-secret name as strong. - Proof:
verify --mutateaddsswap-param:<name>(another plausible value) andtruncate-param:<name>(the first half kept) for each non-secret param that a fill ornavigateURL reads and an assertion names, paired with every assertion that names it. A{var}fill'sstale-valuetypes an altered value. Secret params are never mutated, and reports name params only.
Fixture restore outputs (#560). A restore step may bind outputs, which later restore steps use as
${restore.<name>} (find the item a Journey created, then delete it). They are cleared before every restore pass, so each
replay of verify --mutate, a regression or a check suite looks the resource up again. They never join the setup bindings, are never
persisted and cannot choose a URL origin; a lookup that matches nothing sends no request. See per-run unique suite params.
{ "restore": [
{ "name": "find-item", "method": "GET", "url": "/api/items?title=Journey%20item",
"expectStatus": [200], "outputs": { "itemId": "$.items[0].id" } },
{ "name": "delete-item", "method": "DELETE", "url": "/api/items/${restore.itemId}" }
] } Named origins in environments (#561). An environment may declare "origins". Under --env, each named
origin of the Journey's source environment (the one whose baseUrl is the recorded site, else one named default) moves to the
target's same-named origin in end-state http: checks, recording URLs and fixture step URLs, and the target's named origins join the
allowed origins. --base-url alone maps nothing.
{
"local": { "baseUrl": "http://127.0.0.1:3000", "origins": { "api": "http://127.0.0.1:6310" } },
"preview": { "baseUrl": "http://127.0.0.1:3001", "origins": { "api": "http://127.0.0.1:26310" } }
}
jevitate journey run checkout --env preview # recorded on :3000, so :6310 becomes :26310 Take files and recording propose (#564, #566). jevitate record writes a take file
{ recording, values } that recording diff, postdoc, promote and fit accept directly
(promote and fit still accept a bare Recording). values holds the non-secret text and select values the take captured,
stays local, and never has an entry for a secret, credential or one-time-code field. Secret-field detection is wider: a field is secret when its
type or autocomplete says so (now including cc-number and cc-csc) or when its name, id, label, aria-label or placeholder names a credential
(password, token, API key, secret, PIN, OTP, CVV, card number, SSN, security code; whole words, so pinned and spinner do not match).
Such a field is a handback, gets no take value and contributes no value to an exploration snapshot. A password a "show password" toggle switches to
type=text stays a handback for the rest of the take.
jevitate recording propose <take...> takes two or more takes of the same flow (varying what a user would vary) and proposes, with no model and
no browser: params (values that vary, and varying navigate path segments), constants (a credential is never one),
handbacks, outputs (an identifier shown before the first write that matches exactly one param, as an extract step),
checks (a pre-write identity check and a post-write valueEquals on each filled param), stopBefore and
ambiguities that it leaves for a person (identical-param-columns, untied-observation, multiply-tied-observation,
identity-after-write, unclassified-value, varying-target, varying-url, no-submit-step).
Evidence is redacted to length, shape and word count; typed text and credentials never appear.
- The draft is unpromoted. By default it is saved as a Journey (
--id, defaultproposed-draft) that ends beforestopBefore;--out <file>writes it elsewhere and--no-draftproposes only. It never replaces an existing Journey (E_PROPOSAL_DRAFT_EXISTS), and nothing is approved or promoted. - Usage errors (exit 64): fewer than two takes (
E_PROPOSAL_TOO_FEW_TAKES), an unreadable take (E_INVALID_TAKE,E_PROPOSAL_INVALID_TAKE), takes that did not record the same steps (E_PROPOSAL_MISALIGNED). - Next: decide every ambiguity, then
journey review <id> --strictandjourney verify <id> --mutate --param ...; only a person promotes. - MCP: the
recordingstool'sproposeaction takes confined take paths and returns the same redacted proposal.recordings diffreturns each value as a length/shape hint andpostdocredacts constants, so typed text never reaches the model; the CLI shows values to the person running it.
Linked Journeys, readiness and targeted proof (0.17.0 to 0.20.0)
A workflow of two Journeys, where one finds records and the other prepares something for a person, is runnable and provable end to end. Nothing here approves, promotes or sends: every human step stays a person's.
- Chain (0.17.0,
--paramin 0.19.0).jevitate journey chain --from <discover> --to <reply> --requests approved.jsonl(MCPchain_journeys) runs the discover Journey once and the reply Journey once per approved request, matched to a discovered record bycommentIdandaccount. Unmatched requests never run, an empty discovery stops the chain, and the result names ids and outcomes only.--param name=value(repeatable; MCPparams) supplies the discover Journey's params, so one Journey serves any account or thread. They are validated against its param schema first: a missing or unknown param isE_CHAIN_PARAMSand no browser opens. Promoted Journeys only, unless--allow-draft(CLI). - Indexed
forEachoutputs (0.17.0). AforEachchildextractalso binds<as>.<i>.<field>and, after the loop,<as>.count, so a run returns every row. - Typed handoffs (0.18.0). A producer declares
metadata.outputs(a contract name, a key and typed fields:id,text,path,url,timestamp); a consumer declaresmetadata.inputs(what it accepts, the request a caller supplies, and bindings to its params).journey chainchecks contracts, records and requests before any reply runs: incompatible contracts areE_HANDOFF_INCOMPATIBLE, and an invalid, ambiguous or malformed one is listed underrejectedand never run. Each run is stamped with the contract, a record fingerprint and the capture time. A well-typed but stale record is not live verification, so the reply Journey's own checks must still refuse it. - Prepare-only boundary and handback (0.18.0).
metadata.boundary(kind: "prepare-only",stopBefore,nextStep) refuses at load a click on the stop target, anEnterpress, any click after the last fill or select and ahandbackstep; self-heal is refused (E_BOUNDARY_NO_HEAL); first-party writes are observed, or blocked with"blockWrites": true.journey runreportshandback.state:prepared(exit 0),verification-failed,incompleteorsubmitted(exit 1),inconclusive(exit 2). A run that sent a write never exits 0, and code, not a model, decides the state. - Readiness receipt (0.18.0).
jevitate journey readiness <id> [--with <toId>] [--mode report|replay|prove] [--env] [--requests] [--param](MCPjourney_readiness) gives one receipt for a Journey or a linked pair: lint, schema, handoff, boundary, strict review, annotations, catalog links and approvals, proof freshness, replay and promotion, eachpass,fail,inconclusive,stale,not-runorawaiting-human.reportopens no browser;replayandproverefuse a missing target, a production environment and missing or unknown params (E_READINESS_PARAMS) before any browser. A passing replay never hides an inconclusive proof. Exit 0 technically ready, 1 blocked, 2 inconclusive or incomplete. In 0.19.0 an unvetted (draft or stale) job or persona link is only awaiting a person;journey promotestill refuses it without approval or--accept-unvetted. - Review bundle (0.18.0).
jevitate journey bundle <id> [--with <toId>] --out review/(MCPjourney_bundle) writes a localbundle.mdandmanifest.jsonfor sign-off, ending with the ordered human-only commands;journey bundle --check review/fails closed withE_BUNDLE_STALEwhen a Journey or catalog entry changed. It holds no values, page text or secrets and cannot approve anything. - Where things are (0.18.0). Every Journey and catalog command takes
--project <path>(a.jevitate/directory or the directory containing it; sets the catalog and the default Journey store) and--journeys-dir <path>(the Journey store only).jevitate where [--project] [--journeys-dir] [--json]prints what resolves where and why, paths only. Refusals exit 64 with a next action; there is no silent fallback to~/.jevitatefor the catalog.--diris deprecated but keeps its meaning and prints one notice on stderr. MCP tools never take these flags.
jevitate journey chain --from discover-conversations --to prepare-reply \
--requests approved.jsonl --param account=mcpl --env staging --json
jevitate journey readiness discover-conversations --with prepare-reply --mode replay --env staging --requests approved.jsonl --param account=mcpl
jevitate journey bundle discover-conversations --with prepare-reply --out review/
jevitate where --project ../app Proof for these workflows. verify --mutate uses a reviewed mutationValues list on a param when there is one
(0.17.0; up to 20 values, refused on a secret param), makes swap-param on a select pick another enabled option, and adds
guard-param:<name>, which gives the whole run a wrong input to prove a check that compares the page with an input before any action.
Targeted proof (0.19.0). When a later assertion is a cascade only because an earlier intentional defense caught the same mutation first,
verify relaxes those earlier step postconditions one at a time in a copy (at most 4; the target is never relaxed) and judges the target on its own. A
targeted failure counts only after an unmutated control replay with the same relaxations passes; if relaxing breaks the target by itself, it stays
a cascade and the proof inconclusive. To let an unpaired earlier check be relaxed, declare it a defense with metadata.mutationPairs. The
report carries mutations[].targeted, relaxedSites, targetedRefusal and a targeted summary count, and the
line reads (proved by swap-param:account, relaxed step:2).
Declared targeted proof (0.20.0). When the automatic bound of 4 can't reach a target behind layered pre-fill defenses, declare the
plan in metadata.targetedProofs: [{ "check", "mustFailWhen", "relax" }], with relax listing at most 8
earlier step:<n> page postconditions. A declared plan runs exactly one targeted replay and one unmutated control replay, which must
pass. Every relaxed site must be paired with the mutation and itself proven sensitive to it (transitively); otherwise the target stays a cascade with
a named refusal (not in relax, not paired, not itself proven sensitive). A relax entry before the mutated step is
refused at load. When the automatic bound stopped a proof, the report carries suggestedTargetedProofs, and
journey readiness --mode prove prints that entry as the fix. The plan is part of the proof and review hashes, so declaring one needs
re-approval; journey review lists it under "Proves at the end". The same soundness rule now applies to automatic targeted proofs, so a
Journey proven in 0.19.0 can become inconclusive when a relaxed site's own proof did not hold. Production replay is unchanged.
Request checks by identity (0.20.0). A requestMade check counts only requests whose start its capture saw, so a late
response from an earlier attempt (a heal probe or a retried step) no longer satisfies a restarted step, even within the same clock tick.
Campaigns
jevitate campaign run <spec.json> runs many anchored missions and reads them as
one report. Each job's Journey is replayed once (discovery; a stale one skips its missions), then
each anchored mission runs in spec order in a fresh browser context, with the spec's
fixtures restoring state around every run. The defects are consolidated as
report does, into campaign.json and campaign.md.
{
"version": 1,
"name": "release-candidate",
"env": "staging",
"fixtures": "restore.json",
"maxRuns": 40,
"maxActions": 60,
"jobs": [
{ "id": "checkout", "journey": "checkout", "params": { "email": "a@example.test" },
"anchors": ["payment", 4], "strategies": ["adversarial", "exploratory"] },
{ "id": "invite", "journey": "invite-teammate", "strategies": ["adversarial"], "maxActions": 40 }
]
}
Bounded: at most 50 jobs and maxRuns missions (default 50, at most 200). An invalid
spec is refused with every problem listed (E_CAMPAIGN_SPEC, exit 64) before a browser
opens. Over MCP: journey_anchors, run_exploration with
fromJourney / atStep, and run_campaign. In a
check suite, a mission item takes fromJourney, atStep,
params, env and baseUrl. The
jevitate-test-campaign skill plans a whole-release campaign around these.
Campaign preflight (0.11.0)
Before any mission, campaign run checks its own preconditions and, if any fail, refuses with exit
64 and E_CAMPAIGN_PREFLIGHT, listing every problem: a start URL that is unreachable, a job's
storageState that is no longer signed in, and a discovery replay that wrote to an origin the environment's
allow list misspells or leaves out (localhost vs 127.0.0.1), naming the exact origin to add.
The verdict is preflight: { ok, problems } in campaign.json and the human summary.
--skip-preflight (MCP run_campaign skipPreflight, recorded as preflight.skipped) runs without it.
A journey run result lists offAllowlistWrites (origins only) when the replay wrote outside its allow list.
Slow steps and cold starts (0.11.0)
journey run waits for an in-flight request instead of quarantining on it. A responseStatus check whose
request was sent but has no response yet (for example a cold start after a deploy) waits up to
--step-response-timeout <ms> (default 15000, at most 300000; a step's own
responseTimeoutMs overrides it). A 2xx that arrives in time passes and is reported as an advisory
slowSteps finding with its measured latency; a non-2xx, or no response within the bound, still fails.
jevitate check and campaign run also report a step that is slow on a Journey's first replay and fast on later ones as a
cold-start note (slowness.coldStarts). It is advisory and never changes an exit code.
Over MCP, run_journey takes stepResponseTimeoutMs (1 to 300000).
Since 0.14.0, each step's request window closes when the step ends. Before the next step runs, the step's checks wait, within the same bound, for the network to settle and for the step's own requests to be answered. Two Saves to the same endpoint on one page, or a Save followed by a navigation, each get their own response.
Upload fixtures (0.11.0)
A relative file path in a Journey's upload step resolves against the project root (the folder holding .jevitate/),
not the working directory, so journey run works from any directory or CI working directory.
journey lint reports a missing fixture as upload-file-missing (an error) with the resolved path.
Upload fixtures stay inside the project (0.12.0, security)
After following symlinks, a Journey's or mission's upload file must be inside the folder that holds .jevitate/. That folder is
never the working directory, the git root, your home folder or /; with none, an upload is refused
(fixture-no-root).
- Refused outside the project: an absolute path outside it, a
../escape, or a symlink pointing out (fixture-outside-project). - Refused inside the project: a dot-file or dot-folder (
.env,.git,.jevitate), keys and certificates (*.pem,*.key,*.p12,*.pfx,id_rsa*), HAR files and traces, and storage-state files (fixture-sensitive). - Where: everywhere a Journey or mission runs.
journey lintreportsupload-outside-projectandupload-sensitive-file, andjourney promoterefuses both;--accept-weakdoes not waive it. - No override. Move the fixture into the project.
Mission options. campaign run forwards these to every mission:
--allow-destructive, --allow-writes, --deny,
--paid, --invariants, the backend-log flags (--log-source,
--log-defect, --log-scope, --log-ignore,
--log-quiet-ok, --log-correlation-header, --log-id-pattern,
--allow-log-cmd, --server-log-drain-ms), --evidence-video,
--record-video, --screenshots and --log-triage. They are
operator flags, never spec fields. --hook-timeout-ms now reaches a campaign's and an
--at-step all|anchors sweep's hooks; it used to be dropped. MCP
run_campaign takes allowDestructive, allowWrites,
deny, paid, invariants, recordVideo,
screenshots and evidenceVideo; log sources, hooks and triage stay
operator-only.
Self-healing
In-flight self-healing is gated by SelfHealPolicy, chosen per run with --self-heal:
-
fail-closed(default): on divergence the run stops and quarantines rather than guessing. -
hybrid/full: Jev re-learns only the broken step and splices it back, under policy. Both need a gateway (--realor--fake-ai); asking for a heal mode without one fails closed.
In every mode, write, irreversible and real-send steps never auto-heal, and
assertions, success checks and invariants are never rewritten. journey annotate,
journey demo and demo never self-heal.
Change-aware, budgeted heal (0.9.0)
- Change input.
--self-heal hybrid|fullwithout--changes <git-range>or--change-noteis refused with exit 64. The range is read-only git; only resolved commit SHAs reachgit diff, and raw diffs never reach a model. - Only what the change explains. A broken step is healed only when the change explains it, and only by retargeting that step from the observed controls. A click or fill is healed only when its recorded requests are reads and it matches no risky control; mutating requests are blocked during the probe. A break the change does not explain is reported as a likely regression (exit 1).
- Budgets.
--heal-max-attempts,--heal-max-model-callsand--heal-max-msper broken step;--heal-max-run-attemptsand--heal-max-run-msper run. A heal that runs out endsheal-exhausted(exit 1) with a full attempt log. - A heal is never a pass. It ends
healed-pending-review(exit5) and writes a proposed revision to.proposals/<journeyId>.json; the stored Journey is unchanged. Commit the proposal with the PR. - Review and accept.
jevitate journey review <id>shows the proposal.jevitate journey promote <id> --proposal <pid>accepts it through every promote gate, and--reject-proposal <pid> --reasonrejects it. Both are available on the CLI and over MCP, and acceptance is a human approval. - In CI.
jevitate check --suite … --self-heal hybrid --changes origin/main...HEAD --fake-ai|--realre-runs a Journey that quarantined once with the change context. A pending revision makes the check exit 5 (JUnit failure typehealed-pending-review, a SARIF warning). See heal outcomes.
The run context
A single immutable RunPolicy is threaded through every step. It is a required
parameter, and the fallback is never permissive:
RunPolicy (required — no permissive default)
├── SelfHealPolicy fail-closed | hybrid | full
├── DirectionPolicy deterministic ↔ jev-directed ↔ goal-based
├── SecretPolicy vault-autofill | visible-handback | fail-closed
├── PacingPolicy human-speed profile
└── Budget max-steps · per-run/day · concurrency Share
git submodule add <gitUrl> .jevitate/journeys/commerce # shared Journeys: commerce/<id>
jevitate source add acme https://github.com/acme/jevitate-journeys.git --accept-tou
jevitate source trust acme checkout # trust one Journey, content-hash-bound
jevitate journey publish checkout --to acme # share a promoted Journey
A git repository of Journeys mounted under .jevitate/journeys/<shared>/ (usually a
submodule) shows up as <shared>/<id> everywhere a local Journey does,
including MCP. Distributed sources share Journeys from third parties behind an explicit trust and
Terms-of-Use gate; see MCP & CLI.
Load-test
jevitate load run checkout --env staging --authorized-origin https://staging.example.com \
--concurrency 5 --iterations 10 --seed 1 load run replays a promoted Journey with a seeded pool of actors and refuses to start
without --authorized-origin. It stays headless.
Site policies
jevitate site policy set https://app.example.com --file policy.json
jevitate site policy get https://app.example.com
A site policy keeps Journey runs on a site polite and bounded, whoever starts them (you, CI or an
agent): human-like pacing, per-class throttles, hourly and daily budgets, and quiet hours.
journey run, source run, check and MCP
run_journey apply it; a refused run fails with E_SITE_THROTTLED and says
when to retry. load run applies only the pacing.