npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@svilupp/flightplan

v0.4.0

Published

TOML-defined browser workflows with deterministic assertions, effect-aware retry policy, selector locks, and a layered resolver backed by browser-pilot.

Readme

flightplan

TOML-defined browser workflows with deterministic assertions, effect-aware retry policy, selector locks, and a layered resolver backed by browser-pilot.

Quick start

For a published consumer project:

npm install @svilupp/flightplan browser-pilot
# Or use Bun: bun add @svilupp/flightplan browser-pilot
# If `flightplan` is on PATH (for example, after a global install):
flightplan --help
flightplan --version
flightplan lint path/to/flow.toml
flightplan run path/to/flow.toml --frozen --no-lock-write --json
# In a project, the same commands can use `npx flightplan` or `bunx flightplan`.

Run Chrome with CDP on localhost:9222, or set [config.connect] mode = "launch" in the flow.

For repository development:

bun install
bun run check
bun run flightplan --help

Checks are quiet on success: each leg prints a short status block and a temporary log path. If a leg fails, its captured diagnostics are printed. Run bun run lint, bun run typecheck, or bun run test to execute one leg.

Run a deterministic fixture flow:

bun run fixtures
bun run flightplan run examples/flows/wizard.toml --frozen --no-lock-write --json

The fixture server listens on http://localhost:3000. The example flows use the default CDP attach at localhost:9222, so start Chrome/Chromium with remote debugging enabled first, or add a [config.connect] block with mode = "launch". Deterministic L0/L1 flows need no API key. AI resolver, vision, planner, and ai_judge paths need OPENROUTER_API_KEY by default. The L2 element-choice step can instead (or additionally) use the TypeSafe JEV classifier via TYPESAFE_API_KEY (keys available at https://console.typesafe.ai/keys) — see "AI tiers and planner" below.

The simplest way to pick a model is [config.ai.models.default]: it seeds every AI role (resolver, advisor, vision, planner, planner_capable) at once, so you don't repeat the same block per role:

[config.ai.models.default]
model = "gpt-5.6-luna:xhigh"
fallbacks = []

This is the recommended way to force a single model across the whole flow. Resolution per role is field-by-field: an explicit field in that role's own block wins, then the same field in default, then the built-in registry. Role blocks deep-merge over default — set model in default and override just pricing for one role, for example — but fallbacks arrays replace wholesale rather than merging. If default sets model but not fallbacks, roles still fall back to the built-in OpenRouter fallback slugs; set fallbacks = [] explicitly in default when using a non-OpenRouter provider to avoid an unexpected fallback to an OpenRouter model id. Older flightplan versions reject [config.ai.models.default] as an unknown key (strict schema), so it is not forward-compatible with older CLIs.

For advanced setups, keep per-role [config.ai.models.<role>] blocks — e.g. a cheap resolver with a stronger planner — or set [config.ai] provider = "google" | "openai" to route through @ai-sdk/google / @ai-sdk/openai instead (with GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY as the matching default key env, overridable via api_key_env); model ids in [ai.models.*] are then the provider's OWN ids, not OpenRouter slugs. Any model id may carry a :effort suffix (minimal|low|medium|high|xhigh), e.g. "openai/gpt-5.6-luna:xhigh", to set the reasoning effort for that model (Google has no native xhigh; it maps to high). Example — Gemini with high reasoning, per-role:

[config.ai]
provider = "google"          # uses GOOGLE_GENERATIVE_AI_API_KEY

[config.ai.models.resolver]
model = "gemini-3-pro:high"  # native Google model id + reasoning effort suffix

Install

Install the public packages with npm or Bun. The direct browser-pilot dependency exposes the bp discovery and WebMCP diagnostics CLI; Flightplan also declares it as its runtime driver. The CLI and library run on Node.js 18+ or Bun:

npm install @svilupp/flightplan browser-pilot
# or
bun add @svilupp/flightplan browser-pilot

