Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
17fed3f
feat(walmart_careers): add Walmart Careers mirror site
evanz37 Sep 6, 2026
12cfb03
docs: bump site count to 18
evanz37 Sep 6, 2026
3d23719
feat(walmart_careers): add 20 benchmark tasks
evanz37 Sep 6, 2026
573a4a7
feat(walmart_careers): match the live header, results and job-detail …
evanz37 Sep 6, 2026
5fab41d
fix(walmart_careers): harden all 20 tasks against leaks and shortcuts
evanz37 Sep 6, 2026
ddc6355
docs(walmart_careers): record the frozen seed and the verification run
evanz37 Sep 6, 2026
0068e14
fix(walmart_careers): salaried cards drop the ZIP and shift label
evanz37 Sep 6, 2026
0ac55b8
Merge upstream/main: phys_org and target land as sites 18 and 19
evanz37 Sep 6, 2026
a9ab990
fix(walmart_careers): migrate to port 40019 (site 20)
evanz37 Sep 6, 2026
900d24b
fix(walmart_careers): move ground truth out of the shipped tree; reba…
evanz37 Sep 6, 2026
85a4c9f
fix(walmart_careers): push task targets off rank 1, widen distractor …
evanz37 Sep 6, 2026
707f673
fix(walmart_careers): de-risk prior-knowledge answers, clarify tasks …
evanz37 Sep 7, 2026
4916213
docs: sweep site count to 20 and port range to 40019
evanz37 Sep 7, 2026
07bea17
chore(walmart_careers): drop dead queries, harden username suffixing
evanz37 Sep 7, 2026
7137d29
chore(walmart_careers): pin assets to HF dataset PR (ChilleD/WebHarbo…
evanz37 Sep 7, 2026
f7f5bf1
fix(walmart_careers): match reference layouts below the fold on detai…
evanz37 Sep 7, 2026
69160cc
test(walmart_careers): add deterministic task verifiers and verify_lib
evanz37 Sep 9, 2026
683338d
chore(walmart_careers): backfill verifier_path and judge_rubric
evanz37 Sep 9, 2026
5c43738
test(walmart_careers): LLM-free validation matrix for the grading con…
evanz37 Sep 9, 2026
8f606c2
fix(walmart_careers): loosen count matcher after real agent runs (ver…
evanz37 Sep 9, 2026
b29d7e2
fix(walmart_careers): loosen verifier 5 route gate after real agent runs
evanz37 Sep 9, 2026
8c5c6d2
docs(walmart_careers): grading-contract validation summary
evanz37 Sep 9, 2026
0c6a4dc
chore(walmart_careers): rephrase judge rubrics around visible evidenc…
evanz37 Sep 9, 2026
5a9eff1
Merge remote-tracking branch 'upstream/main' into feat/walmart_careers
evanz37 Sep 9, 2026
a264909
fix(walmart_careers): migrate to port 40022 (site 23)
evanz37 Sep 9, 2026
e28e918
Merge current main and harden Walmart Careers mirror
Raibows Sep 9, 2026
249631e
Fix header menu dismissal behavior
Raibows Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .assets-revision
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# is a git revision (branch name like `main`, a tag, or a specific commit
# sha). Override at runtime with the ASSETS_REVISION env var.
repo: ChilleD/WebHarbor
revision: 070123d74c01a8b29808201be85462fd7d0ec3c4
revision: 65c479f894763f64c6073e0d180ebf542d1d2c02
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ sites/*/instance/
# Don't ship — runtime doesn't need scrape intermediate (data is in instance_seed/*.db).
sites/*/scraped_data/

# Don't ship — per-site local dev helpers (asset harvest, smoke tests, leak audits).
sites/*/scripts_dev/
sites/*/tests/
sites/*/verify/tests/
agent_demo/runs/
.venv/

# Don't ship — bytecode / venvs.
sites/*/__pycache__/
**/__pycache__/
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ sites/*/static/external_cache/
# =============================================================
# scrape pipeline intermediate; runtime data lives in instance_seed/*.db
sites/*/scraped_data/
/scraped_data/
# rebuilt at every container boot from instance_seed/
sites/*/instance/
sites/*/venv/
Expand Down Expand Up @@ -95,3 +96,7 @@ secrets.json
# Agent demo results
# =============================================================
agent_demo/runs/

# walmart_careers verifier validation runs (run signatures + DB snapshots): never committed.
sites/*/scripts_dev/runs/
sites/*/scripts_dev/**/*.db
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn/<site>/`. The path predates the ren
# fresh clone
./scripts/fetch_assets.sh # pulls assets from HF
./scripts/build.sh # docker build -t webharbor:dev .
docker run -d -p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev
docker run -d -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev
```

Or use the published image directly:

```bash
docker run -d -p 8101:8101 -p 40000-40022:40000-40022 \
docker run -d -p 8101:8101 -p 40000-40023:40000-40023 \
battalion7244/webharbor:latest
```

Sites are on `40000`-`40022` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane:
Sites are on `40000`-`40023` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane:

| Method | Path | Purpose |
|--------|---------------------|-------------------------------------------|
Expand Down Expand Up @@ -136,13 +136,13 @@ python3 -m py_compile sites/<site>/app.py

# 3. run on alt ports (don't collide with anything you already have running)
docker run -d --rm --name wh-test \
-p 8201:8101 -p 41000-41022:40000-40022 webharbor:dev
-p 8201:8101 -p 41000-41023:40000-40023 webharbor:dev

# 4. control plane healthy, all sites alive
curl -s http://localhost:8201/health | python3 -m json.tool | head

# 5. every site renders 200
for p in $(seq 41000 41022); do
for p in $(seq 41000 41023); do
curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/
done

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl

## Existing containers

If a container is already running on `:8101` / `:40000-40022`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41022`).
If a container is already running on `:8101` / `:40000-40023`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41023`).
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ git clone https://github.com/<you>/webharbor && cd webharbor
./scripts/fetch_assets.sh # pull current assets
./scripts/new_site.py mywebsite # OR edit an existing site
./scripts/build.sh && docker run -d --rm \
-p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev
-p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev
# iterate locally...

./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out
Expand Down
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# WebHarbor — slim, self-contained image.
# 23 Flask mirror sites + control plane on :8101.
# 24 Flask mirror sites + control plane on :8101.

FROM python:3.12-slim-bookworm

Expand Down Expand Up @@ -44,6 +44,12 @@ RUN python3 /opt/check_asset_inventory.py /opt/WebSyn/compass
RUN cd /opt/WebSyn/compass && rm -rf instance instance_seed && \
PYTHONHASHSEED=0 python migrate_seed.py && rm -rf instance

# Walmart Careers validates source-backed media and rebuilds its deterministic SQLite seed from tracked source data.
RUN python3 /opt/check_asset_inventory.py /opt/WebSyn/walmart_careers && \
python3 /opt/WebSyn/walmart_careers/check_tracked_assets.py
RUN cd /opt/WebSyn/walmart_careers && rm -rf instance instance_seed && \
PYTHONHASHSEED=0 python seed_data.py && rm -rf instance

COPY websyn_start.sh /opt/websyn_start.sh
COPY control_server.py /opt/control_server.py
COPY site_runner.py /opt/site_runner.py
Expand All @@ -66,6 +72,6 @@ os.makedirs('instance_seed', exist_ok=True); \
shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \
print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance

EXPOSE 8101 40000-40022
EXPOSE 8101 40000-40023

CMD ["/opt/websyn_start.sh"]
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod
- **Deep features unlocked** — carts, checkouts, accounts, all fully testable
- **Evolving** — harder tasks drive richer mirrors; the environment grows with agents
- **RL-ready** — sub-second database resets between rollouts
- **Community-driven** — 23 sites today, scaling to 100+ together
- **Community-driven** — 24 sites today, scaling to 100+ together

## 🚀 Quickstart

One command to run all web environments:

```bash
docker run -p 8101:8101 -p 40000-40022:40000-40022 battalion7244/webharbor:latest
docker run -p 8101:8101 -p 40000-40023:40000-40023 battalion7244/webharbor:latest
```

Then point your agent at `http://localhost:40000` through `http://localhost:40022` to explore 23 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, and Compass`.
Then point your agent at `http://localhost:40000` through `http://localhost:40023` to explore 24 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, Compass, and Walmart Careers`.

For sub-second reset between rollouts, expose the control plane and call `/reset/<site>`:

Expand Down
2 changes: 1 addition & 1 deletion agent_demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export OPENAI_BASE_URL=https://api.openai.com/v1 # or your Azure / vLLM endpoi

## Run a task

WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40022:40000-40022 battalion7244/webharbor:latest`).
WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40023:40000-40023 battalion7244/webharbor:latest`).

