9l plan: resolve Playwright specs before launching any process and explain every skipped input.9l run: execute planned jobs with a run-wide budget, bounded concurrency, attempt limits, per-job/shared deadlines, and process-tree cancellation.9l status,9l result, and9l cancel: inspect or control a foreground run from another terminal using its persisted run ID.- Execution receipts: legacy
receipt.jsonplus additive strictexecution-receipt-1.0.json, with redacted stdout/stderr evidence under.9lives/receipts/<run-id>/<job-id>/<attempt-id>/.receipt.jsonis the commit point: if it cannot be written, the canonical export is removed too. A run whose heartbeat is more than 10 seconds stale is reported as interrupted; local receipt persistence does not claim recovery. - Controlled environment: test processes inherit only what a browser test
runner needs to start and reach the network (
PATH,HOME, temp and locale variables,DISPLAY/WAYLAND_DISPLAY,XDG_*,PLAYWRIGHT_BROWSERS_PATH, proxy and CA-certificate variables). Application settings and credentials are forwarded only when named:9l run tests/ --pass-env BASE_URL,TEST_USER. Values are read from the caller's environment and never written to the plan or receipts. - Visual mode:
9l run --headedshows the browser while tests run (both adapters; Playwright's own--headedflag, which overridesheadlessin the config). Headed runs execute one job at a time unless--workersis given, so windows do not pile up. Linux needs an X11 or Wayland display; without one the run is refused before planning, so usexvfb-run -a 9l run --headed ...on a headless machine. Headed mode changes only how the browser is shown, never what is asserted or how evidence is validated. To slow actions down while watching, setuse: {launchOptions: {slowMo: 250}}in the project's Playwright config: a config'slaunchOptionsreplaces any value a fixture library sets, so 9l does not offer a flag that could be silently ignored. - Test selection:
9l run tests/login.spec.ts:12runs only the test at that line (Playwright'sfile:linefilter), and--grep <regexp>/--grep-invert <regexp>pass Playwright's title filters to every job. The plan shows each job'sselection, the receipt's evidence command records the filter, and a selection that matches no test leaves the run incomplete with a reason naming the selection, never green. - Readiness:
--wait-for <url|host:port>(repeatable) waits, inside the started run and before any test process starts, until an http(s) target answers without a server error (a status below 500 other than 429) or a host:port accepts a TCP connection, polling every 500 ms for--wait-timeout(1s to 30m, default 1m). A target still not ready when the wait timeout passes leaves the run incomplete (exit 3): no test process starts, and each planned job gets an error receipt readingnot started: --wait-for target not ready (environment): <target> was not ready after 1m0s: dial: connection refused, so9l resultand9l statusread the run back and an application that never came up is not reported as failing tests. The wait counts against--deadline, and9l cancelor Ctrl-C during it reports the run canceled, like any canceled run. A target is named only asscheme://host:portorhost:port, never with its path, query, fragment or credentials; the reason is an HTTP status, a network error code,no such host(the resolver said the name does not exist),dns lookup timed outordns lookup failed(the resolver did not answer),timed out,certificate not trustedortls handshake failed. An https target with a self-signed certificate is not trusted and never becomes ready; wait for itshost:portinstead, which checks only that it accepts connections. The host is a name, an IPv4 address or a bracketed IPv6 literal ([::1]:8080,http://[fe80::1%25en0]/); any other host with a colon, such ashttp://localhost:80:90, is a usage error.planand--dry-runvalidate the targets but do not wait. - Retries:
--attempts <n>is 9l's own retry of the whole job, with a receipt per attempt. The project's Playwrightretriesstill apply inside each attempt, so a failing test can run several times within one receipt.--retries <n>(0–10) replaces that setting for every job;--retries 0 --attempts 2gives Go the whole retry policy and its evidence. Either way the receipt recordsflakyTests, the tests that failed and then passed on a Playwright retry inside the attempt, and9l run/9l resultprintN test(s) passed only on a Playwright retry inside this attemptunder the job, so a flaky pass is not read as a clean one. Target jobs (--target) already run without retries and refuse--retries. - Config and projects:
--config <file>runs the specs with that Playwright config instead of the one Playwright finds (with--sdkit replaces the detected config), and--project <name>(repeatable, up to 16) runs only those configured projects, for example a second instance of the app on anotherbaseURL. Both are recorded in each job'sselectionand the receipt's evidence command. A spec the config finds no tests in is incomplete with a reason naming the config file and itstestDir; an unknown project is incomplete with Playwright's own "Available projects" line. - Project reporters:
9l runreads Playwright's JSON reporter as evidence, which replaces the reporters in the project's config.--reporter html(comma-separated names or module paths, up to 8) adds them back beside the evidence reporter, so the HTML report is still written;jsonis reserved. Traces and screenshots do not need this: they follow the config'susesettings either way. - Test annotations: receipts carry each test's annotations from its last
attempt (
annotations: test title, type, description; up to 100 per job, descriptions capped at 256 bytes and redacted), including ones a test pushes at run time, such as a note that it returned early on an older build.9l runand9l resultprint them under the job asnote <test> — <type>: <description>, except Playwright's ownskip,fixme,failandslow, which stay in the receipt only. A passed job with such a note may not have exercised everything its tests can check. With--sdk, annotations need--failure-details, because the engine protocol carries no titles. - Assertion steps: with
--sdk, the receipt recordsreportedAssertions, the assertion steps the worker reported as passed in each executed test's last completed attempt, andpassedWithoutAssertions, the tests that passed while reporting none. A retrying assertion (expect.poll,toPass) counts once, including the assertions inside it.9l runand9l resultprintN passed test(s) reported no assertion stepunder such a job. These are the worker's report, not verified coverage:assertionCoveragestaysunknown. Without--sdk, Playwright's JSON reporter carries onlytest.stepsteps, so the counts are absent. See sdk-bridge.md. - Failed tests: below each failed job,
9l runand9l resultname every failed test (up to 20 per job) with its failing line, the first lines of its error and the files Playwright attached to it, such aserror-context.md(the error, an ARIA snapshot of the page and the source with the failing line marked), screenshots and traces. The receipt'sfailuresfield holds the same, with the error capped at 1 KiB and redacted, plus Playwright'sexpectedStatusfor the test and thestatusof its last attempt. Atest.fail()test that passes is a failure in Playwright's model and in 9l's counts, but it has no error: it is listed with⚠instead of✗, the messageExpected to fail, but passed.,expectedStatusfailedandstatuspassed, and the receipt'svalidationtext counts such tests. Atest.fail()test that fails is an expected outcome and counts as passed, also forhealandconfirm. Each failure carries aclass, read from Playwright's structured status and its own error prefixes, never from the message text:assertion(anexpectfailed),timeout(the test's time budget ran out),action-timeout(one action or navigation timed out),environment(navigation failed with a Chromiumnet::ERR_*or FirefoxNS_ERROR_*code: connection refused, name not resolved, certificate rejected),unexpected-pass, orotherwhen the error has no such mark, such as a test's own thrown error or a WebKit navigation failure. The text output shows the class after the failing line, as[environment], so a target that never came up is not read as a broken test. An HTTP error status on navigation is not an error to Playwright and is not classed. By default the attachments are only referenced: they are Playwright's own files, which the project's next run may delete, and 9l does not retain page content unless asked.--keep-attachmentscopies them into the attempt's receipt directory with a SHA-256 digest (text redacted; at most 32 MiB per file and 128 MiB per attempt; only regular files inside the project). The receipt records the request askeepAttachments, how many attachment files the listed failures reference asfailureAttachments(inline bodies and files outside the project are never referenced) and how many were copied asretainedAttachments; when retention was requested and the listed failures reference none, such as atest.fail()pin that passed,9l runand9l resultsay so under the job. With--sdkthe engine protocol carries no titles or errors by design, so failures are reported only with--failure-details: Playwright's JSON reporter then writes a separate private report beside the evidence stream, which explains failures and never validates the run. - Honest completeness: execution and report validation are separate; unexplained skips, canceled, failed or unvalidated jobs prevent an overall green result.
- Exit codes:
runexits 0 when every planned job passed, 1 when every job ran and a test failed, 2 for usage or setup errors and 3 when the run is incomplete. The JSONoutcomefield carries the same classification. See the machine-readable contract for hosts, schemas and9l version --format json. 9l assess: advisory source assessment of a spec, or a combined report for several specs, a directory or a quoted pattern, optionally against a shared requirements contract (--requirements);--titlesadds literal test titles for local use. Text output sums analysis limits in one line per file, counted per code;--limitslists each limit location instead. Static findings do not establish executed assertion coverage.- Experimental
9l prove: run one spec, then re-run it once per injected network fault (abort,http-500,empty-jsonby default;http-401,http-403,http-429,malformed-json,redirect-cross-originanddelaywith--faults) on each fetch/XHR request it made, and report which faults an assertion caught and which survived. Requires@9l/playwright0.1.1 or newer, 0.1.3 or newer for the--faultsopt-in kinds and 0.5.1 or newer forredirect-cross-originanddelay; see Prove. - Experimental
9l confirm: run one reproduction spec on the revision a finding was reported against (--unfixed) and on the fixing revision (--fixed, default the working tree), each in its own Git worktree, and reportconfirmed,not-reproduced,fix-ineffective,regressedorinconclusive. Exits 0 only for confirmed; see Confirm. 9l provenance: record a declared agent and the current workspace, branch, commit and test source for later assessment/execution checks. Amatchedassessment hasscope: "spec": it compared the selected spec's bytes, its path, the branch, commit and workspace root. Helpers, page objects and the module under test are not compared, somatcheddoes not mean unchanged dependencies; the text output says so.9l tier1: bounded offline healing proposals from a JSON request on stdin; proposals remain unverified and require approval.9l heal: execute a failing Playwright spec, verify a selector-only candidate from offline Tier 1 or a Tier 2 provider in isolation, and save it or apply it with approval.heal-nativeis an alias. See Native Tier 2.9l mcp: serverun_test,heal_test,assess_test,confirm_finding,scan_url,inspect_pageandgenerate_playwright_reproto coding agents over MCP stdio. See MCP server.9l scan <url>: load the page once in Chromium and report console errors and failed requests, broken links, accessibility, the Core Web Vitals LCP and CLS (INP is not measured) with TTFB and FCP, SEO, security headers, cookies and trackers, mixed content and page weight, with a 0–100 score. The score is 100 minus 35 points per critical, 20 per high, 10 per medium, 4 per low and 0 per info finding after deduplication, floored at 0; the grade is A from 90, B from 75, C from 60, D from 40 and F below. Ten medium findings alone reach 0. The JSON result states this inscoring(scale, weights, grade thresholds and grade) and the Markdown report prints it under the score. A finding seen more than once carriesoccurrences; the page's own aborted prefetch and RSC requests are oneinfofinding per page listing up to five distincturls, withomittedUrlscounting the rest, rather than one finding per URL. Other failed requests stay one finding per URL.--checksselects checks by exact name (an unknown name is refused),--formatismarkdown(default),jsonorissue(one tracker-ready block per finding, filtered by--min-severity), and--outwrites the report to a file.--baseline <previous.json>adds the new, fixed and unchanged findings, and--fail-on-newexits 1 when there is a new one.--weight-budget total_bytes=500000,request_count=40scans against your own page-weight budget (alsorender_blocking,image_bytes,script_bytes).9l inspect <url>: the page's ranked locators (test id, role and label before text and position), each control's locator stability and a page testability verdict, as Markdown or--format json;--accessibility-treeadds Chromium's accessibility tree. A control is quoted by its accessible name only; anameattribute is shown as[name=…] (no accessible name).
Both take --viewport WIDTHxHEIGHT (default 1366x900) and, for a page behind
a login, --storage-state <file> with --acknowledge-private-content: the
Playwright storage-state file must be inside the working directory, its
cookies and storage are loaded into a throwaway browser context and never
printed, but the report may reflect private account data. Keep that file
out of version control and out of shared artifacts. Credential shapes in
returned text (tokens in URLs, Bearer/Basic values, JSON Web Tokens) are
redacted on a best-effort basis; that is not a guarantee. An authenticated
scan checks same-origin links with the session, except links whose path or
query names a sign-out or data change (logout, sign_out, delete,
remove, unsubscribe and similar): those are reported as skipped info
findings rather than requested. Use --max-links 0 to request none.
Destinations are checked before each request and again for the address
each connection is made to, on every redirect, so a hostname whose DNS answer
changes after the first check (DNS rebinding) is still refused. Link checks
requested from Node do this themselves; the browser reaches the network only
through a local proxy in the helper that does the same for its navigation,
subrequests, in-page fetches and WebSockets, so a redirect inside the browser
is checked hop by hop as well.
scan and inspect are observations from one cold page load on this
machine, not field data. They run in a Node helper embedded in the binary,
with the project's Playwright and its installed Chromium: run them in the
project directory after npx playwright install chromium. The helper gets
only the variables it needs to find Node and the browser; credentials and
--pass-env values are not passed. Requests to private, link-local and
loopback addresses are refused, including through DNS, redirects and page
subrequests; --allow-private-network permits a loopback target such as a
local dev server, and only its origin. A --baseline file must be inside the
working directory. Both exit 0 with a result, 1 for new findings under
--fail-on-new, 2 for invalid arguments and 3 when no result was produced,
such as a refused or unreachable target, a missing browser or --timeout
(default 300 s, or NINELIVES_RUN_TIMEOUT).
9l scan https://example.com --format markdown
9l scan http://localhost:3000/ --allow-private-network --format json --out scan.json
9l scan http://localhost:3000/ --allow-private-network --baseline scan.json --fail-on-new
9l inspect http://localhost:3000/login --allow-private-network
9l inspect http://localhost:3000/account --allow-private-network \
--storage-state playwright/.auth/user.json --acknowledge-private-contentPlaywright execution supports existing projects with a local
@playwright/test dependency. The installed node_modules/.bin/playwright is
taken from the package that declares it or, in an npm, pnpm or Yarn workspace
whose dependencies are hoisted, from the nearest ancestor that has one; the
test runs in the declaring package's directory. It never uses npx to
download tooling during a run.
Release packaging targets macOS, Linux and Windows on amd64 and arm64. Windows ZIPs start with CLI v0.2.0. Owned process-tree cancellation uses Unix process groups or Windows kill-on-close Job Objects; native CI exercises real Chromium timeout and cancellation cleanup.
The runner classifies interruption and persists terminal receipts. Automatic restart, durable leases and crash recovery remain planned.
Go 1.27.3 or newer is required; CI pins 1.27.3. The Go runner and local SDK do not require a platform account. Python is needed only for the optional compatibility checks in the development guide.
go build -o 9l ./cmd/9l
./9l plan --format json 'tests/*.spec.ts'
./9l run --workers 4 --timeout 5m tests/login.spec.tsFlags may appear before or after spec arguments. Directories are searched
recursively; shell-style globs use Go's filepath.Glob rules.
./9l plan --max-jobs 20 tests/
./9l run tests/ --receipt-dir .9lives/receipts --pass-env BASE_URL
./9l status <run-id>
./9l cancel <run-id>
./9l result <run-id> --format json9l heal and 9l mcp are native and need no Python. Healing uses the
provider named with --provider or NINELIVES_PROVIDER, else an installed
claude, codex or opencode CLI, else a configured API key; with none, or
with --provider none, it is offline Tier 1 only. Auto-detection is on by
default; NINELIVES_AUTODETECT_PROVIDER=off limits healing to a provider
named with --provider or NINELIVES_PROVIDER. The resolved provider is
printed before healing starts, and the session JSON records provider and
providerCalls. --provider claude, codex or opencode uses that
CLI's own login, so a Claude, ChatGPT or OpenCode subscription works without
an API key; see
native Tier 2. --run-timeout takes a duration or, like the Python CLI,
whole seconds. --format json (the default) prints the session result on
stdout; --format text prints its state and reason, one line per run
(original, tier1, verified; a run the session never reached is
not attempted), where a verified candidate was saved or applied, and the
candidate's diff. In the JSON, tier1 and verified are null when that
stage was not attempted. The Python CLI's other healing options, its Cypress
and Selenium adapters and its watch/report commands remain in the Python
package: run them with 9lives.
./9l heal tests/login.spec.ts --run-timeout 900 --pass-env BASE_URL
./9l heal tests/login.spec.ts --provider claude --yes
./9l heal tests/login.spec.ts --provider none --format text