Once the executable is on PATH, call it as flightplan .... In a project, npx flightplan ... and bunx flightplan ... are equivalent package runners. From this repository, use bun run flightplan ....

The library is published as ESM with TypeScript declarations; import it from a Node.js ESM project or use the flightplan executable for command-line workflows.

Portable / Workers entry

Three subpath exports let a host pick exactly the surface it needs, with . staying the full Node-flavored default:

  • @svilupp/flightplan/worker — a curated, Node-free barrel: runFlow, lintText, RunOptions/RunResult/RunInterruptedError, FileSystemPort, MockDriver, BrowserPilotDriver (host-injected acquisition), acquireDriverLease, memoryFileSystem. Bundles for Cloudflare Workers.
  • @svilupp/flightplan/adapters/node — the real node:fs-backed nodeFileSystem.
  • @svilupp/flightplan/adapters/memory — an in-memory FileSystemPort for tests.
import { runFlow, memoryFileSystem, MockDriver, resolveConfigWithDefaults } from "@svilupp/flightplan/worker";

const result = await runFlow({
  flowPath: "/virtual/flow.toml",
  flowSource: `version = 1
kind = "flow"
id = "demo"
description = "Worker smoke test"
[[steps]]
id = "open"
do = "goto"
url = "https://example.com/"
`,
  fs: memoryFileSystem(),
  env: {},
  config: resolveConfigWithDefaults([{}]),
  driverFactory: () => new MockDriver(),
});
console.log(result.summary.verdict);

Shell hosts

@svilupp/flightplan/shell exposes runFlightplan(args, context, ports) for shell hosts. It shares the native CLI's lint/run implementation and returns { stdout, stderr, exitCode }.

import { runFlightplan, type FileSystemPort } from "@svilupp/flightplan/shell";
import type { Driver } from "@svilupp/flightplan/worker";

function command(args: string[], fs: FileSystemPort, cwd: string, driver: Driver) {
  return runFlightplan(args, { fs, cwd }, {
    driverFactory: () => driver,
  });
}

Pass an argv array such as ["run", "flows/demo.toml", "--json"] and an absolute cwd in the supplied filesystem. The host registers its command and maps its storage to FileSystemPort. Just-bash is one tested consumer; it is a dev dependency only. Production code and public types have no dependency on it.

ports.env defaults to {}. AI requires an injected aiRuntimeFactory or explicit provider credentials in that environment. run requires driverFactory and otherwise returns exit 2. Hosts can disable lint or run with ports.capabilities and cap each output stream with ports.limits.maxOutputBytes (default 1 MiB).

Pass cancellation as context.signal and a run deadline as ports.timeoutMs. Flightplan returns 130 for cancellation and 124 for a deadline; the surrounding shell may translate these codes. explain, report, sweep, and migrate-effects require the native CLI.

Cloudflare Workers with just-bash

Use /worker for direct execution or /shell to expose a shell command. The runnable Worker example registers flightplan with just-bash/browser and adapts its VFS to FileSystemPort. The adapter belongs to the host.

From a new Worker project, copy worker.ts and wrangler.toml from that example, then run:

npm install @svilupp/flightplan@^0.2.0 just-bash
npm install -D wrangler
npx wrangler dev

In another terminal:

curl http://localhost:8787

The example handler uses a fresh shell and filesystem per request:

const bash = createShell(() => new MockDriver(), request.signal);
const { stdout, stderr, exitCode } = await bash.exec(
  "flightplan lint demo.toml --json > lint.json && " +
  "flightplan run demo.toml --json --frozen --no-lock-write -o /runs",
);
return Response.json({ mode: "mock", stdout, stderr, exitCode });

createShell is defined in the example. The default uses MockDriver for an offline smoke test; it does not open a browser. For live automation, supply your host's DriverFactory instead. It must return a fresh, unconnected Driver; Flightplan then calls connect() and teardown(). Keep browser credentials and session allocation in that factory or driver. Workers need a remote browser connection or driver bridge; local Chrome launch is unavailable.

The flow, lint.json, and /runs artifacts share the shell VFS. That VFS lasts for one request; copy artifacts to R2 or another durable store before returning if you need them later. request.signal reaches the command, and timeoutMs: 30_000 bounds each Flightplan run. Provider credentials are not taken from the shell environment; inject aiRuntimeFactory for host-managed AI.

The sample pins compatibility_date = "2026-06-01" with nodejs_compat. Cloudflare enables Node compatibility by default for dates from 2026-08-04; see its Node.js compatibility guide.

Embed a workflow

runFlow accepts host filesystem and driver implementations. The ordinary Node/Bun defaults remain available. Workers should use the /worker or /shell entry above and supply their filesystem and driver. The root entry also exports Node adapters and provider SDK helpers.

import {
  runFlow, resolveConfigWithDefaults,
  type Driver, type FileSystemPort,
} from "@svilupp/flightplan";

function execute(fs: FileSystemPort, driver: Driver, signal: AbortSignal) {
  return runFlow({
    flowPath: "/workspace/flows/health.toml",
    fs,
    driverFactory: () => driver,
    config: resolveConfigWithDefaults([{}]),
    env: {},
    out: "/workspace/runs",
    frozen: true,
    noLockWrite: true,
    signal,
    timeoutMs: 60_000,
    cleanupTimeoutMs: 1_000,
  });
}
  • fs supplies text reads, writes, appends, existence checks, and directory creation. Imports, hooks, locks, and run artifacts use it. flowSource can supply the root TOML text directly.
  • With fs, recording requires writeBinaryFile(path, bytes). Flightplan saves per-step PNG frames through that method and disables native browser-pilot recording output. The same frame path is used for deadline-controlled runs. Existing native recording stays unchanged otherwise.
  • Pass an explicit env and driverFactory in isolated hosts. Omitting them retains ambient environment lookup and native browser connection defaults. Pass a resolved config; filesystem injection does not change loadConfigFile or the file-based lint commands.
  • Adapters own path authorization, symlink checks, write limits, and atomic commit rules. Paths follow host filesystem syntax; relative hooks/imports are normalized against their parent flow. Filesystem errors must reject; fileExists returns false only for a missing entry.

Cancellation and deadlines reject with the exported RunInterruptedError (RUN_CANCELLED or RUN_TIMEOUT). The deadline covers loading, browser work, and artifact writes; interruption adds at most cleanupTimeoutMs of cleanup waiting. New driver, filesystem, and AI calls stop, and late results cannot resume the workflow. Driver factories receive the combined signal as their second argument's signal; AI factories receive it in their dependencies and generators in the request. Custom implementations should pass it to their underlying operations.

Inspect pendingWrites, pendingBrowserOperations, and cleanup on the error. Already-dispatched writes or effects may finish after the run rejects. Reconcile them before retrying; Flightplan does not delete destinations on cancellation. cleanup: "completed" means driver teardown returned, not that a remote provider confirmed release. "failed" and "pending" require host follow-up; a late connection still gets a best-effort teardown. Persist the interruption report outside the interrupted filesystem, even if a previously dispatched summary write later finishes. Provider usage may be unknown after cancellation. Session release and durable recovery belong to the host.

Cloudflare and host-owned sessions

[config.connect] mode = "hosted" supports cloudflare, cloudflare:chromium and explicit cloudflare:kitesurf, with environment credential names. mode = "session" borrows a host-owned browser through an injected acquirer and an explicit target policy. Borrowed teardown preserves its owner; owned cleanup may require provider reconciliation. See hosted/session contracts for TOML and embedding examples, host requirements, stress evidence and live-service limits.

For an external demo checkout, use the SauceDemo example. Local fixture and Node/workerd evidence does not establish hosted Kitesurf or real payment support.

Why the tiered resolver

Flightplan uses deterministic lock replay and DOM resolution first, then pays for AI only when a flow needs it. In the current 54-run comparison, the tiered path passed 27/27 runs in 104.2s for $0.0077; the AI-only baseline passed 24/27 in 200.5s for $0.0440. That is 1.93x faster and 5.72x cheaper, with zero drift and stable locks. Warm lock replay made 98.92% of resolutions deterministic. See docs/BENCHMARK.md for the methodology, limits, and full results.

The savings compound on repeat runs. After locks are learned, the warm benchmark reached L0 on 97.85% of resolving steps, and reported cost per pass fell from $0.004604 cold to $0.000533 warm: 8.6x lower, or roughly 10x cheaper. The exploration cost is paid while learning a workflow, not on every test rerun.

HOW TO: write a Flightplan flow

Start with a small flow that states its target, connection mode, inputs, budget, and safety policy:

version = 1
kind = "flow"
id = "orders.pay-seeded"
description = "Pay the seeded draft once"
goal = "Leave the seeded draft in the exact Paid state"

[config.connect]
mode = "launch"
headless = true

[config.plan]
enabled = false

[inputs]
base_url = "http://localhost:3000"

[run]
max_steps = 12
assertions = "eager"
assert_timeout_ms = 6000

[[steps]]
id = "open_draft"
do = "goto"
url = "${inputs.base_url}/shopify/draft_orders/D1236?cookie=hide"

[[steps.assert]]
type = "text"
selector = "[data-testid='draft-id']"
text = "D1236"
match = "exact"
purpose = "precondition"

[[steps]]
id = "pay_once"
do = "click"
effect = "at_most_once"
retry = { policy = "never" }
target = ["[data-testid='mark-as-paid']", "the Mark as paid action for draft D1236"]

[[steps.assert]]
type = "text"
selector = "[data-testid='payment-badge']"
text = "Paid"
match = "exact"
purpose = "postcondition"

1. Classify the effect

| effect | Use | |---|---| | observe | goto, waits, assertions, read-only review | | idempotent | Safe repeated setup or navigation | | at_most_once | Create, approve, pay, save, submit, confirm |

Mark action steps explicitly. The linter treats clicks, fills, selects, emit, and webmcp_call as mutation-capable, even when a click only filters a table or changes tabs. emit (WebSocket command injection, see docs/BROWSER_PILOT_INTEGRATION.md) is always at_most_once - the linter rejects any other value. Do not add on_fail = { goto = "self" } to a step that may have dispatched. Use retry = { policy = "never" } for dangerous steps and let an exact postcondition rescue an uncertain result.

WebMCP calls

Flightplan uses browser-pilot's WebMCP bridge through the webmcp_call step. It invokes one exact page-provided tool with structured input and can assert or capture a typed result:

WebMCP is experimental and page-scoped. The target must satisfy the browser's secure-context, origin-isolation, and Permissions Policy requirements. Chrome's origin trial starts at version 149; for local testing, enable chrome://flags/#enable-webmcp-testing and use browser-pilot's bp webmcp status to diagnose availability before running a flow.

[[steps]]
id = "lookup"
do = "webmcp_call"
tool = "orders.lookup"
input = { order_id = "${inputs.order_id}" }
effect = "observe" # requires the tool's readOnlyHint annotation

[[steps.assert]]
type = "result"
path = "order.status"
equals = "ready"

Use origin to disambiguate same-named tools and from_origins for explicit cross-origin discovery. Mutation-capable tools require effect = "idempotent" or "at_most_once"; invocation failures are treated as uncertain and are never automatically replayed. Raw inputs/results stay out of artifacts; mark a result capture secret = true when its value must be masked in summaries and step events. See the browser-pilot integration guide for the full contract.

1a. Fill verification (verify)

page.fill verifies by reading the field back and comparing it to what was typed. Some fields auto-format as you type (a phone field re-spacing +447881122333 into +44 7881 122333, a card field spacing out digits) — that legitimate reformat looks identical to a real fill failure. Flightplan's verify option maps straight onto browser-pilot's native fill verification, forwarded through the batch step:

[[steps]]
id = "enter_phone"
do = "fill"
target = "phone number"
value = "+447881122333"
verify = "normalized" # default — tolerates a whitespace/formatting-only mismatch