Run a single task from a site's `tasks.jsonl`:

Expand Down
28 changes: 22 additions & 6 deletions control_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking',
'github', 'google_flights', 'google_map', 'google_search',
'huggingface', 'wolfram_alpha', 'cambridge_dictionary',
'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass',
'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers',
]
BASE_PORT = 40000
WEBSYN_DIR = '/opt/WebSyn'
Expand All @@ -48,6 +48,7 @@
# for those.
_site_procs: dict = {}
_site_procs_lock = threading.Lock()
_reap_lock = threading.Lock()

# We tried graceful SIGTERM. Werkzeug's threaded serve_forever() doesn't
# honor it. Since /reset wipes instance/ next anyway, in-flight transactions
Expand Down Expand Up @@ -91,6 +92,18 @@ def is_alive(pid) -> bool:
return False


def reap_exited_children() -> None:
"""Reap every exited direct child, including re-parented Flask workers."""
with _reap_lock:
while True:
try:
pid, _status = os.waitpid(-1, os.WNOHANG)
except ChildProcessError:
return
if pid <= 0:
return


def kill_site(site: str, reap_grace: float = REAP_GRACE_SECS):
pid = read_pid(site)
if not pid:
Expand All @@ -101,24 +114,25 @@ def kill_site(site: str, reap_grace: float = REAP_GRACE_SECS):
os.killpg(pid, signal.SIGKILL)
except ProcessLookupError:
pass
# If we own a Popen for this supervisor, wait()+reap so it doesn't
# linger as a zombie. (Supervisors started at boot via websyn_start.sh
# aren't tracked here; we still adopted them as children via container
# init, but Python won't reap them — they stay zombies until container
# exit. That's harmless: is_alive() correctly reports them as dead.)
# Reap supervisors created through Popen and boot-time supervisors that
# became direct children when websyn_start.sh exec'd this control process.
with _site_procs_lock:
proc = _site_procs.pop(site, None)
if proc is not None:
try:
proc.wait(timeout=reap_grace)
except subprocess.TimeoutExpired:
pass
reap_exited_children()
# Belt-and-suspenders: confirm the supervisor is actually dead before
# returning, even when we don't own the Popen. is_alive() looks at
# /proc state and returns False for zombies, so this loop exits in ms.
deadline = time.time() + reap_grace
while time.time() < deadline:
if not is_alive(pid):
for _ in range(10):
reap_exited_children()
time.sleep(0.01)
return
time.sleep(0.01)
raise RuntimeError(f'failed to stop {site} process group {pid}')
Expand Down Expand Up @@ -187,6 +201,8 @@ def restart_one(site: str) -> dict:

