From 6530dc769ba10d779e549f01af2403fd7697df38 Mon Sep 17 00:00:00 2001 From: Ruslan Strazhnyk Date: Tue, 13 Oct 2026 10:37:16 +0200 Subject: [PATCH 1/2] Move the qmax.run page checks onto 9lives.run and install Go v0.4.0 qmax.run now serves a different QualityMax page, and the checks it presented (scan, inspect, repro) ship in the Go runner from v0.4.0 as 9l scan, 9l inspect and the scan_url, inspect_page and generate_playwright_repro MCP tools. - Landing page: a "Check the page you changed" command in the hero, a "Page checks" verification-loop section with the nine checks and the MCP setup command, and updated feature and Python-section copy. The qmax-mcp ecosystem card, which linked to qmax.run, is removed and the grid becomes 2x2. - Installer: defaults to Go v0.4.0 instead of v0.1.1 and names 9l scan after installing; both copies stay byte-identical. - README: Go v0.4.0, go install @v0.4.0, and the page checks described as part of the runner instead of a separate npm server. - Tests: no qmax.run link, the page-check content and the installer default are asserted. Merge only after the v0.4.0 GitHub Release exists: the installer default downloads it. --- README.md | 8 ++-- install/9l.run.sh | 7 +-- tests/test_website.py | 18 +++++++- website/app/globals.css | 15 +++++-- website/app/page.test.tsx | 15 +++++++ website/app/page.tsx | 92 ++++++++++++++++++++++++++++++++++----- website/public/install.sh | 7 +-- 7 files changed, 136 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 827a645..dc8cd3a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Read the [migration guide](docs/MIGRATING_TO_GO.md) for installation, command di [Small UI change. Big test drama. Explore the developer memes on 9lives.run →](https://9lives.run/#memes) -**Linux. macOS. Windows. One local workflow.** The [Go runner v0.3.0](https://github.com/Quality-Max/9lives-runner/releases/tag/v0.3.0) has native binaries for all three, on x64 and ARM64. Keep your Playwright project, configuration, and assertions across your team’s machines. Windows users: follow the [ZIP download and checksum guide](https://github.com/Quality-Max/9lives-runner/blob/main/docs/install.md). Project dependencies and Chromium are separate; Windows provider CLI shell integrations remain unqualified. +**Linux. macOS. Windows. One local workflow.** The [Go runner v0.4.0](https://github.com/Quality-Max/9lives-runner/releases/tag/v0.4.0) has native binaries for all three, on x64 and ARM64. Keep your Playwright project, configuration, and assertions across your team’s machines. Windows users: follow the [ZIP download and checksum guide](https://github.com/Quality-Max/9lives-runner/blob/main/docs/install.md). Project dependencies and Chromium are separate; Windows provider CLI shell integrations remain unqualified. ## Recommended: Go runner for Playwright @@ -33,14 +33,14 @@ The installer uses a pinned, checksum-verified release and installs into a separ Alternatively, download a [prebuilt release](https://github.com/Quality-Max/9lives-runner/releases), or with Go 1.25.13 or newer: ```bash -go install github.com/Quality-Max/9lives-runner/cmd/9l@v0.1.1 +go install github.com/Quality-Max/9lives-runner/cmd/9l@v0.4.0 # Add your Go binary directory to PATH, then confirm which CLI you selected: 9l --version 9l plan tests/ 9l run tests/ --timeout 5m --format json ``` -Go requires an existing Playwright project with locally installed tooling and browsers. It provides planning, bounded execution, cancellation, and validated receipts. The optional SDK adds steps and bounded goals; see the [runner documentation](https://github.com/Quality-Max/9lives-runner/blob/main/docs/README.md). Go v0.3 has experimental native `9l heal` and native `9l mcp`. The website installer is pinned to v0.1.1, whose `heal` command delegates to this Python package; use the release downloads for v0.3 features. +Go requires an existing Playwright project with locally installed tooling and browsers. It provides planning, bounded execution, cancellation, and validated receipts. The optional SDK adds steps and bounded goals; see the [runner documentation](https://github.com/Quality-Max/9lives-runner/blob/main/docs/README.md). Go v0.4 has experimental native `9l heal`, native `9l mcp`, and the `9l scan` and `9l inspect` page checks. The website installer installs v0.4.0. ## Existing Python workflows @@ -119,7 +119,7 @@ claude mcp add 9lives -- uvx --from 9lives 9lives mcp The behavior-vs-drift guard applies to agents too: a failing assertion comes back as `needs-human`, with an explicit note that forcing it green would mask a real bug. `heal_test` only accepts existing test files under the directory `9lives mcp` was started in (`NINELIVES_MCP_UNRESTRICTED=1` lifts this), since healing a spec ultimately executes it. -Healing is half the loop — the spec has to come from somewhere. 9lives' sibling MCP server, [qmax-mcp](https://github.com/Quality-Max/qmax-mcp) (`npx -y @qualitymax/qmax-mcp`, MIT), covers the other half: scan a page for defects, inspect it for stability-ranked locators and a testability score, generate a Playwright repro, and run it. Same rules — local, free, no account. A spec born on qmax-mcp's stability-ranked locators is exactly the kind 9lives can keep alive when it drifts. +Healing is half the loop — the spec has to come from somewhere. The Go runner now covers the other half, with the checks that used to be the separate qmax-mcp server: `9l scan ` finds console, link, accessibility, Core Web Vitals, SEO, security-header, cookie, mixed-content and page-weight problems from one page load; `9l inspect ` ranks a page's locators by stability; and `9l mcp` gives coding agents `scan_url`, `inspect_page` and `generate_playwright_repro` beside its run, heal, assess and confirm tools. Same rules — local, free, no account. A spec built on stable locators is exactly the kind 9lives can keep alive when it drifts. See [Check the page you changed](https://github.com/Quality-Max/9lives-runner#check-the-page-you-changed). ## Heal on save & pre-commit diff --git a/install/9l.run.sh b/install/9l.run.sh index 80f2e7e..0481002 100755 --- a/install/9l.run.sh +++ b/install/9l.run.sh @@ -4,7 +4,7 @@ # Python compatibility: pip install 9lives / uv tool install 9lives. set -eu -VERSION="${NINELIVES_VERSION:-v0.1.1}" +VERSION="${NINELIVES_VERSION:-v0.4.0}" INSTALL_DIR="${NINELIVES_INSTALL_DIR:-$HOME/.local/share/9lives-runner/bin}" REPOSITORY="https://github.com/Quality-Max/9lives-runner" @@ -12,10 +12,10 @@ fail() { printf '9lives: %s\n' "$1" >&2; exit 1; } say() { printf '9lives: %s\n' "$1"; } case "$VERSION" in - *[!v0-9.]*) fail "NINELIVES_VERSION must be a stable tag such as v0.1.1" ;; + *[!v0-9.]*) fail "NINELIVES_VERSION must be a stable tag such as v0.4.0" ;; esac printf '%s\n' "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || - fail "NINELIVES_VERSION must be a stable tag such as v0.1.1" + fail "NINELIVES_VERSION must be a stable tag such as v0.4.0" case "$INSTALL_DIR" in /*) ;; *) fail "NINELIVES_INSTALL_DIR must be an absolute directory path" ;; @@ -109,6 +109,7 @@ say "add this directory to PATH, then check '9l --version' selects the Go runner quoted_dir="$(printf '%s' "$INSTALL_DIR" | sed "s/'/'\\\\''/g")" printf " export PATH='%s':\"\$PATH\"\n" "$quoted_dir" say "from an existing Playwright project: 9l plan tests/ && 9l run tests/" +say "check the page you changed: 9l scan https://your-app.dev --format markdown" say "install project dependencies and Playwright browsers separately" say "guide: https://quality-max.github.io/9lives-runner/" say "Python compatibility remains available via '9lives' or 'python -m ninelives.cli'" diff --git a/tests/test_website.py b/tests/test_website.py index e680b9b..1aa255c 100644 --- a/tests/test_website.py +++ b/tests/test_website.py @@ -16,7 +16,6 @@ def test_landing_page_keeps_install_and_ecosystem_links(): assert "https://github.com/Quality-Max/9lives-runner" in page assert page.count('href="https://qualitymax.io"') == 2 for destination in ( - "https://qmax.run", "https://aiinqa.com/ai-in-qa-issue-18/", "https://github.com/Quality-Max/qmax-code", "https://github.com/Quality-Max/qualitymax-grader", @@ -25,6 +24,23 @@ def test_landing_page_keeps_install_and_ecosystem_links(): assert destination in page +def test_landing_page_presents_the_page_checks_built_into_the_runner(): + page = (WEBSITE / "app" / "page.tsx").read_text() + + assert "https://qmax.run" not in page + assert "9l scan https://your-app.dev --format markdown" in page + assert "claude mcp add 9lives -- 9l mcp" in page + for tool in ("scan_url", "inspect_page", "generate_playwright_repro", "confirm_finding"): + assert tool in page + + +def test_installer_defaults_to_the_release_with_page_checks(): + installer = (ROOT / "install" / "9l.run.sh").read_text() + + assert 'VERSION="${NINELIVES_VERSION:-v0.4.0}"' in installer + assert "9l scan" in installer + + def test_site_uses_the_original_qualitymax_favicon(): layout = (WEBSITE / "app" / "layout.tsx").read_text() diff --git a/website/app/globals.css b/website/app/globals.css index d8c9f74..6fed736 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -80,7 +80,7 @@ code { font-family: var(--font-geist-mono), monospace; } .hero-note a, .compatibility-copy a { color: var(--cyan); text-decoration: underline; text-underline-offset: 3px; } .compatibility-copy { max-width: 820px; color: var(--muted); line-height: 1.7; } #python .inline-command { margin: 24px 0; } -.command-stack .command-card:last-child { grid-column: 1 / -1; } +.command-stack .command-card:last-child:nth-child(odd) { grid-column: 1 / -1; } .featured-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 20px; } .featured-link { display: inline-flex; align-items: center; gap: 5px; padding: 8px 12px; border: 1px solid rgba(85, 230, 255, .18); border-radius: 999px; color: #9f9ab5; background: rgba(85, 230, 255, .04); font-size: 12px; transition: border-color .2s, color .2s, background .2s; } .featured-link strong { color: #dbd7ea; } @@ -121,8 +121,7 @@ code { font-family: var(--font-geist-mono), monospace; } .ecosystem-section { padding-bottom: 120px; } .ecosystem-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; } -.ecosystem-card { position: relative; grid-column: span 2; min-height: 230px; padding: 24px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); transition: border-color .2s, translate .2s, background .2s; } -.ecosystem-card:nth-child(4) { grid-column: 2 / span 2; } +.ecosystem-card { position: relative; grid-column: span 3; min-height: 230px; padding: 24px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); transition: border-color .2s, translate .2s, background .2s; } .ecosystem-card:hover, .ecosystem-card:focus-visible { translate: 0 -4px; border-color: rgba(157, 124, 255, .55); background: var(--surface-2); outline: none; } .ecosystem-card > span { color: var(--violet); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; } .ecosystem-card h3 { margin: 28px 0 10px; font-size: 22px; } @@ -161,7 +160,7 @@ footer .legal-note { grid-column: 1 / -1; } .step-card { min-height: 210px; } .step-card h3 { margin-top: 42px; } .ecosystem-grid { grid-template-columns: 1fr; } - .ecosystem-card, .ecosystem-card:nth-child(4) { grid-column: auto; } + .ecosystem-card { grid-column: auto; } .framework-strip { gap: 12px; font-size: 10px; letter-spacing: .08em; } footer { grid-template-columns: 1fr; justify-items: center; } footer p, footer > span { text-align: center; } @@ -180,3 +179,11 @@ footer .legal-note { grid-column: 1 / -1; } .feature-grid h3 { font-size: 20px; margin: 0 0 10px; } .feature-grid p { color: var(--muted); line-height: 1.7; margin: 0; } @media (max-width: 760px) { .feature-grid { grid-template-columns: 1fr; } } +#page-checks .step-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } +#page-checks .step-card h3 { font-size: 20px; overflow-wrap: anywhere; } +#page-checks .feature-grid { margin: 48px 0 40px; } +#page-checks .feature-grid h3 { font-size: 17px; } +#page-checks .inline-command { margin-bottom: 24px; } +@media (max-width: 1080px) { #page-checks .step-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 760px) { #page-checks .step-grid { grid-template-columns: 1fr; } } + diff --git a/website/app/page.test.tsx b/website/app/page.test.tsx index f1b3d7f..0fe9edc 100644 --- a/website/app/page.test.tsx +++ b/website/app/page.test.tsx @@ -52,6 +52,7 @@ describe('Go runner onboarding', () => { expect(screen.getAllByText('curl -fsSL https://9lives.run/install.sh | sh')).toHaveLength(2); expect(screen.getByText('export PATH="$HOME/.local/share/9lives-runner/bin:$PATH"')).toBeTruthy(); expect(screen.getByText('9l run tests/ --timeout 5m --format json')).toBeTruthy(); + expect(screen.getByText('9l scan https://your-app.dev --format markdown')).toBeTruthy(); expect(screen.getByRole('link', { name: 'Documentation' }).getAttribute('href')) .toBe('https://quality-max.github.io/9lives-runner/'); expect(screen.getByRole('link', { name: /github/i }).getAttribute('href')) @@ -62,3 +63,17 @@ describe('Go runner onboarding', () => { .toBe('https://github.com/Quality-Max/9lives/blob/main/docs/MIGRATING_TO_GO.md'); }); }); + +describe('Page checks', () => { + it('presents the verification loop and the nine checks built into the runner', () => { + render(); + + expect(screen.getByRole('heading', { name: 'From “looks done” to evidence it works.' })).toBeTruthy(); + for (const check of ['console', 'links', 'accessibility', 'performance', 'seo', 'security_headers', 'cookies', 'mixed_content', 'weight']) { + expect(screen.getByRole('heading', { name: check })).toBeTruthy(); + } + expect(screen.getByText('claude mcp add 9lives -- 9l mcp')).toBeTruthy(); + expect(screen.getByRole('link', { name: 'Page checks' }).getAttribute('href')).toBe('#page-checks'); + expect(screen.queryAllByRole('link').some((link) => link.getAttribute('href')?.startsWith('https://qmax.run'))).toBe(false); + }); +}); diff --git a/website/app/page.tsx b/website/app/page.tsx index ea1b768..0feb76d 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -4,6 +4,8 @@ import { CopyButton } from './copy-button'; const installCommand = 'curl -fsSL https://9lives.run/install.sh | sh'; const pathCommand = 'export PATH="$HOME/.local/share/9lives-runner/bin:$PATH"'; const runCommand = '9l run tests/ --timeout 5m --format json'; +const scanCommand = '9l scan https://your-app.dev --format markdown'; +const mcpCommand = 'claude mcp add 9lives -- 9l mcp'; const pythonCommand = 'uv tool install 9lives'; const runnerSite = 'https://quality-max.github.io/9lives-runner/'; const migrationGuide = 'https://github.com/Quality-Max/9lives/blob/main/docs/MIGRATING_TO_GO.md'; @@ -42,6 +44,41 @@ const memes = [ }, ]; +const pageChecks = [ + ['console', 'JavaScript errors, warnings, and failed requests'], + ['links', 'Broken and redirecting links'], + ['accessibility', 'Missing alt text, unlabelled controls, heading structure'], + ['performance', 'Core Web Vitals: LCP, CLS, TTFB, FCP'], + ['seo', 'Title and meta description'], + ['security_headers', 'CSP, HSTS, X-Content-Type-Options, Referrer-Policy'], + ['cookies', 'Cookie flags, third-party cookies, trackers before consent'], + ['mixed_content', 'HTTP resources and form actions on HTTPS pages'], + ['weight', 'Transfer size, requests, render-blocking and oversized assets'], +]; + +const verificationSteps = [ + { + number: '01', + title: 'scan_url — find what broke', + copy: 'Nine checks from one Chromium page load, with graded findings, reproduction steps, and suggested fixes.', + }, + { + number: '02', + title: 'inspect_page — see what a test can use', + copy: 'Page structure, accessible names, and ranked Playwright locators with a stability verdict for each control.', + }, + { + number: '03', + title: 'generate_playwright_repro — keep the failure', + copy: 'A deterministic, reviewable spec for a finding, written inside the project. Nothing runs until you choose.', + }, + { + number: '04', + title: 'run_test or confirm_finding — prove it', + copy: 'Execute the repro with validated receipts, or check that it fails before the fix and passes after it.', + }, +]; + const healingSteps = [ { number: '01', @@ -61,12 +98,6 @@ const healingSteps = [ ]; const ecosystem = [ - { - eyebrow: 'Find defects', - title: 'qmax-mcp', - copy: 'Scan a live page, inspect stable locators, and generate a Playwright reproduction.', - href: 'https://qmax.run', - }, { eyebrow: 'Ship in the cloud', title: 'QualityMax Cloud', @@ -101,6 +132,7 @@ export default function Home() { QualityMax