Skip to content

CLI flags reference

Generated from packages/lang/src/spec-data.ts (this table used to live in README.md — it moved here as part of the docs-site cluster). For the full prose description of each subcommand, see SPEC.md §12.

Every subcommand the shipped tflw binary dispatches has a section on this page, and verify-docs.mjs fails the build if one is added without one.

Terminal window
npx tflw run --env staging --parallel 4 --seed 42 --now 2026-01-01T00:00:00.000Z --no-color
tflw run [--env <name>] [--tag <name>[,<name>...]] [--only <name>]
[--kind <kind>[,<kind>...]] [--parallel <n>] [--workers <n>]
[--skip-workload] [--seed <n>] [--now <iso>] [--no-color] [--verbose]
[--forbid-insecure] [--evidence <level>] [--teardown <level>]
[--allow-public-target <origin>] [--failed] [--shard i/n] [--bail]
[--no-keep] [--format ndjson] [--no-timestamps] [--log-file <path>]
[--browser <engine>] [--headed] [--trace] [--update-snapshots]
[--no-helpers] [--log-output <dest>] [--log-level <level>]
[--fail-on <severity>] [--baseline <file>] [--baseline-write <file>]
[--probe-seeded <n>]
--env <name>
selects a named env block from tflw.config instead of the default one — e.g. run the same suite against staging
--tag <name>[,<name>...]
only runs tests carrying any of the listed @names (comma-separated OR; combines with --only as AND); a ! excludes — --tag !slow runs everything not tagged @slow, exclusions AND together and beat an inclusion
--only <name>
runs a single test by its exact declared name (composes with --tag's OR-list as AND)
--kind <kind>[,<kind>...]
only runs the tests and crawls of the listed kinds — api, browser, load, scan — as their own statements classify them (comma-separated OR; combines with --tag/--only/--failed as AND). A test is of every kind whose statements it carries, so one with a request and a page is both api and browser
--parallel <n>
runs up to n files concurrently in this process (default: tflw.config's workers key) — distinct from --workers below, which scales one workload-bearing test's own load generation across processes, not files
--workers <n>
forks n generator processes to produce one file's workload-bearing test(s)' load — each an equal striped share of the target population/rate, merged back into one report; a no-op warning on a file with no workload-bearing tests; default 1, no forking
--skip-workload
skips every workload-bearing test (any test containing a ramp/hold/step/spike/run … iterations line), regardless of which parallel/sequential batch it's in — for fast iteration on the functional tests alone
--seed <n>
fixes every random-family value for the run, so a failure is reproducible byte-for-byte
--now <iso>
pins the run's notion of "now" to an exact instant (combine with --seed to reproduce a run's exact absolute generated values)
--no-color
disables ANSI color in CLI output — useful for CI logs or piping to a file
--verbose
additionally prints one line per step (pass or fail); buffered per-file under --parallel > 1 so concurrent files never interleave (--workers is the unrelated load-generation axis and has no effect here)
--forbid-insecure
CI policy gate — fails before any test runs if insecure true is active for the env actually running
--evidence <level>
overrides tflw.config's evidence key (full/headers-only/none) for this run only
--teardown <level>
overrides tflw.config's teardown key (always/on-success/never) for this run only — hyphenated here, two bare words in the config
--allow-public-target <origin>
affirms that an originating scan (authorization violations) may point at a host outside the private address ranges — TF065. Repeatable, one origin each (scheme + host + port), and it must match an authorized target this env declares. Has no tflw.config key by design: the declaration layer lives in config, and this is the layer that makes it impossible for a committed config to send CI at the internet on its own. A security violations scan needs no flag — it only inspects a response the suite already asked for
--failed
replays the tests that are failing — each test's newest verdict in the kept runs (report/runs/); falls back to the full suite with a note if none is
--shard i/n
runs every nth file of the sorted discovered suite, starting at the ith — the n shards are disjoint and together the whole suite, so a CI matrix of n jobs runs everything once; --tag/--only/--failed then narrow within the shard, and every shard still validates the whole suite first
--bail
stops after the first failing test's final (post-retry) verdict; under --parallel > 1, in-flight files still finish (the file pool stops pulling new work, it does not abort a running file)
--no-keep
does not copy this run into report/runs/<id>/ — every other run is kept there, bounded by runs keep N (default 50), for the history the summary and the page read; report/ itself is written as always
--format ndjson
streams the event log as one JSON object per line to stdout (plus report/events.ndjson) instead of human text; always full detail regardless of --verbose
--no-timestamps
omits the HH:MM:SS.mmm prefix every console line otherwise gets by default
--log-file <path>
duplicates console output to a file, always plain text (ANSI stripped) regardless of stdout's own color state
--browser <engine>
switches every browser step to one engine — chromium/firefox/webkit (default chromium)
--headed
shows the browser window instead of running headless (local debugging only)
--trace
keeps the Playwright trace of every browser test, even when everything passed (needs evidence full); open it with npx playwright show-trace
--update-snapshots
writes/overwrites matches snapshot baselines instead of just comparing against them
--no-helpers
refuses every use before the first request, whatever tflw.config's helpers allows — for a run that must not execute a JS/TS module (TF083)
--log-output <dest>
overrides tflw.config's log destination key (console/html/both/none) for this run's bare log "…" calls only — a log … to … statement's own destination always wins
--log-level <level>
overrides tflw.config's log level key (debug/info/warn/error) — the minimum level a log step must clear to be rendered in console output/report.html (never affects whether it's recorded in results.json/ndjson)
--fail-on <severity>
security findings below the given severity (minor/moderate/serious/critical) are reported and do not fail the build. The gate can only relax, never tighten: a test that wrote its own floor (has no serious security violations) keeps it, and where the two disagree the stricter wins — a command-line flag that could turn a green suite red would produce a failure nobody can locate from the source. Never applies to the negated not has no … form, where a finding is what makes the assertion succeed. A withheld finding still renders, badged, and the passing line names the count
--baseline <file>
accepted findings, matched on the 16-hex fingerprint alone — the rule/endpoint beside each entry are for the human reading the file, since matching on a name would let a renamed rule silently un-accept every entry that mentioned it. Listed findings still render, badged known/accepted, and do not fail the build; delete a line to un-accept it. A malformed file is refused rather than degraded to "accepted nothing", which would look exactly like a codebase that fixed everything
--baseline-write <file>
writes this run's findings out as the accepted set, sorted and deduplicated — it ships with --baseline rather than after it because fingerprints are hashes and hand-transcribing forty of them is not an adoption path. Entries the run did not reproduce are reported and never removed: a --tag run legitimately produces a subset of the suite's findings, so pruning on absence would delete acceptances the next full run still needs
--probe-seeded <n>
adds n generated mutation payloads per already-granted class to the input-handling scan's fixed corpus. It cannot widen what authorized target permitted — seeding is a capability of the run and a mutation class is a claim in the config. Its findings are reported and never gate: a generated payload has no stable fingerprint, appearing under one seed and vanishing under the next, so gating on it would churn a baseline or fail a build on a coin flip. Each renders with its payload and seed under promote this payload into the corpus. Capped at 64 per class, because probes are strictly sequential and this is wall-clock one assertion pays