| verify | Behavior | |---|---| | "normalized" (default) | browser-pilot compares the typed value to the field's value after Unicode NFKC normalization and whitespace-collapse; if that still doesn't match, it falls back to a whitespace-stripped compare (covering an auto-spacing formatter, e.g. a phone or card field). A genuine mismatch still fails the step. | | "off" | Verification is skipped entirely. Use when a formatter inserts punctuation (dashes, parens) that a normalized compare won't tolerate. | | "exact" | The strict !== compare — any mismatch fails the step. |

verify never changes how the value is TYPED — browser-pilot's own char-by-char typing fallback (when a direct value-set doesn't stick) still applies regardless of verify; this option only controls whether a post-fill readback mismatch is treated as pass or fail. The "normalized" compare is case-sensitive by design (emails, passwords, and codes are case-significant) and never strips punctuation like -()./.

2. Resolve before acting

Targets are ordered locator lists. Put durable selectors first and one natural-language anchor last:

target = [
  "[data-row-id='D1235'] [data-testid='duplicate-order']",
  "the Duplicate order action for draft D1235",
]

The resolver must reject ambiguity before dispatch. Scope repeated controls to a row, panel, landmark, or seeded resource. Do not rely on the first More actions, Save, or matching status label.

Author-facing selector prefixes are ref:, role:, text:, css:, and leading [ CSS selectors. Keep the natural-language entry concise. Internal fingerprint: lock tokens are not author-facing selectors.

3. Treat readiness as a semantic condition

A changed URL or committed navigation is not application readiness. Add a wait or a semantic precondition for delayed hydration, empty first snapshots, loading overlays, and same-document navigation.

[[steps.assert]]
type = "visible"
selector = "[data-testid='page-title']"
text = "Orders"
purpose = "precondition"
timeout_ms = 6000

4. Make assertions exact and scoped

Available URL matches: exact, origin_path, glob, contains.

Available text matches: exact, contains, regex.

Use state for enabled, disabled, checked, unchecked, selected, dialog, menu, and new_page. Use transition for url_changed, text_changed, value_changed, and state_changed. Use capture when a reversible flow must restore an observed value.

Do not let Paid match Unpaid, a timeline message satisfy a status badge, or a broad URL match a different tenant. Use a selector or landmark for every business-state assertion.

5. Arm popups on the trigger action

Declare the popup on the step that opens it. Flightplan arms observation before dispatch and preserves the opener page:

[[steps]]
id = "sign_in_store"
do = "click"
effect = "at_most_once"
target = ["[data-store-id='store-001'] [data-testid='store-sign-in']", "the sign-in action in store-001"]

[steps.popup]
type = "page"
url = "/swap-admin/signed-in?popup=1&storeId=store-001"
title = "uat.swap-os.com · Store session"
timeout_ms = 5000

Filter by opener, URL, type, title, and creation time. Handle about:blank followed by delayed navigation. Never select a popup by tab index.

6. Compose flows with imports

imports registers a library; it does not execute it. A do = "run" step executes the imported flow. The entry flow owns [connect], run budgets, and safety policy. Expanded child steps count toward the parent budget; on_fail targets cannot cross a run boundary.

Run, lock, and dependency workflows

Run repository checks from a checkout:

bun run check
bun run lint
bun run typecheck
bun run test

Lint and preview effect policy before execution:

flightplan lint path/to/flow.toml
flightplan migrate-effects path/to/flow.toml

Use frozen, no-lock-write runs for proof and CI:

flightplan run path/to/flow.toml \
  --frozen --no-lock-write --json -o /tmp/flightplan-run

Locks contain learned target strategies and a source_hash. A stale lock must not replay against a changed flow. Refresh a lock only after reviewing the flow diff and the new target strategies.

Development-only: proving Shopify and Swap fixtures

The deterministic admin fixtures live in the sibling browser-pilot-testing checkout. This is a development-only local-fixture workflow, not a consumer setup. Start the server, reset between lanes, and run the TOMLs through Flightplan:

export BROWSER_PILOT_TESTING_ROOT=/path/to/browser-pilot-testing
export FLIGHTPLAN_ROOT=/path/to/flightplan

cd "$BROWSER_PILOT_TESTING_ROOT"
PORT=3000 bun run start

cd "$FLIGHTPLAN_ROOT"
bun run flightplan run \
  "$BROWSER_PILOT_TESTING_ROOT/automations/flightplan/shopify-duplicate.toml" \
  --frozen --no-lock-write --json -o /tmp/flightplan-proof

For seeded local mutations only:

ALLOW_MUTATIONS=1 bun run flightplan run \
  "$BROWSER_PILOT_TESTING_ROOT/automations/flightplan/swap-return-approve.toml" \
  --frozen --no-lock-write --json -o /tmp/flightplan-proof

Proof requires all of the following:

  • Shopify duplicate, pay, create, and fulfillment ledger entries each equal 1.
  • Swap review leaves the ledger at 0.
  • Swap approval, settings save, and settings restore each equal 1.
  • Popup artifacts identify the matched target and opener.
  • Every run contains valid summary.json, run.jsonl, and trace.jsonl.
  • Reversible settings flows end at the original persisted value.
  • An uncertain transport result is observed, never replayed.

ALLOW_MUTATIONS=1 is a local fixture gate. Live mutations also require an allowlisted tenant/store, a seeded disposable resource, a mutation budget, and cleanup or restoration.

Inspect failures

flightplan explain /tmp/flightplan-proof/<run-id>
flightplan report /tmp/flightplan-proof
flightplan sweep examples/flows --trials 3 --compare-baseline -o /tmp/flightplan-campaign

Read the result as a state machine:

| Result | Meaning | Next action | |---|---|---| | not_dispatched | No effect reached the page | Fix readiness, ambiguity, or actionability; retry only if policy allows | | dispatched | Input may have reached the page | Observe the postcondition; never dispatch again | | uncertain | Transport result cannot prove effect state | Poll state/ledger; never dispatch again | | Assertion timeout after a commit | Action happened; oracle is wrong or late | Fix assertion scope/timing, not mutation retry | | Stale lock | Flow and learned recipe differ | Review diff, refresh lock intentionally, or run frozen to fail closed |

Artifacts are the source of truth. Exit code alone cannot prove that an action committed once.

Troubleshooting

"You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer." Flightplan launches Chrome with --no-sandbox by default so it also works in containerized and root environments. In headed mode Chrome shows this as a yellow warning bar; it's expected and harmless outside those environments. If you don't need it, override the defaults with an explicit chromeFlags array in [config.connect] (an empty array falls back to the defaults, so list the flags you want, and add "--headless=new" yourself for headless runs):

[config.connect]
mode = "launch"
headless = false
chromeFlags = ["--disable-gpu", "--window-size=1280,720"]

A fill step fails with Fill value did not stick. Expected "+447881122333" but got "+44 7881 122333". The target field auto-formats as you type (a phone field re-spacing digits, a card field inserting spaces). The default verify = "normalized" (see Fill verification) already tolerates whitespace/NFKC formatting differences, so this usually only surfaces on older browser-pilot builds or with an explicit verify = "exact". If the formatter inserts punctuation instead (dashes, parens, etc. — normalized won't strip those), set verify = "off" on that step to skip verification entirely. Set verify = "exact" to restore strict verification once you've confirmed the field's real behavior.

AI tiers and planner

L0 lock replay and L1 deterministic DOM resolution are the default path. L2 resolver, L3 vision, L4 advisor, AI assertions, and the L5 path-repair planner need an AI runtime when invoked.

L2 classifier: JEV vs LLM vs heuristic

L2 ("which candidate element matches this intent") is answered by an ordered chain of choosers, selected by [config.ai] classifier:

[config.ai]
# "auto" (default): JEV -> LLM -> heuristic, by key availability.
# Explicit "jev" | "llm" | "heuristic": that chooser ONLY; a missing key fails the run at start.
classifier = "auto"
jev_api_key_env = "TYPESAFE_API_KEY"  # env var NAME, never a value (default shown); keys at https://console.typesafe.ai/keys

# The generative provider still powers L3 vision / L4 advisor / L5 planner / ai_judge, and (with
# classifier="auto") is the JEV-abstain fallback for L2 too. With classifier="jev" it never
# backs L2, only the deeper tiers.
provider = "openrouter"               # uses OPENROUTER_API_KEY

API key: JEV is TypeSafe's model, hence the default env var NAME TYPESAFE_API_KEY. When jev_api_key_env is left unset, JEV_API_KEY is ALSO accepted as an alias (both names are tried, TYPESAFE_API_KEY first) — useful if you'd rather name the env var after the classifier itself. Setting jev_api_key_env explicitly OVERRIDES the name and disables the alias: only that one name is then consulted. Either way, [config.ai] and every error/telemetry surface hold only the env var NAME, never the key value.

  • "auto" (default) is a PREFERENCE ORDERING, not a strict requirement: JEV first when TYPESAFE_API_KEY is set (a ~200-500ms non-generative classification call), then the LLM resolver when a generative provider key is available, then a deterministic zero-key heuristic as the terminal safety net. Any subset of keys still produces a working "auto" chain; zero keys still builds no AI runtime at all (today's rule — AI-less runs stay byte-identical).
  • An explicit value is STRICT: classifier = "jev" or "llm" selects exactly that one chooser, and a missing prerequisite key fails the run at start with a clear config error naming the missing env var (never a silent no-op, never a value in the error). "heuristic" needs no key at all.
  • A JEV-only setup (TYPESAFE_API_KEY set, no generative provider key) covers L2 element choice only — L3 vision, L4 advisor, the L5 planner, and ai_judge are all unavailable. An ai_judge assertion under a JEV-only (or fully AI-less) runtime fails that assertion with a clearly-marked "not available" message (pass: false) — it is a real, counted assertion failure, never a silent skip. It is still distinct from a judge FAILING CLOSED on a model-call error: no model call is ever attempted or charged, and the message is always the same not-available text, never a judge verdict.

L0 zero-call replay needs a UNIQUE healed selector. After an L2 (JEV or LLM) heal, the next run replays at L0 with zero model calls ONLY when the learned selector durably resolves to one element. A page with several visually/semantically identical candidates (e.g. three unlabelled "Save" buttons distinguished only by surrounding context) re-resolves at L2 on EVERY run — there is no selector that is durable AND unique for that target, so warm replay never engages.

Keep the planner off for deterministic safety proofs:

[config.plan]
enabled = false

For an AI experiment, enable it only with a bounded max_replans, a clear flow goal, and a separate run directory. tier_hint = "vision" is for icon-only or unlabeled controls after deterministic resolution has failed. Consecutive vision targets can batch into one request.

Cloudflare Access auth ([config.auth])

Flows behind Cloudflare Access can authenticate via [config.auth], layered like every other [config.*] block (built-in defaults → global flightplan.toml → entry flow → CLI), so an org can set the cf_access block once in a global flightplan.toml and have every flow inherit it. Secrets are always expressed as env var names, never values — the same convention as [config.ai] api_key_env / [config.telemetry.logfire] token_env:

# --- sugar for the common case: out-of-band service-token exchange, then a CF_Authorization cookie ---
[config.auth.cf_access]
# Target website credentials, usable with either a local or hosted browser.
# Hosted Cloudflare browsers separately require an account ID and API token.
url = "https://prodej.wikov.app"                # origin to mint against
client_id_env = "CF_ACCESS_CLIENT_ID"           # env var NAME, never a value
client_secret_env = "CF_ACCESS_CLIENT_SECRET"
mode = "cookie"                                 # "cookie" (default) | "headers"

# --- generic escape hatches, each usable standalone without cf_access ---
[config.auth.extra_headers.from_env]            # header name -> env var NAME
"CF-Access-Client-Id" = "CF_ACCESS_CLIENT_ID"
"CF-Access-Client-Secret" = "CF_ACCESS_CLIENT_SECRET"

[[config.auth.cookies]]                         # maps 1:1 onto browser-pilot's SetCookieOptions
name = "CF_Authorization"
value_from_env = "CF_ACCESS_JWT"
domain = "prodej.wikov.app"

mode = "cookie" (the default, Method B) mints a CF_Authorization JWT via an out-of-band service-token exchange and applies it as a cookie; mode = "headers" (Method A) instead sends the raw CF-Access-Client-Id/CF-Access-Client-Secret on every request — broader blast radius (every origin the tab visits gets the headers, not just the Access-protected one), so prefer cookie mode unless your policy specifically requires headers. [[config.auth.cookies]] entries take either a literal value or a value_from_env env var name (never both). Auth is applied once, right after connect() and before the setup hook / first goto; an unset *_env name or a rejected service token fails the run before any navigation happens. Requires a browser-pilot build that exports Page.setExtraHTTPHeaders and mintCfAccessJwt; an older browser-pilot leaves [config.auth] parsed but un-applied (the driver feature-detects the capability).

Saved auth state (cookie snapshots)

[config.auth] also accepts a browser-pilot cookie snapshot (format browser-pilot-cookie-auth v1 JSON), produced by bp env auth save <name> on the browser-pilot side:

[config.auth]
# Exactly one of (optional):
cookie_file = "./secrets/app.cookies.json"   # path, relative to this flow file's directory
# cookie_file_env = "APP_COOKIE_FILE"        # env var NAME holding the path (relative → resolved against CWD)
cookie_save = false                          # optional, default false

The snapshot is loaded and restored into the browser right after connect() and before the first navigation — before Cloudflare Access minting and literal [[config.auth.cookies]] (those still win on conflict). With cookie_save = false (the default), any load/restore failure (file missing, expired, empty, nothing restored, invalid format) is fatal: the run verdict is error and no steps execute. With cookie_save = true (warm-cache mode), a missing/expired/empty/ nothing-restored snapshot is only a warning — restore is skipped and the flow is expected to log in itself; after a successful run (the same gate as lock flush) cookies are recaptured and the file is overwritten before the flow's teardown hook runs. A save failure is a non-fatal warning; an invalid-format or I/O error is still fatal either way.

This requires a Node/Bun host (the browser-pilot node adapter); a MockDriver or worker host never touches the file. The snapshot holds live session cookies — treat it as a credential: gitignore it (e.g. *.cookies.json), it is never copied into .flightplan-runs/ artifacts (only counts/path appear in warnings), and "restored ≠ logged in" — assert on real page state, don't trust the snapshot blindly. If you attach to a page that already has a URL, cookies still land context-wide, but the "no unauthenticated request" guarantee only holds when the flow does its own first goto.

Notes:

  • cookie_file must be a regular file — symlinks and directories are reported as not_found (browser-pilot refuses to follow symlinks or write through them).
  • ~ is not expanded — use an absolute path, or cookie_file_env with an already-expanded value.
  • On save, the parent directory is created automatically (0700 dirs, 0600 file).
  • Browsers cap cookie lifetime (~400 days in Chrome), so snapshots expire by then regardless of the server's Expires/Max-Age.
  • A cookie that expires mid-run yields a non-fatal "save failed" warning, not a run failure.

Development reference

Run bun run test:package before release to build and test the npm tarball's public types, CLI, VFS artifacts, and cancellation. To check an unpublished browser-pilot candidate without installing it into this checkout, run bun run test:package /absolute/path/browser-pilot.tgz.

Skills

The release includes two Codex skills under docs/skills/:

The second skill makes the boundary explicit: use browser-pilot for reconnaissance or narrow driver diagnosis, then run the final automation through Flightplan.

Flightplan is under active development. Treat the flow, lock, and artifact checks as part of the workflow, not as optional polish.