tamash-playwright 0.13.0-beta.3 → 0.14.0

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.
package/CHANGELOG.md CHANGED
@@ -1,485 +1,480 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented here. Format loosely follows
4
- [Keep a Changelog](https://keepachangelog.com/); dates are when each version was published.
5
-
6
- ## [Unreleased]
7
-
8
- ### Added
9
-
10
- - **AI-powered analysis of a genuinely-failed test.** Healing has always deliberately left `expect()` untouched — silently "fixing" an assertion could mask a real defect. This adds a separate capability instead: once a test's retries are exhausted and it's still failing, classify *why* — `likely-defect`, `likely-wrong-locator`, `likely-timing-or-environment`, or `inconclusive` — with a short explanation naming what was actually found, so it's clear whether that's worth filing a bug for, fixing the test's selector, or investigating test stability. Covers any final failure, not just `expect()`: an action (click/fill/etc.) that healing already tried and reported on is analyzed too, folding healing's own diagnosis (provider, failure stage, its own reason for declining or for a replay that still failed) in as extra context for the classifier e.g. healing declining because nothing plausible existed anywhere on the page is itself a strong signal toward `likely-defect` that the raw error message alone doesn't carry — rather than skipping the action failure or re-deriving everything from scratch. One unified verdict per genuinely-failed test, whatever kind of failure it was, not two separate, potentially conflicting explanations of the same thing. `likely-wrong-locator` is checked first, deliberately, ahead of `likely-defect`: it's the one verdict the AI is uniquely positioned to catch that skimming a plain error message would miss — the full ARIA snapshot is checked for something that plausibly matches under different text/structure before concluding the app itself is broken. On by default (`FAILURE_ANALYSIS_ENABLED`, same polarity as `HEALER_ENABLED` — unset or anything but `false`/`0` leaves it on; independent of `HEALER_ENABLED` itself, so turning healing off doesn't also silence this) — no reporter to register, no config file to edit: it's a `test.extend()` auto-fixture, the same mechanism `bindContext`/`bindPageActions`/`bindBrowser` already use, so it runs automatically the moment `test` is imported from this package. Chose default-on over mirroring `HEALER_ACTION_RECOVERY_ENABLED`'s opt-in-off polarity deliberately: this is a standalone capability, not a second-order escalation on top of an already-running mechanism the way action-recovery is — structurally closer to `HEALER_ENABLED` itself. The real tradeoff: unlike healing, which only spends tokens on a genuine action failure and is opportunistic about it (most actions succeed), this spends on *every* genuinely-failed test regardless of outcome — a red run with several broken tests means several AI calls, every run, until each is fixed. Each call's usage is its own `failure-analysis-tokens-used` annotation, deliberately a different type from healing's own `llm-tokens-used`, so the two costs can be told apart and weighed against each other rather than requiring free-text parsing to separate. (An earlier draft of this shipped as a standalone Playwright Reporter instead — reverted before release: it would have meant a *second* reporter line for anyone already running `tamash-playwright-dashboard`, on top of a real bug found only by actually running it — a fire-and-forget async call inside `onTestEnd` was silently abandoned when the process exited before the AI call resolved, since that hook's return type is strictly `void`, never awaited.) Reuses whichever `HEALER_PROVIDER` is already configured; `tamash` always declines (no reasoning capability). Exactly one AI call per genuinely-failed test when retries are configured at the project level (`playwright.config.ts`'s own `retries:`, or `--retries`) — the common case; a per-file `test.describe.configure({ retries })` override isn't visible to a fixture, so that specific case falls back to one call per attempt instead of one overall, confirmed safe (no crash, no wrong verdict) but not exactly-once. Reported the same way healing's own reports already are (`attachReportToRunningTest`, `healer/index.ts`): a `failure-analysis` annotation summarizing the verdict, the token-usage annotation described above, and a JSON attachment with the full attempt history — all visible in Playwright's own HTML report, not just the console. Deliberately no separate results log file: unlike `heals.jsonl` (a real cache, and read by `apply-heals`), nothing consumes one for this feature, and `tamash-playwright-dashboard` already reads healing's own reports straight off the test result's attachments rather than `heals.jsonl` — confirmed directly against its source (`reporter-helpers.ts`'s `extractHealReports`) — so a future dashboard integration needs no separate file either. Reads the exact same ARIA snapshot Playwright itself already captures at failure time for its own "Copy prompt for AI" feature when available, falling back to a live snapshot (the page is still open from a fixture, unlike from a Reporter) otherwise. Same non-negotiable property as healing itself: `expect()` is never touched, so this can only ever add information next to a failure that already happened, never change pass/fail.
11
-
12
- Four real correctness bugs found and fixed via deliberate corner-case testing after the initial implementation, not caught by the happy-path tests alone: (1) the per-attempt working log was originally keyed by `testFile:testLine`, mirroring `heal-log.ts`'s own keying for a locator's call site — but that does NOT uniquely identify a *test*, since a loop generating parameterized tests (`for (const x of [...]) test(...)`) produces several tests sharing the exact same source line; four such tests failing concurrently corrupted each other's attempt history (one ended up with 5 attempts pulled from 3 different tests) before switching the key to `testInfo.testId` (confirmed stable across retries, genuinely unique even when the line collides). (2) A flaky test — fails once, recovers on retry — used to leave that failed attempt's row behind forever, since cleanup only happened after a genuine final-attempt analysis, which a recovered test never reaches; fixed by clearing a test's own rows on the passed branch too. (3) `testInfo.status !== 'passed'` fires for *any* reason a test fails, not just a genuine `expect()` assertion — a plain action (click/fill) that healing already tried and reported on (`self-heal-failed` already pushed onto `testInfo.annotations` synchronously, mid-test-body, by healing's own catch block) reached this same fixture and got explained a second, redundant time by an AI call reasoning about the same failure with strictly less context. An intermediate fix skipped analysis whenever `self-heal-failed` was already present, scoping the whole feature to `expect()` only — reconsidered before release: that meant an action failure never got the "is this worth filing a bug for" verdict either, just healing's own (different) diagnosis of why the *locator* couldn't be fixed. Landed instead as described above — analyze every final failure, folding healing's own diagnosis in as context — which needed the same stale-row cleanup discipline as bug (2), just reached via a different path: a mixed sequence (attempt 0 a genuine `expect()` failure, attempt 1 an action failure) must still end up with both attempts in the one final analysis, and the working log fully cleared afterward either way. (4) `readFailureAttempts` rebuilt each entry via an explicit field list (`{ retry, errorMessage, ariaSnapshot }`) that predated the `healingAttempt` field added for bug (3)'s fix — `appendFailureAttempt` wrote it to disk correctly the whole time, but every *read* silently dropped it again, so an action failure's healing context never actually reached the AI despite being captured; fixed by adding `healingAttempt` to that same explicit list, now covered by a unit test asserting the round trip specifically (not just that *a* value round-trips, since that would have passed before this fix too, given the other fields did).
13
-
14
- ### Fixed
15
-
16
- - **A context/page built off `browser` in `test.beforeAll` (shared across tests) was never healing-aware.** Only the `context`/`page` fixtures were wrapped with the healing proxy a consumer using `test.beforeAll(async ({ browser }) => { context = await browser.newContext(); page = await context.newPage(); })` to log in once and reuse the session across every test in a file bypassed both, so nothing built that way ever healed. Found via a real support report. Fixed: the `browser` fixture is now wrapped too (`bindBrowser()`, `src/bindings/locator.binding.ts`) `browser.newContext()`, `browser.newPage()`, and `browser.contexts()` all now hand back healing-aware objects, exactly like `context.newPage()` already did. Doesn't cover a browser obtained entirely outside the fixture system (e.g. `chromium.launch()` in `globalSetup`) see "What else it heals" in README.md/usage.md for the manual `bindContext()`/`bindPageActions()` escape hatch for that case.
17
-
18
- ### Docs
19
-
20
- - **README.md rewritten for readability — 666 lines down to ~220, no content lost.** The published npm listing had grown into dense, run-on paragraphs padded with self-qualifying phrases ("confirmed live," "confirmed by real testing, not just docs") — an audit-trail tone that belongs in this CHANGELOG and RELEASE-TESTING.md, not in a first-time visitor's landing page. Rewritten with short sentences and a plain, confident tone; the top-level "get started" flow (install → connect a provider → set `actionTimeout` → use it) now reads in under a minute instead of being buried under ~230 lines of provider-auth edge cases and a 110-line raw GitHub Actions YAML block. Nothing was deleted: the subscription-provider deep-dives (Claude/Copilot/Cursor/Kiro/Codex, including the Copilot org-vs-personal-token gotcha) moved into usage.md's "2. Connect an AI model" section as new subsections (it hadn't documented them at all before — README was the only copy), and README now links out to usage.md's existing sections for CI YAML, caching, and everything else usage.md already covered in full. Also fixed a real gap found while doing this: the `.env`/`usage.md` "pick one" provider comment was missing `cursor-subscription`/`kiro-subscription`/`codex-subscription` even though all three are real, documented providers.
21
-
22
- - **README.md/usage.md/feature.md now lead with the actual rule, not just examples of it.** The `browser`-in-`beforeAll` fix above was documented as a bullet point alongside popups/iframes, which left the real boundary implicit: *any* browser/context/page from Playwright's own fixtures heals automatically, however many hops away and however it's reused, while anything built outside the fixture system (`chromium.launch()` directly, or `globalSetup`) still needs manual `bindContext()`/`bindPageActions()`. Now stated as the first thing in "What else it heals," not inferred from bullets.
23
-
24
- ## [0.12.0] - 2026-09-13
25
-
26
- ### Added
27
-
28
- - **Playwright 1.63 support.** Dev/CI now tracks `@playwright/test` 1.63; `peerDependencies`
29
- stays `>=1.40.0`. Verified: existing iframe healing (`page.frameLocator('#id')`) still heals,
30
- and the full unit + representative e2e suite passes against 1.63.
31
-
32
- - **Healing through Playwright 1.63's no-argument `page.frameLocator()`.** 1.63 made
33
- `frameLocator()`'s selector optional — with no argument it matches inside *any* frame on the
34
- page. As a healing scope that's ambiguous the moment the page has more than one frame (the
35
- healer's `locator('body')` snapshot then throws *"frameLocator() matched elements in multiple
36
- frames"*, and healing silently no-op'd with `stage=no_snapshot`). Now, when the page has
37
- exactly one frame, the healer re-expresses the no-arg `frameLocator()` as an explicit
38
- single-frame `FrameLocator` and heals normally including deriving a **durable, persistable**
39
- cross-frame selector (`apply-heals` can write it back to source), identical to what
40
- `page.frameLocator('#id')` produces. With zero or several frames it can't know which was meant,
41
- so it steps aside cleanly the original error is re-thrown, never a wrong-frame guess. For
42
- healable work on a multi-frame page, pass an explicit selector: `page.frameLocator('#id')`.
43
-
44
- *(`0.12.0-beta.1` collapsed the scope to a raw `page.frames()` `Frame` instead of a
45
- `FrameLocator`. `Locator.normalize()` on a `Frame`-rooted locator returns a correct selector
46
- string but an object that resolves to nothing, so the heal worked once at runtime but couldn't
47
- be persisted it fell back to a transient one-shot element reference with a `needsReview`
48
- note. Fixed here.)*
49
-
50
- - **Documented [`tamash-playwright-dashboard`](https://www.npmjs.com/package/tamash-playwright-dashboard).**
51
- A separate, zero-config reporter package: pass-rate trends, per-test history across runs, and —
52
- specific to this package a Self-Healing Analytics page (tests/elements healed, token usage
53
- per run and cumulatively, every heal event across recorded history), read directly from the
54
- `self-healing-<action>` JSON attachment this package already writes. No code change on this
55
- side — README and usage.md gained a "Trends across runs" section pointing to it.
56
-
57
- ### Fixed
58
-
59
- - **`describeFactoryCall` rendered `iframe "undefined"` for a no-arg `frameLocator()`.** The
60
- factory-call label helper assumed a selector argument was always present; with 1.63's optional
61
- selector it produced the literal string `iframe "undefined"`. Now renders `any iframe`. (Latent
62
- — the label isn't currently surfaced to users or the model, but it would be the moment any
63
- "healing inside iframe X" context is added to a report or prompt.)
64
-
65
- ## [0.11.0] - 2026-08-31
66
-
67
- ### Fixed
68
-
69
- - **`skills/tamash-playwright/references/heal.md` never told the agent the Playwright HTML report exists at the point it matters.** It only pointed at `npx playwright show-report` at the very end (the REPORT step), after the loop was already finished — not right after RUN, where the report's per-attempt annotation and JSON attachment (provider, vision/action-recovery involvement, suggested selector, token cost, failure stage) would actually help decide what to do next. Found by directly auditing the skill against every documented user workflow step, not by running it. Fixed: RUN now explicitly says what the report contains and when it's worth opening.
70
-
71
- - **`doctor`'s connectivity check gave false confidence for `claude-subscription`/`copilot-subscription`.**
72
- It tested against a fixed, generous 15s timeout, completely decoupled from the project's real
73
- configured `actionTimeout` — so a project using the README's own example value (`actionTimeout:
74
- 8000`) could get `[OK] Connected successfully` from `doctor`, then have its very first real heal
75
- in a fresh test run fail with a misleading "not authenticated?" warning, because these two
76
- providers spawn the vendor's own CLI as a subprocess and the first call in a process pays a
77
- one-time cold-start cost (spawning + authenticating) on top of the actual model call — a cost
78
- `doctor`'s own generous timeout never had to absorb. Found via a real, unbriefed agent walkthrough
79
- of a from-scratch setup (see the skill entry below): it hit this exact failure, correctly
80
- diagnosed the root cause from a source-code comment nothing user-facing ever surfaced, and worked
81
- around it by raising `actionTimeout` to 20000ms. Fixed at the actual source: `doctor` now tests
82
- connectivity within the project's real `actionTimeout` when one's configured (falling back to the
83
- old generous default only when none is set yet), and a `[FAIL]` for either of these two providers
84
- now explicitly suggests it may be the cold-start cost rather than a real auth problem. Verified
85
- against the real `copilot-subscription` provider confirmed the check now genuinely uses the
86
- configured timeout (not just that it compiles); could not force a deterministic before/after
87
- repro of the timeout itself in this environment, since the underlying race is timing/environment-
88
- dependent (a warm local CLI session can mask it, exactly as the original cold-start comment
89
- already described).
90
-
91
- - **The published package was silently carrying stale, orphaned compiled output.** `npm run build`
92
- (plain `tsc`, no `--build`/incremental mode) never deletes `dist/` output for a source file that
93
- was later removed found in the `0.11.0-beta.1` publish itself: `dist/healer/providers/
94
- claude-cli.js`/`copilot-cli.js` shipped in the tarball with no corresponding source file
95
- anywhere in the repo (from an abandoned, never-committed, never-wired-up piece of past work) —
96
- dead code nothing actually imports, but real, needless bloat in every install. Fixed by cleaning
97
- `dist/` automatically before every build (`prebuild` script), verified by confirming a full
98
- rebuild no longer produces those two files.
99
-
100
- - **A popup/new tab opened via `context.waitForEvent('page')`, `page.on('popup', ...)`, or
101
- `page.waitForEvent('popup')` was never healing-aware.** `bindContext`/`bindPageActions` already
102
- made `context.newPage()`/`context.pages()`/`context.on('page', ...)` heal correctly, but these
103
- three including `context.waitForEvent('page')`, the pattern most commonly taught for popup
104
- handling fell through to a raw, unwrapped Playwright page. A broken locator inside the
105
- resulting page just threw a plain timeout, with no `[self-healer]` line at all: healing was never
106
- attempted, not just unsuccessful. Found while building the first-ever test for popup healing
107
- (previously implemented with zero coverage); fixed by extending both bindings to wrap whatever
108
- page these three hand back, the same way `newPage()` already did.
109
-
110
- - **`tamash` (the rule-based provider) declined on a real, common page pattern it should have
111
- resolved.** A floating label rendered as its own text node right next to a field whose accessible
112
- name is *also* that same text (confirmed on a real login page: a "Username" label next to a field
113
- that's itself named "Username") was miscounted as two competing candidates and declined, even
114
- though there's genuinely only one field. `findRuleBasedMatch` now only treats a match as a real
115
- competing candidate when it's independently resolvable on its own (has a ref and a plausible
116
- role) a bare text node repeating the same words is decorative, not a second option. Genuine
117
- ambiguity (two actually-independent candidates) still declines exactly as before.
118
-
119
- - **The HTML report's self-healing attachment was a wall of escape sequences on a failed
120
- heal.** Playwright's own error messages embed ANSI colour codes in their "Call log" section;
121
- the healer stored those raw in `report.reason` / `report.warning` / `attempts[].error`, and
122
- `JSON.stringify` (which builds the `self-healing-<action>` attachment) escapes every ESC byte in the JSON, so a *not-healed* report rendered as unreadable escape soup. A
123
- *successful* heal has short or absent error strings, which is why it only showed up on failures.
124
- Fixed by stripping ANSI CSI sequences in `normalizeError` — the single point every captured
125
- error passes through so the attachment, the console line, and `heals.jsonl` are all plain
126
- text. Verified against a real not-healed run.
127
-
128
- - **The three CLI-based subscription providers (`cursor-subscription`, `kiro-subscription`,
129
- `codex-subscription`) were verified against their real vendor CLIs for the first time — and none
130
- of the three actually worked as shipped.** Four separate bugs, all found by running them, all now
131
- fixed:
132
- 1. **`cursor-subscription` never returned a heal.** `agent -p` refuses to run in any directory
133
- without workspace trustit prints "Workspace Trust Required" and exits without answering.
134
- Now passes `--trust --mode ask`; `--mode ask` is Cursor's own read-only Q&A mode ("read-only"
135
- per its `--help`), which *removes* the old "has access to all tools, could edit a file"
136
- caveat this provider carried a heal call in `ask` mode cannot write or run anything.
137
- 2. **`codex-subscription` refused to run outside a Git repository** ("Not inside a trusted
138
- directory and --skip-git-repo-check was not specified"). Now passes `--skip-git-repo-check`;
139
- this only relaxes the where-may-I-run guard, not the sandbox `codex exec` still runs
140
- `approval: never` + read-only, the property this provider depends on.
141
- 3. **`codex exec` hung on stdin.** It drains stdin ("Reading additional input from stdin") and
142
- blocks until EOF; `runCliPrompt` left the child's stdin as an open pipe, so every call ran to
143
- the timeout and was killed confirmed: a prompt the CLI answers in ~8s "timed out" at 15s.
144
- `runCliPrompt` now gives every child an already-closed stdin (`stdio: ['ignore', …]`),
145
- harmless for `agent`/`kiro-cli`, the fix for `codex`.
146
- 4. **The JSON parsers failed on conversational output.** `parseSuggestion` /
147
- `parseVisionSuggestion` / `parseActionTacticSuggestion` extracted JSON from prose with a
148
- greedy first-`{`-to-last-`}` slice, which breaks the moment the response contains more than
149
- one JSON object exactly what cursor's `agent` produces (it echoes the system prompt's
150
- example objects back before its real answer). Replaced with a balanced-brace-span scan that
151
- takes the last valid object (a model states its answer last), old greedy slice kept as a last
152
- resort. Improves robustness for every provider, not just the CLI ones.
153
-
154
- Verified live, all authenticated: `doctor` reports `[OK] Connected` for all three; a real
155
- `npx playwright test` heal (broken locator, cache cleared) went green with
156
- `HEALED [provider=codex-subscription]`, and `kiro-subscription` healed the same spec.
157
-
158
- - **`cursor-subscription` reliability note.** Verifying it (see above) showed Cursor's `agent` CLI
159
- is an interactive assistant rather than a one-shot completion endpoint given the heal prompt it
160
- often answers conversationally instead of with the required JSON, so the heal is declined more
161
- often than with `kiro`/`codex`. It's now invoked in the safest/best-effort form
162
- (`agent -p --trust --mode ask --output-format json`, read-only) and is marked **experimental**;
163
- the docs point to `kiro`/`codex` as the steadier local-subscription options. Report issues.
164
- Also: the full e2e healing suite (12 tests) was run end to end against `copilot-subscription` and
165
- `ollama` — **12/12 each** — to confirm the parser rewrite and provider changes don't regress it.
166
-
167
- ### Added
168
-
169
- - **`npx tamash-playwright init-skill` one command to install the orchestration skill, plus a
170
- `doctor` check that flags when it's missing or stale.** The skill this package ships
171
- (`skills/tamash-playwright/`) was previously a copy-this-shell-one-liner-per-agent step buried in
172
- `SKILL.md`, with nothing verifying it had been done — so a project could `npm install` the
173
- package, never copy the skill, and its assistant would silently never load the workflow.
174
- `init-skill` copies the skill (`SKILL.md` + `references/`) into **both** standard locations —
175
- `.claude/skills/tamash-playwright/` (Claude Code) and `.agents/skills/tamash-playwright/` (the
176
- emerging cross-tool standard, read by Cursor, GitHub Copilot, Windsurf, Kiro, Zed, dotnet Aspire,
177
- the same convention Playwright's own `playwright-cli install --skills` uses). Same content in
178
- both; no per-agent format conversion. `--target claude` / `--target agents` installs one; `--user`
179
- installs under your home directory; `--force` overwrites a hand-edited copy; `--dry-run` previews.
180
- Each install carries a `tamash-playwright-skill-version:` marker; `doctor`'s **Skill** section
181
- reads it and reports `[OK]` current / `[WARN]` behind-the-package-version / `[INFO]` not-installed
182
- or unmanaged, with the same one-line fix. Covered by unit tests (`skill.test.js`, real installs
183
- into a temp dir) and verified live end to end.
184
-
185
- *(0.11.0-beta.7 shipped an earlier `init-skill` that instead auto-detected the agent and wrote a
186
- Cursor-specific `.mdc` file / appended blocks to `.github/copilot-instructions.md` and
187
- `AGENTS.md`. That approach is gone `init-skill` and `doctor` now point out any such leftovers
188
- from a beta.7 install so you can delete them by hand.)*
189
-
190
- - **`doctor`'s AI Provider check now tells you *what kind* of failure it hit and what to do about
191
- it, instead of one generic "no valid response".** Every provider gained an optional, diagnostics-
192
- only `diagnose()` method (never on the healing hot path) that runs one trivial round trip and
193
- reports a structured category `not-installed`, `not-authenticated`, `timeout`, `bad-model`,
194
- `network`, `bad-response` each of which `doctor` maps to a specific next step: the missing
195
- `npm install`/CLI installer for a missing SDK; "run `claude login` / check your API key / confirm
196
- your subscription is active and within quota" for a rejected request; "raise `actionTimeout`" (plus
197
- the existing subprocess cold-start note) for a timeout; "check `<MODEL>` in your .env" for a
198
- rejected model id; a network/proxy/base-URL hint for a connection failure. The raw error line is
199
- still printed verbatim beneath the guidance. Previously `doctor` could only distinguish "worked"
200
- from "didn't" a missing SDK, an expired login, a slow network and a wrong model name all
201
- produced the same line. Providers that don't implement `diagnose()` fall back to the old
202
- `suggestSelector()` probe (worked/didn't, plus a timing-based timeout guess). Verified live:
203
- `openai` bad key -> `not-authenticated` (401 body shown), `openai` bad base URL -> `network`,
204
- `copilot-subscription` bad model id -> `bad-model`, 1ms `actionTimeout` -> `timeout` with the
205
- raise-the-timeout guidance, and all three not-installed shapes by moving the dependency out of
206
- resolution and restoring it: `@github/copilot-sdk` (CJS `MODULE_NOT_FOUND`),
207
- `@anthropic-ai/claude-agent-sdk` (ESM `ERR_MODULE_NOT_FOUND`), and a missing `kiro-cli` binary
208
- (`ENOENT`) -- each surfacing `not-installed` with its exact install command.
209
-
210
- - **Three new local-development-only subscription providers: `cursor-subscription`,
211
- `kiro-subscription`, `codex-subscription`.** Extends the same "use what you're already paying
212
- for" idea `claude-subscription`/`copilot-subscription` already give, to three more real
213
- subscriptions but each is explicitly scoped to local development, never documented or
214
- recommended for CI, for two genuinely different reasons found by checking real vendor docs before
215
- writing any code. Cursor's and Kiro's own headless-mode docs confirm neither has a way to fully
216
- disable tool/file/command access the way `claude-subscription`'s `tools: []`/`copilot-subscription`'s
217
- `availableTools: []` do Cursor's `agent -p` "has access to all tools" by its own docs, and Kiro's
218
- headless mode requires `--trust-all-tools`/`--trust-tools` specifically because there's no one to
219
- approve a tool call unattended; `kiro-subscription` deliberately never grants that trust, so a
220
- tool-requiring response just times out and declines rather than being granted broad access.
221
- `codex-subscription` is local-only for a different, simpler reason: `codex exec` (used here, not
222
- the interactive REPL) defaults to a read-only sandbox with no approval prompts — already as safe
223
- as the two existing subscription providers but no long-lived, subscription-only token for
224
- unattended CI use is confirmed to exist for it yet. None of the three vendors ship a Node SDK, so
225
- all three are `child_process` wrappers around the vendor's own CLI binary (`agent`, `kiro-cli`,
226
- `codex`) via a new shared `runCliPrompt` helper, rather than the SDK-based design the existing two
227
- use meaning every call pays a fresh process-spawn cost, not just the first the way
228
- `copilot-subscription`'s warm shared client does; `doctor` now has a distinct hint explaining this
229
- when one of these three fails within a tight `actionTimeout`. Investigated and explicitly ruled
230
- out this round: Antigravity its dedicated `google-antigravity` SDK is Python-only (this is a
231
- Node/TypeScript package), has no documented tool-restricted mode at all, and its authentication
232
- story isn't published, so it isn't even confirmed to ride on an existing subscription. Verified:
233
- real graceful-decline behavior confirmed against genuinely uninstalled CLIs (no fabricated
234
- simulation), and the full `doctor` integration (including the new every-call-cost hint) confirmed
235
- against a real sample repo. **Not yet verified**: the actual happy path against a real installed
236
- and authenticated CLI for any of the three none were available in the development environment,
237
- and all three require a real paid subscription account.
238
-
239
- - **A skill for running this package's local workflow inside an AI coding assistant.** Ships at
240
- `skills/tamash-playwright/` a `SKILL.md` entry point that branches on `npx tamash-playwright
241
- doctor`'s actual output (never assumed), plus two reference docs: `onboarding.md` (bringing a
242
- project up to standard provider setup, `actionTimeout`, `.describe()` labels, Page Object
243
- extraction) and `heal.md` (a gated review/apply/verify/land loop over `apply-heals` and
244
- `verify-heals.cjs`most runs proceed start to finish unattended, pausing only for a genuinely
245
- ambiguous fix or anything after a failed verification, and never landing/committing/opening a PR
246
- without asking first, no matter how clean the run was). Pure orchestration over commands that
247
- already exist no new healing capability. Ships with adapters for Claude Code, Kiro (identical
248
- `SKILL.md` format, confirmed against Kiro's own docs), Cursor (`.mdc` rule), GitHub Copilot
249
- (`copilot-instructions.md` section), and an `AGENTS.md` covering the broader cross-tool standard
250
- (Antigravity, Gemini CLI, Windsurf, Zed, Aider, and others) none of which are auto-discovered
251
- from `node_modules` by any of these tools, confirmed rather than assumed, so every adapter's
252
- install step is one explicit copy command, documented in `SKILL.md` itself.
253
- - **A new `tamash` heal provider rule-based healing, no AI at all.** `HEALER_PROVIDER=tamash`
254
- needs no API key, no subscription, and makes no network call: it resolves a broken locator by
255
- text-matching the same description an AI provider would receive (`.describe()`, or a decoded
256
- variable name see below) against the already-captured accessibility snapshot, then reuses the
257
- exact same structural widening (`near`/`adjacent`) the AI-backed path already uses once it finds
258
- the right anchor same output shape, same downstream code, zero duplicated logic. It shares the
259
- same non-negotiable discipline as every text-matching step in this package: zero or more than one
260
- match, at any point, means it declines rather than guesses verified with a dedicated permanent
261
- e2e suite covering direct matches, widened matches, and every decline path (including the known,
262
- accepted limitation that a misleading type hint can't be second-guessed against). Genuinely a
263
- different tool than the AI providers, not a free replacement for one: no vision fallback (nothing
264
- to reason over a screenshot with) and no action-recovery tactics (those require understanding
265
- *why* an action failed, which is inference this provider deliberately doesn't attempt) a fast,
266
- free, fully deterministic first line of defense, best suited to well-`.describe()`d,
267
- Page-Object-style suites.
268
- - **A new `ollama-local` heal provider for self-hosted Ollama servers.** `HEALER_PROVIDER=ollama-local`
269
- targets your own `ollama serve` instance or an internal company deployment, instead of Ollama
270
- Cloud a deliberately separate provider from `ollama`, not a flag on it, since the two have
271
- genuinely different auth defaults: Ollama Cloud always requires `OLLAMA_API_KEY`, while
272
- `OLLAMA_LOCAL_API_KEY` is optional, since a bare `ollama serve` has no authentication at all. Set
273
- it only if your internal deployment sits behind a reverse proxy or API gateway that requires a
274
- bearer token verified against a real HTTP server both ways, confirming the `Authorization`
275
- header is omitted entirely when no key is set, and sent correctly when one is. Prompted by a real
276
- support request from a team wanting to use their own internally-hosted `gpt-oss:120b`.
277
- - **Undescribed, POM-style locator variables now get a real description automatically.** When
278
- `.describe()` was never called, the healer already fell back to the locator's own variable/
279
- property name (`txtEmployeeId`) as its best guess at intent — now that raw identifier is decoded
280
- into the same kind of human-readable phrase `.describe()` would give: `txtEmployeeId` becomes
281
- "Employee Id (textbox)", `submitButton` becomes "Submit (button)", recognizing both prefix-style
282
- (`btnSubmit`) and suffix-style (`submitBtn`) naming, camelCase/snake_case/kebab-case, and correct
283
- acronym boundaries (`employeeIDNumber` "Employee ID Number"). Falls back to the raw identifier,
284
- exactly as before, whenever nothing meaningful survives decoding (a placeholder name like `el1`,
285
- or a bare affix with nothing else) never a guess dressed up as a real description. Verified
286
- live against a real, unpublished build in both sample repos (a real `copilot-subscription` call,
287
- and after finding and fixing a genuinely stale API key along the way — a real `ollama` call),
288
- confirming the decoded description actually reaches the configured AI provider unchanged.
289
-
290
- ## [0.10.0] - 2026-08-27
291
-
292
- ### Fixed
293
-
294
- - **`waitFor()` is never sent to the AI.** It's a state check, not an action — a timeout on it can
295
- mean a genuinely broken selector, or it can mean the element correctly never reached the expected
296
- state (verifying something does NOT appear, or a real app issue), and there's no way to tell those
297
- apart from the error alone. `expect(locator).toBeVisible()` was already permanently excluded from
298
- healing for exactly this reason, but never reached this code at all (a separate path from the
299
- Proxy-intercepted actions) `waitFor` just never got the same treatment. Real, reported case: a
300
- user's `waitFor` on a locator verifying an absence (where timing out was the *correct* outcome)
301
- still burned 8175 tokens across a failed text attempt and a failed vision attempt before giving
302
- up, for a heal that could never have succeeded. Now fails fast with a clear `state-wait-not-healed`
303
- stage and zero AI calls; a real action (fill/click/...) on the same kind of broken locator is
304
- unaffected.
305
- - **`claude-subscription` was burning a real, unnecessary amount of extra tokens.** The SDK's
306
- `effort` option silently defaults to `'high'` ("deep reasoning") when left unset, and this
307
- provider never set it a small prompt-complexity increase (the `nearbyRef`/`nearbyText` addition
308
- below) pushed adaptive thinking to reason unpredictably harder for a task that only ever needs to
309
- return one line of JSON. Confirmed live via real CI runs: output tokens for the same heal went
310
- from a 188-445 baseline up to 422-881, inconsistently, run to run. `thinking: { type: 'disabled' }`
311
- + `effort: 'low'` removes the variability entirely verified repeatedly at a steady 28-30 output
312
- tokens, below even the pre-regression baseline, with no change to correctness.
313
- - **The primary ariaSnapshot capture no longer requests `boxes:true`.** Every node was paying for a
314
- `[box=x,y,w,h]` annotation that nothing on the text/`ref` path (including the `nearbyRef`/
315
- `adjacent`-strategy widening logic below) ever reads — it's purely topological. The one real
316
- consumer (vision's own nearest-candidate lookup) already captures its own separate, fresh
317
- snapshot, so this is genuinely free: verified live, input tokens dropped ~23% on a large real
318
- page (3794 2937) with no loss of accuracy, and no change on small pages (box overhead scales
319
- with node count).
320
-
321
- ### Added
322
-
323
- - **Search-scoped snapshots**: before falling back to the full page, the healer now searches the
324
- already-captured snapshot for the description's identifying phrase and, only when it matches
325
- exactly one node, sends the AI a scoped excerpt (that node's own subtree plus every sibling
326
- branch's subtree at each ancestor level up to the root) instead of the whole page — zero extra
327
- browser round-trips, since it's pure processing on data already in memory. Falls back to the full
328
- snapshot automatically whenever the search is empty or ambiguous, never a guess. Verified live
329
- across every real failure mode found this session: 41% token reduction on a deeply-nested field,
330
- 66% when the target was inside what looked like an unrelated navigation menu (proving it finds
331
- wherever the relevant text actually is, not "excludes the nav"), ~29% on a pair of identical
332
- sibling fields that still had to be correctly disambiguated, and a clean, correct fallback when
333
- the description doesn't match the page's real text at all.
334
- - **A new `adjacent` selector strategy**, fixing a real ambiguity in the existing `near` strategy:
335
- when two fields with no identity of their own share a row/section (two dropdowns side by side,
336
- say), `near`'s "climb to a shared ancestor, then search it for any element of this role"
337
- approach matches both and gives up rather than risk the wrong one. The AI's `ref` response can
338
- now optionally report `nearbyRef`/`nearbyText`/`nearbyRole` for a nameless target it identified;
339
- `deriveDurableLocator` uses that hint to find the true common ancestor between the target and
340
- its label via each ref's own full ancestor chain, not by assuming either sits at a matching
341
- depth and, when they're proven to be immediate sibling branches, builds a precise CSS
342
- `:text() + *` sibling match (or an xpath climb-then-step, when the label text turns out to be
343
- nested below its own branch root). Verified live against a real configured provider, resolving
344
- the correct field and never its same-row neighbor in both directions.
345
- - **Full attempt-history logging**: `SelfHealingReport` now carries an `attempts[]` array one
346
- entry per cache/ref/text/vision/action-recovery attempt actually made, each with its own
347
- `succeeded`/`stage`/`error`. Previously only the *last* attempt's stage survived; an earlier
348
- attempt's real failure (and the specific error it threw) was silently discarded the moment a
349
- later attempt also failed — the exact shape of a real user-reported bug, where a genuine
350
- candidate selector was shown next to an unrelated `vision_provider_error` with no way to tell
351
- why the candidate itself hadn't worked. The attempt history now also prints directly to
352
- console/CI output (not just the JSON attachment) whenever more than one attempt was made, on
353
- both pass and fail — since that plain-text output is what most bug reports actually paste, not
354
- an attachment nobody opens.
355
- - **`ariaSnapshot` is attached to the test report on failure** — the exact accessibility tree the
356
- AI reasoned over, so a confusing report can be diagnosed against real evidence instead of a
357
- separately-captured DevTools screenshot.
358
-
359
- ## [0.9.0] - 2026-08-26
360
-
361
- ### Fixed
362
-
363
- - **Heal-log visibility**: a heal with no reusable selector (a one-shot `ref` resolution, or a
364
- vision-tagged point) was silently dropped from `heals.jsonl` entirely, even though it genuinely
365
- fixed that run. Now logged for audit with a clear `reviewNote` — whenever it has a suggestion
366
- *or* a review note, without letting an audit-only entry shadow an older, real cached fix for the
367
- same location.
368
- - **Vision support for `claude-subscription`/`copilot-subscription`**: both providers had
369
- `supportsVision` hardcoded to `false` regardless of model. Both SDKs genuinely support image
370
- input (Claude Agent SDK via an image content block; Copilot SDK via a `blob` attachment) —
371
- implemented for real and verified live with actual screenshots, not just doctor's yes/no label.
372
- - **Argument forwarding in healed replays**: `replayAction()` only ever forwarded the first call
373
- argument. A trailing options object (`click({ modifiers: [...] })`, `fill(value, { timeout })`,
374
- `dispatchEvent`'s 3rd argument) was silently dropped on a healed replay changing the action's
375
- real behavior while still reporting a clean `HEALED`.
376
- - **`ref`-strategy replays now act through the derived durable locator, not the raw ref**:
377
- `aria-ref=` locators never resolve for `dispatchEvent` at all (a Playwright-level limitation,
378
- confirmed directly via `DEBUG=pw:api`), even though the identical element resolves instantly via
379
- a `normalize()`-derived locator for that same action. Fixed by deriving the durable locator
380
- *before* replaying and acting through it, falling back to the raw ref only when nothing durable
381
- could be found. A related accuracy bug is fixed alongside it: the report/heal-log could
382
- previously claim a derived selector "worked" even when its own replay had actually failed and a
383
- fallback silently took over now only the locator that genuinely performed the action is
384
- reported or cached.
385
- - **`copilot-subscription` could hang a non-Playwright test runner indefinitely** — its shared
386
- client keeps a connection open across calls for performance, which Playwright's own test runner
387
- tolerates by force-exiting regardless, but a runner like Cucumber does not, so the process never
388
- returned even after every test had already passed. Found live running a real Cucumber suite in
389
- CI. `closeCopilotSubscriptionClient()` is now exported from the package's own entry point so any
390
- non-Playwright consumer can call it from their own teardown hook (e.g. Cucumber's `AfterAll`).
391
-
392
- ### Added
393
-
394
- - **`locator.getDurable(action?)`** resolves any locator (most usefully one built from
395
- `aria-ref=...`) to a durable, reusable equivalent (`getByRole`/`getByLabel`/a css selector/…),
396
- using the same derivation logic self-healing already uses internally. Throws if nothing durable
397
- could be derived, rather than silently handing back something untrusted.
398
- - **`apply-heals` now previews before writing**: every run — dry or real shows a styled table of
399
- exactly what it found (location, before, after, review status) instead of a plain scrolling log.
400
- A real run also asks for confirmation before writing anything, but *only* at a genuine
401
- interactive terminal CI and any non-interactive/piped invocation proceed automatically exactly
402
- as before, so no existing unattended CI workflow is affected. `--yes`/`-y` skips the prompt at a
403
- real terminal too.
404
- - **`doctor`'s output is styled** with colors and tables (including a new end-of-run Summary
405
- section) instead of a plain scrolling log.
406
- - **Exact locator code in console lines and reports**: a healed suggestion is now shown as the
407
- real, copy-pasteable Playwright call (`getByRole("textbox", { name: "Username" })`) instead of
408
- an abbreviated shorthand (`role:textbox:Username`) — the same code `apply-heals` would write to
409
- source, so what you see is never a lossier stand-in for what was actually used.
410
- - A canonical Page Object Model usage example (`tests/pages/` +
411
- `tests/example-orangehrm-add-employee.spec.ts`), verified live against a real OrangeHRM demo,
412
- including a genuine self-heal via the `near`/widening strategy on a real unlabeled field.
413
-
414
- ### Changed
415
-
416
- - README/usage.md's vision-fallback documentation now correctly lists `claude-subscription`/
417
- `copilot-subscription` as vision-capable (with a suitable model), not just the API-key providers.
418
-
419
- ## [0.8.0] - 2026-08-25
420
-
421
- ### Added
422
-
423
- - `claude-subscription` and `copilot-subscription` heal providers — self-healing backed by a
424
- personal Claude or GitHub Copilot subscription instead of a pay-per-token API key, working both
425
- locally and unattended in CI (`CLAUDE_CODE_OAUTH_TOKEN` for Claude; the ambient `GITHUB_TOKEN` or
426
- a personal-account PAT for Copilot on GitHub Actions).
427
-
428
- ### Fixed
429
-
430
- - Missing CLI install step in setup docs for `claude-subscription`/`copilot-subscription` — the
431
- standalone `claude`/`copilot` CLIs are genuinely required for the login step even though the SDKs
432
- don't need them installed to function at runtime.
433
-
434
- ### Documentation
435
-
436
- - The org-vs-personal-account Copilot licensing gotcha in CI (a repo owned by an organization
437
- without its own Copilot enablement can't use a personal subscription via the ambient token).
438
- - Action Recovery (`HEALER_ACTION_RECOVERY_ENABLED`) hidden from docs and `doctor` output —
439
- disabled by default already; this only reduced its visibility, no behavior change.
440
-
441
- ## [0.7.0] - 2026-08-21
442
-
443
- ### Added
444
-
445
- - `apply-heals` turns a runtime heal into a permanent source-code fix, plus an opportunistic
446
- cache (`heals.jsonl`) so a previously-confirmed selector is tried before a fresh AI call, and
447
- history archival so a run's report/log isn't silently overwritten by the next one.
448
- - Position/relationship-based locator strategies (`near`, `scoped`, `containing`) for elements with
449
- no accessible identity of their own.
450
- - Self-healing rebuilt around `ariaSnapshot({ mode: 'ai' })` + `aria-ref=` resolution and
451
- `Locator.normalize()`, replacing pixel-distance guessing for both the text path and the vision
452
- fallback's durability upgrade.
453
- - A real unit test suite (32 tests at the time, zero new dependencies).
454
-
455
- ### Fixed
456
-
457
- - `doctor`'s `actionTimeout` check being fooled by a commented-out config value.
458
- - `apply-heals --logs-dir` silently losing raw heal-log archival.
459
- - `apply-heals` only replacing the first call when re-healing an already-`near`-fixed line.
460
- - `apply-heals` `ENOENT` on a fresh CI checkout with no `.tamash-playwright/` directory yet — the
461
- sharded "apply-heals" CI job checks out fresh and has never run tests itself, so the directory
462
- genuinely doesn't exist there the way it always does in every local recipe.
463
- - File path corruption (and a `require()` crash in the generated verification script) for ESM
464
- (`"type": "module"`) consumer projects — `Error.stack` renders as a `file://` URL there, which
465
- broke path resolution and a plain `.js` verification script alike.
466
- - The AI being misled by the broken selector still present in its own prompt context; a failed
467
- label guess now auto-upgrades to a structural `near` match instead of giving up.
468
-
469
- ### Verified
470
-
471
- - All four API-key providers (Ollama, OpenAI, Anthropic, Gemini) confirmed working with real API
472
- calls, not just documented.
473
-
474
- ## [0.6.0] - 2026-08-17
475
-
476
- ### Added
477
-
478
- - iframe and popup healing, vision fallback (screenshot-based recovery when text alone isn't
479
- enough), AI-driven action recovery (scroll/force/wait/dispatch), and source-location reporting.
480
- - `doctor`'s `actionTimeout` configuration check.
481
-
482
- ### Initial release
483
-
484
- - Self-healing Playwright bindings: broken locators are recovered at runtime via an AI provider,
485
- with `.describe()` for human-readable context.
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/); dates are when each version was published.
5
+
6
+ ## [0.14.0] - 2026-09-18
7
+
8
+ ### Changed
9
+
10
+ - **License terms updated.** The software remains free to use, including commercially, via ordinary installation as a project dependency. Copying, modifying, redistributing, or reselling the software outside of that ordinary use requires prior written permission. Copyright is held by VibeTestQ. See the LICENSE file for the full terms.
11
+
12
+ ## [0.13.0] - 2026-09-17
13
+
14
+ ### Added
15
+
16
+ - **AI-powered analysis of why a test failed.** Classifies a genuinely-failed test (every retry exhausted) as `likely-defect`, `likely-wrong-locator`, `likely-timing-or-environment`, or `inconclusive`, with a short explanation. Covers any final failure, not just `expect()` an action failure healing already reported on is analyzed too, using healing's own diagnosis (provider, failure stage, reason) as context. On by default (`FAILURE_ANALYSIS_ENABLED`), independent of `HEALER_ENABLED`. No reporter to register runs as a `test.extend()` auto-fixture, the same mechanism `bindContext`/`bindPageActions`/`bindBrowser` use. Reuses whichever `HEALER_PROVIDER` is configured; `tamash` always returns `inconclusive`. One AI call per genuinely-failed test when retries are set at the project level (`playwright.config.ts`'s `retries:`, or `--retries`); a per-file `test.describe.configure({ retries })` override is not visible to the fixture, so that case analyzes every failing attempt instead of one. Reported the same way healing's own reports are: a `failure-analysis` annotation, a `failure-analysis-tokens-used` annotation (separate from healing's `llm-tokens-used`), and a `failure-analysis` JSON attachment with the full attempt history. `expect()` itself is never touched.
17
+
18
+ Four bugs were fixed during development, each covered by a permanent regression test: (1) the working log was keyed by `testFile:testLine`, which is not unique for parameterized tests sharing a source line — fixed by keying on `testInfo.testId`. (2) A flaky test that passes on retry left a stale row in the working log — fixed by clearing it on the passing branch too. (3) An action failure that healing already reported on received no failure-analysis verdict — fixed by analyzing it too, passing healing's own diagnosis in as context. (4) `readFailureAttempts` dropped the `healingAttempt` field on every read despite writing it to disk correctly — fixed by adding the field to its return list.
19
+
20
+ ### Fixed
21
+
22
+ - **A context/page built off `browser` in `test.beforeAll` was not healing-aware.** Only the `context`/`page` fixtures were wrapped with the healing proxy; `test.beforeAll(async ({ browser }) => { context = await browser.newContext(); page = await context.newPage(); })` bypassed both. Fixed: `browser.newContext()`, `browser.newPage()`, and `browser.contexts()` now return healing-aware objects, matching `context.newPage()`. Does not cover a browser obtained outside the fixture system (e.g. `chromium.launch()` in `globalSetup`) see "What else it heals" in README.md/usage.md for the manual `bindContext()`/`bindPageActions()` option.
23
+
24
+ ### Docs
25
+
26
+ - **README.md rewritten — 666 lines to ~220, no content removed.** Deep-dive content (subscription-provider setup, the full CI YAML example) moved into usage.md's existing sections; README links out to them. Fixed a gap: the `.env`/usage.md "pick one" provider comment was missing `cursor-subscription`/`kiro-subscription`/`codex-subscription`.
27
+
28
+ - **README.md/usage.md/feature.md state the fixture-healing rule directly.** "What else it heals" now leads with the rule: any browser/context/page from Playwright's own fixtures heals automatically, however many hops away; anything built outside the fixture system needs manual `bindContext()`/`bindPageActions()`.
29
+
30
+ ## [0.12.0] - 2026-09-13
31
+
32
+ ### Added
33
+
34
+ - **Playwright 1.63 support.** Dev/CI now tracks `@playwright/test` 1.63; `peerDependencies`
35
+ stays `>=1.40.0`. Verified: existing iframe healing (`page.frameLocator('#id')`) still heals,
36
+ and the full unit + representative e2e suite passes against 1.63.
37
+
38
+ - **Healing through Playwright 1.63's no-argument `page.frameLocator()`.** 1.63 made
39
+ `frameLocator()`'s selector optional with no argument it matches inside *any* frame on the
40
+ page. As a healing scope that's ambiguous the moment the page has more than one frame (the
41
+ healer's `locator('body')` snapshot then throws *"frameLocator() matched elements in multiple
42
+ frames"*, and healing silently no-op'd with `stage=no_snapshot`). Now, when the page has
43
+ exactly one frame, the healer re-expresses the no-arg `frameLocator()` as an explicit
44
+ single-frame `FrameLocator` and heals normally including deriving a **durable, persistable**
45
+ cross-frame selector (`apply-heals` can write it back to source), identical to what
46
+ `page.frameLocator('#id')` produces. With zero or several frames it can't know which was meant,
47
+ so it steps aside cleanly the original error is re-thrown, never a wrong-frame guess. For
48
+ healable work on a multi-frame page, pass an explicit selector: `page.frameLocator('#id')`.
49
+
50
+ *(`0.12.0-beta.1` collapsed the scope to a raw `page.frames()` `Frame` instead of a
51
+ `FrameLocator`. `Locator.normalize()` on a `Frame`-rooted locator returns a correct selector
52
+ string but an object that resolves to nothing, so the heal worked once at runtime but couldn't
53
+ be persisted it fell back to a transient one-shot element reference with a `needsReview`
54
+ note. Fixed here.)*
55
+
56
+ - **Documented [`tamash-playwright-dashboard`](https://www.npmjs.com/package/tamash-playwright-dashboard).**
57
+ A separate, zero-config reporter package: pass-rate trends, per-test history across runs, and —
58
+ specific to this package — a Self-Healing Analytics page (tests/elements healed, token usage
59
+ per run and cumulatively, every heal event across recorded history), read directly from the
60
+ `self-healing-<action>` JSON attachment this package already writes. No code change on this
61
+ side README and usage.md gained a "Trends across runs" section pointing to it.
62
+
63
+ ### Fixed
64
+
65
+ - **`describeFactoryCall` rendered `iframe "undefined"` for a no-arg `frameLocator()`.** The
66
+ factory-call label helper assumed a selector argument was always present; with 1.63's optional
67
+ selector it produced the literal string `iframe "undefined"`. Now renders `any iframe`. (Latent
68
+ — the label isn't currently surfaced to users or the model, but it would be the moment any
69
+ "healing inside iframe X" context is added to a report or prompt.)
70
+
71
+ ## [0.11.0] - 2026-08-31
72
+
73
+ ### Fixed
74
+
75
+ - **`skills/tamash-playwright/references/heal.md` never told the agent the Playwright HTML report exists at the point it matters.** It only pointed at `npx playwright show-report` at the very end (the REPORT step), after the loop was already finished — not right after RUN, where the report's per-attempt annotation and JSON attachment (provider, vision/action-recovery involvement, suggested selector, token cost, failure stage) would actually help decide what to do next. Found by directly auditing the skill against every documented user workflow step, not by running it. Fixed: RUN now explicitly says what the report contains and when it's worth opening.
76
+
77
+ - **`doctor`'s connectivity check used a fixed 15s timeout, not the project's own `actionTimeout`.**
78
+ A project using `actionTimeout: 8000` could get `[OK] Connected successfully` from `doctor`, then
79
+ have its first real heal fail with a misleading "not authenticated?" warning: `claude-subscription`/
80
+ `copilot-subscription` spawn the vendor's CLI as a subprocess, and the first call in a process pays
81
+ a one-time cold-start cost on top of the model call, which the old 15s check absorbed but a smaller
82
+ `actionTimeout` does not. Found during a from-scratch setup walkthrough (see the skill entry
83
+ below). Fixed: `doctor` now tests connectivity within the project's real `actionTimeout` when one
84
+ is configured, falling back to the 15s default only when none is set. A `[FAIL]` for either
85
+ provider now names the cold-start cost as a possible cause. Verified against `copilot-subscription`
86
+ that the check uses the configured timeout.
87
+
88
+ - **The published package was silently carrying stale, orphaned compiled output.** `npm run build`
89
+ (plain `tsc`, no `--build`/incremental mode) never deletes `dist/` output for a source file that
90
+ was later removed — found in the `0.11.0-beta.1` publish itself: `dist/healer/providers/
91
+ claude-cli.js`/`copilot-cli.js` shipped in the tarball with no corresponding source file
92
+ anywhere in the repo (from an abandoned, never-committed, never-wired-up piece of past work)
93
+ dead code nothing actually imports, but real, needless bloat in every install. Fixed by cleaning
94
+ `dist/` automatically before every build (`prebuild` script), verified by confirming a full
95
+ rebuild no longer produces those two files.
96
+
97
+ - **A popup/new tab opened via `context.waitForEvent('page')`, `page.on('popup', ...)`, or
98
+ `page.waitForEvent('popup')` was never healing-aware.** `bindContext`/`bindPageActions` already
99
+ made `context.newPage()`/`context.pages()`/`context.on('page', ...)` heal correctly, but these
100
+ three including `context.waitForEvent('page')`, the pattern most commonly taught for popup
101
+ handling — fell through to a raw, unwrapped Playwright page. A broken locator inside the
102
+ resulting page just threw a plain timeout, with no `[self-healer]` line at all: healing was never
103
+ attempted, not just unsuccessful. Found while building the first-ever test for popup healing
104
+ (previously implemented with zero coverage); fixed by extending both bindings to wrap whatever
105
+ page these three hand back, the same way `newPage()` already did.
106
+
107
+ - **`tamash` (the rule-based provider) declined on a real, common page pattern it should have
108
+ resolved.** A floating label rendered as its own text node right next to a field whose accessible
109
+ name is *also* that same text (confirmed on a real login page: a "Username" label next to a field
110
+ that's itself named "Username") was miscounted as two competing candidates and declined, even
111
+ though there's genuinely only one field. `findRuleBasedMatch` now only treats a match as a real
112
+ competing candidate when it's independently resolvable on its own (has a ref and a plausible
113
+ role) a bare text node repeating the same words is decorative, not a second option. Genuine
114
+ ambiguity (two actually-independent candidates) still declines exactly as before.
115
+
116
+ - **The HTML report's self-healing attachment was a wall of escape sequences on a failed
117
+ heal.** Playwright's own error messages embed ANSI colour codes in their "Call log" section;
118
+ the healer stored those raw in `report.reason` / `report.warning` / `attempts[].error`, and
119
+ `JSON.stringify` (which builds the `self-healing-<action>` attachment) escapes every ESC byte in the JSON, so a *not-healed* report rendered as unreadable escape soup. A
120
+ *successful* heal has short or absent error strings, which is why it only showed up on failures.
121
+ Fixed by stripping ANSI CSI sequences in `normalizeError` the single point every captured
122
+ error passes through so the attachment, the console line, and `heals.jsonl` are all plain
123
+ text. Verified against a real not-healed run.
124
+
125
+ - **The three CLI-based subscription providers (`cursor-subscription`, `kiro-subscription`,
126
+ `codex-subscription`) were verified against their real vendor CLIs for the first time — and none
127
+ of the three actually worked as shipped.** Four separate bugs, all found by running them, all now
128
+ fixed:
129
+ 1. **`cursor-subscription` never returned a heal.** `agent -p` refuses to run in any directory
130
+ without workspace trust it prints "Workspace Trust Required" and exits without answering.
131
+ Now passes `--trust --mode ask`; `--mode ask` is Cursor's own read-only Q&A mode ("read-only"
132
+ per its `--help`), which *removes* the old "has access to all tools, could edit a file"
133
+ caveat this provider carried a heal call in `ask` mode cannot write or run anything.
134
+ 2. **`codex-subscription` refused to run outside a Git repository** ("Not inside a trusted
135
+ directory and --skip-git-repo-check was not specified"). Now passes `--skip-git-repo-check`;
136
+ this only relaxes the where-may-I-run guard, not the sandbox `codex exec` still runs
137
+ `approval: never` + read-only, the property this provider depends on.
138
+ 3. **`codex exec` hung on stdin.** It drains stdin ("Reading additional input from stdin…") and
139
+ blocks until EOF; `runCliPrompt` left the child's stdin as an open pipe, so every call ran to
140
+ the timeout and was killed — confirmed: a prompt the CLI answers in ~8s "timed out" at 15s.
141
+ `runCliPrompt` now gives every child an already-closed stdin (`stdio: ['ignore',]`),
142
+ harmless for `agent`/`kiro-cli`, the fix for `codex`.
143
+ 4. **The JSON parsers failed on conversational output.** `parseSuggestion` /
144
+ `parseVisionSuggestion` / `parseActionTacticSuggestion` extracted JSON from prose with a
145
+ greedy first-`{`-to-last-`}` slice, which breaks the moment the response contains more than
146
+ one JSON object exactly what cursor's `agent` produces (it echoes the system prompt's
147
+ example objects back before its real answer). Replaced with a balanced-brace-span scan that
148
+ takes the last valid object (a model states its answer last), old greedy slice kept as a last
149
+ resort. Improves robustness for every provider, not just the CLI ones.
150
+
151
+ Verified, all authenticated: `doctor` reports `[OK] Connected` for all three; a real
152
+ `npx playwright test` heal (broken locator, cache cleared) went green with
153
+ `HEALED [provider=codex-subscription]`, and `kiro-subscription` healed the same spec.
154
+
155
+ - **`cursor-subscription` reliability note.** Verifying it (see above) showed Cursor's `agent` CLI
156
+ is an interactive assistant rather than a one-shot completion endpoint — given the heal prompt it
157
+ often answers conversationally instead of with the required JSON, so the heal is declined more
158
+ often than with `kiro`/`codex`. It's now invoked in the safest/best-effort form
159
+ (`agent -p --trust --mode ask --output-format json`, read-only) and is marked **experimental**;
160
+ the docs point to `kiro`/`codex` as the steadier local-subscription options. Report issues.
161
+ Also: the full e2e healing suite (12 tests) was run end to end against `copilot-subscription` and
162
+ `ollama` **12/12 each** to confirm the parser rewrite and provider changes don't regress it.
163
+
164
+ ### Added
165
+
166
+ - **`npx tamash-playwright init-skill` — one command to install the orchestration skill, plus a
167
+ `doctor` check that flags when it's missing or stale.** The skill this package ships
168
+ (`skills/tamash-playwright/`) was previously a copy-this-shell-one-liner-per-agent step buried in
169
+ `SKILL.md`, with nothing verifying it had been done so a project could `npm install` the
170
+ package, never copy the skill, and its assistant would silently never load the workflow.
171
+ `init-skill` copies the skill (`SKILL.md` + `references/`) into **both** standard locations
172
+ `.claude/skills/tamash-playwright/` (Claude Code) and `.agents/skills/tamash-playwright/` (the
173
+ emerging cross-tool standard, read by Cursor, GitHub Copilot, Windsurf, Kiro, Zed, dotnet Aspire,
174
+ the same convention Playwright's own `playwright-cli install --skills` uses). Same content in
175
+ both; no per-agent format conversion. `--target claude` / `--target agents` installs one; `--user`
176
+ installs under your home directory; `--force` overwrites a hand-edited copy; `--dry-run` previews.
177
+ Each install carries a `tamash-playwright-skill-version:` marker; `doctor`'s **Skill** section
178
+ reads it and reports `[OK]` current / `[WARN]` behind-the-package-version / `[INFO]` not-installed
179
+ or unmanaged, with the same one-line fix. Covered by unit tests (`skill.test.js`, real installs
180
+ into a temp dir) and verified end to end.
181
+
182
+ *(0.11.0-beta.7 shipped an earlier `init-skill` that instead auto-detected the agent and wrote a
183
+ Cursor-specific `.mdc` file / appended blocks to `.github/copilot-instructions.md` and
184
+ `AGENTS.md`. That approach is gone — `init-skill` and `doctor` now point out any such leftovers
185
+ from a beta.7 install so you can delete them by hand.)*
186
+
187
+ - **`doctor`'s AI Provider check now tells you *what kind* of failure it hit and what to do about
188
+ it, instead of one generic "no valid response".** Every provider gained an optional, diagnostics-
189
+ only `diagnose()` method (never on the healing hot path) that runs one trivial round trip and
190
+ reports a structured category `not-installed`, `not-authenticated`, `timeout`, `bad-model`,
191
+ `network`, `bad-response` — each of which `doctor` maps to a specific next step: the missing
192
+ `npm install`/CLI installer for a missing SDK; "run `claude login` / check your API key / confirm
193
+ your subscription is active and within quota" for a rejected request; "raise `actionTimeout`" (plus
194
+ the existing subprocess cold-start note) for a timeout; "check `<MODEL>` in your .env" for a
195
+ rejected model id; a network/proxy/base-URL hint for a connection failure. The raw error line is
196
+ still printed verbatim beneath the guidance. Previously `doctor` could only distinguish "worked"
197
+ from "didn't" a missing SDK, an expired login, a slow network and a wrong model name all
198
+ produced the same line. Providers that don't implement `diagnose()` fall back to the old
199
+ `suggestSelector()` probe (worked/didn't, plus a timing-based timeout guess). Verified:
200
+ `openai` bad key -> `not-authenticated` (401 body shown), `openai` bad base URL -> `network`,
201
+ `copilot-subscription` bad model id -> `bad-model`, 1ms `actionTimeout` -> `timeout` with the
202
+ raise-the-timeout guidance, and all three not-installed shapes by moving the dependency out of
203
+ resolution and restoring it: `@github/copilot-sdk` (CJS `MODULE_NOT_FOUND`),
204
+ `@anthropic-ai/claude-agent-sdk` (ESM `ERR_MODULE_NOT_FOUND`), and a missing `kiro-cli` binary
205
+ (`ENOENT`) -- each surfacing `not-installed` with its exact install command.
206
+
207
+ - **Three new local-development-only subscription providers: `cursor-subscription`,
208
+ `kiro-subscription`, `codex-subscription`.** Extends the same "use what you're already paying
209
+ for" idea `claude-subscription`/`copilot-subscription` already give, to three more real
210
+ subscriptions but each is explicitly scoped to local development, never documented or
211
+ recommended for CI, for two genuinely different reasons found by checking real vendor docs before
212
+ writing any code. Cursor's and Kiro's own headless-mode docs confirm neither has a way to fully
213
+ disable tool/file/command access the way `claude-subscription`'s `tools: []`/`copilot-subscription`'s
214
+ `availableTools: []` do Cursor's `agent -p` "has access to all tools" by its own docs, and Kiro's
215
+ headless mode requires `--trust-all-tools`/`--trust-tools` specifically because there's no one to
216
+ approve a tool call unattended; `kiro-subscription` deliberately never grants that trust, so a
217
+ tool-requiring response just times out and declines rather than being granted broad access.
218
+ `codex-subscription` is local-only for a different, simpler reason: `codex exec` (used here, not
219
+ the interactive REPL) defaults to a read-only sandbox with no approval prompts already as safe
220
+ as the two existing subscription providers but no long-lived, subscription-only token for
221
+ unattended CI use is confirmed to exist for it yet. None of the three vendors ship a Node SDK, so
222
+ all three are `child_process` wrappers around the vendor's own CLI binary (`agent`, `kiro-cli`,
223
+ `codex`) via a new shared `runCliPrompt` helper, rather than the SDK-based design the existing two
224
+ use meaning every call pays a fresh process-spawn cost, not just the first the way
225
+ `copilot-subscription`'s warm shared client does; `doctor` now has a distinct hint explaining this
226
+ when one of these three fails within a tight `actionTimeout`. Investigated and explicitly ruled
227
+ out this round: Antigravity its dedicated `google-antigravity` SDK is Python-only (this is a
228
+ Node/TypeScript package), has no documented tool-restricted mode at all, and its authentication
229
+ story isn't published, so it isn't even confirmed to ride on an existing subscription. Verified:
230
+ real graceful-decline behavior confirmed against genuinely uninstalled CLIs (no fabricated
231
+ simulation), and the full `doctor` integration (including the new every-call-cost hint) confirmed
232
+ against a real sample repo. **Not yet verified**: the actual happy path against a real installed
233
+ and authenticated CLI for any of the three none were available in the development environment,
234
+ and all three require a real paid subscription account.
235
+
236
+ - **A skill for running this package's local workflow inside an AI coding assistant.** Ships at
237
+ `skills/tamash-playwright/` a `SKILL.md` entry point that branches on `npx tamash-playwright
238
+ doctor`'s actual output (never assumed), plus two reference docs: `onboarding.md` (bringing a
239
+ project up to standard provider setup, `actionTimeout`, `.describe()` labels, Page Object
240
+ extraction) and `heal.md` (a gated review/apply/verify/land loop over `apply-heals` and
241
+ `verify-heals.cjs` most runs proceed start to finish unattended, pausing only for a genuinely
242
+ ambiguous fix or anything after a failed verification, and never landing/committing/opening a PR
243
+ without asking first, no matter how clean the run was). Pure orchestration over commands that
244
+ already exist no new healing capability. Ships with adapters for Claude Code, Kiro (identical
245
+ `SKILL.md` format, confirmed against Kiro's own docs), Cursor (`.mdc` rule), GitHub Copilot
246
+ (`copilot-instructions.md` section), and an `AGENTS.md` covering the broader cross-tool standard
247
+ (Antigravity, Gemini CLI, Windsurf, Zed, Aider, and others) none of which are auto-discovered
248
+ from `node_modules` by any of these tools, confirmed rather than assumed, so every adapter's
249
+ install step is one explicit copy command, documented in `SKILL.md` itself.
250
+ - **A new `tamash` heal provider rule-based healing, no AI at all.** `HEALER_PROVIDER=tamash`
251
+ needs no API key, no subscription, and makes no network call: it resolves a broken locator by
252
+ text-matching the same description an AI provider would receive (`.describe()`, or a decoded
253
+ variable name see below) against the already-captured accessibility snapshot, then reuses the
254
+ exact same structural widening (`near`/`adjacent`) the AI-backed path already uses once it finds
255
+ the right anchor — same output shape, same downstream code, zero duplicated logic. It shares the
256
+ same non-negotiable discipline as every text-matching step in this package: zero or more than one
257
+ match, at any point, means it declines rather than guesses verified with a dedicated permanent
258
+ e2e suite covering direct matches, widened matches, and every decline path (including the known,
259
+ accepted limitation that a misleading type hint can't be second-guessed against). Genuinely a
260
+ different tool than the AI providers, not a free replacement for one: no vision fallback (nothing
261
+ to reason over a screenshot with) and no action-recovery tactics (those require understanding
262
+ *why* an action failed, which is inference this provider deliberately doesn't attempt) a fast,
263
+ free, fully deterministic first line of defense, best suited to well-`.describe()`d,
264
+ Page-Object-style suites.
265
+ - **A new `ollama-local` heal provider for self-hosted Ollama servers.** `HEALER_PROVIDER=ollama-local`
266
+ targets your own `ollama serve` instance or an internal company deployment, instead of Ollama
267
+ Cloud — a deliberately separate provider from `ollama`, not a flag on it, since the two have
268
+ genuinely different auth defaults: Ollama Cloud always requires `OLLAMA_API_KEY`, while
269
+ `OLLAMA_LOCAL_API_KEY` is optional, since a bare `ollama serve` has no authentication at all. Set
270
+ it only if your internal deployment sits behind a reverse proxy or API gateway that requires a
271
+ bearer token verified against a real HTTP server both ways, confirming the `Authorization`
272
+ header is omitted entirely when no key is set, and sent correctly when one is. Prompted by a real
273
+ support request from a team wanting to use their own internally-hosted `gpt-oss:120b`.
274
+ - **Undescribed, POM-style locator variables now get a real description automatically.** When
275
+ `.describe()` was never called, the healer already fell back to the locator's own variable/
276
+ property name (`txtEmployeeId`) as its best guess at intent now that raw identifier is decoded
277
+ into the same kind of human-readable phrase `.describe()` would give: `txtEmployeeId` becomes
278
+ "Employee Id (textbox)", `submitButton` becomes "Submit (button)", recognizing both prefix-style
279
+ (`btnSubmit`) and suffix-style (`submitBtn`) naming, camelCase/snake_case/kebab-case, and correct
280
+ acronym boundaries (`employeeIDNumber` "Employee ID Number"). Falls back to the raw identifier,
281
+ exactly as before, whenever nothing meaningful survives decoding (a placeholder name like `el1`,
282
+ or a bare affix with nothing else) never a guess dressed up as a real description. Verified
283
+ live against a real, unpublished build in both sample repos (a real `copilot-subscription` call,
284
+ and after finding and fixing a genuinely stale API key along the way — a real `ollama` call),
285
+ confirming the decoded description actually reaches the configured AI provider unchanged.
286
+
287
+ ## [0.10.0] - 2026-08-27
288
+
289
+ ### Fixed
290
+
291
+ - **`waitFor()` is never sent to the AI.** It's a state check, not an action — a timeout on it can
292
+ mean a genuinely broken selector, or it can mean the element correctly never reached the expected
293
+ state (verifying something does NOT appear, or a real app issue), and there's no way to tell those
294
+ apart from the error alone. `expect(locator).toBeVisible()` was already permanently excluded from
295
+ healing for exactly this reason, but never reached this code at all (a separate path from the
296
+ Proxy-intercepted actions) `waitFor` just never got the same treatment. Real, reported case: a
297
+ user's `waitFor` on a locator verifying an absence (where timing out was the *correct* outcome)
298
+ still burned 8175 tokens across a failed text attempt and a failed vision attempt before giving
299
+ up, for a heal that could never have succeeded. Now fails fast with a clear `state-wait-not-healed`
300
+ stage and zero AI calls; a real action (fill/click/...) on the same kind of broken locator is
301
+ unaffected.
302
+ - **`claude-subscription` used more tokens than necessary.** The SDK's `effort` option defaults to
303
+ `'high'` when left unset, and this provider never set it a small prompt-complexity increase (the
304
+ `nearbyRef`/`nearbyText` addition below) pushed adaptive thinking higher for a task that only needs
305
+ to return one line of JSON. Output tokens for the same heal ranged 188-445 before, 422-881 after,
306
+ across real CI runs. `thinking: { type: 'disabled' }` + `effort: 'low'` fixes this: a steady 28-30
307
+ output tokens, below the original baseline, with no change to correctness.
308
+ - **The primary ariaSnapshot capture no longer requests `boxes:true`.** Every node was paying for a
309
+ `[box=x,y,w,h]` annotation that nothing on the text/`ref` path (including the `nearbyRef`/
310
+ `adjacent`-strategy widening logic below) ever reads it's purely topological. The one real
311
+ consumer (vision's own nearest-candidate lookup) already captures its own separate, fresh
312
+ snapshot, so this is genuinely free: verified, input tokens dropped ~23% on a large real
313
+ page (3794 2937) with no loss of accuracy, and no change on small pages (box overhead scales
314
+ with node count).
315
+
316
+ ### Added
317
+
318
+ - **Search-scoped snapshots**: before falling back to the full page, the healer now searches the
319
+ already-captured snapshot for the description's identifying phrase and, only when it matches
320
+ exactly one node, sends the AI a scoped excerpt (that node's own subtree plus every sibling
321
+ branch's subtree at each ancestor level up to the root) instead of the whole page — zero extra
322
+ browser round-trips, since it's pure processing on data already in memory. Falls back to the full
323
+ snapshot automatically whenever the search is empty or ambiguous, never a guess. Verified across
324
+ several real cases: 41% token reduction on a deeply-nested field,
325
+ 66% when the target was inside what looked like an unrelated navigation menu (proving it finds
326
+ wherever the relevant text actually is, not "excludes the nav"), ~29% on a pair of identical
327
+ sibling fields that still had to be correctly disambiguated, and a clean, correct fallback when
328
+ the description doesn't match the page's real text at all.
329
+ - **A new `adjacent` selector strategy**, fixing a real ambiguity in the existing `near` strategy:
330
+ when two fields with no identity of their own share a row/section (two dropdowns side by side,
331
+ say), `near`'s "climb to a shared ancestor, then search it for any element of this role"
332
+ approach matches both and gives up rather than risk the wrong one. The AI's `ref` response can
333
+ now optionally report `nearbyRef`/`nearbyText`/`nearbyRole` for a nameless target it identified;
334
+ `deriveDurableLocator` uses that hint to find the true common ancestor between the target and
335
+ its label via each ref's own full ancestor chain, not by assuming either sits at a matching
336
+ depth — and, when they're proven to be immediate sibling branches, builds a precise CSS
337
+ `:text() + *` sibling match (or an xpath climb-then-step, when the label text turns out to be
338
+ nested below its own branch root). Verified against a real configured provider, resolving
339
+ the correct field and never its same-row neighbor in both directions.
340
+ - **Full attempt-history logging**: `SelfHealingReport` now carries an `attempts[]` array one
341
+ entry per cache/ref/text/vision/action-recovery attempt actually made, each with its own
342
+ `succeeded`/`stage`/`error`. Previously only the *last* attempt's stage survived; an earlier
343
+ attempt's real failure (and the specific error it threw) was silently discarded the moment a
344
+ later attempt also failed — the exact shape of a real user-reported bug, where a genuine
345
+ candidate selector was shown next to an unrelated `vision_provider_error` with no way to tell
346
+ why the candidate itself hadn't worked. The attempt history now also prints directly to
347
+ console/CI output (not just the JSON attachment) whenever more than one attempt was made, on
348
+ both pass and fail since that plain-text output is what most bug reports actually paste, not
349
+ an attachment nobody opens.
350
+ - **`ariaSnapshot` is attached to the test report on failure** the exact accessibility tree the
351
+ AI reasoned over, so a confusing report can be diagnosed against real evidence instead of a
352
+ separately-captured DevTools screenshot.
353
+
354
+ ## [0.9.0] - 2026-08-26
355
+
356
+ ### Fixed
357
+
358
+ - **Heal-log visibility**: a heal with no reusable selector (a one-shot `ref` resolution, or a
359
+ vision-tagged point) was silently dropped from `heals.jsonl` entirely, even though it genuinely
360
+ fixed that run. Now logged for audit — with a clear `reviewNote` — whenever it has a suggestion
361
+ *or* a review note, without letting an audit-only entry shadow an older, real cached fix for the
362
+ same location.
363
+ - **Vision support for `claude-subscription`/`copilot-subscription`**: both providers had
364
+ `supportsVision` hardcoded to `false` regardless of model. Both SDKs genuinely support image
365
+ input (Claude Agent SDK via an image content block; Copilot SDK via a `blob` attachment)
366
+ implemented for real and verified with actual screenshots, not just doctor's yes/no label.
367
+ - **Argument forwarding in healed replays**: `replayAction()` only ever forwarded the first call
368
+ argument. A trailing options object (`click({ modifiers: [...] })`, `fill(value, { timeout })`,
369
+ `dispatchEvent`'s 3rd argument) was silently dropped on a healed replay changing the action's
370
+ real behavior while still reporting a clean `HEALED`.
371
+ - **`ref`-strategy replays now act through the derived durable locator, not the raw ref**:
372
+ `aria-ref=` locators never resolve for `dispatchEvent` at all (a Playwright-level limitation,
373
+ confirmed directly via `DEBUG=pw:api`), even though the identical element resolves instantly via
374
+ a `normalize()`-derived locator for that same action. Fixed by deriving the durable locator
375
+ *before* replaying and acting through it, falling back to the raw ref only when nothing durable
376
+ could be found. A related accuracy bug is fixed alongside it: the report/heal-log could
377
+ previously claim a derived selector "worked" even when its own replay had actually failed and a
378
+ fallback silently took over now only the locator that genuinely performed the action is
379
+ reported or cached.
380
+ - **`copilot-subscription` could hang a non-Playwright test runner indefinitely.** Its shared
381
+ client keeps a connection open across calls for performance; Playwright's own test runner
382
+ force-exits regardless, but a runner like Cucumber does not, so the process never returns even
383
+ after every test has passed. `closeCopilotSubscriptionClient()` is now exported from the
384
+ package's own entry point so a non-Playwright consumer can call it from their own teardown hook
385
+ (e.g. Cucumber's `AfterAll`).
386
+
387
+ ### Added
388
+
389
+ - **`locator.getDurable(action?)`** resolves any locator (most usefully one built from
390
+ `aria-ref=...`) to a durable, reusable equivalent (`getByRole`/`getByLabel`/a css selector/…),
391
+ using the same derivation logic self-healing already uses internally. Throws if nothing durable
392
+ could be derived, rather than silently handing back something untrusted.
393
+ - **`apply-heals` now previews before writing**: every run — dry or real — shows a styled table of
394
+ exactly what it found (location, before, after, review status) instead of a plain scrolling log.
395
+ A real run also asks for confirmation before writing anything, but *only* at a genuine
396
+ interactive terminal CI and any non-interactive/piped invocation proceed automatically exactly
397
+ as before, so no existing unattended CI workflow is affected. `--yes`/`-y` skips the prompt at a
398
+ real terminal too.
399
+ - **`doctor`'s output is styled** with colors and tables (including a new end-of-run Summary
400
+ section) instead of a plain scrolling log.
401
+ - **Exact locator code in console lines and reports**: a healed suggestion is now shown as the
402
+ real, copy-pasteable Playwright call (`getByRole("textbox", { name: "Username" })`) instead of
403
+ an abbreviated shorthand (`role:textbox:Username`) — the same code `apply-heals` would write to
404
+ source, so what you see is never a lossier stand-in for what was actually used.
405
+ - A canonical Page Object Model usage example (`tests/pages/` +
406
+ `tests/example-orangehrm-add-employee.spec.ts`), verified against a real OrangeHRM demo,
407
+ including a genuine self-heal via the `near`/widening strategy on a real unlabeled field.
408
+
409
+ ### Changed
410
+
411
+ - README/usage.md's vision-fallback documentation now correctly lists `claude-subscription`/
412
+ `copilot-subscription` as vision-capable (with a suitable model), not just the API-key providers.
413
+
414
+ ## [0.8.0] - 2026-08-25
415
+
416
+ ### Added
417
+
418
+ - `claude-subscription` and `copilot-subscription` heal providers — self-healing backed by a
419
+ personal Claude or GitHub Copilot subscription instead of a pay-per-token API key, working both
420
+ locally and unattended in CI (`CLAUDE_CODE_OAUTH_TOKEN` for Claude; the ambient `GITHUB_TOKEN` or
421
+ a personal-account PAT for Copilot on GitHub Actions).
422
+
423
+ ### Fixed
424
+
425
+ - Missing CLI install step in setup docs for `claude-subscription`/`copilot-subscription` — the
426
+ standalone `claude`/`copilot` CLIs are genuinely required for the login step even though the SDKs
427
+ don't need them installed to function at runtime.
428
+
429
+ ### Documentation
430
+
431
+ - The org-vs-personal-account Copilot licensing gotcha in CI (a repo owned by an organization
432
+ without its own Copilot enablement can't use a personal subscription via the ambient token).
433
+ - Action Recovery (`HEALER_ACTION_RECOVERY_ENABLED`) hidden from docs and `doctor` output —
434
+ disabled by default already; this only reduced its visibility, no behavior change.
435
+
436
+ ## [0.7.0] - 2026-08-21
437
+
438
+ ### Added
439
+
440
+ - `apply-heals` — turns a runtime heal into a permanent source-code fix, plus an opportunistic
441
+ cache (`heals.jsonl`) so a previously-confirmed selector is tried before a fresh AI call, and
442
+ history archival so a run's report/log isn't silently overwritten by the next one.
443
+ - Position/relationship-based locator strategies (`near`, `scoped`, `containing`) for elements with
444
+ no accessible identity of their own.
445
+ - Self-healing rebuilt around `ariaSnapshot({ mode: 'ai' })` + `aria-ref=` resolution and
446
+ `Locator.normalize()`, replacing pixel-distance guessing for both the text path and the vision
447
+ fallback's durability upgrade.
448
+ - A real unit test suite (32 tests at the time, zero new dependencies).
449
+
450
+ ### Fixed
451
+
452
+ - `doctor`'s `actionTimeout` check being fooled by a commented-out config value.
453
+ - `apply-heals --logs-dir` silently losing raw heal-log archival.
454
+ - `apply-heals` only replacing the first call when re-healing an already-`near`-fixed line.
455
+ - `apply-heals` `ENOENT` on a fresh CI checkout with no `.tamash-playwright/` directory yet — the
456
+ sharded "apply-heals" CI job checks out fresh and has never run tests itself, so the directory
457
+ genuinely doesn't exist there the way it always does in every local recipe.
458
+ - File path corruption (and a `require()` crash in the generated verification script) for ESM
459
+ (`"type": "module"`) consumer projects `Error.stack` renders as a `file://` URL there, which
460
+ broke path resolution and a plain `.js` verification script alike.
461
+ - The AI being misled by the broken selector still present in its own prompt context; a failed
462
+ label guess now auto-upgrades to a structural `near` match instead of giving up.
463
+
464
+ ### Verified
465
+
466
+ - All four API-key providers (Ollama, OpenAI, Anthropic, Gemini) confirmed working with real API
467
+ calls, not just documented.
468
+
469
+ ## [0.6.0] - 2026-08-17
470
+
471
+ ### Added
472
+
473
+ - iframe and popup healing, vision fallback (screenshot-based recovery when text alone isn't
474
+ enough), AI-driven action recovery (scroll/force/wait/dispatch), and source-location reporting.
475
+ - `doctor`'s `actionTimeout` configuration check.
476
+
477
+ ### Initial release
478
+
479
+ - Self-healing Playwright bindings: broken locators are recovered at runtime via an AI provider,
480
+ with `.describe()` for human-readable context.