@app.route('/health')
def health():
reap_exited_children()

def status(site):
pid = read_pid(site)
alive = is_alive(pid)
Expand Down
64 changes: 64 additions & 0 deletions review-reports/PR-86-FINAL-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# PR 86 Final Audit

## Scope

This audit covers `aiming-lab/WebHarbor#86`, whose reviewed head was `a2649095bbef7d0fbce011d4510e6ee1cbd08d68`. The PR was reviewed against upstream `main` at `129a274230070abb90b6d4ec209d81a911754ec9`. Eight independent review agents examined security and state, application and data behavior, task ground truth, verifier resistance, UI and responsive behavior, repository and container integration, test evidence, and asset provenance.

The original PR was conflicting with current `main`. Current `main` already assigned internal port `40022` to Compass, so this integration preserves Compass and registers Walmart Careers as the twenty-fourth site on internal port `40023`.

## Agent Findings and Resolutions

| Agent | Review area | Verified findings | Resolution |
| --- | --- | --- | --- |
| 1 | Security and state | `sites/walmart_careers/app.py` used a fixed Flask secret, application contact data was stored in the client session, input boundaries and transaction failure handling were incomplete, and SQLite foreign-key enforcement was connection-dependent. | Added environment/random secret handling, cookie and request-size controls, CSRF protection, strict field limits, per-connection foreign keys, transaction rollback, race handling, and server-side one-time application drafts with ownership and replay checks. |
| 2 | Application and data | Unknown locations could degrade into an unfiltered result set, query semantics were permissive, and profile values could be silently truncated. | Unknown or unsupported locations now return zero results; duplicate and invalid query parameters, facets, sorting, pagination, radii, tabs, and excessive search tokens are rejected; field validation rejects overlong input rather than truncating it. |
| 3 | Tasks and ground truth | Several task verifiers omitted required filter constraints, one task description was ambiguous, and the signed-in email behavior conflicted with one rubric. | Clarified affected task text, enforced every required filter, and derived task targets and candidate sets from the supplied initial database through `verify/ground_truth.py`. |
| 4 | Verifier resistance | Screenshot bytes were not decoded, trajectory URLs could be fabricated, protected schema/catalog rows were not comprehensively checked, stateful verifiers allowed unrelated writes, and several numeric/address checks accepted weak context. | Added complete PNG decoding, loopback origin and port checks, exact nine-table schema hashing, protected-table checks, exact allowed row deltas, required workflow ordering, candidate-detail coverage, and strict contextual matching for addresses, identifiers, confirmation numbers, time, counts, and experience. |
| 5 | UI and responsive behavior | The original implementation overflowed at 320 px, exposed misleading inert controls, and had menu, dialog, table, focus, heading, and reduced-motion defects. | Reworked responsive layout and overlays, added scroll containment and focus treatment, corrected headings and form semantics, honored reduced-motion preferences, and removed non-functional play, pause, carousel, and share controls. |
| 6 | Repository, assets, and Docker | Compass already occupied `40022`; the original asset proposal was not based on current asset `main`; the repository and container registries still assumed 23 sites. | Merged current `main`, retained Compass on `40022`, placed Walmart on `40023`, updated all 24-site registries and port ranges, and pinned assets to revision `65c479f894763f64c6073e0d180ebf542d1d2c02`. |
| 7 | Tests and release evidence | The original tests were predominantly hand-built verifier fixtures and did not cover Flask routes, container behavior, assets, complete schema integrity, or all integration contracts. | Added route, security, state, seed, asset, task-ground-truth, verifier-adversarial, and 24-site integration tests; validated the release image with real browser workflows and control-plane operations. |
| 8 | Asset provenance and classification | Media, icons, and fonts lacked complete machine-verifiable inventories; synthetic benchmark data was not explicitly classified; old documentation and dependency declarations were incomplete. | Added checksummed inventories for 35 downloaded media assets and 27 tracked assets, added provenance documentation, classified jobs/users/applications/tasks as synthetic benchmark data, removed three unused or unverified tracked assets, and pinned runtime dependencies. |

