Skip to content

release: anc.dev routing-drift fix + post-#73 promotion#85

Merged
brettdavies merged 12 commits into
mainfrom
release/2026-05-15-routing-drift
May 15, 2026
Merged

release: anc.dev routing-drift fix + post-#73 promotion#85
brettdavies merged 12 commits into
mainfrom
release/2026-05-15-routing-drift

Conversation

@brettdavies
Copy link
Copy Markdown
Owner

Summary

Second production release since the v0.1 launch (#60, 2026-04-30) and the first since #73 (2026-05-03). The headline is the routing-drift fix: anc.dev is currently bound to the staging Worker (agentnative-site-staging), not the named-production Worker (agentnative-site). This release brings agentnative-site current with every dev-side change since #73, manually detaches anc.dev from staging via the CF API immediately before merge, and lets deploy.yml reattach the domain to the named-prod Worker per the top-level routes: field in wrangler.jsonc.

This is also the first deploy that applies the live-scoring Durable Object migration (v1: new_sqlite_classes: ["Sandbox"]) to the named-prod Worker. DO migrations are one-way walls: once this deployment lands, wrangler rollback cannot cross the v1 boundary on production. Treated as a milestone.

The R2 bucket anc-score-cache (referenced by the top-level wrangler config) did not exist on the account. It was created out-of-band via wrangler r2 bucket create anc-score-cache before opening this PR so binding validation passes at deploy time.

This release bundles 11 PRs from dev (post-#73):

  • Live-scoring scaffolding: 4 units, 1 pre-implementation gate, 1 production-readiness followup (#77, #78, #79, #80, #81, #84). The /api/score route is NOT user-facing yet (U5 onward still pending). This release ships the wrangler bindings, the DO stub returning {error: 'sandbox_stub_until_u6'}, the input parser plus 4-step GitHub URL discovery chain, the registry and discovery indexes, and the Alpine plus musl sandbox image pinned at registry.cloudflare.com/<acct>/anc-sandbox:30f61f1.
  • Spec v0.4.0 sync (#83): adds principle P8 (Discoverable Through Agent Skill Bundles), renames docs/DESIGN.md to DESIGN.md, renames .impeccable.md to PRODUCT.md, refreshes prose-tooling channel.
  • Site-side prose-check enforcement (#82): vendored Vale rule packs (brand plus site channels) and prose-check.sh orchestrator with a blocking-category whitelist (TYPOS|GRAMMAR|CONFUSED_WORDS). Pre-push only; not in CI.
  • Dev-loop hardening: pre-push wrangler dry-run (#76), biome warning silenced (#75), project-scoped wrangler telemetry opt-out (#74).

Changelog

Added

  • Principle P8: Discoverable Through Agent Skill Bundles. Eighth principle in the spec, with full content surfaced at /principles/p8-discoverable-skill-bundle and listed on /. Spec advances from v0.3.0 to v0.4.0 (#83).
  • Live-scoring infrastructure scaffolding (not yet user-facing): wrangler bindings for Containers, Durable Objects, R2 buckets, and Rate Limits on both env blocks; Sandbox Durable Object stub at src/worker/score/do.ts; build-time registry-index and discovery-hints-index; Alpine plus musl sandbox image at docker/sandbox/Dockerfile with anc baked in via the brew-installable musl binary; input parser and 4-step GitHub URL discovery chain at src/worker/score/{parse-install,registry-lookup,discover-binary,validate}.ts. The /api/score route itself ships in a later release (#77, #78, #79, #80, #81).
  • Site-side prose-check enforcement at pre-push: vendored Vale rule packs (styles/brand/*, styles/site/*, styles/config/vocabularies/{brand,site}/{accept,reject}.txt), scripts/prose-check.sh orchestrator, scripts/check-banned-fonts.sh deployment-layer scan, and scripts/sync-prose-tooling.sh rule-pack sync. LanguageTool blocking-category whitelist (TYPOS|GRAMMAR|CONFUSED_WORDS) keeps signal high (#82).
  • PRODUCT.md at the repo root (channel-context file expected by the /impeccable skill). Renamed from .impeccable.md (#83).
  • wrangler deploy --dry-run step in the pre-push hook, catching deploy-time binding validation failures before push (#76).
  • Project-scoped send_metrics: false in wrangler.jsonc. Belt-and-suspenders with the per-user shell env var and per-machine wrangler config; travels with the repo (#74).

Changed

  • docs/DESIGN.md renamed to DESIGN.md at the repo root, matching the /impeccable channel-context layout (#83).
  • Site spec version (footer, OG card, badges) advances from v0.3.0 to v0.4.0, reflecting the vendored spec snapshot bump (#83).
  • Adopt staging-leads-prod as the default sandbox image workflow. The two wrangler.jsonc pins (containers[0].image top-level, env.staging.containers[0].image) are independent: staging advances during development, prod advances at release. RELEASES.md § Sandbox image releases documents the soak-then-promote default and the lockstep shortcut for low-risk bumps (#84).
  • Sandbox image now lives in the Cloudflare managed registry (registry.cloudflare.com/<acct>/anc-sandbox:30f61f1) instead of the deprecated Docker Hub URI. Build is decoupled from deploy via wrangler containers build -p; deploy never rebuilds (#84).

Fixed

  • Resolve staging container ImagePullError caused by Docker Hub registry deprecation. The sandbox image now lives in the Cloudflare managed registry and is pinned by <git-sha> tag in both env blocks (#84).
  • Add CI guard that verifies every PR's pinned sandbox image tags exist in the CF managed registry. Main-targeting PRs additionally enforce pin equality (released state). This release exercises the main-targeting equality branch for the first time. Both pins are :30f61f1 (lockstep), so the equality check passes by default (#84).
  • Silence biome noTemplateCurlyInString warning on a footer test name. The string was intentional and is now annotated with a biome-ignore comment (#75).

Documentation

  • RELEASES.md § Sandbox image releases (live-scoring): full spec for image bumps including local-build-once via wrangler containers build -p, soak-then-promote default flow, lockstep shortcut, image-retention discipline (never delete a tag that backed a shipped Worker version), and the DO-migration one-way wall note (#84).
  • docker/sandbox/README.md: developer-facing image build and push reference (#84).

Type of Change

  • feat: New feature (non-breaking change which adds functionality)

The release is multi-typed (feat plus fix plus chore plus docs) but feat headlines because the live-scoring scaffolding (4 units) is the largest user-facing surface introduced, even though the /api/score route is not yet wired.

Related Issues/Stories

Files Modified

Modified:

  • wrangler.jsonc: bindings for Containers, Durable Objects, R2 buckets, and Rate Limits on both env blocks; image pinned at registry.cloudflare.com/<acct>/anc-sandbox:30f61f1; send_metrics: false; DO migrations v1 (#74, #81, #84).
  • RELEASES.md: sandbox image release workflow, status-check context pitfall, telemetry opt-out documentation (#74, #84).
  • package.json: spec v0.4.0 bump and prose-check scripts (#82, #83).
  • src/worker/index.ts, src/worker/headers.ts: routing prep for /api/score (stub still returns 503 until U5) (#81).
  • src/build/build.mjs: registry-index plus discovery-hints-index emission (#78).
  • tests/regression.test.ts: prose-check plus live-scoring scaffolding coverage (#78, #82).
  • 16 other files. Full list in git diff origin/main..HEAD --name-only.

Created:

  • content/principles/p8-discoverable-skill-bundle.md, src/data/spec/principles/p8-discoverable-skill-bundle.md, content/principles/README.md (#83).
  • docker/sandbox/Dockerfile, docker/sandbox/README.md, docker/sandbox/.dockerignore, docker/sandbox/.ignored-sentinel.txt, tests/dockerfile-sandbox.test.ts (#79, #84).
  • discovery-hints.yaml, src/build/registry-index.mjs, tests/registry-index.test.ts (#78).
  • src/worker/score/{parse-install,registry-lookup,discover-binary,validate,do}.ts and 4 paired test files (#80, #81).
  • styles/{brand,site,config/vocabularies/brand,config/vocabularies/site}/* (Vale rule packs), scripts/prose-check.sh, scripts/check-banned-fonts.sh, scripts/sync-prose-tooling.sh, scripts/__fixtures__/prose-check/**, .vale.ini, BRAND.md (#82).
  • scripts/measure-discovery-hit-rate.mjs (#77).
  • .github/workflows/ci.yml additions: sandbox image registry-existence guard plus pin-equality guard (#84).

Renamed:

  • docs/DESIGN.md to DESIGN.md at repo root (#83).
  • .impeccable.md to PRODUCT.md (#83).

Deleted:

  • None.

Testing

  • Unit tests added/updated
  • All tests passing

Test Summary:

  • 315 unit and regression tests pass on the release branch (pre-push gate verified locally).
  • bun run build clean: 8 principles, 112 HTML pages, 112 MD pages, 97 scorecard pages, 96 badges.
  • bun x wrangler deploy --dry-run validates the named-prod environment bindings end-to-end: Sandbox DO, R2 bucket anc-score-cache (created out-of-band before this PR), SCORE_LIMITER (10 requests per 60 s), ASSETS. Container image anc-sandbox:30f61f1 resolves to the CF managed registry.
  • CI guard: top-level pin equals env.staging pin (both :30f61f1, lockstep), so the new main-targeting equality check is expected to pass on its first exercised PR run.
  • Pre-push prose-check: 0 blocking, 1108 warning (below threshold).
  • DO migration v1: this is the first deploy applying the migration to the named-prod Worker. The container application on the named-prod side will start at v1 (independent from staging's v2 history).

Post-merge verification plan (executed after the deploy.yml run on the merge SHA):

  • deploy.yml production-deploy log lists ONLY anc.dev (custom domain) under triggers (no workers.dev URL, since named-prod has workers_dev: false).
  • Next push to dev triggers the staging deploy. That deploy log lists ONLY the staging workers.dev URL, no anc.dev.
  • curl -sI https://anc.dev/ returns 200 with NO x-robots-tag header.
  • curl -sI https://agentnative-site-staging.brettdavies.workers.dev/ returns 200 WITH x-robots-tag: noindex.
  • CF API /accounts/<acct>/workers/domains shows one record for hostname anc.dev with service agentnative-site.

## Summary

Adds `send_metrics: false` to `wrangler.jsonc`. Project-scoped opt-out
of Cloudflare Wrangler's CLI telemetry. Belt-and-suspenders with two
existing layers:

- **User shell env var** `WRANGLER_SEND_METRICS=false` (set in dotfiles
at `config/shell/telemetry.sh:40`).
- **Per-machine config** via `wrangler telemetry disable` (writes to
`~/.config/.wrangler` or equivalent).

The project-scoped layer travels with the repo, so CI runs and any
contributor's local wrangler invocations stay opted out regardless of
their shell environment or per-machine config.

## Changelog

### Changed

- `wrangler.jsonc`: add `send_metrics: false` to opt the project out of
Wrangler CLI telemetry.

## Type of Change

- [x] `chore`: Maintenance tasks (dependencies, config, etc.)

## Related Issues/Stories

- Story: Telemetry opt-out hardening (project-scoped layer to back up
the env-var and per-machine settings).
- Issue: n/a
- Architecture: n/a
- Related PRs: n/a

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Pre-push hook: 200/200 unit + regression tests pass
- `bun x wrangler deploy --dry-run --env staging` succeeds with the new
field present
- Wrangler reads `send_metrics` from the project config (Cloudflare
docs: same field name in both `.toml` and `.jsonc` formats)

## Files Modified

**Modified:**

- `wrangler.jsonc`: add `send_metrics: false` with a comment explaining
the three-layer opt-out (env var, machine config, project config).

**Created:** None.

**Renamed:** None.

**Deleted:** None.

## Key Features

n/a (config field addition).

## Benefits

- CI runners (GitHub Actions) inherit the opt-out without needing a
workflow-level `WRANGLER_SEND_METRICS` env addition.
- New contributors' local wrangler runs stay opted out without touching
their shell config.
- Three independent layers means any single mechanism breaking (env not
exported, machine config wiped, etc.) still leaves the project opted
out.

## Breaking Changes

- [x] No breaking changes.

## Deployment Notes

- [x] No special deployment steps required.

The field affects CLI telemetry only, not Worker runtime behavior.
Production behavior on anc.dev is unchanged.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/)
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible

## Additional Context

User-level dotfile already has `WRANGLER_SEND_METRICS=false` at
`~/dotfiles/config/shell/telemetry.sh:40`. `wrangler telemetry status`
confirmed "Status: Disabled (set by environment variable)". This PR adds
the project-scoped third layer.
#75)

## Summary

Silences the lone biome warning surfaced by `bun run lint`. The test
name in `tests/build.test.ts:312` used `${SITE_SPEC_VERSION}` inside a
single-quoted string, which biome's `noTemplateCurlyInString` rule flags
as a likely typo (developer probably meant a template literal). The
actual assertion on line 320 already uses real template-string
interpolation; only the human-readable test name needed the fix.

Switched the placeholder notation to angle brackets
(`v<SITE_SPEC_VERSION>`) so the description still reads as "renders the
version with a v prefix" without tripping the lint.

## Type of Change

- [x] `chore`: Maintenance tasks (dependencies, config, etc.)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: n/a
- Related PRs: n/a

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- `bun run lint`: 0 errors, 0 warnings (was: 0 errors, 1 warning)
- `bun test tests/build.test.ts`: 128 of 128 pass

## Files Modified

**Modified:**

- `tests/build.test.ts` (one-line change: rename test description
placeholder from `${SITE_SPEC_VERSION}` to `<SITE_SPEC_VERSION>`)

**Created:** None.

**Renamed:** None.

**Deleted:** None.

## Breaking Changes

- [x] No breaking changes

## Deployment Notes

- [x] No special deployment steps required

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible
## Summary

Closes the parity gap between `scripts/hooks/pre-push` and
`.github/workflows/ci.yml`. CI's gate is install, lint, build, test,
`wrangler deploy --dry-run`. The local hook only ran the first three.
The header comment justified the omission with "requires worker secrets
and is already enforced in CI", but `--dry-run` needs no secrets (CI
runs it without any wrangler credentials), so the claim was stale.

Net effect: pushes that would fail wrangler config or bundle validation
now fail locally instead of round-tripping through CI.

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: n/a
- Related PRs: n/a

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

Verified end-to-end on the current dev tip by piping a synthetic
non-delete ref to the hook. All four steps pass:

- `bun run lint`: 0 errors
- `bun run build`: 111 pages, 97 scorecards, 96 badges, no orphan
warnings
- `bun test`: 200 of 200 pass
- `bun x wrangler deploy --dry-run`: 237 KiB upload (gzip 28 KiB),
`env.ASSETS` binding resolved

Wall time roughly 10 seconds.

## Files Modified

**Modified:**

- `scripts/hooks/pre-push` (add wrangler dry-run as step 4; update
header comment to reflect new step list and remove the stale "requires
worker secrets" justification)
- `README.md` (describe the new step in the contributor onboarding
block)

**Created:** None.

**Renamed:** None.

**Deleted:** None.

## Key Features

- Local pre-push gate now mirrors the CI workflow exactly (lint, build,
test, wrangler dry-run).

## Benefits

- Catches wrangler config or bundle errors locally before push, saving
the round-trip of pushing, watching CI fail, and force-pushing a fix.
- Aligns with the project's local-CI-parity rule: green local push
almost always means a green CI run.

## Breaking Changes

- [x] No breaking changes

## Deployment Notes

- [x] No special deployment steps required. Contributors already on the
hook (those who ran the one-time `git config core.hooksPath
scripts/hooks` per the README) pick up the new step automatically on
next push. New clones still need the same one-time setup.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible
…77)

## Summary

Pre-Implementation Validation gate for the live-scoring v3 plan

([docs/plans/2026-04-28-002](docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md),
lines 795-832).
Measures whether the install-binary-only constraint (R8) bounces out the
bulk of HN-typical traffic before U2 starts on
the sandbox image. Result: 76.0% tight hit rate against 50 trending CLI
repos. Gate decision: `pass-ship-as-written`.
U2 unblocked.

The companion plan-amend commit on `dev` (08a9a24) folds this gate's two
findings (F1, F4) back into the U1, U4, U6,
U8 specs so reviewers can see the plan reflecting reality.

## Changelog

### Added

-

### Changed

-

### Fixed

-

### Documentation

-

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)
- [ ] `fix`
- [ ] `refactor`
- [ ] `perf`
- [ ] `docs`
- [ ] `test`
- [ ] `chore`
- [ ] `ci`
- [ ] `style`
- [ ] `build`
- [ ] `BREAKING CHANGE`

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md
(Pre-Implementation Validation gate, lines 795-832)
- Related PRs: dev commit 08a9a24 (plan amend with F1+F4+musl
satisfied+gate passed)

## Testing

- [x] Unit tests added/updated
- [ ] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 200 passing (no test surface for the new files; script is
local-only, report is data)
- Integration tests: n/a for this PR
- Coverage: gate report itself IS the integration evidence, against 50
real-world repos

The script ran end-to-end against live GitHub + brew + crates.io + npm +
pypi + go.proxy endpoints. Aggregate stats in
the report match the per-repo classification table.

## Files Modified

**Modified:**

- None

**Created:**

- `scripts/measure-discovery-hit-rate.mjs` (local-only, not deployed;
biome-ignored under `scripts/`)
- `docs/research/2026-05-04-discovery-chain-hit-rate.md` (gate write-up;
markdownlint-excluded under `docs/research/`)

**Renamed:**

- None

**Deleted:**

- None

## Key Features

- **Paper version of U4's 4-step discovery chain.** Steps 2-4 only (step
1 needs a binary-asset URL paste, not relevant here). Plus
registry-fast-path (step 0) and per-language quotas (Rust/Python/Go/JS,
~12-13 each).
- **Dual classification per repo: tight + loose.**
- **Tight (production-realistic, gate decision):** step-3 also requires
repository-field match against the input GitHub repo, plus where
available a binary-target check (crates.io `bin_names`, npm `bin`, pypi
`bdist_wheel`, brew `homepage`). Result: 76.0% hit rate.
- **Loose (U4-spec-as-written):** original predicates (just 200 +
has-bin/wheel). Result: 92.0% hit rate, but inflated by cross-registry
name collisions (`cobra` on crates.io is an unrelated Python-Haskell
joke crate).
- **Five findings, two of which land back in the plan amend (F1 = U1+U4
spec change; F4 = U6+U8 spec change).** F1 is the load-bearing one:
without per-registry repo-match, U4 produces wrong-answer failures, not
just missed-opportunity bounces.

## Benefits

- Gate evidence committed before U2 starts, per the plan's verification
requirement.
- Shipped as a reproducer (`bun
scripts/measure-discovery-hit-rate.mjs`), not a one-off measurement, so
the gate can be re-run on a different sample window or different
language mix.
- Surfaces a real wrong-answer risk in U4 as written; absorbing F1
prevents a class of correctness bugs at U4 implementation time.

## Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes described below

## Deployment Notes

- [x] No special deployment steps required
- [ ] Deployment steps documented below

The script writes `.context/discovery-hit-rate-results.json`
(gitignored) on each run. No site or worker changes.

## Screenshots/Recordings

n/a (no UI surface).

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (full suite green: 200/200)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (additive only)

## Additional Context

The 16 percentage-point gap between loose (92%) and tight (76%) is the
size of the U4 spec gap that needs closing at
U4 implementation time. The plan-amend commit on `dev` (08a9a24) makes
the F1 tightening a HARD requirement on U4 and
introduces a new step 0.5 (`discovery-hints.yaml` lookup) to absorb
known false-negatives where ecosystem metadata is
incomplete (Aider, OpenHands, Sherlock).
…ild (U1) (#78)

## Summary

Plan U1 ([docs/plans/2026-04-28-002, lines
856-907](docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md)).
Two precomputed indexes emit at build time so the Worker (U4 + U5 + U6)
can do O(1) lookups without parsing
`registry.yaml` at request time. Pure build-step change, no Cloudflare
bindings touched.

The discovery-hints surface absorbs known false-negatives surfaced by
the Pre-Implementation Validation gate
(finding F1 in
[docs/research/2026-05-04-discovery-chain-hit-rate.md](docs/research/2026-05-04-discovery-chain-hit-rate.md))
— three trending CLIs (Aider, OpenHands, Sherlock)
that real users will paste but whose ecosystem registry metadata is
incomplete enough to bounce U4's tightened step 3.

## Changelog

### Added

- Build emits `dist/registry-index.json` (dual-keyed slug + owner/repo
lookup) and `dist/discovery-hints-index.json` (owner/repo install-spec
hints) so the live-scoring path can resolve inputs without re-parsing
`registry.yaml`.

### Changed

-

### Fixed

-

### Documentation

-

## Type of Change

- [x] `feat`
- [ ] `fix`
- [ ] `refactor`
- [ ] `perf`
- [ ] `docs`
- [ ] `test`
- [ ] `chore`
- [ ] `ci`
- [ ] `style`
- [ ] `build`
- [ ] `BREAKING CHANGE`

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md (U1,
lines 856-907)
- Related PRs: #77 (Pre-Implementation Validation gate; surfaced F1
which seeded the hints file)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 224 passing (up from 200; new tests/registry-index.test.ts
+ regression #7)
- Integration tests: regression #7 asserts both dist/*.json files emit
with the documented shapes from the actual `bun run build` artifact
- Coverage: every documented behavior in the plan U1 spec has a test
(happy path, url-only fallback, missing-identifier warning, owner/repo
collision warning, hint shape validation, hint+registry collision drop,
KNOWN_PM contract)

## Files Modified

**Modified:**

- `src/build/build.mjs` (13 lines: import, path constant, orchestrator
call after `loadRegistry`)
- `tests/regression.test.ts` (35 lines: regression #7 block with 3
dist-artifact assertions)

**Created:**

- `src/build/registry-index.mjs` (121 lines: pure-data emit module)
- `discovery-hints.yaml` (45 lines: 3 seed hints with rationale notes)
- `tests/registry-index.test.ts` (162 lines: 22 unit tests)

**Renamed:**

- None

**Deleted:**

- None

## Key Features

- **Two indexes from one build pass.** `emitBuildIndexes` is the single
side-effecting orchestrator; pure functions (`buildRegistryIndex`,
`buildDiscoveryHintsIndex`, `deriveOwnerRepo`) are individually
testable.
- **Drift guard between data sources.** Hints colliding with
`registry.yaml` entries are dropped at build time (registry wins,
committed scorecards beat opinionated hints). Regression #7 enforces
zero overlap as an invariant on every PR.
- **Typo guard on hint pm field.** `KNOWN_PM` mirrors U4's parse-install
table; an unknown value (e.g. `yum`) fails the build immediately rather
than landing as a runtime bounce.
- **Hint format encodes intent for future maintainers.** Each hint
carries a `note:` explaining why ecosystem metadata fell short, so the
hint can be dropped when upstream improves.

## Benefits

- Unblocks U4 (input parser + discovery chain), U5 (Worker route), and
U6 (sandbox install + score) — they can all import and consume the
indexes without touching `registry.yaml` or `discovery-hints.yaml`
directly.
- Lifts the discovery chain's expected production hit rate from the
gate's tight 76.0% closer to 82-85% by absorbing the 3 known
false-negatives (no need to wait for upstream pypi/brew metadata fixes
from Aider/OpenHands/Sherlock).
- Surfaces registry collisions explicitly (the `cf`/`wrangler` alias
warning is now visible in build output).

## Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes described below

## Deployment Notes

- [x] No special deployment steps required
- [ ] Deployment steps documented below

The new dist artifacts are static assets; they will ship via the
existing `env.ASSETS` binding once U3 + U5 land. No
Worker, DO, R2, or container changes here.

## Screenshots/Recordings

n/a (no UI surface).

## Checklist

- [x] Code follows project conventions and style guidelines (biome
formatted, markdownlint clean)
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (224/224)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (additive only — no existing
artifact shape changed)

## Additional Context

The cf/wrangler alias case is documented as expected behavior — both
`name: wrangler` and `name: cf` in `registry.yaml`
deliberately point at `cloudflare/workers-sdk`. The collision warning is
real but harmless: `by_slug` keeps both
entries; `by_owner_repo` resolves to whichever loaded last. For
live-scoring's purpose (resolve a pasted GitHub URL to
SOME committed scorecard) either choice is correct.
#79)

## Summary

Plan U2 ([docs/plans/2026-04-28-002, lines
945-988](docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md)).
Live-scoring sandbox image: Alpine 3.21 + Cloudflare Sandbox SDK
0.9.2-musl + cargo-binstall + pip + npm + go runtime,
with the agentnative-cli v0.3.1 musl bottle baked in at
`/usr/local/bin/anc`. NO COMPILERS, NO TOOLCHAINS — install
paths are precompiled-only per Premise #2 of the 2026-04-17 CEO design.

This unblocks U3 (`wrangler.jsonc` Containers binding by digest) and U6
(sandbox install + score). It also produces
the artifact the Pre-Implementation Validation gate (#77) was
empirically validating against.

## Changelog

### Added

- New `docker/sandbox/` image for the live-scoring path (Alpine + musl,
no toolchains, anc v0.3.1 musl baked in).

### Changed

-

### Fixed

-

### Documentation

- `docker/sandbox/README.md` documents build, smoke-test, and SHA-pin
bump procedures for the four pinned external assets.

## Type of Change

- [x] `feat`
- [ ] `fix`
- [ ] `refactor`
- [ ] `perf`
- [ ] `docs`
- [ ] `test`
- [ ] `chore`
- [ ] `ci`
- [ ] `style`
- [ ] `build`
- [ ] `BREAKING CHANGE`

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md (U2,
lines 945-988)
- Related PRs: #77 (gate measurement), #78 (U1 build indexes)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 236 passing (up from 224; new
tests/dockerfile-sandbox.test.ts adds 12)
- Integration tests: not in CI (no docker daemon); manual smoke commands
documented in `docker/sandbox/README.md`
- Coverage: SHA-pin discipline, no-toolchains invariant, pm coverage,
brew-absence rationale, sandbox runtime wiring (COPY, ENTRYPOINT, PATH)

The image-size budget check (≤350 MB compressed) lives in the README as
a manual step rather than a test, since CI
doesn't have docker. Static text assertions guard the parts that can be
verified without a daemon.

## Files Modified

**Modified:**

- None

**Created:**

- `docker/sandbox/Dockerfile` (multi-stage Alpine + musl image; 4
SHA-pinned external assets)
- `docker/sandbox/README.md` (build, smoke-test, push-by-digest, SHA-pin
bump, omissions)
- `tests/dockerfile-sandbox.test.ts` (12 static-shape tests)

**Renamed:**

- None

**Deleted:**

- None

## Key Features

- **Multi-stage Alpine 3.21 + musl.** Stage 1 pulls
cloudflare/sandbox:0.9.2-musl@sha256... and stage 2 copies just
`/container-server/sandbox` + `libstdc++.so.6` into a fresh
alpine:3.21@sha256... base, keeping the final image lean.
- **Four SHA-pinned external assets.** cloudflare/sandbox digest, alpine
digest, cargo-binstall v1.19.0 musl tarball sha256, and the agentnative
v0.3.1 musl tarball sha256. Each verifies via `sha256sum -c` after
download.
- **No-toolchains invariant.** apk add includes runtimes only (bash,
python3, py3-pip, nodejs, npm, go, ca-certificates, curl, git). Static
test rejects rust, rustup, cargo, build-base, gcc, g++, clang, make.
- **brew intentionally absent.** Linuxbrew on Alpine + musl isn't
supported (linuxbrew assumes glibc symbols). Inputs that resolve to `pm:
brew` via U4's chain hit U6's `chain_resolved_install_failed` bounce
class. Trade-off documented in-Dockerfile so a future maintainer doesn't
silently re-add brew without revisiting U8's CTA copy.

## Benefits

- Validates end-to-end that the upstream musl release (v0.3.1) drops
cleanly into Alpine: the Dockerfile's `anc --version` step would fail
the build if linkage were broken.
- Lifts the gate's tight 76% paper hit rate to a real install ceiling
once U6 lands.
- Sets the SHA-pin discipline pattern U3 will inherit (pin Containers
`image:` to digest, never tag).

## Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes described below

## Deployment Notes

- [x] No special deployment steps required
- [ ] Deployment steps documented below

The image is not yet referenced by any deployed surface — U3 wires the
wrangler.jsonc Containers binding by digest in
a follow-up PR. Until U3 + U6 land, this image is a build artifact only.

## Screenshots/Recordings

n/a (no UI surface).

## Checklist

- [x] Code follows project conventions and style guidelines (biome,
markdownlint clean)
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (236/236)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (additive only)

## Additional Context

The plan's verification gate for this unit was "Image builds, fits
`basic` instance type, runs `anc --version` via the
Sandbox SDK." All three are documented as manual steps in
`docker/sandbox/README.md` rather than CI tests, because CI
runs `bun test` (no docker). The static shape assertions are the part
that survives without a daemon. Once a docker
runner exists in CI (or via a separate workflow), the smoke-test
commands in the README port directly.

The four pinned digests / sha256s should be reviewed for staleness
before each deploy of U6 (the consumer). The
README's "SHA pinning" section names the bump procedure for each asset.
## Summary

Plan U4 ([docs/plans/2026-04-28-002, lines
1061-1191](docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md)).
Pure logic + injectable HTTP boundary; no Cloudflare runtime bindings
touched. Test-first per the plan's Execution note
for the parser tables. Consumes U1's two indexes (registry-index +
discovery-hints-index) and emits the install spec
shape U6 will execute.

This closes Phase 1 of the plan (foundation: data + image + parser).
Phase 2 (Worker DO + sandbox install + R2 cache)
starts at U5 in a fresh session.

## Changelog

### Added

- Input parser + 4-step GitHub URL discovery chain (registry-fast-path
-> hint short-circuit -> GitHub Releases asset ->
brew/crates/npm/pypi/go distribution lookup -> README install-block
parse) under `src/worker/score/`.

### Changed

-

### Fixed

-

### Documentation

-

## Type of Change

- [x] `feat`
- [ ] `fix`
- [ ] `refactor`
- [ ] `perf`
- [ ] `docs`
- [ ] `test`
- [ ] `chore`
- [ ] `ci`
- [ ] `style`
- [ ] `build`
- [ ] `BREAKING CHANGE`

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md (U4,
lines 1061-1191)
- Related PRs: #77 (gate), #78 (U1 indexes), #79 (U2 image), 08a9a24
(plan amend with F1+F4)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 315 passing (up from 236; 91 new across four test files)
- Integration tests: discover-binary tests use a mocked fetcher
exercising every step and every F1 tightening case (cobra-class
collision, library-only crate, npm repo mismatch, pypi project_urls
match, go proxy match, brew priority over crates)
- Coverage: every plan-listed test scenario for U4 has a test, plus the
F1-finding-specific scenarios (hint short-circuiting step 3 collision;
step 3 repo-mismatch and bin-target rejects)

## Files Modified

**Modified:**

- None

**Created:**

- `src/worker/score/parse-install.ts` (122 lines: pure-function
install-command parser table)
- `src/worker/score/validate.ts` (75 lines: input classifier with
HTTPS+github.com-only URL validation, Punycode-aware homoglyph guard)
- `src/worker/score/registry-lookup.ts` (76 lines: registry+hints lookup
with case-insensitive owner/repo match)
- `src/worker/score/discover-binary.ts` (315 lines: 4-step live
discovery chain with F1-tightened step 3 + per-registry loose/tight
breadcrumbs)
- `tests/score-parse-install.test.ts` (33 tests: every plan-listed
install-command shape + flag handling + shell-prompt + error paths)
- `tests/score-validate.test.ts` (21 tests: slug/command/url
classification + URL error matrix + homoglyph)
- `tests/score-registry-lookup.test.ts` (10 tests: registry-beats-hint
ordering, case-insensitive lookup, miss propagation)
- `tests/score-discover-binary.test.ts` (18 tests: hint short-circuit,
releases asset, F1 tightening cases per registry, README parse)

**Renamed:**

- None

**Deleted:**

- None

## Key Features

- **Test-first parser tables.** parse-install.ts is a pure function with
a switch over package-manager prefix; the 33 tests in
score-parse-install.test.ts ARE the spec for the supported shapes.
- **F1 tightening lands as a HARD requirement.** Every step-3 predicate
now requires a repository-field match against the input GitHub repo
(case-insensitive substring); crates.io additionally requires non-empty
`bin_names` on the latest version. Without this, U4 would produce
wrong-answer failures (R9 violation) — the 16-percentage-point delta
between loose 92% and tight 76% measured in the gate.
- **Step 0.5 hints in two places (defense in depth).**
registry-lookup.ts checks hints right after the registry;
discover-binary.ts also checks hints at step 0.5 before any HTTP call.
The orchestrator path goes through registry-lookup first, so
discover-binary's hint check never fires in normal flow — but if a
future caller invokes discover-binary directly without registry-lookup,
the short-circuit still fires.
- **Injectable fetcher.** discover-binary takes an optional `fetcher:
typeof fetch` argument so tests can mock the entire HTTP surface
deterministically. Default is `globalThis.fetch.bind(globalThis)`.
- **Per-registry loose/tight breadcrumbs in the chain_no_resolve
envelope.** When the chain exhausts, the response carries each
registry's loose/tight booleans plus a tight-rejection reason (e.g.
`crate_is_library_only`). Sets U6/U8 up to surface diagnostic detail in
the bounce CTA.

## Benefits

- Closes Phase 1 of the plan; U5/U6/U7 can build directly on this
contract.
- Eliminates the wrong-answer failure class surfaced by the gate
(cobra/Python-Haskell crate, etc.); the chain now bounces correctly when
registries disagree about which project a name refers to.
- Establishes the testing pattern (mocked fetcher + canned per-URL
responses) that U6's sandbox-side tests will inherit.

## Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes described below

## Deployment Notes

- [x] No special deployment steps required
- [ ] Deployment steps documented below

These modules aren't yet imported by `src/worker/index.ts` — that wiring
lands in U5 (`/api/score` route). This PR ships
the contracts that U5/U6 will consume; nothing user-visible changes in
production until U5 + U6 + U7 + U8 land.

## Screenshots/Recordings

n/a (no UI surface).

## Checklist

- [x] Code follows project conventions and style guidelines (biome,
markdownlint clean; 3 prior optional-chain warnings auto-fixed)
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (315/315)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (additive only)

## Additional Context

**Known degradation, deferred to U6:** parse-install.ts sets `binary =
package` for npm and bun, where the real binary
name often differs (`npm i -g typescript` -> `tsc`). The U6 `which
<binary>` check will catch the mismatch and bounce as
`chain_resolved_no_binary_produced` — that's the correct UX; the
diagnostic copy in U8's CTA explicitly handles this
case. Full resolution would require fetching `package.bin` from npm
metadata at parse time, which would push
parse-install.ts from pure-sync to async. Out of scope for U4's parser.

**The `direct` install spec shape** (returned by step 2 — releases-asset
hits) carries `{pm: 'direct', url, binary}`
rather than the `{pm, package, binary}` shape U6 will see for ecosystem
hits. U6's install-spec executor needs to
discriminate on `pm === 'direct'` and call `curl -fsSL <url> | tar xz -C
/usr/local/bin/` (per plan U6 line 1311) for
that branch.
## Summary

Plan U3 ([docs/plans/2026-04-28-002, lines
1026-1080](docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md)).
First-ever DO + Containers + R2 + ratelimits bindings on this Worker.
One-way migration (`new_sqlite_classes`) gated
by the standard pre-push `wrangler dry-run` check. The U2 sandbox image
was pushed to docker.io ahead of this PR so
`containers[].image` pins to a real digest, not a placeholder.

This unblocks U5 (Worker `/api/score` route) and U6 (sandbox install +
score). Phase 1 + this binding step is the
last work before the Worker code that actually consumes any of it.

## Changelog

### Added

- New Cloudflare bindings on the live-scoring path: `SCORE` Durable
Object, `SCORE_CACHE` R2 bucket, `SCORE_LIMITER` rate-limit (10
req/60s), and a digest-pinned `Sandbox` Containers image. Mirrored under
`env.staging` with staging-distinct bucket name and rate-limit
namespace.

### Changed

-

### Fixed

- `cargo-binstall -V` (not `--version`) in the U2 Dockerfile —
cargo-binstall reserves `--version` for the package version to install,
not the binary version.

### Documentation

-

## Type of Change

- [x] `feat`
- [ ] `fix`
- [ ] `refactor`
- [ ] `perf`
- [ ] `docs`
- [ ] `test`
- [ ] `chore`
- [ ] `ci`
- [ ] `style`
- [ ] `build`
- [ ] `BREAKING CHANGE`

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md (U3,
lines 1026-1080)
- Related PRs: #77 (gate), #78 (U1 indexes), #79 (U2 image), #80 (U4
parser)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 315 passing (cargo-binstall regex fix preserved test
count)
- Integration tests: `bun x wrangler deploy --dry-run --env=""` AND
`--env staging` both pass; bindings listed correctly with the
digest-pinned container
- Coverage: pre-push hook validates the dry-run on every push to this
branch

## Files Modified

**Modified:**

- `docker/sandbox/Dockerfile` (1 line: `cargo-binstall --version` ->
`cargo-binstall -V`)
- `src/worker-configuration.d.ts` (regenerated via `bun run types`; adds
`SCORE`, `SCORE_CACHE`, `SCORE_LIMITER` to `Env`)
- `src/worker/index.ts` (re-exports `Sandbox` so wrangler's binding
resolver finds it)
- `tests/dockerfile-sandbox.test.ts` (regex updated for `-V`)
- `wrangler.jsonc` (containers + durable_objects + migrations +
r2_buckets + ratelimits at top-level + env.staging mirror)

**Created:**

- `src/worker/score/do.ts` (stub `Sandbox` DO; full impl in U6)

**Renamed:**

- None

**Deleted:**

- None

## Key Features

- **Image pinned by digest, not tag.**
`docker.io/brettdavies/anc-sandbox@sha256:f4cc85fa6e39ab5a7901bbf7f291228e04e36fe126f0ca994c793800b8288ad4`
(v3-rc1 pushed 2026-05-04). Tags can be force-moved upstream; digests
can't.
- **Migration uses `new_sqlite_classes`, not legacy `new_classes`.** Per
the plan's one-way-gate framing — the DO is created with SQLite-backed
storage. Reverting needs a follow-up migration with `deleted_classes`
(documented in RELEASES.md when U9 lands).
- **env.staging mirrors all five binding sets explicitly.** Wrangler
does not inherit `durable_objects` / `containers` / `migrations` /
`ratelimits` / `r2_buckets` from top-level — it emits a warning when
they're missing under an env. Staging distinctions: distinct R2 bucket
name (`anc-score-cache-staging`), distinct rate-limit namespace (1002),
explicit Worker name (`agentnative-site-staging`) needed for the
containers app-name derivation.
- **Sandbox DO uses legacy class-form pattern.** `extends DurableObject`
from `cloudflare:workers` would break Bun's test runtime (the virtual
module isn't bun-resolvable). U6 switches to `extends Sandbox` from
`@cloudflare/sandbox`, which IS a real npm package and bun-resolvable.

## Benefits

- Closes the binding-declaration phase of the plan; U5/U6/U7 can now
reference SCORE / SCORE_CACHE / SCORE_LIMITER directly without further
wrangler.jsonc changes.
- Validates end-to-end that the U2 image artifact actually pulls
correctly via Cloudflare's Containers binding (dry-run resolves the
digest).
- Surfaces the env.staging inheritance gotcha (DO + containers +
migrations don't inherit) explicitly in code, with a comment, so future
binding additions don't repeat the trap.

## Breaking Changes

- [x] No breaking changes
- [ ] Breaking changes described below

The migration is additive — no existing binding semantics changed. The
Worker's asset-first invariant is preserved (no new request routing
wired up; that's U5).

## Deployment Notes

- [ ] No special deployment steps required
- [x] Deployment steps documented below:

**Soft prerequisites for first deploy** (per plan U3 line 776):

- R2 buckets must exist: `anc-score-cache` (prod) and
`anc-score-cache-staging` (staging). Created via wrangler R2 CLI or
dashboard.
- CF API token scopes must include: `Workers Scripts: Write` +
`Containers: Write` + `Durable Objects: Write` + `R2 Storage: Write`.
- Docker Hub repo `docker.io/brettdavies/anc-sandbox` must be pullable
(already is — `v3-rc1` tag + digest both reachable).

These are environment-side preconditions, not blockers for the `dev`
merge. The first deploy of these bindings to staging is its own
milestone PR per the plan's "First v3 deploy is its own milestone PR; no
other changes bundled" framing — separate from this binding-declaration
PR.

## Screenshots/Recordings

n/a (no UI surface).

## Checklist

- [x] Code follows project conventions and style guidelines (biome +
markdownlint clean)
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (315/315)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (additive only)

## Additional Context

The Bun-can't-resolve-`cloudflare:workers` finding is a real gotcha the
plan didn't anticipate. The legacy class-form
DO pattern is fine as a stub but unsightly long-term. U6 will replace it
entirely with the Sandbox-SDK-extending form,
which is bun-resolvable as a real npm dep. If you'd rather force the
modern form now (with a vitest/bun mock for
`cloudflare:workers`), happy to switch — but the legacy form here costs
zero (it's stub code that gets replaced) and
keeps the test setup simple.

The `DOCKERHUB_PAT_ANC` token used for the push is in 1Password
(`secrets-dev` vault). To push a future image:

```sh
/home/brett/.claude/skills/1password/scripts/read_field.sh "DOCKERHUB_PAT_ANC" credential | \
  /home/brett/.claude/skills/docker-engine/scripts/sg-docker.sh login -u brettdavies --password-stdin
/home/brett/.claude/skills/docker-engine/scripts/sg-docker.sh build -f docker/sandbox/Dockerfile \
  -t docker.io/brettdavies/anc-sandbox:v3-rcN --platform linux/amd64 .
/home/brett/.claude/skills/docker-engine/scripts/sg-docker.sh push docker.io/brettdavies/anc-sandbox:v3-rcN
```

Capture the `digest:` line from the push output and update
`wrangler.jsonc` `containers[].image` to the new digest.
Stand up site-side prose-check enforcement parallel to the stack that
shipped on `agentnative-spec` v0.3.1. Vale rule packs (universal `brand`
vendored from spec at v0.4.0; fresh site-channel `site` pack), the
`write-good` and `proselint` baselines, a CSS-grep deployment-layer scan
that catches font-family swaps Vale cannot see, and the orchestrator
(Vale plus LanguageTool over Tailscale) all wire into the existing
pre-push hook. Pre-push runs seven stages end-to-end with 0 prose-check
blocking on the cleaned corpus.

- New `scripts/sync-prose-tooling.sh` (parallel to `sync-spec.sh`) that
vendors the prose-check tooling from the latest `agentnative-spec` v*
tag: `BRAND.md`, `styles/brand/*.yml`, `styles/brand/README.md`,
`styles/config/vocabularies/brand/{accept,reject}.txt`,
`scripts/prose-check.sh`, `scripts/generate-pack-readme.mjs`. Separate
sync clock from `sync-spec.sh` because prose tooling and the principles
contract release on different cadences.
- New `.vale.ini` composing Vale, brand, site, write-good, proselint,
with brand-tier and site-tier rules at error severity. Two new
site-channel rules: `site.BannedFonts` (nine reflex font names) and
`site.BannedAesthetics` (seven banned visual patterns). Site vocabulary
at `styles/config/vocabularies/site/accept.txt` (213 entries).
- New `scripts/check-banned-fonts.sh` (yq-driven CSS/TS/build-script
scan that reads tokens from `styles/site/BannedFonts.yml`, single SoT
downstream of the Vale rule pack).
- New `scripts/test-prose-check.mjs` and five fixture cases under
`scripts/__fixtures__/prose-check/` (3 brand cases, 2 site cases).
Manual invocation via `bun scripts/test-prose-check.mjs`.
- Three new pre-push stages after the existing lint, build, test,
wrangler pipeline: pack-README drift check, banned-font deployment scan,
prose-check (Vale and LanguageTool).

- `RELEASES.md` prose scrubbing section now uses the local `.vale.ini`
directly rather than pointing at the spec checkout, and gains a "Scrub
before submit" framing paragraph and a new top-level "PR body" section
copied from `agentnative-cli/RELEASES.md`.
- `.impeccable.md` trimmed from 165 to 109 lines: inherits universal
voice and audience framing from the vendored `BRAND.md`; keeps only
site-channel-specific content (visual system, palette, typography stack,
code-block treatment, OG image, tech stack, JS budget).
- `scripts/SYNCS.md` documents the new sync vehicle, the prose-tooling
row in the upstream table, the new mermaid edge, the new orchestration
trigger, and the SITE-LOCAL DIVERGENCE note on `prose-check.sh`.

- `docs/plans/2026-05-07-001-feat-prose-check-site-plan.md` rescoped:
introduces U0 (sync-prose-tooling.sh authoring and initial vendor)
before the existing U1 to U4. Drops the deferred follow-up framing of
"consumer sync extension" since the new sync vehicle ships in this PR.

- [x] `feat`: New feature (non-breaking change which adds functionality)
- [ ] `fix`: Bug fix (non-breaking change which fixes an issue)
- [ ] `refactor`: Code refactoring (no functional changes)
- [ ] `perf`: Performance improvement
- [ ] `docs`: Documentation update
- [ ] `test`: Adding or updating tests
- [ ] `chore`: Maintenance tasks (dependencies, config, etc.)
- [ ] `ci`: CI/CD configuration changes
- [ ] `style`: Code style/formatting changes
- [ ] `build`: Build system changes
- [ ] `BREAKING CHANGE`: Breaking API change (requires major version
bump)

- Story: n/a
- Issue: n/a
- Architecture:
`docs/plans/2026-05-07-001-feat-prose-check-site-plan.md` (this repo).
Spec-side companion:
`agentnative-spec/docs/plans/2026-05-06-001-feat-prose-check-stack-plan.md`.
Voice-enforcement reference:
`agentnative-spec/docs/architecture/voice-enforcement.md`.
- Related PRs: agentnative-spec#22 (v0.3.1 prose refresh that originally
shipped the stack), agentnative-spec#25 (v0.4.0 contract additions, the
tag this PR vendors against).

- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

End-to-end pre-push verification on synthetic push protocol input:

```text
==> bun run lint                            pass
==> bun run build                           7 principles, 111 pages, 97 scorecards
==> bun test                                315/315 pass
==> bun x wrangler deploy --dry-run         succeeded
==> Pack-README drift check                 clean
==> Banned-font deployment scan             clean
==> prose-check (Vale + LanguageTool)       0 blocking, 1083 warning
==> pre-push checks passed
```

Fixture runner: `bun scripts/test-prose-check.mjs` reports 5/5 OK on the
brand and site cases.

`scripts/sync-prose-tooling.sh` runs cleanly against `agentnative-spec`
v0.4.0 and is idempotent (re-running produces zero `git diff` against a
fixed spec tag).

`scripts/check-banned-fonts.sh` exits 0 on the current corpus; planted
`font-family: 'Plus Jakarta Sans'` in `src/_test.css` reliably fires the
deployment-layer check (verified before commit).

**Modified:**

- `.gitignore` (vale-sync baseline pack ignore lines)
- `.impeccable.md` (trimmed, 165 to 109 lines, inherits from BRAND.md)
- `RELEASES.md` (scrub procedure update, PR body section copied from
CLI)
- `content/badge.md`, `content/methodology.md`,
`content/scorecard-schema.md` (prose drift fixes)
- `docker/sandbox/README.md` (one prose drift fix)
- `docs/DESIGN.md` (one prose drift fix; per-file Vale disable for
`site.BannedFonts` rationale-doc)
- `docs/plans/2026-05-07-001-feat-prose-check-site-plan.md` (rescoped to
introduce U0)
- `package.json` (markdownlint ignore patterns extended for vale-sync
baseline pack dirs)
- `scripts/SYNCS.md` (new sync vehicle entry; SITE-LOCAL DIVERGENCE
note)
- `scripts/hooks/pre-push` (three new stages: pack-README drift,
banned-font scan, prose-check)
- `.vale.ini` (NEW)
- `scripts/prose-check.sh`, `scripts/generate-pack-readme.mjs` (vendored
from spec)

**Created:**

- `BRAND.md` (vendored from spec at v0.4.0)
- `scripts/sync-prose-tooling.sh`
- `scripts/check-banned-fonts.sh`
- `scripts/test-prose-check.mjs`
-
`scripts/__fixtures__/prose-check/{marketing-register,hedge-words,filler-adjectives,banned-fonts,banned-aesthetics}/case.md`
- `styles/brand/{MarketingRegister,HedgeWords,FillerAdjectives}.yml`
- `styles/brand/README.md`
- `styles/site/{BannedFonts,BannedAesthetics}.yml`
- `styles/site/README.md`
- `styles/config/vocabularies/{brand,site}/{accept,reject}.txt`

**Renamed:** None.

**Deleted:** None.

- Two-tier deterministic prose check (Vale plus LanguageTool) wired into
pre-push without any new CI workflow.
- Single source of truth on banned font tokens: the YAML rule pack. Both
the prose-layer Vale rule and the deployment-layer CSS-grep read the
same file.
- Sync vehicle (`sync-prose-tooling.sh`) shares the `SPEC_REMOTE_URL`
and `SPEC_ROOT` env vars with `sync-spec.sh` so the existing operator
workflow stays familiar.

- Catches narrative drift on banned fonts (current example:
`docs/DESIGN.md` historically enumerated rejected fonts in narrative;
the rule fired and the cleanup pivoted the narrative to reference
`styles/site/README.md`).
- Catches deployment-layer drift on banned fonts that Vale
(markdown-only) cannot see.
- Brand pack vendored verbatim, including its released README. No
downstream regeneration; sync-script atomicity is the integrity
guarantee.

- [x] No breaking changes
- [ ] Breaking changes described below:

- [x] No special deployment steps required
- [ ] Deployment steps documented below:

Pre-push activation requires no contributor action beyond the existing
`git config core.hooksPath scripts/hooks` (one-time, already done by
site contributors). New tools needed locally: `vale` (brew install
vale), `jaq` (already in the dev tool list). `vale sync` runs once to
materialize the gitignored baseline packs.

n/a (no UI changes).

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/)
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (or breaking changes documented)

The vendored `scripts/prose-check.sh` carries a SITE-LOCAL DIVERGENCE
marker for two consumer-side carveouts the upstream orchestrator does
not yet support natively (nested `*/node_modules/*` matching;
`src/data/spec/` and `content/principles/` exclusions for vendored
content; `dist/` and `.claude/` build/tooling directories; three
additional LT denylist rules for site domain jargon: `IN_PRINCIPAL`,
`CONTRACT_CONTACT`, `TO_DO_HYPHEN`). Tracked upstream at
`agentnative-spec/.context/compound-engineering/todos/010-pending-p0-prose-check-consumer-exclusion-config.md`
with a Phase 1 (`--exclude PATTERN` CLI flag) and Phase 2
(`.proseignore` unified SoT consumed by the orchestrator, Vale, and
`markdownlint-cli2`).

Each `bash scripts/sync-prose-tooling.sh` run overwrites the patch; the
operator re-applies and `git diff scripts/prose-check.sh` post-sync
surfaces any regression.
Reconciles the site to spec v0.4.0 (eight principles; the new P8 covers
Discoverable Through Agent Skill Bundles), migrates the channel-context
filename to the canonical `PRODUCT.md` expected by the `/impeccable`
skill loader, hoists `DESIGN.md` from `docs/` to the repo root for the
same reason, and refreshes the vendored `BRAND.md` plus Vale stack from
spec `main` HEAD. Decouples `sync-prose-tooling.sh` from spec release
tags (now tracks `main` so prose-tooling iterates on its own cadence),
un-vendors `scripts/prose-check.sh` (consumer-owned now; ends the
regression where every sync clobbered the SITE-LOCAL DIVERGENCE block),
and decouples the OG image's version footer from anc's binary so social
shares show the spec version the site currently presents.

- New principle **P8: Discoverable Through Agent Skill Bundles** at
`/p8` and `/p8.md`, plus card on the homepage index. Cites
`p8-must-bundle-install`, `p8-should-bundle-exists`,
`p8-may-install-all`, `p8-may-bundle-update` from spec v0.4.0.
- `## Inheritance` section in `PRODUCT.md` self-documenting the
three-tier waterfall (BRAND.md universal, PRODUCT.md channel delta,
DESIGN.md visual system).
- `## Channel artifacts` table and "Source of truth" header arrive in
`BRAND.md` from spec main.
- Voice docs (`docs/research/VOICE.md`) codify Register 1a (homepage
lede) vs 1b (principle page) split, with surface-specific notes for
both.
- `Definition-as-card coupling` documented in
`content/principles/README.md`: the Definition first paragraph is also
the homepage card and budgets to 2-3 sentences.

- "Seven principles" sweeps to "Eight principles" across `_intro.md`,
`about.md`, `check.md`, `install.md`, `methodology.md`,
`scorecard-schema.md`, build comments, leaderboard hero, AI-summary
prompt.
- OG image decoupled from per-feature couplings on two axes: card text
no longer cites the principle count (now reads "a standard for CLIs that
agents can operate"), and the version footer now reads from
`content/principles/VERSION` (SITE_SPEC_VERSION, what the site presents)
instead of the highest `scorecards/anc-v*.json` (what anc was last
compiled against). The OG and in-page footer now always agree on the
version. Per-tool badge SVGs continue to use each scorecard's own
`spec_version` since they describe the binary that scored each tool.
- Rename `.impeccable.md` to `PRODUCT.md` (canonical filename for the
`/impeccable` skill loader's `load-context.mjs`).
- Hoist `docs/DESIGN.md` to `DESIGN.md` (root) so the loader finds it
without `IMPECCABLE_CONTEXT_DIR=docs/`.
- `scripts/sync-prose-tooling.sh` tracks spec `main` HEAD instead of
latest `v*` tag. The principle contract still pins to tags via
`sync-spec.sh`; tooling iterates faster than the contract and does not
need release ceremony.
- Re-sync `BRAND.md` and Vale stack (`scripts/generate-pack-readme.mjs`,
`styles/brand/README.md`, `styles/site/README.md`,
`styles/config/vocabularies/brand/accept.txt`) from spec main HEAD
`1625416`.
- `content/principles/p8-discoverable-skill-bundle.md` Definition
compressed from 4 sentences to 2 so the homepage card matches P1-P7
weight.

- `scripts/prose-check.sh` un-vendored (consumer-owned). The SITE-LOCAL
DIVERGENCE block (consumer-specific path exclusions and LT denylist
additions for `IN_PRINCIPAL`, `CONTRACT_CONTACT`, `TO_DO_HYPHEN`) was
being clobbered on every prose-tooling sync. Long-term sidecar-config
fix tracked at
`agentnative-spec/.context/compound-engineering/todos/012-pending-p3-prose-check-sidecar-config-and-revendor.md`.
- Vale file-pattern exemptions in `.vale.ini` updated for the renamed
paths (`PRODUCT.md`, `DESIGN.md`) so the policy docs that legitimately
enumerate banned fonts and aesthetics stop firing the rules they define.
- Stale `.impeccable.md` references swept across
`styles/site/BannedFonts.yml`, `styles/site/BannedAesthetics.yml`,
`styles/site/README.md`, `scripts/check-banned-fonts.sh`.
- Pre-existing biome and markdownlint blockers cleared (do.ts
suppression named the wrong rule; vendored `coverage-matrix.json`
flagged for format; one over-long bullet in `RELEASES.md`).

- Broaden `RELEASES.md` "no explainer prose" rule from `## Summary` only
to the entire PR body. Same do-not-paste guidance now covers every
section.

- [x] `feat`: New feature (P8 + Inheritance section + Channel artifacts
table)
- [x] `refactor`: Code refactoring (PRODUCT.md / DESIGN.md renames,
sync-prose-tooling.sh main-tracking, prose-check.sh un-vendor, OG
version-source decoupling)
- [x] `docs`: Documentation update (Voice register codification,
Definition-as-card coupling note, RELEASES.md prose rule)
- [ ] `BREAKING CHANGE`: not breaking (anchor slugs preserved, all
`/p<n>` URLs stable, internal renames only)

- Story: None.
- Issue: None.
- Architecture: spec PR
[#29](brettdavies/agentnative#29) (`release:
PRODUCT.md migration + channel artifact stack docs`) merged to spec
`main` 2026-05-13.
- Related PRs: cli `docs/migrate-impeccable-to-product` and skill
`docs/migrate-impeccable-to-product` (parallel migrations, not yet
PRed).

- [x] Unit tests added/updated
- [ ] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 315 passing (regression #1 LOCKED_SLUGS extended to
include `p8-discoverable-skill-bundle`; regression #2 llms.txt bullet
count assertion updated to 8 in p1..p8 order; build.test.ts OG alt-text
snapshot updated to the count-decoupled string)
- Pre-push hook: passes (md-wrap, markdownlint 35 files / 0 errors,
check-links 48 files, validate-principles 8 files / 57 IDs, pack-README
drift, prose-check 0 blocking / 1100 warning)
- Coverage: not measured for site (no coverage tool wired)

**Modified:**

- Content: `content/_intro.md`, `content/about.md`, `content/check.md`,
`content/install.md`, `content/methodology.md`,
`content/scorecard-schema.md`, `content/changelog.md`,
`content/principles/README.md`, `content/principles/VERSION` (and all
`content/principles/p1-*.md` through `p7-*.md` reconciled to spec
v0.4.0)
- Build: `src/build/build.mjs`, `src/build/coverage.mjs`,
`src/build/llms.mjs`, `src/build/scorecards-render.mjs`,
`src/build/shell.mjs`
- Voice docs: `docs/research/VOICE.md`
- Scripts: `scripts/sync-prose-tooling.sh`, `scripts/SYNCS.md`,
`scripts/og/og.html`, `scripts/og/generate.ts`,
`scripts/check-banned-fonts.sh`, `scripts/design/generate-palette.mjs`,
`scripts/prose-check.sh`
- Styles: `styles/site/README.md`, `styles/site/BannedFonts.yml`,
`styles/site/BannedAesthetics.yml`, `styles/brand/README.md`,
`styles/config/vocabularies/brand/accept.txt`,
`styles/config/vocabularies/site/accept.txt`
- Vendored: `BRAND.md`, `scripts/generate-pack-readme.mjs`,
`src/data/spec/` (vendored to v0.4.0), `src/data/coverage-matrix.json`
- Top-level: `AGENTS.md`, `RELEASES.md`, `.vale.ini`, `biome.json`
- Tests: `tests/build.test.ts`, `tests/regression.test.ts`
- Worker: `src/worker/score/do.ts`
- Public: `public/og-image.png`

**Created:**

- `content/principles/p8-discoverable-skill-bundle.md`

**Renamed:**

- `.impeccable.md` to `PRODUCT.md`
- `docs/DESIGN.md` to `DESIGN.md`

**Deleted:**

- None.

- [x] No breaking changes.
- [ ] Breaking changes described below.

Anchor slugs preserved (`#p1-non-interactive-by-default` through
`#p8-discoverable-skill-bundle` are permanent). Per-principle URLs
unchanged for `/p1` through `/p7`; `/p8` is new. The `.impeccable.md` to
`PRODUCT.md` and `docs/DESIGN.md` to `DESIGN.md` renames are source-tree
paths, not user-facing URLs.

- [x] No special deployment steps required.
- [ ] Deployment steps documented below.

OG image regenerated as part of this PR (`public/og-image.png`).
Cloudflare Worker deployment proceeds normally on merge. Social-share
platforms cache OG previews aggressively; expect Twitter, Slack, etc. to
keep showing the v0.3.0 card until they re-scrape `https://anc.dev/`.

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible

This PR completes the site-side half of the cross-repo migration
coordinated with `agentnative-spec`. Parallel work on `agentnative-cli`
and `agentnative-skill` is staged on
`docs/migrate-impeccable-to-product` branches in those repos pending
separate PRs.

The un-vendoring of `scripts/prose-check.sh` is a workaround. The
long-term sidecar-config fix (Option C in the cross-repo migration
analysis) is tracked upstream at
`agentnative-spec/.context/compound-engineering/todos/012-pending-p3-prose-check-sidecar-config-and-revendor.md`
(P3). When that lands, this site can re-vendor the script and drop the
CONSUMER-OWNED header.

The OG version-source decoupling is symmetric in spirit to the
prose-tooling decoupling: both sever a coupling that made the site lag
behind a downstream binary release. Per-tool badge SVGs intentionally
remain coupled to each scorecard's `spec_version` since they describe
the binary that scored that specific tool.
…#84)

## Summary

Migrate the live-scoring sandbox container image off the deprecated
Docker Hub registry to the Cloudflare managed
registry. Build is now a separate local operation (`wrangler containers
build -p`) and deploy never rebuilds.
The PR also revises the two-env-block pin model: instead of forcing both
pins to match, the default workflow is
staging-leads-prod (new images land on `env.staging` only, soak on the
staging Worker, then a release PR to main
promotes the top-level pin). A new CI guard enforces this discipline:
every PR verifies its pinned tags exist in
the CF managed registry, and main-targeting PRs additionally enforce pin
equality (released state). Two prose-check
fixes ride along.

Before this PR: the staging container app was degraded with repeating
`ImagePullError "the image registry credentials
are invalid"` against the docker.io digest. After this PR's staging
deploy (already executed locally before opening
the PR), the container app reports 6/6 healthy on
`registry.cloudflare.com/<acct>/anc-sandbox:30f61f1`.

The top-level (production) pin is updated to the same tag, but
production is intentionally NOT deployed in this PR.
A separate release PR will handle the dev to main promotion together
with the routing-drift fix (anc.dev is currently
bound to the staging Worker rather than the named-production Worker;
both will be reconciled in the follow-up
release).

Plan reference:
`docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md` §
U3-followup.

## Changelog

### Changed

- Adopt staging-leads-prod as the default sandbox image workflow. The
two `wrangler.jsonc` pins are now described as
independent (staging advances during development; prod advances at
release). `RELEASES.md § Sandbox image releases`
documents the soak-then-promote flow and the lockstep shortcut for
low-risk bumps.

### Fixed

- Resolve staging container `ImagePullError` caused by Docker Hub
registry deprecation. The sandbox image now lives
in the Cloudflare managed registry and is pinned by `<git-sha>` tag in
both env blocks.
- Add CI guard that verifies every PR's pinned sandbox image tags exist
in the CF managed registry. Catches the
failure mode where a contributor bumps a pin but forgets `wrangler
containers build -p`. The dry-run did not
validate tag existence; this surfaces the issue at PR time rather than
deploy time.

### Documentation

- Document the local-build-once + soak-then-promote image release
workflow in `RELEASES.md` and
`docker/sandbox/README.md`. Includes image-retention discipline (never
delete a tag that backed a shipped Worker
  version) and the Durable Object migration one-way wall note.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: live-scoring plan U3-followup
- Issue: None.
- Architecture:
`docs/plans/2026-04-28-002-feat-live-scoring-cf-sandbox-plan.md`
- Related PRs: None.

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

Four experimental-verification items from the U3-followup plan, all
answered on the staging deploy:

1. `.dockerignore` semantics: the sentinel file is absent from the built
image.
`docker run --entrypoint sh anc-sandbox:30f61f1 -c "find / -name
'.ignored-sentinel.txt'"` returns empty.

2. Multi-env image-share: partial in this PR (only staging deployed).
The Cloudflare registry shows one
`anc-sandbox` tag (`30f61f1`); the prod side of this check completes in
the follow-up release PR when the
   named-prod Worker also pins the same tag.

3. Recorded image reference shape per Worker version: the image
reference does NOT live on the Worker version
(binding types observed: `assets`, `durable_object_namespace`,
`r2_bucket`, `ratelimit`). It lives on the
container application's `configuration.image` field in `name:tag` form
(`anc-sandbox:30f61f1`). The container
application is at version `v2`; `v1` was the docker.io URI. Rollback
semantics: Worker code rollback is
independent from container image rollback; image rollback is via the
container application version, not
   `wrangler rollback`.

4. No-op deploy fast path: a second consecutive `wrangler deploy --env
staging` with no changes reported
`no changes agentnative-site-staging-sandbox-staging` and `No changes to
be made`. The container application was
   not re-uploaded.

Live state observed after the staging deploy:

- Container application `a0309fd2-9622-4dd8-a6a8-faf95292f08e`: state
`ready`, 6 healthy instances, 0 failed,
  configured image `registry.cloudflare.com/<acct>/anc-sandbox:30f61f1`.
- `anc.dev` returns 200 (no `x-robots-tag`, served via the custom
domain).
- `agentnative-site-staging.brettdavies.workers.dev` returns 200 with
`x-robots-tag: noindex` (the staging-host
  guard correctly distinguishes the two hosts on the same Worker).

CI guard verified end-to-end on the PR's own commits:

- Output: `verified: top-level pin anc-sandbox:30f61f1 is in the CF
managed registry` and
`verified: env.staging pin equals top-level pin (lockstep)`. Step
duration ~4s.
- Pin equality is enforced only on main-targeting PRs; this PR
(dev-targeting) would accept divergent pins. Pins
are equal in this PR because the migration sets both env blocks to the
same starting tag.

Pre-push `wrangler --dry-run` passes for both env blocks; `prose-check`
reports 0 blocking after the
`LT.PLURAL_MODIFIER` denylist addition.

## Files Modified

**Modified:**

- `wrangler.jsonc`: pin both env blocks at
`registry.cloudflare.com/<acct>/anc-sandbox:30f61f1`, replacing the
Docker Hub digest pin. Comments rewritten to describe each pin's role
(top-level advances at release; staging
  advances during development) and the soak-then-promote workflow.
- `docker/sandbox/README.md`: replace the `docker push` recipe with
`wrangler containers build -p` instructions.
Defer workflow choice to `RELEASES.md`. Add sections for image-retention
discipline, build-context exclusions,
  and the GHA fallback path.
- `RELEASES.md`: new subsection "Sandbox image releases (live-scoring)"
under `## Deploy`. Documents the default
soak-then-promote flow, the lockstep shortcut, the CI invariants
enforced per PR target, image-retention
discipline, the DO migration one-way wall, and the GHA fallback caveat.
- `scripts/prose-check.sh`: add `LT.PLURAL_MODIFIER` to the site-local
denylist for Cloudflare CLI subcommand
chains (`wrangler containers images list`, `kv namespaces list`, `r2
buckets list`, `hyperdrive configs list`).
- `.github/workflows/ci.yml`: add the pinned-image existence guard step
to the `gate` job. Independent
registry-existence check per env block on every PR; pin-equality check
fires only when `base_ref == main`.
Gated to same-repo PRs since forks do not have access to the CF API
secrets.

**Created:**

- `docker/sandbox/.dockerignore`: forward-looking regression scaffold.
Excludes the sentinel + standard editor/OS
  junk patterns.
- `docker/sandbox/.ignored-sentinel.txt`: regression probe. Must always
stay excluded from the build context; if
it appears in a deployed image layer, `.dockerignore` is no longer being
read by the builder.

**Renamed:** None.

**Deleted:** None.
docs/research/ should be guarded from main like docs/plans/ et al. The
note doesn't affect production runtime (build doesn't ingest docs/), and
the release pipeline excluding it matches the spirit of guard-main-docs.
Follow-up: add docs/research/ to the brettdavies/.github reusable
guard-main-docs workflow.
@brettdavies brettdavies merged commit e79b7ce into main May 15, 2026
5 checks passed
@brettdavies brettdavies deleted the release/2026-05-15-routing-drift branch May 15, 2026 04:58
brettdavies added a commit that referenced this pull request May 15, 2026
Release PR #85 (merge SHA e79b7ce, 2026-05-15) shipped the routing-drift
fix + named-prod promotion + production-side U3-followup deploy. anc.dev
now serves from agentnative-site (named-prod); DO migration v1 applied
to the named-prod Worker on this deploy. Surprise finding during
execution: the staging binding had already cleared between the prior
session's audit and the start of the release session, so the planned
mid-merge detach was a no-op; CF derives custom-domain record ids
deterministically from (account, zone, hostname), so the new prod
binding reused the same id as the prior staging binding.

Plan updates:
- Discovered post-U3 shipment: routing drift OPEN to RESOLVED with
  PR #85 / merge SHA / verification summary.
- U3 entry: Production-Worker side now shipped (was pending).
- U3-followup entry: Production-Worker container app shipped (was
  pending).
- Pending list: drop item 1 (routing-drift); U5-U9 renumbered to 1-5.
- Branch baseline: post-#85 state; next-session work begins with U5.
- U3 unit body and U3-followup spec body: stale 'pending' lines
  updated to reference PR #85.

Follow-ups (out of scope for this plan):
- Orphan DO namespace on staging (a4fb92ed020241cb802c1d5176a39608)
  needs quarterly cleanup.
- Add docs/research/ to the brettdavies/.github reusable
  guard-main-docs.yml so it blocks from main like docs/plans/.
brettdavies added a commit that referenced this pull request May 15, 2026
## Summary

Address the wrangler 4.x "Multiple environments are defined ... but no
target environment was specified" warning that appears in
production-deploy logs and in every pre-push run. The fix is to pass
`--env=""` explicitly on production (which means "use the top-level
config") and to mirror the dual-env pattern in the pre-push hook so both
production and staging configurations are validated on every push.

No behavior change. Wrangler was already using the top-level config on
bare `wrangler deploy`; this just makes the intent explicit and removes
the cosmetic warning.

Surfaced during the post-deploy log review for release PR #85.

## Changelog

## Type of Change

- [x] `chore`: Maintenance tasks (dependencies, config, etc.)

## Related Issues/Stories

- Story: Cleanup item from the post-deploy log audit of release PR #85.
- Issue: None.
- Architecture: None.
- Related PRs: #85.

## Files Modified

**Modified:**

- `.github/workflows/deploy.yml`: production-step `command:` now reads
`deploy --env=""` with a brief inline comment explaining why.
- `scripts/hooks/pre-push`: replaces the single bare `wrangler deploy
--dry-run` with two dry-runs, one per environment (`--env=""` and `--env
staging`). The hook now catches binding mistakes in either environment
before push instead of only validating the top-level config.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Local `bun x wrangler deploy --dry-run --env=""`: clean, binding list
matches the production-side `anc-score-cache` R2 bucket.
- Local `bun x wrangler deploy --dry-run --env staging`: clean, binding
list matches the staging-side `anc-score-cache-staging` R2 bucket.
- Pre-push hook fired both dry-runs in succession when pushing this
branch and passed.
- The next production deploy (via a release PR to `main` later) will
exercise the `deploy.yml` change; the warning should be absent from the
run log.
brettdavies added a commit that referenced this pull request May 15, 2026
…r npm URL (#87)

## Summary

Correct the `cf` entry in `registry.yaml`. The previous entry claimed
`repo: cloudflare/workers-sdk`, but `cf` is not in that repo. I
enumerated all 29 packages in `cloudflare/workers-sdk` and none has
`bin: cf`. The npm `cf` package itself (currently v0.0.5) ships
pre-bundled and declares no `repository`, `homepage`, `bugs`, or
`author` fields. Cloudflare's Technical Preview is published to npm
without disclosing the source repository.

The wrong `repo:` had two downstream effects:

1. The build's registry-index emitted a deterministic but incorrect
mapping from `cloudflare/workers-sdk` to `cf` (overwriting wrangler in
YAML order). A user pasting `github.com/cloudflare/workers-sdk` into the
future live-scoring form would land on `/score/cf` instead of
`/score/wrangler`. The build's "duplicate owner/repo" warning was a
symptom of this.
2. The `/score/cf` page linked to the wrong upstream repository.

Replace `repo:` with `url: https://www.npmjs.com/package/cf` (the
canonical distribution surface for `cf` today) and add an inline comment
explaining why there is no GitHub repo. The registry schema treats
`url:` as the fallback when `repo:` is absent, so the scorecard page
still renders.

Surfaced during the post-deploy log review for release PR #85.

## Changelog

### Fixed

- `/score/cf` and the registry-index now reflect `cf`'s actual
distribution surface: no GitHub source repo is publicly declared, so the
project link is the npm package page. The reverse-lookup map for
`cloudflare/workers-sdk` now resolves correctly to `wrangler`.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: Cleanup item from the post-deploy log audit of release PR #85.
- Issue: None.
- Architecture: None.
- Related PRs: #85, #86.

## Files Modified

**Modified:**

- `registry.yaml`: `cf` entry replaces `repo: cloudflare/workers-sdk`
with `url: https://www.npmjs.com/package/cf` and adds an inline comment
explaining the rationale.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- `bun run build`: clean output. The previous "duplicate owner/repo
cloudflare/workers-sdk" warning is gone. `cf` now joins `make` and
`nvidia-smi` in the "no parseable owner/repo, owner/repo entry skipped"
bucket (legitimate; these tools have no canonical source repository).
- Build stats unchanged: 8 principles, 112 HTML pages, 112 MD pages, 97
scorecard pages, 96 badges.
- `dist/score/cf.html` and `dist/score/cf.md` still emit. The fallback
from `repo:` to `url:` does not break scorecard rendering.
- Pre-push hook passed both wrangler dry-runs (production and staging
environments).

**Evidence for the upstream claim:**

- Enumerated all 29 packages under `cloudflare/workers-sdk/packages/`:
`chrome-devtools-patches`, `cli`, `codemod`, `containers-shared`,
`create-cloudflare`, `devprod-status-bot`,
`edge-preview-authenticated-proxy`, `format-errors`, `kv-asset-handler`,
`lint-config-shared`, `local-explorer-ui`, `miniflare`,
`mock-npm-registry`, `pages-shared`, `playground-preview-worker`,
`quick-edit-extension`, `quick-edit`, `solarflare-theme`,
`turbo-r2-archive`, `unenv-preset`, `vite-plugin-cloudflare`,
`vitest-pool-workers`, `workers-editor-shared`, `workers-playground`,
`workers-shared`, `workers-tsconfig`, `workers-utils`,
`workflows-shared`, `wrangler`. None has `bin: cf` in package.json. The
package named `cli` is `@cloudflare/cli-shared-helpers` (internal
helpers, no executable).
- npm registry metadata for `cf`: `repository: null`, `homepage: null`,
`bugs: null`, `author: null`. Tarball contents are entirely pre-bundled
JavaScript under `cf-dist/` (bundler output, no source files).
brettdavies added a commit that referenced this pull request May 15, 2026
…prod assets (#88)

## Summary

Split the wrangler configuration into two fully independent files
(`wrangler.jsonc` for production, `wrangler.staging.jsonc` for staging)
and force a single-character byte divergence between the two compiled
Worker scripts. Workaround for
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925).

The bug we hit: in a single-config multi-env shape (one wrangler.jsonc
with a top-level block plus `env.staging`), staging deploys silently
overwrite what production serves for any URL path whose asset content
differs between the two builds. Cloudflare's asset deduplication appears
to key on the compiled Worker script's etag; the multi-env shape
produces byte-identical scripts (the bindings differ but they're not
part of the script bytes), so the two Workers share an asset namespace.

This was caught during a post-release log audit on PR #85. anc.dev was
observed serving the post-PR-#87 content for `/registry-index.json`,
`/score/cf.html`, and `/score/cf.md`, despite production having no new
deploy since PR #85's merge. Production-deployed assets had been
silently overlaid by the subsequent staging deploy. Full reproduction
with version IDs, etags, and content hashes is in the upstream issue.

## Changelog

### Changed

- Deploy configuration now uses two independent wrangler config files
(`wrangler.jsonc` for production on anc.dev, `wrangler.staging.jsonc`
for staging on the workers.dev subdomain). The previous single-file
multi-env shape (top-level plus `env.staging`) is gone. Adding new
bindings or changing observability now requires editing both files; the
trade-off is real asset isolation between environments.
- Every response now carries an `X-Build-Env` header set to
`production`, `staging`, or `development` (the development value
surfaces only when the Worker code is exercised by the bun test runner).
The header is diagnostic and also the mechanism that keeps the two
compiled Worker scripts at distinct etags: each config's `define`
substitutes a different literal string into `src/worker/index.ts`'s
`__BUILD_ENV__` reference.

### Fixed

- Staging deploys no longer overwrite production-served asset content.
The two compiled Worker scripts now have distinct etags by construction,
which keeps their asset namespaces isolated even on Cloudflare's current
shared-namespace implementation.

### Documentation

- `RELEASES.md` deploy section documents the two-config layout and links
to
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925).
- `RELEASES.md` sandbox image release procedure points at
`wrangler.jsonc` and `wrangler.staging.jsonc` instead of the old
`env.staging` block.
- `docker/sandbox/README.md` updated to match.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: Surfaced during a post-deploy log audit of release PR #85 plus
the cf-vs-wrangler registry-data investigation in PR #87. The first
staging deploy after PR #87 silently replaced anc.dev's
`/registry-index.json` content.
- Issue:
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925)
- Architecture: None (workaround pattern; awaiting upstream resolution).
- Related PRs: #85, #86, #87.

## Files Modified

**Modified:**

- `wrangler.jsonc`: dropped the `env.staging` block, added a `define`
for `__BUILD_ENV__: "production"`.
- `.github/workflows/deploy.yml`: staging job uses `--config
wrangler.staging.jsonc`; production job retains the default `wrangler
deploy`.
- `.github/workflows/ci.yml`: sandbox image-pin guard reads both configs
via `--config`.
- `scripts/hooks/pre-push`: wrangler dry-run step now covers both
configs.
- `src/worker/index.ts`: declared `__BUILD_ENV__` and the `BUILD_ENV`
constant with a `development` fallback for tests.
- `src/worker/headers.ts`: `applyHeaders` accepts a `buildEnv` option
and emits `X-Build-Env` on every response.
- `tests/worker.test.ts`: every `applyHeaders` call site updated with
`buildEnv: 'development'`; three new tests cover the `X-Build-Env`
header for each env value.
- `RELEASES.md`: deploy table updated, new subsection on the two-config
layout, sandbox image release procedure rewritten.
- `docker/sandbox/README.md`: pin location updated.
- `styles/config/vocabularies/site/accept.txt`: legitimate technical
terms (`configs`, `etag`, `etags`, `deduplication`, `namespaces`,
`envs`, `CF's`) added so the new prose passes Vale at the error tier.

**Created:**

- `wrangler.staging.jsonc`: standalone staging config with all the same
fields production needs, plus `define` setting `__BUILD_ENV__:
"staging"`.

**Renamed:**

- None.

**Deleted:**

- None.

## Testing

- [x] Unit tests added/updated
- [x] All tests passing

**Test Summary:**

- 318 unit and regression tests pass (315 prior + 3 new `X-Build-Env`
cases).
- `bun x wrangler deploy --dry-run --config wrangler.jsonc`: clean,
lists the production-side bindings (anc-score-cache R2, rate-limit
namespace 1001, prod container app).
- `bun x wrangler deploy --dry-run --config wrangler.staging.jsonc`:
clean, lists the staging-side bindings (anc-score-cache-staging R2,
rate-limit namespace 1002, staging container app).
- The two dry-run outputs report different compressed sizes (28.36 vs
28.35 KiB), evidence that the compiled scripts now have distinct content
thanks to the `define` substitution.
- Pre-push gate (lint, build, 318 tests, both wrangler dry-runs,
pack-README, banned-fonts, prose-check) passes end-to-end.
- prose-check: 0 blocking, 1108 warning.

**Post-merge verification plan:**

After the staging deploy on this PR's merge to dev:

- `curl -sI https://agentnative-site-staging.brettdavies.workers.dev/`
returns 200 with `X-Build-Env: staging` and `X-Robots-Tag: noindex`.
- The deployed staging Worker version has a new script etag (different
from the previously-staged version).

After the next release PR to main:

- `curl -sI https://anc.dev/` returns 200 with `X-Build-Env: production`
and no `X-Robots-Tag` header.
- Compare the new production Worker version's script etag against the
staging version's: they should differ. That confirms the fix is in
effect.
brettdavies added a commit that referenced this pull request May 15, 2026
## Summary

Reverts PR #88. The split-config + `__BUILD_ENV__` substitution shipped
as an asset-sharing fix, but the underlying observation it was meant to
fix had a different cause.

What actually triggered the revert: PR #88's staging deploy failed with
`DURABLE_OBJECT_ALREADY_HAS_APPLICATION` (the new config produced a
different container-app name than the existing app's, and a DO can only
bind to one app). During that failed deploy, anc.dev's custom-domain
binding moved from `agentnative-site` (prod) to
`agentnative-site-staging`. Restoring anc.dev to prod via the CF API
immediately reverted the served content to PR #85's deployed baseline,
which suggests the "asset overlay" symptoms we saw earlier in the day
were actually anc.dev routing drift to staging, not cross-environment
asset sharing.

Concretely, this revert restores:

- Single `wrangler.jsonc` with `env.staging` block. The container app
naming returns to `agentnative-site-staging-sandbox-staging`
(env-suffixed), which matches the existing CF resource so deploys
succeed again.
- `src/worker/index.ts` and `src/worker/headers.ts` lose the
`__BUILD_ENV__` / `X-Build-Env` plumbing. No diagnostic header on
responses; tests stop asserting it.
- `deploy.yml`, `ci.yml`, `scripts/hooks/pre-push`, `RELEASES.md`,
`docker/sandbox/README.md` all go back to the multi-env shape.
- `styles/config/vocabularies/site/accept.txt` loses the seven technical
terms added for PR #88's prose (`configs`, `etag`, `etags`,
`deduplication`, `namespaces`, `envs`, `CF's`). These were only added
because PR #88's RELEASES.md prose needed them; without that prose, the
additions are noise.

The upstream issue at
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925)
needs a follow-up correction comment noting the routing-drift
alternative explanation. That's a follow-up, not part of this revert.

## Changelog

### Changed

- Roll back the two-config wrangler split. Production and staging
deploys continue to use a single `wrangler.jsonc` with an `env.staging`
block; deploys use `wrangler deploy` (prod) and `wrangler deploy --env
staging` (staging) respectively.

## Type of Change

- [x] `revert`: Reverting a previous change

## Related Issues/Stories

- Story: Failed staging deploy for PR #88, plus the recurrence of the
anc.dev routing-drift bug during that deploy. The routing-drift
explanation is a better fit for the asset-overlay symptoms we saw
earlier in the day than the asset-sharing theory PR #88 was built on.
- Issue:
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925)
(will be updated with a follow-up comment).
- Architecture: None.
- Related PRs: #88 (the PR being reverted).

## Files Modified

**Modified:**

- `wrangler.jsonc`: restore `env.staging` block, drop the `define` for
`__BUILD_ENV__`.
- `.github/workflows/deploy.yml`: staging job back to `wrangler deploy
--env staging`; production job back to `deploy --env=""`.
- `.github/workflows/ci.yml`: image-pin guard reads both pins via the
multi-env shape again.
- `scripts/hooks/pre-push`: dry-runs go back to `--env=""` and `--env
staging`.
- `src/worker/index.ts`: `__BUILD_ENV__` declaration and `BUILD_ENV`
constant removed; `applyHeaders` call loses the `buildEnv` field.
- `src/worker/headers.ts`: `ApplyHeadersOptions` loses `buildEnv`;
`X-Build-Env` header no longer emitted.
- `tests/worker.test.ts`: all `applyHeaders` call sites have `buildEnv`
removed; the three `X-Build-Env` tests are removed.
- `RELEASES.md`: deploy table and sandbox-image-release procedure revert
to the multi-env wording.
- `docker/sandbox/README.md`: pin location reverts to
`env.staging.containers[0].image`.
- `styles/config/vocabularies/site/accept.txt`: seven added terms
removed.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- `wrangler.staging.jsonc` (the standalone staging config introduced by
PR #88).

## Testing

- [x] Unit tests added/updated
- [x] All tests passing

**Test Summary:**

- 315 unit and regression tests pass (back to the pre-PR-#88 count of
315, after the three `X-Build-Env` tests are removed by the revert).
- `bun x wrangler deploy --dry-run --env=""`: clean, prod bindings
(anc-score-cache R2, rate-limit namespace 1001).
- `bun x wrangler deploy --dry-run --env staging`: clean, staging
bindings (anc-score-cache-staging R2, rate-limit namespace 1002).
- Pre-push gate (lint, build, tests, both dry-runs, pack-README,
banned-fonts, prose-check) passes end-to-end.

**Post-merge plan:**

Once the staging deploy on this revert lands cleanly (it should: the
multi-env shape matches the existing CF resources), curl staging and
prod and verify both still respond. Then file a follow-up comment on
workers-sdk#13925 explaining the routing-drift alternative explanation
and what we learned today.
brettdavies added a commit that referenced this pull request May 15, 2026
…eritance from top-level (#90)

## Summary

Explicitly override two inheritable keys in `env.staging` so they stop
silently inheriting destructive values from the top-level config:
`routes` (which has been quietly stealing anc.dev's custom-domain
binding away from production on every dev push since the 2026-04-30 v0.1
launch) and `triggers` (prophylactic; no current scheduled triggers, but
the same trap shape).

The "routing-drift bug" we have been chasing for two weeks turns out to
be documented Wrangler behavior. Per the [Inheritable keys
list](https://developers.cloudflare.com/workers/wrangler/configuration/),
`routes` is an inheritable key. The top-level config declares `routes:
[{ pattern: "anc.dev", custom_domain: true }]`. `env.staging` had no
`routes` field, so it silently inherited that array. Every `wrangler
deploy --env staging` ran with `routes: [{anc.dev}]` in scope and
re-attached anc.dev to `agentnative-site-staging`, transferring the
custom-domain binding away from `agentnative-site`. The deployment log
includes a `Deployed agentnative-site-staging triggers ... anc.dev
(custom domain)` line on every staging deploy that nobody had read as
"the prod custom domain just moved".

Explicit empty arrays break the inheritance without changing any other
behavior. Today's filed-then-retracted upstream issue at
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925)
(rewritten to describe this trap honestly) suggests Wrangler add a
deploy-time warning when an env block inherits a `routes` array
containing custom domains.

## Changelog

### Fixed

- Stop staging deploys from re-attaching `anc.dev` to the staging Worker
on every dev push. The "routing-drift bug" tracked since 2026-04-30 was
caused by `env.staging` silently inheriting the top-level `routes`
array. Explicit `routes: []` override on `env.staging` makes the staging
Worker's deployment stop asserting ownership of `anc.dev`.

### Changed

- Added explicit `triggers: { crons: [] }` override on `env.staging` as
a prophylactic against the same inheritance pattern firing on a future
scheduled-trigger addition.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: Closes the chronic routing-drift bug. anc.dev should now stay
on the production Worker across staging deploys.
- Issue:
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925)
(rewritten to document the actual inheritance trap).
- Architecture: None.
- Related PRs: #85 (manual routing-drift fix that this PR addresses
structurally), #88 (asset-sharing fix that was reverted by #89 once the
routes-inheritance explanation surfaced), #89 (the revert of #88).

## Files Modified

**Modified:**

- `wrangler.jsonc`: two explicit overrides added on `env.staging`
(`routes: []` and `triggers: { crons: [] }`) with inline comments
documenting why. No other config changed.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Testing

- [x] Unit tests added/updated
- [x] All tests passing

**Test Summary:**

- 315 unit and regression tests pass (unchanged from pre-PR baseline).
- `bun x wrangler deploy --dry-run --env=""`: clean. Production-side
bindings unchanged.
- `bun x wrangler deploy --dry-run --env staging`: clean. Staging-side
bindings unchanged.
- The deployment log's `Deployed agentnative-site-staging triggers`
section is now empty (dry-run output suppresses the section entirely
when there are no triggers, which matches the expected post-fix
behavior).
- Pre-push gate passes end-to-end.

**Audit of every inheritable key:**

| Key | Top-level | env.staging | Status |
|---|---|---|---|
| `name` | `agentnative-site` | `agentnative-site-staging` | Explicit
override |
| `main` | `src/worker/index.ts` | inherited | Safe (same source by
design) |
| `compatibility_date` | `2026-04-01` | inherited | Safe |
| `compatibility_flags` | `["nodejs_compat"]` | inherited | Safe |
| `account_id` | via env var | via env var | Same account |
| `workers_dev` | `false` | `true` | Explicit override |
| `routes` | `[{anc.dev}]` | `[]` | **Explicit override (this PR)** |
| `triggers` | not set | `{ crons: [] }` | **Explicit override (this PR,
prophylactic)** |
| `observability` | `{enabled: true, ...}` | inherited | Safe (same
intent) |
| `assets` | `{directory: "./dist", ...}` | inherited | Safe (per-Worker
asset stores; the "asset overlay" symptoms were routing drift) |
| `send_metrics` | `false` | inherited | Safe (both opt out) |
| `migrations` | `[{tag: v1, ...}]` | explicitly declared | Already
overridden |
| `preview_urls`, `route` (singular), `tsconfig`, `rules`, `build`,
`no_bundle`, `find_additional_modules`, `base_dir`,
`preserve_file_names`, `minify`, `keep_names`, `logpush`, `limits`,
`placement` | not set | not set | n/a |

If any of the currently-unset inheritable keys gets set at the top level
later, re-audit `env.staging` and add an explicit override if the
inherited value would be destructive for staging.

**Post-merge plan:**

After this PR's staging deployment on dev, verify:

- Staging deploy log's `Deployed agentnative-site-staging triggers`
section is empty (no `anc.dev (custom domain)` line).
- CF API: `anc.dev` custom-domain binding stays on `service:
agentnative-site` after the deployment completes.
- `curl https://anc.dev/` returns 200 from the production Worker (no
`X-Robots-Tag` header, content matches main's deployed assets).
- `curl https://agentnative-site-staging.brettdavies.workers.dev/`
returns 200 with `X-Robots-Tag: noindex`.

If anc.dev binding moves to staging after this deployment, the fix is
wrong and we need a different mechanism.
brettdavies added a commit that referenced this pull request May 15, 2026
## Summary

Third production release of the day. Bundles all post-PR-#85 dev work
that survived today's incident, namely the three PRs that are net
additive after the failed split-config experiment (PR #88) and its
revert (PR #89) cancel out: PR #86 (explicit Wrangler env target +
dual-env pre-push dry-run), PR #87 (corrected `cf` registry entry
pointing at its npm distribution surface), and PR #90 (the actual fix
for the 2-week routing-drift bug: explicit `routes: []` override on
`env.staging` to break Wrangler's inheritable-keys inheritance, plus
prophylactic `triggers: { crons: [] }` override for the same trap
shape).

The headline is PR #90's wrangler.jsonc change. anc.dev is currently
bound to the production Worker (after a manual rebind earlier today),
and PR #90's staging-deploy verification confirmed the routing fix
holds: the `Deployed agentnative-site-staging triggers` block no longer
lists `anc.dev (custom domain)`, and the CF API confirms the binding
stays on `agentnative-site` through staging deploys. Promoting that
wrangler.jsonc state to main means the routing fix is in the committed
source-of-truth, not just in dev's history.

User-facing effects on anc.dev after this deploy: the corrected
`/registry-index.json` and `/score/cf` content from PR #87 reach
production (today they live on staging only); the env-target
explicitness from PR #86 removes the wrangler ambiguity warning from the
next production deploy log. The Worker code itself, bindings, and DO
migrations are unchanged.

## Changelog

### Fixed

- Stop staging deploys from re-attaching `anc.dev` to the staging Worker
on every dev push. The routing-drift bug tracked since 2026-04-30 was
caused by `env.staging` silently inheriting the top-level `routes`
array. Explicit `routes: []` override on `env.staging` breaks the
inheritance. anc.dev now stays on the production Worker across staging
deploys.
- Correct the `cf` entry in `registry.yaml`. The previous entry claimed
`repo: cloudflare/workers-sdk`, but `cf` is not in that repo and the npm
package itself declares no repository. Replaced with a `url:` pointing
at the npm distribution page. Side effect: the build's reverse-lookup
map for `cloudflare/workers-sdk` now correctly resolves to `wrangler`
instead of `cf`.

### Changed

- Pass `--env=""` explicitly on the production wrangler-action deploy
command. Removes the "Multiple environments are defined" ambiguity
warning from production deploy logs.
- Pre-push hook now runs `wrangler deploy --dry-run` for both the
production and staging environments instead of one bare invocation.
Catches binding mistakes in either environment before push.
- Added explicit `triggers: { crons: [] }` override on `env.staging` as
a prophylactic against the same inheritance trap shape on scheduled
triggers. Currently no scheduled triggers; the override forces a
deliberate decision when adding any.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

The release is multi-typed (one fix headline plus two ride-along
changes) but `fix` headlines because PR #90's routes-inheritance fix is
the durable resolution of the 2-week routing-drift incident.

## Related Issues/Stories

- Story: Closes the production side of the routing-drift fix arc. PR #85
brought `agentnative-site` (named-prod) current and manually rebound
anc.dev to prod, but the fix was not durable because the underlying
wrangler.jsonc still inherited the prod route into `env.staging` on
every staging deploy. PR #90 fixed the inheritance in source; this
release ships that to main.
- Issue:
[cloudflare/workers-sdk#13925](cloudflare/workers-sdk#13925),
rewritten today as a docs/UX bug describing the inheritance trap and
recommending a deploy-time warning when env blocks silently inherit a
`routes` array containing custom domains.
- Architecture:
`docs/solutions/integration-issues/wrangler-routes-inheritance-staging-custom-domain-drift-2026-05-15.md`
(dev-only) for the full investigation writeup.
- Related PRs: #85, #86, #87, #88 (reverted), #89 (revert), #90.

## Files Modified

**Modified:**

- `wrangler.jsonc`: env.staging block now explicitly overrides `routes:
[]` and `triggers: { crons: [] }` to break Wrangler's inheritable-keys
inheritance. Top-level production config unchanged (`routes: [{ pattern:
"anc.dev", custom_domain: true }]`, `workers_dev: false`).
- `.github/workflows/deploy.yml`: production job's wrangler-action
command is `deploy --env=""` (was bare `deploy`); staging job's command
is unchanged (`deploy --env staging`).
- `scripts/hooks/pre-push`: replaces the single bare `wrangler deploy
--dry-run` step with two dry-runs, one per environment (`--env=""` and
`--env staging`).
- `registry.yaml`: `cf` entry replaces `repo: cloudflare/workers-sdk`
with `url: https://www.npmjs.com/package/cf` and an inline comment
explaining why.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Testing

- [x] Unit tests added/updated
- [x] All tests passing

**Test Summary:**

- 315 unit and regression tests pass.
- `bun x wrangler deploy --dry-run --env=""`: clean, lists the
production-side bindings (Sandbox DO, R2 bucket `anc-score-cache`,
rate-limit namespace 1001, ASSETS). Container image pinned at
`:30f61f1`.
- `bun x wrangler deploy --dry-run --env staging`: clean, lists the
staging-side bindings (R2 `anc-score-cache-staging`, rate-limit
namespace 1002). The `Deployed triggers` section in dry-run output is
suppressed.
- Pre-push hook (lint, build, both wrangler dry-runs, pack-README,
banned-fonts, prose-check) passes end-to-end.
- prose-check: 0 blocking.

**Post-merge verification plan** (after the production deploy on this
PR's merge):

- Production deploy log's `Deployed agentnative-site triggers` section
lists only `anc.dev (custom domain)`. The wrangler ambiguity warning is
gone from the log thanks to `--env=""`.
- `curl https://anc.dev/registry-index.json` returns the corrected
mapping (`cloudflare/workers-sdk` → `wrangler`, md5 should match the
dev-side build, which is `f50579f244013d2b76e999a9502f4e46`).
- `curl -sI https://anc.dev/` returns 200 with no `X-Robots-Tag` header
(production Worker).
- CF API confirms `anc.dev` Custom Domain record stays on `service:
agentnative-site` after the deployment completes.
- A subsequent dev push (next staging deploy) leaves `anc.dev` on
production. This is the durability test: pre-PR-#90, every staging
deploy would have flipped the binding; post-PR-#90, it should not.
brettdavies added a commit that referenced this pull request May 19, 2026
Per direction: nothing from this plan ships to production until the
entire plan (through U10) is complete. Scrub the two forward-looking
promotion mentions from the in-flight state-of-the-world sections.

Changes:

- "Shipped to dev" U7 entry no longer says "production stays on
  `:30f61f1` until a release PR promotes the image"; replaced with
  "Production is not in scope until U10 ships and the whole plan is
  complete."
- "Branch baseline for next session" no longer describes `main`'s
  current pin or names a future release-PR-to-main batch. Now says
  "U5/U6/U7 are dev-only; nothing about this plan ships to production
  until the entire plan (through U10) is complete."

Pre-existing historical narration of PR #84 / PR #85 (the established
staging-leads-prod workflow and the production-side U3 deploy that
already happened) stays intact: the rule is no forward scheduling of
a promotion, not the erasure of past prod work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant