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/docs/marketing/meme-assets.md b/docs/marketing/meme-assets.md index bd76e8d..566e951 100644 --- a/docs/marketing/meme-assets.md +++ b/docs/marketing/meme-assets.md @@ -2,7 +2,7 @@ Created with the built-in image generation tool, using the approved Product Hunt thumbnail as the brand reference: fluffy black Qmax kitten, violet collar, cyan terminal tag, ivory background, and bold indigo typography. These are illustrated developer jokes, not product screenshots or benchmarks. -Production assets are 1024 × 1024 WebP at quality 84. The gallery uses explicit dimensions, lazy loading, descriptive alt text, and links to the full images. The comparison meme spans both desktop columns; mobile uses one column. +Production assets are 1024 × 1024 WebP at quality 84. The gallery uses explicit dimensions, lazy loading, descriptive alt text, and links to the full images. Hosted on **9lives.run only**. QualityMax's homepage has no meme gallery. The GitHub README links to the gallery. @@ -12,9 +12,6 @@ Hosted on **9lives.run only**. QualityMax's homepage has no meme gallery. The Gi | harry-potter | `website/public/memes/harry-potter-launch.webp` | | lotr | `website/public/memes/lotr-launch.webp` | | batman-catwoman | `website/public/memes/batman-catwoman-launch.webp` | -| distracted-team | `website/public/memes/distracted-team-launch.webp` | - -The comparison uses the official e2e logo from [TesterArmy's repository](https://github.com/tester-army/e2e/blob/main/docs/logo-black.png), retrieved on 2026-10-11. It identifies e2e by TesterArmy. The middle character is a Playwright developer in glasses and a hoodie carrying a laptop. The composition is an original cat parody of the distracted-boyfriend meme; it does not reuse the photograph. The older non-launch assets remain hosted to preserve existing social and email URLs. The email PNG is unchanged. @@ -37,11 +34,3 @@ Epic fantasy cat parody. Headline "ONE DOES NOT SIMPLY". Central black kitten we ### batman-catwoman Original feline superhero parody. Headline "THE BUTTON MOVED." Two panels. Left: fluffy black kitten wearing homemade bat-eared dark cape, exhausted at red FAIL laptop, caption "ANOTHER LONG NIGHT". Right: original black kitten with violet collar and cyan tag, tiny violet eye mask and flowing violet cape, confident beside green PASS laptop, caption "NINE LIVES". Bottom "Keep the assertion. Verify the fix." Footer "9lives.run". Original cats only, no official superhero symbols or human likenesses. - -### distracted-team - -Use case: compositing. -Edit target: image 1, the square distracted-boyfriend cat meme. Supporting insert: image 2 is the authentic official e2e logo. -Make the middle grey cat visibly a software developer: add neat thin dark glasses, a charcoal hoodie, and a slim closed laptop tucked under one front paw; preserve its turning pose and gaze toward the black 9lives kitten. Change the middle label from "Playwright team" to "Playwright developer" on two lines. -Use the official e2e logo from image 2 above the ginger cat at right, faithfully preserving its "/ e2e" shape, dark color, and proportions. Under the logo put "by TesterArmy" as one or two lines of large readable indigo text. Remove the previous "e2e by TesterArmy" text so there is no duplicate e2e. -Keep the left Qmax black kitten, violet collar, cyan terminal tag, fur, pose, face, label "9lives" unchanged. Keep the ivory street scene, square composition, heading "WHEN YOU ALREADY HAVE PLAYWRIGHT TESTS", bottom "Keep the tests. Repair the locator." and "9lives.run" unchanged. Match the original Qmax launch style. Text must be readable at small sizes; no extra words, metrics, logos or watermark. 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..616ca2e 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; } @@ -95,7 +95,6 @@ code { font-family: var(--font-geist-mono), monospace; } .meme-section { padding-top: 24px; } .meme-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } .meme-card { margin: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); } -.meme-card:first-child { grid-column: 1 / -1; width: 100%; max-width: 760px; justify-self: center; } .meme-card > a { display: block; } .meme-card > a:focus-visible { outline: 3px solid var(--cyan); outline-offset: -4px; } .meme-card img { display: block; width: 100%; height: auto; aspect-ratio: 1; } @@ -121,8 +120,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 +159,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 +178,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..bd6ea0d 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -4,18 +4,14 @@ 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'; const interactiveDemo = 'https://app.supademo.com/demo/cmv43czgb0031wi0jzgno1y8r?utm_source=link'; const memes = [ - { - file: 'distracted-team-launch-v2', - title: 'When you already have Playwright tests.', - alt: 'Distracted-boyfriend cat parody: the black Qmax kitten labeled 9lives approaches in the left foreground. Farther back on the right, the grey Playwright developer cat in glasses and a hoodie walks away upright on two paws, carrying a silver laptop under his arm and looking back toward 9lives. The ginger e2e by TesterArmy companion turns toward his face with widened eyes and a slightly open mouth, looking offended and surprised. Keep the tests. Repair the locator.', - copy: 'e2e by TesterArmy vs 9lives, through the eyes of a team with an existing Playwright suite. Try the runner on a real test and review the repair.', - }, { file: 'button-rename-launch', title: 'One button. A whole afternoon.', @@ -42,6 +38,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 +92,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 +126,7 @@ export default function Home() { QualityMax