## Additional Verified Fixes

- `sites/walmart_careers/seed_data.py` now constructs a deterministic database in a temporary path and atomically publishes it with seed version `walmart-careers-v2`.
- Seed startup validation protects immutable catalog counts and required benchmark users while permitting legitimate runtime registrations, saved jobs, and applications. Restart preserves runtime state; reset restores the seed.
- `control_server.py` reaps exited direct children and re-parented Flask workers, preventing reset/restart zombie accumulation under PID 1.
- Health checks validate the immutable catalog and required benchmark identities without treating valid runtime rows as seed corruption.
- The release image builds seed databases from tracked deterministic source and does not package tests, verifier test fixtures, development scripts, or answer-bearing browser evidence.
- The Walmart media archive contains 35 files, is 11,458,196 bytes, and has SHA-256 `b84a0072c603225f29e21edfd8f8038fe02a0261a38a60f8e70ed871ea7ce77a`.

## Validation Results

| Validation | Result |
| --- | --- |
| Walmart application, integration, seed, asset, ground-truth, and verifier suites | 297 passed; 16 unittest subtests passed |
| Compass regression suite | 229 passed |
| Rotten Tomatoes regression suite | 66 passed; 4,143 unittest subtests passed |
| OSU regression suite | 25 passed; 264 unittest subtests passed |
| TED regression suite | 27 passed; 291 unittest subtests passed |
| Ruff fatal-error/undefined-name checks | Passed |
| Python byte compilation | Passed |
| Shell syntax and Git whitespace checks | Passed |
| Walmart media inventory | 35 files verified |
| Walmart tracked asset inventory | 27 files verified |
| Compass media inventory in Docker build | 2,907 files verified |
| Release Docker image | `sha256:13df807773246725ab8b1999c03d87b5efb1be0bab8eaaf625bf14d1c31a68f7` |
| Browser execution of all Walmart tasks | 20/20 passed; 106 browser steps; deterministic verifier passed each task |
| Responsive/browser matrix | 21 routes × 5 viewport widths = 105 checks; zero overflow, clipping, broken images, unlabeled controls, page errors, or remote requests |
| Narrow-screen overlays | Four navigation menus and three result popovers remained within the viewport |
| No-JavaScript static behavior | Passed; no misleading media controls remained |
| Release container control plane | 24/24 sites ready; all 24 roots returned successful responses |
| Release reset-all | Passed; all 24 runtime seed trees matched their seed trees |
| Walmart restart and reset | Valid runtime state survived restart; reset restored byte-identical seed state |
| Ten consecutive Walmart resets | Passed; PID 1 file descriptors remained 4 → 4 and zombie count remained 0 → 0 |