tflw run drives functional and workload-bearing tests alike in one pass — a test becomes workload-bearing the moment it contains a ramp/hold/step/spike/run … iterations line (see the load testing guide); there's no separate load command (folded into run). parallel/sequential (a test-header modifier, not a flag) controls which tests in a file run concurrently with each other; --workers N is the unrelated, workload-only axis above — it scales one workload-bearing test's own generated load across N forked processes, never files. Every run with at least one workload-bearing test also self-diagnoses its own generator process's event-loop lag/CPU and warns if tflw itself was the bottleneck. A live ~1Hz console line tracks iterations/rps/error-rate for the workload-bearing tests currently in flight. The first Ctrl-C is a graceful stop for any run: no new file, test or iteration starts, the tests in flight finish, and a partial report marked aborted is written and kept instead of losing the run; a second Ctrl-C quits at once. The page's Cancel is the same stop. Everything — functional and workload-bearing test results alike — renders into the one report/report.html/junit.xml/ results.json, in file-declaration order; there are no separate load-* artifacts. Exit 0 = every test passed and every threshold was met (or none declared), 1 = a test failed or a threshold was breached, 2 = usage error, 3 = inconclusive (a workload-bearing test ran and tflw's own generator process saturated — the numbers describe tflw contending with itself, not the system under test; every threshold's junit <testcase> comes back skipped, not passed/failed), 130 = aborted via Ctrl-C (the standard "died from SIGINT" code).

tflw check [--format json] [--allow-public-target <origin>]
[--env <name>] [--no-color]
--format json
prints one { file, diagnostics } entry per file checked as JSON instead of text — for editor and CI integrations
--allow-public-target <origin>
the same affirmation run takes, accepted here because check answers will this run?, and the answer depends on it — without it a suite legitimately scanning a public staging host could never get a clean tflw check. check still sends nothing: the flag only changes which diagnostics it reports
--env <name>
selects a named env block from tflw.config instead of the default one — decides which env-scoped checks (service names, insecure) run
--no-color
disables ANSI color in CLI output

Validate-only: the same parse + checker pipeline run executes before it does anything, with no HTTP traffic and no secrets required. Exit 0 when every file is clean, 2 otherwise.

tflw init [--load] [--scan] [--example] [--force]
--load
also scaffolds a load.tflw — a workload-bearing test in the open (rps) model, runnable with plain tflw run
--scan
also scaffolds a scan.tflw and a tflw.config carrying a commented-out authorized target — the scan refuses (TF060) until a person uncomments it and writes the reason, because that declaration is an affirmation only its author can make; the config points at a real host rather than the demo service, whose scheme has no origin to authorize
--example
writes the Coffee Shelf instead of the one-test scaffold: a stdlib node:http shop (npm run shop), its order page, 37 tests across every kind, and a tflw.config whose URLs read SHOP_URL
--force
with --example only: overwrites files of the same name instead of refusing

Scaffolds tflw.config, example.tflw, .env.example, and a .gitignore covering .env and report/ — appending to an existing .gitignore rather than duplicating entries.

tflw init --example writes the Coffee Shelf instead: a small shop (server.mjs, started with npm run shop), its order page, and twelve test files covering every kind tflw runs. It is the project the walkthrough uses. Its tflw.config reads SHOP_URL for every address, and so does the shop, so one variable moves both off port 4720. It refuses to write over any file already there unless --force, and adds the shop script to an existing package.json rather than replacing the file.

tflw install-browsers [--browser <engine>]
--browser <engine>
downloads chromium/firefox/webkit (default chromium) — runs the playwright CLI inside the optional peer dependency, resolved from the consuming project; refuses if that peer is absent rather than fetching one

One-time browser binary download for UI steps — playwright is an optional peer, so this only works once the consuming project installs it.

tflw pick <url> [--browser <engine>] [--cdp-port <n>]
--browser <engine>
the engine to launch: chromium (default), firefox or webkit
--cdp-port <n>
opens the browser's DevTools endpoint on 127.0.0.1:<n>, announced on stderr, so a script can drive the window with Playwright's connectOverCDP; chromium only, and never on any address but loopback

Opens a real, visible browser at <url> and prints one verified locator per click; runs until the window is closed or Ctrl+C. <url> is an absolute URL, or a path (/order.html) opened against tflw.config's web for the default env, joined the way a test's open joins it.

tflw record <url> [--browser <engine>] [--cdp-port <n>]
--browser <engine>
the engine to launch: chromium (default), firefox or webkit
--cdp-port <n>
opens the browser's DevTools endpoint on 127.0.0.1:<n>, announced on stderr, so a script can drive the window with Playwright's connectOverCDP; chromium only, and never on any address but loopback

Opens a real, visible browser at <url> and prints one tflw step per action taken in it — a click, a filled field, a ticked box, a chosen option, a key press. pick's sibling, and the difference is that nothing is prevented: a click navigates and a form submits, because a recording is a sequence and a sequence needs the page to advance.

A field's keystrokes are coalesced into one fill rather than written per character; a checkbox prints tick or untick; a key press carries the control it was typed into. Actions only — a recorder can see that a page changed but not what about the change mattered, so expectations are yours to add afterwards. Runs until the window is closed or Ctrl+C. <url> is an absolute URL, or a path opened against tflw.config's web, as pick's is.

Every tab the session opens is recorded, not just the one it started in. A click that opens a tab is written as the block that wraps it, because the runtime has to be listening before the click happens; coming back to a tab you left prints the switch that takes you there.

click button "Track parcel"
switch to new tab
click text "View receipt"
click button "Print"
switch to tab 1

A page with twelve Add to cart buttons has no unambiguous name for any of them. Rather than fall back to a selector naming the position the row happened to be in, the recorder scopes the statement to the nearest ancestor that narrows the name to one and has a name of its own:

within css "[aria-label=\"Product 2\"]"
click button "Add to cart"

An ancestor that narrows the name but cannot be named is skipped — a within naming a position is no better than the selector it would replace. When nothing qualifies, a CSS path is still written.

--cdp-port <n> opens the browser's DevTools endpoint on 127.0.0.1:<n> and says so on stderr. Anything that speaks the Chrome DevTools Protocol can then act in the window, and every action is recorded exactly as a person's would be — which is how a recorded session can be checked in CI against a golden file:

import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP('http://127.0.0.1:9333');
const [page] = browser.contexts()[0].pages();
await page.getByRole('link', { name: 'Shop' }).click();

Chromium only, and only ever on loopback. pick takes the same flag.

Clicks the browser synthesised. Pressing Enter in a text field submits the form, and the browser does that by dispatching a click at the submit button — recorded, that click makes a replay submit twice. Activating a focused control from the keyboard is a gesture and is still recorded.

Steps go to stdout and everything else — the opening banner, and the word it writes when it has to skip an action — goes to stderr. So tflw record http://localhost:3000 > steps.tflw gives you a file whose every line is a step, ready to paste under a test declaration.

tflw watch [--env <name>] [--seed <n>] [--browser <engine>] [--no-color]
--env <name>
selects a named env block from tflw.config instead of the default one
--seed <n>
fixes the seed reused by every run for the whole watch session (else one is freshly minted at startup)
--browser <engine>
switches every browser step to one engine — chromium/firefox/webkit (default chromium)
--no-color
disables ANSI color in CLI output

tflw watch [files] [--env E] [--seed S] [--browser engine] [--no-color] re-runs headed on every save, one shared browser window for the whole session; saving tflw.config re-runs everything. Runs until Ctrl+C.

tflw migrate [files] [--env <name>] [--no-color]
--env <name>
selects a named env block from tflw.config instead of the default one — deprecations are checker diagnostics, so this only affects which env-scoped checks run
--no-color
disables ANSI color in CLI output

Mechanically rewrites checker-flagged deprecations in place, then prints which files changed. Three rules carry a rewrite today — scenario → test, think → pause, uncheck → untick.

How to tell whether migrate can fix something: the diagnostic says so. Any diagnostic that ends with = fix: run tflw migrate to apply this automatically is one it can act on. That line is derived from the rewrite itself rather than written next to each rule, so the offer and the capability cannot drift apart.

Bare check <locator> is deliberately not one of them. It has two honest readings — tick field "…" (the old click) and check field "…" is checked (the assertion) — and guessing wrong turns an assertion into a mutation in a test that keeps passing. Migrate reports each one and leaves it to you; on the real migration this was built against, that was 5 of 7 sites.

It works on files that do not parse, which is the only kind it exists for. It splices what it can, writes, re-checks the rewritten source, and prints whatever remains against the new line numbers — repeating until a pass finds nothing left, since a think nested inside a scenario is invisible to the parser until the scenario is fixed. Exit 0 if the suite is clean afterwards, 2 if errors remain, including when it did rewrite something and the file still fails: migrate's job is the rewrite, not the verdict.

It rewrites keywords, not prose. A migrated file can be entirely correct code and still name the old keyword in its comments and test "…" names — this is not a rename-symbol refactor.

tflw fmt [paths] [--check]
--check
writes nothing; lists every file that would change and exits 1 if any would — the CI form. Without it, files are rewritten in place and each one that changed is named

Formats .tflw files in place. A path may be a file or a directory; a directory is walked for .tflw files (node_modules/, .git/ and report/ are skipped — a repro tflw wrote into report/ is not something you meant to format); no path means the current directory. Each file that changed is named, then one summary line.

