Standalone infrastructure PR (no convoy ceremony needed — single-file
scope). Triggered by the GitHub Actions billing block that gated
PRs #124 + #125 today.
Three layers of savings applied per the user's max-savings option:
1. paths-ignore on ci.yml + preview-smoke.yml
- Doc-only PRs (.convoys/**, **/*.md, docs/**, AGENTS.md,
.cursor/**, README.md) now trigger ZERO Actions jobs.
- Vercel still builds (it's not on the Actions billing).
- visual-diff.yml unchanged — it was already cost-conscious via a
positive paths: allowlist (pages/**, components/**, styles/**,
etc.).
2. Consolidate 6 grep-only forbidden-* jobs into 1
- Previously 6 independent jobs each ran their own
actions/checkout (~3s × 6 = 18s of redundant checkout).
- Merged into a single forbidden-patterns job with 6 sequential
::group:: sections, one FAIL flag at the bottom — preserves
"see all violations in one run" diagnostic behavior. Per-file
::error file=...::msg annotations work the same way.
- Removed jobs: forbidden-endpoints, forbidden-cors-headers,
forbidden-client-side-llm-keys,
forbidden-modal-shell-without-primitive,
forbidden-deprecated-color-aliases, forbidden-stale-strings.
- pr-health-rollup.yml only looks up "Lint" and "Schema map up to
date" by name — unaffected.
3. Cache node_modules + Playwright browsers
- actions/cache@v4 for node_modules keyed by package-lock.json
hash, applied to lint / test / migrate / preview-smoke /
visual-diff. Cuts npm ci from ~30-45s to ~3-5s on cache hit.
setup-node@v4's built-in cache: npm stays (caches ~/.npm) —
both layered.
- actions/cache@v4 for ~/.cache/ms-playwright keyed by the
resolved @playwright/test version. Cache invalidates on any
Playwright version bump. On cache hit, only system deps install
runs (npx playwright install-deps chromium) — saves ~15-25s/run.
AGENTS.md updates:
- § 6 Testing § CI behavior: appended "CI minute optimizations" subsection
documenting all three layers.
- § Product vocabulary table caption: updated "CI job
forbidden-stale-strings" reference to "CI check Forbidden patterns
(6 checks) → Check 6/6" with a historical pointer.
- Gotcha #5: updated the standalone forbidden-endpoints reference
similarly.
Estimated savings per typical convoy mix (~30% doc PRs based on
repo history):
- Doc-only PRs: 100% reduction (was ~6-7 min, now 0 Actions min).
- Code-touching PRs: ~30-50% reduction (cache hits for npm + Playwright +
no redundant 6× checkout).
- Weighted average: ~50-60% reduction.
This is short of the 70-80% I floated in chat — the real ceiling is
limited by lint / vitest / migrate / Playwright runtime itself, all of
which are kept on code-touching PRs (they're high-signal).
Verification:
- All 3 workflow YAMLs parse (python3 -c "yaml.safe_load(...)").
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 118/118 tests pass.
- forbidden-patterns logic is byte-equivalent to the 6 original jobs'
bash bodies — the differences are: per-check ::group::/::endgroup::
framing, a shared FAIL flag instead of per-job exit 1, and renamed
local arrays (FOUND → LLM_FOUND / MODAL_FOUND / STRING_FOUND) to
avoid clobbering across the single job's scope.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove dead Lorcana import route and CLI script
The admin card-import UI never wired Lorcana; catalog sync uses
pages/api/admin instead. Drop the unused API route, CLI helper, and
stale docs references to import-lorcana.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove orphaned import-lorcana-simple CLI script.
It POSTed to the deleted /api/cards/import-lorcana endpoint; no remaining callers.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Align UI copy with My Collection vs Lists vocabulary.
Replace stale ownership/list labels across pages and components, add
lib/collection-vocabulary.js as the single copy source, document the
taxonomy in AGENTS.md, and gate retired strings in CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix remaining list/collection copy gaps from review.
Sweep community, settings, share modal, scanner create-list modal,
and invite flows for vocabulary consistency before merge.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes P1 #11 of .convoys/ship-readiness.md (launch sequence step 7) —
"No migration tool — scripts/add-*.js graveyard". Schema changes
post-this-convoy ship as node-pg-migrate migrations under migrations/
at the repo root; the legacy 27 scripts/add-*.js / scripts/fix-*.js /
scripts/seed-*.js jobs remain append-only history per the no-go-zones
rule.
Decisions (full record in .convoys/migration-tool.md § Decisions):
D1 — Tool: node-pg-migrate@^8. Rejected drizzle-kit / prisma migrate /
kysely because each forces broader TypeScript surface than AGENTS.md
Gotcha #9 allows (TS is a devDep only). node-pg-migrate is
JavaScript-native, raw-SQL-friendly via pgm.sql(), and ESM-clean for
the post-bump-next-js "type": "module" repo. Brings pg@^8.21.0 as a
peer dep (dev-only; never loaded in the Next.js bundle).
D2 — Migrations directory: migrations/ at the repo root. Separates
the tool-wrapped artifacts from the historical scripts/migrations/
placeholder folder (which housed the lone pre-tool
2026-05-24-rename-admin-email.js migration and remains preserved for
the audit trail). Matches node-pg-migrate's default flag.
D3 — Tracking table: default pgmigrations (no name collision with
the existing 7-table bootstrap; zero CLI noise).
D4 — Backfill strategy: hand-translate scripts/setup-neon-db.js's
DDL into the initial migration verbatim. Each await sql`...` block
becomes one pgm.sql(`...`) call. Each CREATE uses IF NOT EXISTS, so
the migration is idempotent against fresh AND pre-existing envs —
re-running setup-db on an env that already has the schema is a no-op
DDL-wise (only records the pgmigrations row). Documented assumption:
prod has drifted via the 27 historical add-*.js scripts; reconciling
those into the migration history is the queued
reconcile-historical-add-scripts follow-up convoy.
D5 — Bootstrap reconciliation: split. setup-neon-db.js now (1)
validates ADMIN_INITIAL_PASSWORD + POSTGRES_URL, (2) spawns
`npm run migrate up` via child_process with stdio inherited, (3)
seeds the admin row with ON CONFLICT (email) DO NOTHING. The seven
DDL blocks are deleted from setup-neon-db.js; success/error message
copy is updated to mention the migration step explicitly.
D6 — CI integration: defer. Wiring a CI job that runs migrate up
against a test DB needs either a dedicated Neon branch + secret OR a
Postgres service container; both are real work. Surface as
wire-migrate-into-ci follow-up. Risk acknowledged in
.convoys/migration-tool.md § R3.
D7 — Down-migration on the initial backfill: hard stub. Rolling back
the initial schema would drop every user / card / collection / deck
row in the DB. The stub throws with a long-form error pointing at
the recommended alternative (branch the Neon database + forward-apply).
Future migrations that touch one of the seven bootstrap tables write
their own dated migration with a real down().
Verification (pre-PR):
- npm run lint → 128 problems (baseline preserved, zero regression;
migration file is lint-clean, no new ignore patterns)
- npm run test:run → 21/21 pass
- node --check on migrations/1779853647564_initial-schema.js + on
scripts/setup-neon-db.js → exit 0
- Module load + down() throw verified via dynamic import
- npm run migrate -- --help reaches the node-pg-migrate CLI through
the wrapper
Live verification against a Neon branch is deferred (no throwaway
branch available); the operator's optional post-merge sequence is
documented in .convoys/migration-tool.md § Operator runbook.
See .convoys/migration-tool.md § Follow-ups for the queued
wire-migrate-into-ci / reconcile-historical-add-scripts /
retire-graveyard-scripts-after-audit / audit-node-pg-migrate-transitive-deps
/ add-migration-template follow-up convoys.
Co-authored-by: Cursor <cursoragent@cursor.com>
`lib/use-auth.js` is now the sole client-side auth surface (P1 §9 of
`.convoys/ship-readiness.md`). The legacy `lib/auth-context.js`
(`AuthProvider` + `useAuth`) and `lib/admin-auth.js` (`AdminProvider` +
`useAdmin` + `useIsAdmin`) are deleted; every importer is migrated to
the canonical hook. Pre-convoy a worst-case page mount issued THREE
identical `GET /api/auth/verify` requests (one per provider/hook); the
post-convoy floor is one verify per page mount (3 → 1 on
`pages/card/[id].js`, 2 → 1 elsewhere).
Importer inventory swept (7 source files):
- `pages/_app.js` — removed `<AuthProvider>` wrapper; `<ThemeProvider>`
is now the only top-level provider. `lib/use-auth.js` is hook-only,
no replacement provider needed.
- `pages/index.js`, `pages/scanner.js`, `pages/decks.js`,
`pages/deck/[id].js`, `pages/deck-builder.js` — `import { useAuth }`
path swap from `../lib/auth-context` to `../lib/use-auth`. All five
pages destructured only `{ user }` or `{ user, loading }`; verified
no consumer reads `login` / `register` from useAuth (those flows are
in `pages/login.js` / `pages/signup.js` which call the API directly),
so no shape-parity gap on `lib/use-auth.js`.
- `pages/card/[id].js` — replaced `useIsAdmin()` (the only consumer of
`lib/admin-auth.js` anywhere in the tree) with synchronous
`user?.role === 'admin'` derived from the existing `useAuth()` call.
Render condition at line 524 stays byte-identical.
Decisions documented in `.convoys/single-auth-provider.md`:
- D1: no extension to `lib/use-auth.js` (zero call sites for `login` /
`register` from useAuth — those flows are direct fetches in
`login.js` / `signup.js`).
- D2: `useIsAdmin()` collapses onto `useAuth()`; no separate hook.
- D3: provider tree `<ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>`
→ `<ThemeProvider>{children}</ThemeProvider>`.
- D4: 3 → 1 verify roundtrip on `card/[id].js`; 2 → 1 on every other
page-load.
- D5: zero test files modified; the 21-test vitest suite is server-
side or prop-driven (`Layout.test.js` passes `user` as a prop, never
imports the legacy hooks).
Doc / config updates so the deletion lands cleanly:
- `.github/CODEOWNERS` — drop the two CODEOWNERS lines for the deleted
files.
- `AGENTS.md` § 2 architecture row + § 3 "Auth (client)" bullet —
rewritten for the post-convoy single-surface state.
- `.cursor/rules/auth-and-permissions.mdc` — § "Legacy" reframed to
"deleted by this convoy"; § "Authentication state on the client"
updated to the post-convoy `useAuth()` shape and the direct-fetch
login flow used by `login.js` / `signup.js`.
- `.cursor/rules/no-go-zones.mdc` — auth-refactors bullet drops the
deleted files from the canonical list.
- `.cursor/skills/add-page/SKILL.md` — checklist + anti-pattern row
refer to the deletion.
Verification:
- `rg "lib/auth-context|lib/admin-auth" --type js` → 0 hits in source.
- `npm run lint` → 128 → 125 problems (3 fewer errors from the deleted
unused-import lines; no regression).
- `npm run test:run` → 21/21 pass (including the 5 Layout regression
locks from `fix-layout-default-user`, which are prop-driven and
unaffected).
- `npm run build` → all 26 pages compile end-to-end; no SSR / static-
generation breakage that would have surfaced if a page tried to use
the legacy context hook unwrapped.
- Manual smoke deferred to operator post-merge per convoy doc.
Risks (full discussion in convoy file):
- R1 shape parity gap — verified zero consumers of legacy-only
surface; mitigated.
- R2 SSR mismatch from removing `<AuthProvider>` — `useEffect`-
guarded `localStorage` read; identical SSR shape pre/post; build
passes.
- R3 missed importer — post-delete grep + build pass would surface
any miss.
- R5 stale `useAuth` cache across components — pre-existing
pattern, called out as follow-up rather than addressed here.
Out of scope: any change to `lib/permission-middleware.js` (server-
side; resolved P0 #1), `lib/auth-secret.js` (resolved P0 #2),
`pages/api/**` route handlers, login / register API contracts, or
the seeded admin account flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
Post-merge doc cleanup for PR #21. Resolves the AGENTS.md line-5 'Pick one before launch' branding question. Updates AGENTS.md top branding note + Gotcha #4 + Gotcha #12, adds full as-shipped to .convoys/pick-a-name.md, adds 4 queued-convoy entries to ship-readiness.md, adds pick-a-name attribution to .cursor/rules/api-routes.mdc. Pure docs (+357/-15 across 4 files).
Reflects the merged add-rate-limiting convoy (PR #20, squash commit
708ef45) in repo documentation. **This is the milestone cleanup** —
add-rate-limiting closed P0 #6 (No rate limiting anywhere), the LAST
open P0 ship-blocker. `.convoys/ship-readiness.md`'s § Status summary
flips from "7 of 8 RESOLVED; 1 remains" to **"8 of 8 RESOLVED.
Launch-readiness P0 checklist is empty."** One brief in the convoy:
Brief 1 shipped as planned with no scope expansions and no implementer
deviations from the verbatim spec; all six architect decisions ratified
verbatim at gate 1 (D1 operator-ratified Option A; D2-D6
architect-self-ratified).
.convoys/add-rate-limiting.md:
- frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
- new ## As-shipped section. Opens with the milestone language
pointing back at ship-readiness.md's flipped § Status summary.
Decisions section captures all 6 ratifications (D1 operator-
ratified Option A — Critical: WHY the atomic admin UI fix in
pages/admin/card-import.js was Decision 1's hidden coupling
requirement, since API gating alone would have broken every
"Import Cards" click; D2 hybrid named-limiter shape with
Map<className, Ratelimit> cache; D3 per-class table including
the two D3 tuning-evidence raises — search 30 -> 60/min because
ShareModal.handleSearch has no debounce so a 17-char email = 16
requests in <5s, and generate kept at 5/hour because DiceBear is
free not paid AI; D4 two-extractor shape with defensive THROW
on null/empty userId; D5 uniform 429 message; D6 no new vitest
or playwright specs deferred to fill-vitest-handler-coverage).
As-shipped surface broken into 4 layers (1 lib refactor + 6 route
gates + 1 atomic admin UI fix + 1 rule extension) mirroring the
cors-tighten cleanup's pattern-split shape. Empirical CI metrics
from post-merge run 26382185019 (Playwright smoke 59s 3/3 in
3.8s, forbidden-cors-headers pass, vitest 21/21, lint 128 baseline,
Screenshot diff continue-on-error swallow per Decision 4).
Cross-validation finding: smoke test 2 still passes against the
post-rate-limit preview — that's three convoys in a row (PR #15
Layout default-user, PR #19 CORS-tighten, PR #20 rate-limiting)
where the same 3-test smoke spec defended the auth surface
through sweeping changes. Operator-action-required: none. What
did NOT change audit trail.
.convoys/ship-readiness.md:
- § Status summary at the top flipped from 7/8 to 8/8 RESOLVED.
Header text updated to "Launch-readiness P0 checklist is empty."
P0 #6 row in the table flips from PARTIAL to RESOLVED with the
two-convoy lineage (fix-auth-bypass Brief 4 + add-rate-limiting).
Trailing paragraph rewritten as a milestone note: security gate
closed; remaining launch work is P1 quality bar + P2/P3 polish.
- P0 #6 entry flipped from PARTIAL to RESOLVED with the full
add-rate-limiting as-shipped block (8 sub-bullets covering the
lib refactor shape, the per-class table, the defensive THROW,
the three import routes' auth-gating, the atomic admin UI fix
and WHY, the rule extension, the 6 decisions, and the diff
breakdown). Brief 4's 2026-05-23 partial is preserved as the
prior as-shipped layer to maintain the audit trail.
- Launch sequence step 4 marked RESOLVED 2026-05-24 with the
squash commit + smoke metrics inline.
- Queued convoys: removed the add-rate-limiting entry (it shipped).
Added a new delete-dead-lorcana-import entry (P3 polish; the
Lorcana import route was gated defensively in PR #20 despite
zero current frontend callers — pages/admin/card-import.js's
<select> only offers mtg + pokemon — so if Lorcana stays
permanently out of the admin UI, this is the cleanup PR).
Added three "flagged but kept out of scope" follow-ups per the
convoy's § What did NOT change: harden-multipart-parser (P2;
5MB body still consumed before the 429 path on avatar.js),
god-function-split / refactor-cards-search-sql (P2; 240-line
7-branch SQL in cards/search.js), and withAdmin(handler) wrapper
extraction (P3 DX; the three import routes are call sites #3-5
in the codebase but uniform inline shape was preserved for
convoy atomicity). Updated tighten-visual-diff-path-filter to
note PR #20 also tripped the same false-positive.
AGENTS.md:
- Gotcha #12 extended end-to-end. Was the single-class auth-only
lib + the env-var contract; is now the 5-class reality with a
full per-class table (helper / limit-window / key / routes),
the defensive THROW pattern in extractUserIdentifier, the
gate-ordering rule for per-user limiters, and the
auth → admin-role → rate-limit ordering for the three import
routes. Prominent milestone line opens the new content:
"add-rate-limiting convoy (squash 708ef45, PR #20, 2026-05-24)
closed P0 #6 — all 8 P0s now RESOLVED." Original env-var
contract paragraph (KV_REST_API_URL / KV_REST_API_TOKEN,
fail-closed-in-prod / warn-and-noop-in-dev) is preserved
verbatim above the new content.
- § 6 Testing: intentionally untouched (no test surface changed;
vitest 21/21 and smoke 3/3 still apply).
- § 7 Deployment: intentionally untouched (no deployment-shape
changed; same KV_REST_API_* env vars from Brief 4).
.cursor/rules/api-routes.mdc:
- The implementer extended § Rate limiting in PR #20 with the
per-class table + verbatim call shape + gate-ordering rules +
identifier-extraction + uniform 429 + fail-closed env-var
contract + fail-open Upstash-outage behavior. Doc-writer pass
verified completeness; added a one-sentence convoy-attribution
line at the top of § Rate limiting citing the two-convoy
lineage (fix-auth-bypass Brief 4 for the auth class +
add-rate-limiting for the other four classes and 7 newly-gated
routes), mirroring the post-cors-tighten § CORS attribution
shape. No other touch-ups needed.
No changes to: package.json, package-lock.json, lib/rate-limit.js,
pages/**, components/**, scripts/**, test/**, tests/**,
.github/workflows/**, README.md, TESTING_GUIDE.md, playwright.config.js,
eslint.config.mjs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged adopt-playwright-smoke convoy (PR #18, squash
commit 7b6f751) in repo documentation. Closes the test-infrastructure
side of P1 #10 step 2 (launch sequence step 10). One commit in the
convoy: Brief 1, with two small lint-baseline-preserving deviations
from the brief's verbatim shape that the implementer report flagged.
.convoys/adopt-playwright-smoke.md:
- frontmatter status: in-progress -> shipped (added shipped:
2026-05-24)
- new ## As-shipped section: operator-ratified Decisions (D1 keep
.ts, D4 defer baselines, D6 simple scripts); two implementer
deviations (removed unused eslint-disable-no-console directive
that would have regressed lint 128 -> 129; placed @playwright/test
first in devDeps for alphabetical correctness); cross-validation
that smoke test 2 ("sign-in page renders") locks in PR #15's
logged-out CTA work in components/Layout.js; empirical metrics
from post-merge run 26376162598 (59s workflow, 3/3 in 2.9s,
0-leak); operator-action-required note pointing at the queued
seed-visual-baselines-on-linux follow-up; What did NOT change
audit trail.
.convoys/ship-readiness.md:
- Queued convoys: new entry seed-visual-baselines-on-linux (Linux-
Docker baseline generation per Decision 4 + Boot-the-brief
Finding 7; Mac-generated baselines would silently overwrite Linux
CI baselines because the custom snapshotPathTemplate has no
{platform} token).
- Queued convoys: new RESOLVED block for adopt-playwright-smoke
(PR #18, 7b6f751) — as-shipped surface, implementer deviations,
empirical metrics (59s workflow / 3/3 in 2.9s / 0 secret leaks),
the PR #15 cross-validation finding, operator-action-required
going forward (the seed-visual-baselines-on-linux follow-up),
flagged-but-deferred items, and ownership trail (3 architect-
self-ratified decisions + 3 operator-ratified).
- Launch sequence step 10: marked RESOLVED 2026-05-24 with the
commit + metrics inline.
- P1 #10 No tests Fix sequence: step 2 marked RESOLVED with the
convoy + metrics ref; step 3 (re-enable test: job in
ci.yml) called out as the next remaining task; step 5 (wire
preview-smoke.yml to the Vercel preview URL) marked RESOLVED
across PR #17 + PR #18 since both contributed.
AGENTS.md:
- Section 6 Testing: rewritten end-to-end. Was "E2E/smoke runner
still pending"; is now "@playwright/test@^1.60.0 wired, two
projects (smoke + visual), npm run test:smoke / test:visual /
test:visual:update". Documents the local-run convention (boot
next dev separately, then BASE_URL=... npm run test:smoke);
the one-time npx playwright install --with-deps chromium step;
the no-baselines-yet state + the Linux-Docker seed command +
the cross-platform mismatch reason (no {platform} token in
snapshotPathTemplate); the CI behavior split (vitest blocking,
smoke on every PR with pipeline:skip-smoke escape hatch,
Screenshot diff path-filtered with the first-red-on-missing-
baseline state documented). Updates vitest coverage count
16 -> 21 (the +5 Layout regression-lock tests from PR #15).
Notes TESTING_GUIDE.md is being eclipsed and will be renamed
to docs/MANUAL_QA.md in a future cleanup convoy.
- Section 7 Deployment: rewrites the Vercel-bypass paragraph from
a single "query param now / header reserved for future" bullet
into a two-shape audit ((1) query param on the wait-action's
path: input per PR #17; (2) HTTP header in playwright.config.js's
use.extraHTTPHeaders per PR #18). Documents the Decision-2
fail-loud-in-CI / warn-in-dev predicate and references Gotcha #12
as the established precedent (lib/rate-limit.js). Picked Section
7 over a new Gotcha because the bypass plumbing is operationally
a deployment concern, not an app pitfall.
No changes to: package.json, package-lock.json, playwright.config.js,
eslint.config.mjs, lib/**, pages/**, components/**, scripts/**,
.github/workflows/**, .cursor/rules/**, README.md,
tests/visual/homepage.spec.ts (JSDoc is already neutral-tense, no
future-tense references to clean up).
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged fix-vercel-deployment-protection-in-ci convoy
(PR #17, squash commit 9a3e077) in repo documentation. Closes the
CI-infra side-effect of P0 #7. Three commits in the convoy: Brief 1
(bypass plumbing per spec), plus two scope expansions discovered
during CI validation (shell-injection hardening, and a Decision-A
shape correction to drop the cookie-bypass param).
.convoys/ship-readiness.md:
- Queued convoys: mark fix-vercel-deployment-protection-in-ci as
RESOLVED 2026-05-24 with 9a3e077. Document the 3-commit reality
(365e9f0 Brief 1 bypass plumbing, b6f8688 shell-injection
hardening of the gate Decide step, 043a6ee dropping
&x-vercel-set-bypass-cookie=true), the empirical metrics
(wait-action: 10-min timeout -> 194ms; workflow runtime:
10+ min -> 59s), the documented expected red on Playwright
smoke (npx playwright test fails because playwright.config.js
doesn't exist yet -- adopt-playwright-smoke owns that), and the
operator-rotation caveat (R6).
AGENTS.md:
- Section 7 Deployment: correct the noun "header" -> "query param
on wait-action's path:" since that's what actually landed per
Decision A. Also document the without-cookie form (the cookie
variant 401s through axios's missing cookie jar) and the
operator re-seed runbook for token rotation.
- Section 7 Deployment: fold in a one-liner about the GitHub
Actions ${{ }}-in-shell-is-injection-vector pattern, with the
env: + quoted-shell fix shape. Picked Section 7 over a new
Gotcha #13 because the existing Gotchas list is dominated by
app-level pitfalls (auth, SQL clients, ESLint), and CI YAML
hardening is naturally co-located with deployment.
.convoys/fix-vercel-deployment-protection-in-ci.md:
- frontmatter status: in-progress -> shipped (added shipped:
2026-05-24)
- new ## As-shipped section: 3-commit reality, Decision-A shape
deviation (we shipped without &x-vercel-set-bypass-cookie=true),
empirical timings (194ms wait, 59s total), remaining-red
attribution to adopt-playwright-smoke, and the
operator-rotation caveat.
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/workflows/**, .cursor/rules/**, README.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged fix-layout-default-user convoy (PR #15) and the
companion CI permissions fix (PR #16) in repo documentation. Also
queues the new fix-vercel-deployment-protection-in-ci convoy that
PR #16 exposed.
.convoys/ship-readiness.md:
- P0 #7: mark RESOLVED 2026-05-24 with squash commit ca302a8.
Document the as-shipped Layout default-null change, the 7-page
sweep, the 5 new regression-lock vitest assertions, and the
queued follow-ups (single-auth-provider, MobileNavigation
cleanup) that stayed explicitly out of scope.
- Queued convoys: add fix-vercel-deployment-protection-in-ci
(P2, CI infra) — PR #16's permissions fix exposed that Vercel
Deployment Protection 401s anonymous CI requests; needs a
bypass-secret plumb to land cleanly. New section also captures
other in-flight follow-ups (rotate-default-admin, cors-tighten,
add-rate-limiting, purge-weak-creds-from-helpers,
single-auth-provider, cleanup-mobile-nav-dead-props,
bump-eslint-10) so the audit trail is centralized.
AGENTS.md:
- § 4 Gotcha #8: mark RESOLVED with commit ref ca302a8. Mirror
the convention used by the prior cleanup commits for #2, #3,
#4, #5 (entry kept, not renumbered).
- § 3 Key conventions: add a new "Layout user prop" bullet
documenting the new default-null + logged-out-CTA contract so
the convention is discoverable from the conventions list, not
just the resolved-gotcha entry.
.convoys/fix-vercel-deployment-protection-in-ci.md (new):
- Queued scaffold. Operator must seed
VERCEL_AUTOMATION_BYPASS_SECRET as a repo secret before the
implementer can run. Decisions to ratify (query param vs.
header), known constraints, acceptance criteria, and
out-of-scope all enumerated.
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md, .cursor/rules/**.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged drop-public-setup convoy (PR #13) in repo
documentation. Small focused pass; no behavior changes.
AGENTS.md:
- § 1 auth bullet: replace "seed admin row still ships in
setup-neon-db.js" claim with the new env-var-gated reality and
the R1 operator-rotation caveat.
- § 4 Gotcha #4: mark RESOLVED with commit refs (ff80753 + b63b509),
document the as-shipped behavior, the Brief 2 CJS→ESM Node 22.x
fix, and the R1 operator caveat. Entry kept (not renumbered) per
the same convention used for resolved gotchas #2, #3, #5.
- § 5 Running locally: add ADMIN_INITIAL_PASSWORD to the env-var
template list with a note that setup-db exits 1 if it's unset.
.convoys/ship-readiness.md:
- P0 #3: mark RESOLVED 2026-05-23 with commit refs, document the
full as-shipped behavior including Brief 2's CJS→ESM bonus,
the R1 operator caveat (Decision A — going-forward only),
and the deferred sibling weak-cred references queued for
purge-weak-creds-from-helpers.
.cursor/rules/no-go-zones.mdc:
- Editing rules of thumb: clarify the schema-vs-operational
distinction for scripts/setup-neon-db.js. drop-public-setup
set the precedent that operational changes (env-var gating,
pre-flight validation, module-system fixes) are allowed in
place, while DDL changes still need a separate migration
script. Documented so future agents don't have the same
confusion the drop-public-setup architect did (see Decision B
in .convoys/drop-public-setup.md).
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md (already updated in PR #13).
Co-authored-by: Cursor <cursoragent@cursor.com>
Doc-writer pass for convoy bump-next-js (PR #4 / commit e57ea17).
Single file touched: AGENTS.md (+5 / -1).
- § 1 Project overview: Framework line bumped Next.js 15 -> 16, with
a cross-reference to new Gotcha #9 for the typescript-is-just-for-lint
context.
- § 4 Common gotchas: three new entries that future agents need to
know about but wouldn't infer from the code:
- #9: typescript@^5.9.3 is installed purely so eslint-config-next@16's
bundled typescript-eslint chain can satisfy its hard require('typescript')
at module load. No tsconfig.json, no .ts files, no @ts-check. Decision C.
- #10: ESLint pinned to ^9.39.4 (maintenance), not v10 (latest). v10
surfaced Risk R15 empirically (TypeError: scopeManager.addGlobals)
via @typescript-eslint/scope-manager@8.59.4 predating v10 GA.
Do not bump independently — wait for queued bump-eslint-10
follow-up convoy. Decision D.
- #11: Turbopack is now the default bundler in next dev/build.
Fallback per-command is --webpack. Do not pre-emptively switch.
- § 7 Deployment: reference VERCEL_AUTOMATION_BYPASS_SECRET (env var
name only, no value) for the queued adopt-playwright-smoke convoy
to use against protected preview deploys.
CHANGELOG.md / DEVELOPER_CHANGELOG.md not created — those are deferred
to launch-polish per the convoy's roles section.
README.md staleness (line 16 still says "Next.js 15, React 18,
TypeScript") flagged in the PR description but NOT fixed here per the
docs-pass scope. Pickup: launch-polish.
Co-authored-by: Cursor <cursoragent@cursor.com>