## Post-review Navigation Correction

A reported header-navigation defect was reproduced at the 1080 × 397 CSS-pixel viewport represented by the supplied screenshot. Opening Career areas, Brands, and Resources left all three native `details` elements open; clicking outside or pressing Escape did not close them. The live Walmart Careers header was independently checked and kept only one primary menu expanded while closing it on outside pointer interaction and Escape.

`static/js/navigation.js` now provides mutually exclusive header menus, same-trigger close, outside-pointer close, focus-leave close, Escape close with focus restoration, and mutual exclusion with the account menu. Packaged Chromium checks passed at 1080 × 397 and 320 × 480, the 20-task browser/verifier suite remained 20/20 with 106 steps, and the 105-case responsive matrix remained free of overflow, clipping, broken images, unlabeled controls, page errors, and remote requests.

## GitHub Write Policy

No GitHub comment or review was submitted during this audit. The permitted GitHub write operation is limited to pushing the reviewed commit to the existing PR head branch.
2 changes: 1 addition & 1 deletion scripts/check_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ for site in sites/*/; do
warnings=$((warnings + 1))
fi
done
if [[ -f "sites/$s/asset_inventory.json" && -d "sites/$s/static/images" && -d "sites/$s/static/external_cache" ]]; then
if [[ -f "sites/$s/asset_inventory.json" ]]; then
python3 scripts/check_asset_inventory.py "sites/$s"
fi
done
Expand Down
4 changes: 2 additions & 2 deletions sites/rotten_tomatoes/tests/test_environment_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def test_task_manifest_and_registry(self):
self.assertEqual(row['web'], 'http://localhost:40021/')
self.assertTrue((ROOT / row['verifier_path']).is_file())
self.assertNotIn('answer', row)
self.assertIn('ted osu rotten_tomatoes compass)', (ROOT / 'websyn_start.sh').read_text())
self.assertIn('ted osu rotten_tomatoes compass walmart_careers)', (ROOT / 'websyn_start.sh').read_text())
self.assertIn("'ted', 'osu', 'rotten_tomatoes'", (ROOT / 'control_server.py').read_text())
self.assertIn('40000-40022', (ROOT / 'Dockerfile').read_text())
self.assertIn('40000-40023', (ROOT / 'Dockerfile').read_text())
self.assertTrue((SITE / '.build-generated-seed').is_file())
self.assertTrue((SITE / '.requires-images').is_file())
self.assertTrue((SITE / '.requires-external-cache').is_file())
Expand Down
1 change: 1 addition & 0 deletions sites/walmart_careers/.build-generated-seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Dockerfile generates instance_seed/walmart_careers.db deterministically from tracked source data and pinned image assets.
2 changes: 2 additions & 0 deletions sites/walmart_careers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Local build/inspection helpers: never shipped with the site.
scripts_dev/
Loading