What it decides, and what it never touches. Blocks indent two spaces per level. On a line, tokens are separated by one space, with none before , : ) ] and none after ( [; objects are padded { a: 1 }, arrays are [1, 2], an interpolation {name} stays tight (that is the language's own rule: { IDENT } is an interpolation and an object always has a key:). A multi-line body { … } indents its content one level past the line that opened it and puts the closer back at that line's indent. Data tables align by column. Blank lines collapse to one, trailing whitespace goes, the file ends in exactly one newline. Comments never move: a comment line stays a line, at the indentation of the code that follows it (or of the block it closes, when it sits at the end of one); a trailing comment gets two spaces before its #; comment text is never edited. Strings, numbers, paths and everything else inside a token are emitted byte for byte.

Three spaces are kept as you wrote them, because the same two tokens mean two things and only the parser tells them apart: the space after a -/+ (price: -1 against today - 10 days), the space between a number and the word after it (4s is a duration, 3 seconds is one too, 4 s is an error), and the space between a name and a [ (body[0] is an index, equals [1, 2] is a list).

A file that does not lex is not formatted. It is named on stderr with the diagnostic, left byte-for-byte alone, and the command exits 1 — a formatter that silently skips a broken file is a check that says "clean" about a file it never read. Fix the file (tflw check says how), then format it.

The guarantee. For every file it writes, the formatted text lexes to the same tokens in the same order (indentation and comments excluded), keeps every comment in text and order, keeps the block structure, and formats to itself on a second pass. That property is what lets the rules be this opinionated: nothing the formatter does can change what a file means. The same function answers the editor's Format Document through tflw lsp, so format-on-save in VS Code needs no setting beyond the extension.

tflw export otlp [report-dir] --endpoint <url> [--header <name=value>]
--endpoint <url>
where tflw export otlp POSTs the trace — a collector's OTLP/HTTP traces address, e.g. http://localhost:4318/v1/traces; required
--header <name=value>
a header sent with the trace, for a collector's auth; repeatable

Sends a finished run to an OpenTelemetry collector as one trace — a span for the run, each file, each test and each step — over OTLP/HTTP's JSON encoding. It reads results.json from the report directory (report/ when none is named). A report records the run's start and every duration but not when each test started, so the spans are laid end to end and each carries tflw.timing = "reconstructed". Exits 0 when the collector answers 2xx, 1 when it answers anything else, and 2 for a usage problem. See CI, reporting & safety.

tflw merge <report-dir>... --out <dir> [--no-color]
--out <dir>
where tflw merge writes the merged report.html, junit.xml, results.json and findings.sarif; required, and never one of the inputs
--no-color
plain text for the merged summary, as run --no-color

Joins finished runs — the shards of a sharded CI job, or a sweep's groups — into one report. Tests keep their own files and appear in the order the directories were given; the counts are re-derived from them, findings are deduplicated by fingerprint so SARIF uploads each once, and the verdict is the one tflw run would give (an aborted input makes the merge not ok). env names every input's env and mergedFrom in results.json lists the inputs. Each input's events.ndjson is joined in order, and assets/ is copied across. Exits 0 when the merged run passed, 1 when it did not, and 2 for a usage problem. See splitting a suite across CI jobs.

tflw doctor [files...] [--env <name> | --all-envs] [--browser <engine>]
[--allow-public-target <origin>] [--forbid-insecure] [--no-helpers]
[--json]
--env <name>
which env block tflw doctor resolves — default as run: TFLW_ENV, then the block marked default; an undeclared name is a usage error
--json
the same facts as one JSON object — ok, node, config, services, proxy, tls, secrets, suite, checks, browsers, problems, warnings — for a CI step or a support ticket
--all-envs
one verdict per env the config declares — ✓, or ✗ with the first problem — exiting 1 if any env cannot run; not beside --env
--browser <engine>
the engine run --browser would launch, which must be downloaded when the suite has browser tests (default chromium, as run)
--allow-public-target <origin>
the affirmation run takes, so the checks doctor makes are the ones that run makes (TF065); repeatable
--forbid-insecure
fails doctor as it fails run when the env has insecure true
--no-helpers
judges every use as run --no-helpers does (TF083)

What this machine and this project will run with, on one screen: tflw and Node versions, the env and every api base it resolves to, the proxy variables and whether anything reads them, TLS (certificate verification, a client certificate and whether it is on disk), the suite's size and how much of it drives a browser, which Playwright browsers are downloaded, and the secrets the env requires — by name, and whether each is set, never the value. Read-only and offline — it resolves the config (with the project's .env, as run does) and asks Playwright where its browsers are; it sends no request. Exits 1 exactly when tflw run with the same files and flags would refuse before its first test: no tflw.config; Node older than 22; a tflw check error under the env (the first one is quoted, with the tflw check command that lists the rest); browser tests when the engine run will launch is not downloaded; --forbid-insecure beside insecure true; or a required secret not set. Warnings, marked ⚠, never change the exit code: a .env git would commit, the warnings tflw check reports, insecure true. --all-envs prints one verdict per env. See installing tflw.

tflw ui [dir] [--port <n>] [--no-open]
--port <n>
the loopback port the page is served on (default 4141); 0 lets the OS pick and the chosen port is printed
--no-open
do not open the page in a browser after the server starts — the URL is printed either way, which is the form a tunnel (ssh -L) or a script wants

Serves the page for a directory — the one holding tflw.config, the current one by default — on 127.0.0.1 and prints the URL. A directory with no tflw.config opens too, as a blank project: the page says so and offers to create one, which runs tflw init for you. What is refused is a path that is not a directory at all, because that is a typo rather than a new project. The page is a projection of the project and of its report directory, never a second copy of either: it lists the discovered files and the tests in them, it starts a run as tflw run --format ndjson (the same command, the same files, the same report directory a terminal run writes) and relays the stream as it arrives, and it opens every report directory the project holds — the current one, and each run started from the page, which is kept aside as report/runs/<id>/ when it ends.

The page can also author a test, through one write route with one call site: a form per statement previews the exact bytes it is about to write and tflw check judges them before the write, the file on disk is the only truth, and tflw.config is not reachable through that route. The forms and what each kind scaffolds are documented under the UI.

The project's configuration is edited in its own tab, through its own route. The header's Config panel is a plain editor over tflw.config — the bytes you type are the bytes written, and nothing reformats them — refusing only text that does not parse, and writing under the version the page read so a file changed by a terminal in the meantime is reported rather than overwritten. It is a separate capability from the one above and not a widening of it: the test-writing route still refuses this file.

That tab is multi-document once the config declares one. A baseline declaration names a second file the project owns, so the tab carries a switcher — tflw.config first, then one entry per declared document, labelled with its path and the block that declares it — and each document is addressed in the URL, which is what lets a link point at one line of one of them. They are the same editor under the same rules, with one difference the file itself dictates: a declared baseline that has not been written yet opens as an empty document rather than an error, because that is where every project adopting triage starts, and the config's diagnostics are not applied to a file that is not in the config dialect. Beside it, Auth reads what that configuration means for the file you are looking at — which sessions its tests run as and what each adds to a request, the built-in anonymous principal, and every authorized target in force with its reason and what each probe opt-in grants.

What a browser test shows is what the report holds. At evidence full the screenshots a run took are on the page and a failed test's Playwright trace opens in Playwright's own trace viewer, which tflw ui serves under /trace/ from the project's playwright-core — nothing is spawned and the archive never leaves the machine. Below full there is no screenshot and no trace, by decision, and the page says so under each browser test rather than leaving a gap.

A workload test is its metrics, charted. The figures are results.json's own — iterations, failures, error rate, the all-iterations and the successful-only percentiles, every threshold with its actual, one row per endpoint — in the units the console and report.html print. The four charts are the report's per-second timeline (latency p50/p95/p99, throughput, error rate) and its bucketed histogram; hover a chart and the legend reads that second's own numbers. Two runs compare by opening two report directories: pick one under compare with and every table gains the other run's column and the signed difference, and every chart its dashed series.

A security scan is its findings, grouped by rule. The block above the tests is results.json's findings in the order the console and report.html use — gating first, then withheld, worst severity first — each with its endpoint, its source line, its fingerprint, the gate's verdict (known/accepted for a baselined finding, which stays on the page rather than vanishing) and the same possible fixes entry report.html carries. Which rules ran lists what applied and what stood down, with the reasons. With a second run open under compare with, each finding says whether the other run had it and with what verdict, and the other run's findings this one lacks are listed — the baseline diff, drawn from two reports.

A finding can be accepted from the page, and accepting it writes nothing. Each gating finding that carries a fingerprint has an [accept] link: it resolves which baseline document a run under this report's env grades against — the env's own block, or the defaults one it falls back to — splices the entry into that document in the shape --baseline-write emits, and opens the Config tab on the line it added. The document is then unsaved text in an editor, and stays that way until somebody saves it. An acceptance is an affirmation about the application under test that only its author can make, so the page stages it and shows it; it does not make it. A document it cannot splice honestly — not JSON, no accepted array, or already carrying this fingerprint — is returned unchanged and opened anyway, and an env with no baseline declared is refused with the reason. See Findings, baselines & the gate for the workflow this belongs to.

A run's exit is stated where its report cannot state it. tflw run's exit codes 0, 1, 3 and 130 are the report's own verdicts (passed, a failure, inconclusive, aborted) and the header carries them; anything else — a process that died after writing its report, a signal, a cancel from the page — is written above the report with the run's stderr, and a run that wrote no report at all (a refused argument, an unmet require env) keeps a row of its own in the run list rather than disappearing. The environment a page-started run sees is the server's: start tflw ui where a terminal run would work, with the same .env beside the config.

Loopback only. The bind address is not configurable: the page can start a run, and a run reads the project's .env. To reach it from another machine, tunnel it (ssh -L 4141:127.0.0.1:4141 to that machine) rather than exposing it.

Prints a page of these docs in the terminal: the Guide and Reference pages, grouped as this site's sidebar groups them. With no topic it lists them; tflw docs cli prints this page, with every flag table filled in, and tflw docs cli run prints one section of it. A word that is not a topic is looked for among every page's section titles, so tflw docs unique prints unique vs. random. Links to another printed page read (tflw docs <topic>); links anywhere else read as a URL. Takes no flags. The pages are rendered into the CLI at build time, so it needs no network.

Prints the construct manifest of this build: every declaration, step keyword, matcher, generator, locator, tflw.config word and diagnostic code the parser dispatches, followed by a build stamp — version, commit and build time.

It exists for conformance testing. A suite that wants to prove it exercises the whole language has to be able to ask what the whole language is, and reading it out of @tflw/lang is not available to a consumer who installed the published tflw package: that ships one self-contained bundle with no @tflw/* packages to import. --json is the form such a gate reads.

The manifest lists what this binary dispatches, and only that. A construct SPEC marks 🔮 planned is absent rather than listed as planned, so building one makes it appear — which is what lets a downstream coverage gate go red on its own the day it ships, with nobody having to remember.

Declarations are the outermost family: the seven words a file can begin a top-level block with (test, crawl, action, import, use, before, after) and the five clauses a test header takes (tags, with each, as, retry, parallel/sequential). A hook's two scopes are one construct with two forms, the same way switch to new tab and switch to tab N are one switch.

The build stamp is the other half. commit is a short sha, or null where there was no git to ask (a published tarball, a vendored checkout) — never invented, because an invented one would be believed. dirty says whether the working tree had uncommitted changes, and is null when there is no commit for it to be relative to. Together they answer which tflw just produced this output, which is the question a stale vendored copy makes unanswerable.

tflw spec [--json] [--json]
--json
emits the construct manifest as JSON instead of the human listing — the form a conformance check reads. Spelled as a boolean rather than --format json: spec has one machine format and one human one, so there is no open set of renderings to name

Runs the language server over stdio. Takes no flags and is not meant to be typed by hand: stdin and stdout carry the LSP wire protocol, so an editor spawns it. packages/vscode does exactly this; any other LSP-capable editor can point at npx tflw lsp. Diagnostics come from the same checkProgram pass list tflw check runs, so an editor squiggle and a CI failure are the same computation.

Extracts one reuse-pass hint (an RF0xx id from tflw check's output) into a shared action, writing a new file and rewriting every matched call site. Takes exactly one positional argument — no flags — and always scans the whole default suite (no [files]/--env selection, since the hint ids it consumes come from that same whole-suite scan). Re-run tflw check/tflw run afterward to confirm the rewritten suite is still clean and green.

--version, -v
print the installed version
--help, -h
print usage