chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# CLI Tool Mode
|
||||
|
||||
Default Webwright runs (`/webwright:run`, plain prompt) produce a one-shot
|
||||
`final_script.py` that solves the task for the literal values the user
|
||||
provided. **CLI tool mode** (`/webwright:craft`) instead produces a
|
||||
**reusable, parameterized CLI tool**: the same script can be re-run later
|
||||
with different argument values to perform the same kind of task.
|
||||
|
||||
This mode is adapted from `webwright/src/webwright/config/crafted_cli.yaml`'s
|
||||
"Final-Script Shape (CLI Tool, MANDATORY)" contract. The OpenAI-backed
|
||||
`self_reflection` gate is replaced by your own self-verification against
|
||||
`plan.md`.
|
||||
|
||||
## When to use
|
||||
|
||||
Trigger CLI tool mode when:
|
||||
|
||||
- the user invokes `/webwright:craft …`, or
|
||||
- the user says "make it reusable", "parameterize", "turn this into a CLI",
|
||||
"I want to call this again with different X", or similar.
|
||||
|
||||
Otherwise, stay in default one-shot mode.
|
||||
|
||||
## `plan.md` — add a `# Parameters` section
|
||||
|
||||
Before writing the script, identify every requirement the user could
|
||||
plausibly vary and list them in `plan.md` **in addition to** the usual
|
||||
`# Critical Points` checklist:
|
||||
|
||||
```markdown
|
||||
# Task
|
||||
<verbatim task description>
|
||||
|
||||
# Parameters
|
||||
| name | type | source phrase from task | default | allowed / format |
|
||||
|---------|------|-------------------------|-------------|-------------------------|
|
||||
| <arg_a> | str | "..." | "<value>" | <format / allowed set> |
|
||||
| <arg_b> | int | "..." | <value> | <range or units> |
|
||||
| <arg_c> | str | "..." | "<value>" | <format> |
|
||||
|
||||
# Critical Points
|
||||
- [ ] CP1: ...
|
||||
- [ ] CP2: ...
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Every entry in `# Parameters` must (a) become a function argument and
|
||||
(b) become an `argparse --flag` with the listed default.
|
||||
- Items that are truly fixed for the site (start URL, site name, selector
|
||||
strategy) are NOT parameters — keep them hard-coded.
|
||||
- Defaults reproduce the original task exactly. Running
|
||||
`python final_script.py` with no arguments must reproduce the task.
|
||||
- Critical Points are still required; they are the verification contract.
|
||||
|
||||
## `final_script.py` — required shape
|
||||
|
||||
1. **One reusable function** named after the task domain. Examples:
|
||||
- `def search_<domain>(arg_a, arg_b, ...): ...`
|
||||
- `def lookup_<entity>(query, filters): ...`
|
||||
|
||||
2. **Google-style docstring** with summary, full `Args:` block, and
|
||||
`Returns:`. Each `Args:` entry documents:
|
||||
- the argument name and type,
|
||||
- what it represents in the task domain,
|
||||
- accepted format / units / allowed values,
|
||||
- the default (mirroring the `# Parameters` table).
|
||||
|
||||
```python
|
||||
def search_<domain>(arg_a: str, arg_b: int, arg_c: str) -> dict:
|
||||
"""<One-line summary of what this tool does on the target site>.
|
||||
|
||||
Args:
|
||||
arg_a: <what it represents>; <format / allowed values>.
|
||||
Default: "<value>".
|
||||
arg_b: <what it represents>; <range / units>.
|
||||
Default: <value>.
|
||||
arg_c: <what it represents>; <format>.
|
||||
Default: "<value>".
|
||||
|
||||
Returns:
|
||||
dict with keys ``<key1>`` (<type>), ``<key2>`` (<type>),
|
||||
"""
|
||||
```
|
||||
|
||||
3. **`argparse` CLI** under `if __name__ == "__main__":`. Every function
|
||||
argument has a matching `--<arg>` flag with `type=`, `help=` (copied
|
||||
from the docstring), and `default=` equal to the concrete task value:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
description=search_<domain>.__doc__.splitlines()[0])
|
||||
parser.add_argument("--arg-a", dest="arg_a", type=str,
|
||||
default="<value>",
|
||||
help="<copied from docstring>")
|
||||
parser.add_argument("--arg-b", dest="arg_b", type=int,
|
||||
default=<value>,
|
||||
help="<copied from docstring>")
|
||||
parser.add_argument("--arg-c", dest="arg_c", type=str,
|
||||
default="<value>",
|
||||
help="<copied from docstring>")
|
||||
args = parser.parse_args()
|
||||
result = asyncio.run(_run(**vars(args)))
|
||||
print(result)
|
||||
```
|
||||
|
||||
4. **Side-effect-free at import time.** No browser launch, no network
|
||||
call, no file write at module top-level. The reusable function must be
|
||||
importable from another Python process without triggering a run.
|
||||
|
||||
5. **Action-log parameter echo.** The first line written to
|
||||
`final_script_log.txt` after reset MUST be a `step 0 params: ...`
|
||||
line listing every resolved argument as `name=value` pairs, e.g.:
|
||||
|
||||
```
|
||||
step 0 params: arg_a=<value> arg_b=<value> arg_c=<value>
|
||||
```
|
||||
|
||||
so the resolved inputs are visible in any verification pass.
|
||||
|
||||
6. Same instrumentation as default mode: viewport 1280×1800, headless
|
||||
local Firefox, no `full_page=True`, screenshots saved as
|
||||
`final_runs/run_<id>/screenshots/final_execution_<step>_<action>.png`,
|
||||
final datum appended to `final_script_log.txt`.
|
||||
|
||||
## Verification (replaces `self_reflection`)
|
||||
|
||||
In addition to the default self-verification (every CP in `plan.md`
|
||||
ticked with cited screenshot/log evidence), CLI mode requires:
|
||||
|
||||
1. **Reproduce the task with no arguments.** Inside a fresh
|
||||
`final_runs/run_<id>/`:
|
||||
|
||||
```bash
|
||||
cd final_runs/run_<id> && python final_script.py
|
||||
```
|
||||
|
||||
The run must succeed end-to-end and produce the expected screenshots
|
||||
and `step 0 params: ...` log line.
|
||||
|
||||
2. **Import-safety smoke test.** From any other directory:
|
||||
|
||||
```bash
|
||||
python -c "import importlib.util, pathlib; \
|
||||
spec = importlib.util.spec_from_file_location('fs', 'final_runs/run_<id>/final_script.py'); \
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); \
|
||||
print([n for n in dir(m) if not n.startswith('_')])"
|
||||
```
|
||||
|
||||
This must complete instantly with no browser launch and print the
|
||||
reusable function's name.
|
||||
|
||||
3. **Optional second run with a different argument value.** Demonstrates
|
||||
parameterization actually works. Run inside `final_runs/run_<id>_alt/`
|
||||
(or just save its log/screenshot folder there). Skip only if the
|
||||
alternate value would clearly fail (e.g. an unsupported value on the
|
||||
target site).
|
||||
|
||||
4. **Print `--help`.** End by showing the user:
|
||||
|
||||
```bash
|
||||
python final_runs/run_<id>/final_script.py --help
|
||||
```
|
||||
|
||||
## Completion gate (CLI mode)
|
||||
|
||||
Set the task complete only when **all** are true:
|
||||
|
||||
1. `plan.md` contains both `# Parameters` (with name, type, source phrase,
|
||||
default, allowed/format) and `# Critical Points` checklists.
|
||||
2. `final_script.py` defines exactly one reusable function with a
|
||||
Google-style `Args:` docstring covering every parameter.
|
||||
3. Every `# Parameters` entry maps 1-to-1 to a function argument **and**
|
||||
an argparse `--flag` whose default equals the concrete task value.
|
||||
4. The script is import-safe (smoke test passes).
|
||||
5. `python final_script.py` (no args) inside `final_runs/run_<id>/`
|
||||
reproduced the task; all CPs verified against saved screenshots and
|
||||
the action log.
|
||||
6. `step 0 params: ...` line is present in `final_script_log.txt`.
|
||||
7. The user has seen the final datum **and** the `--help` output so they
|
||||
know how to call the tool again with different arguments.
|
||||
|
||||
If any of those is false, do not declare done — diagnose, fix the script
|
||||
(preserving the CLI shape), re-run inside the next `run_<id+1>/`, and
|
||||
re-verify.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Playwright Patterns
|
||||
|
||||
These are the canonical heredoc patterns the Webwright agent uses. In Claude
|
||||
Code you run them via the `Bash` tool — no JSON wrapping, no escaping
|
||||
gymnastics, just one bash command per turn.
|
||||
|
||||
## Browser launch skeleton (local mode)
|
||||
|
||||
The Webwright skill uses **Playwright Firefox** as its default engine. Some
|
||||
sites (e.g. cars.com / other Akamai-protected sites) reject Playwright
|
||||
Chromium with `ERR_HTTP2_PROTOCOL_ERROR` due to TLS/H2 fingerprinting, but
|
||||
load cleanly under Firefox. Run `playwright install firefox` once before
|
||||
the first task.
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
WORKSPACE = Path(os.environ.get("WORKSPACE_DIR", "."))
|
||||
SCREENSHOTS = WORKSPACE / "screenshots"
|
||||
SCREENSHOTS.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.firefox.launch(headless=True)
|
||||
context = await browser.new_context(viewport={"width": 1280, "height": 1800})
|
||||
page = await context.new_page()
|
||||
|
||||
await page.goto("<START_URL>", wait_until="domcontentloaded")
|
||||
await page.screenshot(path=str(SCREENSHOTS / "explore_1_start.png"))
|
||||
|
||||
print("URL:", page.url)
|
||||
print("TITLE:", await page.title())
|
||||
|
||||
# Inspect the region you care about with an ARIA snapshot
|
||||
snapshot = await page.locator("body").aria_snapshot()
|
||||
print("ARIA:", snapshot)
|
||||
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
PY
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **Always** set `viewport={"width": 1280, "height": 1800}`.
|
||||
- **Never** call `page.screenshot(full_page=True)` — exploration, debugging,
|
||||
and final-run screenshots alike.
|
||||
- Each Playwright run is fresh: navigate from the start URL, reapply
|
||||
filters, reconstruct state in code. There is no persistent session.
|
||||
|
||||
## Targeting elements with role + name
|
||||
|
||||
```python
|
||||
await page.get_by_role("button", name="Filters").click()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Snapshot the *parent* of the control to see siblings/options
|
||||
panel = page.get_by_role("button", name="Filters").first.locator("..")
|
||||
print(await panel.aria_snapshot())
|
||||
|
||||
await page.get_by_role("checkbox", name="BMW").check()
|
||||
await asyncio.sleep(1)
|
||||
```
|
||||
|
||||
If a selected state becomes hidden after a drawer/dropdown closes, reopen
|
||||
it before capturing the verification screenshot.
|
||||
|
||||
## Prefer interactive form filling over deep-link URLs
|
||||
|
||||
When a task requires parameterizing a search (locations, dates, filters,
|
||||
query strings), **drive the on-page form interactively** rather than
|
||||
constructing a deep-link URL with the parameters baked into the query
|
||||
string. Deep links are convenient for the one specific case the agent
|
||||
explored, but they are brittle as a CLI surface:
|
||||
|
||||
- Sites silently drop parameters they cannot parse, leaving downstream
|
||||
fields blank.
|
||||
- URL parsers vary by locale, A/B bucket, and signed-in state.
|
||||
- A working deep link for one input set tells you nothing about whether
|
||||
another set will populate.
|
||||
|
||||
Interactive filling using the same controls a human would click is the
|
||||
most reliable strategy across input variations. Make it the **primary**
|
||||
path in the final script; only use a deep link as an opportunistic
|
||||
shortcut, and always verify the form state afterwards and fall back to
|
||||
interactive filling when any field is empty or wrong.
|
||||
|
||||
```python
|
||||
# After navigating, read the visible form state and decide.
|
||||
form_state = await page.locator("input[aria-label]").evaluate_all(
|
||||
"els => els.map(e => ({label: e.getAttribute('aria-label'), "
|
||||
"value: e.value, hidden: e.offsetParent === null}))"
|
||||
)
|
||||
if not form_is_fully_populated(form_state, expected):
|
||||
# Type into each field, pick from the suggestion list, fill grouped
|
||||
# inputs via their shared modal (Tab between siblings to keep one
|
||||
# modal open), then click the submit control.
|
||||
await fill_form_interactively(page, expected)
|
||||
```
|
||||
|
||||
Guidelines for the interactive path:
|
||||
|
||||
- Use `get_by_role` / `aria-label` selectors, not brittle CSS classes.
|
||||
- Type the value, wait for the suggestion listbox, then click the option
|
||||
whose text contains the canonical token for the input.
|
||||
- For paired fields rendered inside a single modal (date range pickers,
|
||||
stepper groups, etc.), open the modal **once** and `Tab` between fields
|
||||
instead of clicking each input separately — clicking the second input
|
||||
while the modal is open often gets blocked by the modal's own overlay.
|
||||
- After filling, click the explicit submit control rather than relying on
|
||||
auto-submit.
|
||||
- Re-read the form state and assert each checkpoint (CP1..CPn) before
|
||||
proceeding to results extraction.
|
||||
|
||||
## Final-script instrumentation
|
||||
|
||||
`final_runs/run_<id>/final_script.py` must:
|
||||
|
||||
- write to `final_runs/run_<id>/screenshots/final_execution_<step>_<action>.png`,
|
||||
- reset and append to `final_runs/run_<id>/final_script_log.txt`,
|
||||
- print the final datum at the end of the log.
|
||||
|
||||
```python
|
||||
import asyncio, os
|
||||
from pathlib import Path
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
RUN_DIR = Path(__file__).parent
|
||||
SCREENSHOTS = RUN_DIR / "screenshots"
|
||||
SCREENSHOTS.mkdir(parents=True, exist_ok=True)
|
||||
LOG = RUN_DIR / "final_script_log.txt"
|
||||
LOG.write_text("") # reset
|
||||
|
||||
def log(step: int, msg: str) -> None:
|
||||
line = f"step {step} action: {msg}\n"
|
||||
LOG.open("a").write(line)
|
||||
print(line, end="")
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.firefox.launch(headless=True)
|
||||
context = await browser.new_context(viewport={"width": 1280, "height": 1800})
|
||||
page = await context.new_page()
|
||||
|
||||
await page.goto("<START_URL>", wait_until="domcontentloaded")
|
||||
await page.screenshot(path=str(SCREENSHOTS / "final_execution_1_open_start_page.png"))
|
||||
log(1, "open start page")
|
||||
|
||||
# ... apply CP1, screenshot, log ...
|
||||
# ... apply CP2, screenshot, log ...
|
||||
|
||||
# End of run: capture the final datum visibly and in the log
|
||||
final_value = "<extracted price / code / winner>"
|
||||
with LOG.open("a") as f:
|
||||
f.write(f"\nFINAL_RESPONSE: {final_value}\n")
|
||||
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Inspection commands
|
||||
|
||||
```bash
|
||||
# Latest run tree + log
|
||||
ls -R final_runs/run_<id>
|
||||
cat final_runs/run_<id>/final_script_log.txt
|
||||
|
||||
# Quick file read
|
||||
sed -n '1,220p' final_runs/run_<id>/final_script.py
|
||||
```
|
||||
|
||||
For visual checks, use the `Read` tool on individual PNG files inside
|
||||
`final_runs/run_<id>/screenshots/` rather than calling an external image-QA
|
||||
service.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Workflow
|
||||
|
||||
Detailed expansion of the six-step Webwright loop, adapted for Claude Code.
|
||||
The original loop relied on `webwright.tools.image_qa` for visual QA and
|
||||
`webwright.tools.self_reflection` for the final verdict. Both are replaced
|
||||
here by your native abilities (`Read` on PNG files + reasoning against
|
||||
`plan.md`). No `OPENAI_API_KEY` is required.
|
||||
|
||||
## 1. Plan
|
||||
|
||||
Parse the task into critical points (CPs) and write `WORKSPACE_DIR/plan.md`:
|
||||
|
||||
```markdown
|
||||
# Task
|
||||
<verbatim task description>
|
||||
|
||||
# Critical Points
|
||||
- [ ] CP1: <constraint / filter / sort / selection / required datum>
|
||||
- [ ] CP2: ...
|
||||
```
|
||||
|
||||
Rules for CPs:
|
||||
|
||||
- One CP per independently verifiable requirement.
|
||||
- Numeric, date, quantity, and unit CPs must be exact.
|
||||
- Ranking CPs ("cheapest", "best-selling", "highest-rated", …) must
|
||||
reference the site's actual sort/filter control.
|
||||
- If the task asks for a final datum, make it its own CP
|
||||
(e.g. `CP5: Record the displayed cheapest economy fare`).
|
||||
|
||||
## 2. Explore
|
||||
|
||||
Goal: discover stable selectors, confirm every required filter control
|
||||
exists, and identify how to capture evidence for each CP.
|
||||
|
||||
- Run scratch Playwright scripts (see `playwright_patterns.md`) inside
|
||||
`WORKSPACE_DIR/`. Save scratch PNGs under `WORKSPACE_DIR/screenshots/`
|
||||
(separate from `final_runs/`).
|
||||
- Print URL, title, and `aria_snapshot()` for the region of interest at
|
||||
every step.
|
||||
- Use `Read` on saved PNGs to confirm UI state when ARIA evidence is
|
||||
ambiguous.
|
||||
- If a filter looks unavailable, expand drawers / accordions / mobile
|
||||
filter panels and inspect again before concluding it doesn't exist.
|
||||
- A search-box query never substitutes for a dedicated filter control.
|
||||
|
||||
## 3. Author `final_script.py`
|
||||
|
||||
Create a fresh `final_runs/run_<id>/` (use the next integer above any
|
||||
existing `run_*`) and place `final_script.py` inside it. Instrument per
|
||||
`playwright_patterns.md`:
|
||||
|
||||
- viewport 1280×1800, headless local Firefox, no `full_page`;
|
||||
- one `final_execution_<step>_<action>.png` per CP;
|
||||
- one `step <n> action: <reason and action>` log line per
|
||||
constraint-relevant interaction;
|
||||
- the final datum printed into `final_script_log.txt` at the end.
|
||||
|
||||
Each screenshot should map to a CP from `plan.md` so verification is
|
||||
trivial.
|
||||
|
||||
## 4. Execute
|
||||
|
||||
Run the script once. If it crashes, fix it inside the same run folder and
|
||||
re-execute — but if a partial run already produced screenshots that don't
|
||||
match the fixed flow, delete them so the run folder reflects a single
|
||||
clean execution.
|
||||
|
||||
## 5. Self-verify (replaces `self_reflection`)
|
||||
|
||||
For every CP in `plan.md`:
|
||||
|
||||
1. Identify the screenshot(s) and/or log line that provide evidence.
|
||||
2. `Read` each cited PNG.
|
||||
3. Confirm the evidence is **unambiguous**:
|
||||
- Filter chip / selected state visibly applied (not hidden behind a
|
||||
closed drawer);
|
||||
- Numeric / date values match exactly (not broadened);
|
||||
- Sort applied via the site's control (not implied by result order);
|
||||
- Required submit / search / apply action visibly taken;
|
||||
- Final datum legibly displayed.
|
||||
4. Tick the CP only when the evidence is concrete. Be harsh on partial,
|
||||
occluded, or ambiguous states.
|
||||
|
||||
If any CP fails, diagnose the *specific* issue — wrong filter value,
|
||||
missing control, hidden chip, broadened range, missing confirmation,
|
||||
missing screenshot, etc. Fix `final_script.py`, run it again inside
|
||||
`final_runs/run_<id+1>/`, and re-verify against `plan.md`.
|
||||
|
||||
Empty result sets are acceptable when the correct filters were demonstrably
|
||||
applied.
|
||||
|
||||
## 6. Done
|
||||
|
||||
Stop only when **all** of the following are true:
|
||||
|
||||
1. `plan.md` exists with every CP enumerated as a checklist item.
|
||||
2. `final_runs/run_<id>/final_script.py` ran cleanly from scratch and
|
||||
produced `final_script_log.txt` plus all CP screenshots.
|
||||
3. Every CP is checked off with a cited screenshot and/or log line.
|
||||
4. The final datum (if the task asked for one) is reported to the user
|
||||
verbatim and is also present in `final_script_log.txt`.
|
||||
5. `ls -R final_runs/run_<id>` and `cat final_runs/run_<id>/final_script_log.txt`
|
||||
show the expected artifacts.
|
||||
|
||||
If any of those is false, do not declare done — diagnose, fix, and re-run
|
||||
in a new `run_<id+1>/`.
|
||||
Reference in New Issue
Block a user