You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#265 C13 — the .NET side owned NO schema; it inserted sequence_number = 0 and leaned on Supabase's trigger. Off that shared Postgres the 2nd message in any conversation dies on unique_sequence. Now ships its own artifact + a test that demonstrates the 23505 collision BEFORE applying the fix
all twelve clause citations in AUTHORIZATION-CONTRACT.md pointed at the wrong line, two at blank lines. Cites by NAME now — line numbers into an append-only 3,535-line migration cannot be kept correct by discipline
#895 — the offline payment queue's auto-drain had zero callers and zero tests. Root cause was the guard order: it probed the network before checking whether anything was queued, so mounting it anywhere cost a Supabase query every 30s
#559 — create-order claimed the idempotency key but never wrote it to the row, so idx_payment_intents_idempotency_key has never applied to a single catalog purchase
Four guards failed their own mutation tests before shipping — three by matching their own
explanatory comments rather than code. If you write a guard this session, break it on purpose
before believing it.
NEEDS THE OWNER — do not proceed on these without a decision
Edge functions are still NOT deployed by CI — no workflow runs functions/deploy, so
every edge-function change is inert until deployed by hand. create-order was deployed
2026-08-21 (v13 → v14, verified: bundle contains buildIntentRow, and an unauthenticated
POST returns 401 from getAuthenticatedUserId rather than a boot error). Any future edge
change needs the same manual step — a merge is not a deploy here.
Purpose: this issue is the durable roadmap for TortoiseWolfe/ScriptHammer. The body below is always-current — active arcs, next 3 sessions queued, backlog. Comments are the audit trail (what shipped + what changed in the roadmap, per session). The prime prompt at the top tells a fresh Claude session to read this body first.
Prime prompt (copy from here into a fresh Claude session)
/prep
Then check this issue (TortoiseWolfe/ScriptHammer#115) — its BODY is the current roadmap. Read everything below the prime prompt: active arcs, next 3 sessions queued, backlog. The most recent COMMENT on this issue is the audit trail of what changed since the previous session.
Supplementary context (read if the body's roadmap references them or if the user's first message takes us off-roadmap):
1. /home/TurtleWolfe/.claude/projects/-home-TurtleWolfe-repos-ScriptHammer/memory/MEMORY.md — top of the index has the most-recently-written memories.
2. `git log --oneline -20` — what shipped in the last couple of sessions.
3. Open issues: `gh issue list --repo TortoiseWolfe/ScriptHammer --state open`
4. Open PRs: `gh pr list --repo TortoiseWolfe/ScriptHammer --state open`
**Staleness is measured, not asserted.** This body ends with a generation stamp recording the commit it was written against, and `~/.claude/hooks/roadmap-drift.sh` reports at session start when commits have landed since. No drift warning means this body is current. If you saw one, believe the number — and note a *failed* check says "UNKNOWN", never zero.
Operational reminders you WILL trip over otherwise:
- **PRODUCTION STILL CANNOT TAKE MONEY (#629) — but the reason CHANGED on 2026-08-16.** The Stripe account is **verified**: `details_submitted`, `charges_enabled`, `payouts_enabled` all true, `currently_due` empty. Live keys EXIST. What remains is three changes plus a ceremony, and #629's body carries a 2026-08-17 audit of exactly which are done. **Two traps live in that audit:** step 4 is ALREADY DONE (all three recurring SKUs carry live `stripe_price_id`s — following the ticket literally creates duplicate live prices), and the DB holds LIVE price ids while the app runs in TEST mode, so activating those SKUs before the key swap breaks recurring checkout for real visitors. `active=false` is the only thing preventing that.
- **THE 2018-ERA STRIPE ACCOUNT LIES TWICE IF YOU LET IT.** (1) An UNPINNED read returns `null` for every `business_profile` field — always send `Stripe-Version: 2024-06-20`. (2) `business_profile` is **PER-MODE**: reading with the TEST key returns an empty profile that looks like "nothing is configured", while the LIVE key shows the real values. Both produced false "unconfigured" reports in one session.
- **THREE REQUIRED CHECKS, NOT TWO: `Test (20.x)`, `accessibility`, AND `E2E (local) result`.** The local lane was promoted to required once #739 closed. CLAUDE.md is now correct and guarded by `scripts/__tests__/required-checks-documented.test.js` (#782). A `gh pr merge` refusing with "base branch policy prohibits the merge" while the two documented checks are green is this, not a broken rule.
- **CLOUDFLARE NOW FRONTS THE SITE (#635 fixed 2026-08-17).** HTML serves `cache-control: no-cache`, `/_next/static/*` serves `max-age=31536000`, both with `cf-ray`. Guarded post-deploy by `scripts/ci/check-cache-headers.mjs`. **Do NOT try to do the HTML half with a Cache Rule** — Browser TTL only RAISES a value, will not go below the origin's own `max-age`, and ignores such values with NO API error (0/1/300 dropped, 700/12345 applied). It needs a Response Header Transform Rule. **Cloudflare ruleset edits take ~45s to propagate**; probing immediately reads the PREVIOUS rule and manufactures convincing wrong conclusions — it produced three in one session.
- **MAIL WORKS NOW, AND IT IS PROVEN, NOT INFERRED.** `admin@scripthammer.com` receives via Cloudflare Email Routing → `jonpohlner@gmail.com` (verified by a delivered message, and again by Google's own send-as confirmation arriving). Gmail *Send mail as* is configured through `smtp.resend.com:587`. Outbound is Resend; DKIM/SPF/`send` records survived the migration. Registrar is still Squarespace (#781).
- **GITHUB'S GRAPHQL ENDPOINT FLAKES; REST DOES NOT.** On 2026-08-17 `gh pr view --json`, `gh pr merge` and `gh issue view --json` returned 503s and a spurious "Bad credentials" for an hour while `gh api` (REST) stayed 200 throughout. When `gh` behaves impossibly, switch to `gh api` before believing the error. `mergeable_state` from REST is also the cheapest way to spot a conflict — it reads `dirty` long before any check reports.
- **A PUSH DURING A GITHUB INCIDENT CAN SILENTLY NOT TRIGGER CI.** A commit sat with ZERO workflow runs and a PR that looked merely "pending" forever. `gh api repos/OWNER/REPO/actions/runs?per_page=100 --jq '[.workflow_runs[]|select(.head_sha=="<sha>")]|length'` distinguishes "still running" from "never started".
- **`docker compose restart` DOES NOT RE-READ `.env`.** Interpolation happens at container CREATION; use `docker compose up -d`. This once ran the dev server for hours against a retired anon key, producing 401s that looked like an app bug.
- **A MIGRATION FILE IS NOT A MIGRATION.** Applying schema to prod is a deliberate step via `POST /v1/projects/{ref}/database/query` with the SQL from a FILE. Prod DRIFTS from the monolithic file — verify against the LIVE DB.
- **TICKETS CAN BE WRONG ABOUT THE SYMPTOM ITSELF, not just the fix.** #497 was titled "zero `postgres_changes` events"; the same shard delivered them seconds later. #391 asserted main was red on a gate that was green. #629 told you to create prices that already existed. **Verify the symptom before accepting the diagnosis.**
- **A PROBE THAT CANNOT REPORT FAILURE PROVES NOTHING.** The most expensive recurring lesson here. Make every probe fail on purpose before trusting a pass — **and confirm the mutation actually applied**: a `sed` whose `|` delimiter collided with `||` silently did not apply, the suite went green, and that green "proved" nothing.
- **GREEN JOBS CAN MEAN NOTHING RAN**, and a gate can fail for reasons that are not your code. 24 green jobs once hid Firefox and WebKit executing ZERO tests. Separately, `E2E (local) result` failed with **0 test failures** because only 23 of 24 shards uploaded an artifact — the gate correctly refusing to score partial data. Compare counts, never green/green.
- **ANOTHER AGENT — OR THE OWNER — MAY BE WORKING THIS SAME CHECKOUT.** Before `checkout -b`: confirm `git status` and HEAD. **If a PR shows commits you did not author, STOP.** And **run `git stash list` BEFORE any `git stash pop`** — `git stash push <pathspec>` does not match untracked files, so it can create nothing, and the following `pop` then applies SOMEONE ELSE'S stash into your branch. That happened on 2026-08-17; a conflicted pop keeps the entry, so nothing was lost, but only by luck.
- **`BASE_URL=…/ScriptHammer` + `page.goto('/docs')` measures the 404 page.** Use a real root build (`DISABLE_BASE_PATH=true`, then `serve out`). That build rewrites `public/manifest.json` (#392) — never `git add -A` after one. **`git push` REBUILDS `out/` with the default basePath**, silently invalidating a root build you are serving.
- **Canvas `fillStyle` DROPS THE ALPHA on a dimmed colour**, and `getComputedStyle` returns `oklch()` unparsed — read colours through a canvas.
- **Tailwind scans `tests/`**, so a spec's own string literals make Tailwind emit those classes (#396).
- Docker-first. Never `pnpm install`/`npx` on the host, never sudo. Commit from INSIDE the container so husky/lint-staged/gitleaks run; push from the host. **Builds go in their OWN container: `docker compose run --rm builder pnpm build`.**
- **`git push` RUNS A FULL CI GATE** (`.husky/pre-push`) and takes MINUTES. Never two at once; never `git push … | tail`.
- **CAPTCHA IS LIVE ON PROD (#353) and gates SIGN-IN AND RECOVERY.** E2E is unaffected — it authenticates with the SERVICE-ROLE key, which GoTrue exempts. Do NOT "fix" a spec by typing into the real form.
- **scripthammer.com RESOLUTION IS FLAKY ON THIS NETWORK, AND IT MASQUERADES AS BROKEN PRODUCTION.** `ERR_NAME_NOT_RESOLVED`/`NXDOMAIN` on every asset = **local DNS**, the site is fine; 404s on content-hashed CSS = a real retention bug. Measured 2026-08-17: Windows DNS points at the Belkin router (`192.168.2.1`), which took >300ms on 7 of 30 lookups while a public resolver failed none. Windows has **no global IPv6 address**, so IPv6 is not provisioned rather than broken. Cross-check with DoH before ever saying "prod is down". **Never persist an `/etc/hosts` entry** — the owner objected in the strongest terms and was right: it hides the real failure.
- **PUBLIC VALUES BELONG IN VARIABLES, NOT SECRETS (#787).** 21 of 24 variables had a same-named secret shadowing them; 20 were deleted 2026-08-17 (33 → 13). A write-only copy of a value that ships in the bundle buys nothing and cost an outage — the deploy read `vars.*` while the operator set `gh secret set`.
- **The FORK (RescueDogs → RaisedPaws) needs our non-3D fixes** → RescueDogs#56. NEVER port Cesium/Three.js. Always pass `--repo`.
- **`gh pr edit` is broken in this repo (#397)** — use `gh api -X PATCH repos/OWNER/REPO/pulls/N -F body=@body.md`. `gh issue edit` is fine.
- **`allow_auto_merge` is ON**; merge queue is unavailable (user-owned repo). Use `gh pr merge --auto --squash` — or REST when GraphQL is flaking.
Then either start on the roadmap's "next session should" item, or ask the user if they want to deviate.
Stop reading here when priming — everything below is the roadmap
Roadmap
Body regenerated 2026-08-22 (evening) against 992a1d58. #758 is CLOSED — not the ordering source.
#945 MERGED at 992a1d58 (2026-08-22 ~17:05Z) — 24/24 shards, all seven required green. #944 is closed. The stamp below is against origin/main, not HEAD, which stays the right call.
🚨 Owner-only blockers (8, and this is now the binding constraint)
The unblocked backlog is thinning faster than the owner-decision queue. Four of these are new
since yesterday.
#629 — the money steps only: sk_live_ + pk_live_ + PAYMENT_MODE_EXPECTED=live +
activate the 3 SKUs last + one real card. #621 — rotate three credentials with no API. #714 — 3D Warehouse licensing, inherited by every fork. #769 — receipt address,
MCC 5734→7372.
NEW:#393 — the CSP now ships as a real header in report-only; flipping it to
enforcing is a dashboard change and an acceptance of risk. #822 — DMARC is p=none, so the
domain that sends payment receipts can be spoofed. #462 — 181 /80 and /85 text
instances: needs a policy call (ban opacity for de-emphasis, or audit 94 files per surface),
not a sweep. #442 — the privacy policy must disclose Cloudflare Turnstile; that is legal
copy an agent should not author.
The arc that closed: CI reliability
Promoted on 2026-08-17 because every PR that day needed a re-run and not once for a failing
test. It is done. Since then 14 PRs landed, every one green on its first attempt, with a
single re-run spent all session — and that one was a genuine Azure apt-mirror outage, not a
defect.
Every fix up to 2026-08-18 hardened the install; it stayed fallible, so the arc stayed open. #762 and #798 (a wall-clock timeout cannot tell slow from stalled — bound the silence instead), #801/#802 (cache the apt debs; 17.7m → 1.0m), #809 (the idle bound, whose catch was then
demonstrated for real during the mirror outage), #766 (upload the traces the lane already pays to
capture), #726 (the budget guard can now see which backend it meters). Each lowered the odds or
the cost of a failure. None removed it: 24 network fetches per run, every one able to go dark on
its own.
2026-08-19 closed the arc by deleting the operation (#829, PRs #830 + #833) — a different kind
of change, and the reason this section moves from "active" to "closed" rather than just getting a
better number. There is no fetch to retry, no mirror to be down, no dpkg lock to orphan, no retry
loop to fall through. The failure class is unreachable rather than damped, which is what makes it
finishable at all.
The clearest illustration is a branch deleted the same night. fix/795-install-failure-is-loud
(412fc86c, never merged) made a failed install fail its job loudly at all ten sites — correct
work, and instantly moot: nothing can fall through a step that no longer exists.
No workflow installs browsers on a runner any more — every lane runs from mcr.microsoft.com/playwright:v1.55.0-noble, which already ships them. The payoff is measured on
two consecutive commits of main: 6a4e8f57 (installs present) failed 8 jobs, every one in Install Playwright, with zero test failures; d45cfdc2 ran 26 jobs green, 0 install steps.
Before, each shard spent a median 11.6 min installing — ~158 runner-minutes per run across 24
independent network fetches.
Three of the four lanes are PROVEN: e2e-local 26/26 across chromium/firefox/webkit, signup-mailer green with a real Mailpit round-trip, and smoke green against live
production. e2e.yml's seven sites are migrated but unexercised — the cloud-quota guard
skips every downstream job until the window reopens (~Sept 2). Treat it as unproven.
#795 and #819 closed against that evidence rather than being fixed.
The recurring theme, again: controls whose presence was asserted and whose effect never was
— a CSP delivered as <meta name> and therefore inert, an email-health gate stuck in
annotate, a keepalive listing two projects that could never answer, a guard reading an env var
nothing passed.
The arc that opened and mostly closed in one night: gates that could not fail (#396)
Started 2026-08-19 as routine backlog work and turned into the most productive thread of the
session. Nine PRs, and the mechanism matters more than the count.
#846 added a reporter that names any test finishing with zero assertions. #847 fixed it,
because the first version printed nothing in CI: playwright test --reporter=X REPLACES the
config's reporter array rather than appending, so it ran in the two lanes that do not override
and in none of the 25 required-lane shards. That is the catalogue's own pattern, committed by
the tool built to detect it, and it was caught only by grepping a real run for the reporter's
own output.
Its first real output named 31 tests. Triage is at 15, recorded in #850, and the split
is the point: roughly half are not defects. checkA11y() throws instead of calling expect,
so four names assert without a countable step; one says "if present" in its own title. What
the other half found:
what
where
a live 139x40px submit button — the gate only ever looked at /
Also closed in the same thread: validate:breakpoints ran in CI and could not fail (#841 — a
lost coverage floor was a warning, and the exit code keys off errors), and the required lane's
own header still called itself a PROTOTYPE (#840).
The rule that came out of it: measure what a selector finds on the route the test visits,
BEFORE reading the test. That turned eight candidates into two defects and six acquittals in a
single pass.
The arc that opened 2026-08-22: the fork path, proven by forking it
This did not exist yesterday because nobody had forked the template end-to-end before. Doing it
for real — TortoiseWolfe/geolarp, from rebrand.sh to a live site on its own domain with its own
Supabase — surfaced ten defects in the one workflow this repo exists to provide. They are not
cosmetic and they are not hypothetical; each was found by the fork failing, not by reading code.
The keystone is #922.rebrand.sh discovers files with find plus an extension allowlist.
That single choice is upstream of most of the rest: the allowlist excludes .mjs/.cjs, and the
comment in the script explains it cannot widen safely because find has no notion of what git
tracks (adding three suffixes pulls in 1,746 files, 1,581 of them in caches and a vendored
virtualenv). Its own comment already proposes git ls-files. Fix that and the widening becomes
safe, which is what #939 and part of #926 actually need.
That comment also judged the residual .mjs gap "cosmetic — ~8 doc strings". #939 is the
counter-example, and it is the most serious item here: a fork's service worker stamped scripthammer- caches while purging geolarp- ones, so every deploy left caches that could never
be collected. Live on geolarp.com. Unbounded storage growth in visitors' browsers.
geoLARP is now a live proving ground rather than a hypothetical. Every defect above came from
it; several are already fixed downstream with mutation-tested guards that can be ported straight
back (#929 was, as #930). Expect the next fork-shaped defect to be found the same way.
Active arc — #559, the payment-write lockdown (UNBLOCKED 2026-08-19)
Corrected the same day it was written: this issue was listed as blocked, and it is not. Both
stated blockers (#556, #558) are closed and Phase 2b shipped as #560.
T024 is already done — the recurring-price allowlist landed as #772 (Stripe) and #774
(PayPal); both refuse a client-sent identifier outright and resolve from the catalog.
Remaining, and ordering is load-bearing:
T025 — migrate three client write sites behind create-order: payment-service.ts:179 (insert), :371 (upsert), offline-queue/payment-adapter.ts:166
(upsert). The queue drain is the awkward one — it replays with no live user gesture,
so create-order needs an idempotency-keyed path. Do not discover this at the end.
T026 — re-enumerate .from('payment_intents'); confirm zero writes remain.
T028 — E2E: browser INSERT refused with a pinned status; off-catalog price 4xx.
T027 must not run before T026 passes, or dropping the grant with an unmigrated write site
breaks payments silently rather than loudly.
Multi-session. The other unblocked items below are smaller and can interleave.
The other constraint is the owner-decision queue above, which now outnumbers the unblocked
engineering work.
Next 3 sessions queued
Session +1:#950 — stop e2e-local gating every merge
Why: it is the required check, it is queue-bound, and three PRs sat at 6/7 for over an hour
today waiting on it. Highest leverage available: e2e.yml already gates firefox/webkit behind
a full-e2e label, and applying the same gate here removes 16 of 26 jobs from the common path
at no backend cost, because the local lane has no shared backend to protect.
First action: read .github/workflows/e2e-local.yml:25-45 before changing anything. The header
argues its shape deliberately and the argument is right — the caps in e2e.yml exist to protect
one shared cloud project. Do NOT copy those. The lever that applies here is the label gate,
which is about latency, not contention, and the header does not address it.
Session +3:#850 — finish triaging the 31 zero-assertion tests
Why: carried forward untouched across three sessions. Still the best yield-per-hour available;
16 remain and the reporter cannot become a GATE until the list is empty.
First action: measure what each remaining selector finds on the route its test visits, before
reading the test.
Standing p1, not displaced:#559 T025–T028, the payment-write lockdown. It has not moved in
three sessions. That is worth noticing rather than repeating — if it keeps not moving, it should
be re-scoped or explicitly parked, not silently carried.
The geoLARP fork's own tickets — TortoiseWolfe/geolarp#25 (the a11y job fails on a missing
CI chromium, so the stale-HTML guard never runs) and #26 (24 E2E jobs inherited wholesale,
14 failing, and no coverage of the one path that matters: signup). Not this repo's issues, but
they are the template's tests failing in a fork, so they are evidence about the template.
Next session should
Take #950.e2e-local is a required check that is now queue-bound, so it gates every merge in
the repo — three PRs were stuck at 6/7 behind it today. The lever already exists in the other
lane: gate firefox and webkit behind a full-e2e label, chromium-only on PRs.
State at hand-off:main is green at 992a1d58 (#945 merged). PRs #946, #947 and #948 are
open, each 6 of 7 required green, all waiting on the same E2E (local) result. Nothing has
failed. A local merge sequence was running them; if it is not still alive, merge them by hand
after verifying 7/7 — they are independent and touch no overlapping files.
Cautions carried forward, all learned by being wrong first:
gh run view <id> --log truncates, and sometimes returns zero bytes. Fetch per job via gh api repos/OWNER/REPO/actions/jobs/<jid>/logs, or reproduce locally.
Never put a closing keyword next to an issue number in a PR body, even to deny it.
A probe that reports success is not evidence of success. Three written on 2026-08-22 were
wrong in the same direction: one counted an unreported check as passing, one printed "all 4
gates green" while listing a failure, and one fell through a timeout silently so nothing merged
and nothing said so. Make every probe fail on purpose before trusting a pass, and verify a
gate independently before merging on it.
Run what CI runs.pnpm test:coverage is ~4,700 tests; a blog-subset run passed locally and
failed CI. And never put a formatter and a test run in the same command — three false failures
came from prettier rewriting files while tests read them.
Measure before attributing. "geoLARP's E2E is starving the runners" was asserted from job
counts and was backwards: ScriptHammer's own e2e-local is the 89-minute, 24-shard lane.
Why this exists
Sessions end. The next session starts cold. Without a single canonical "what was the world like when we last stopped + here's what to do next" pointer, the new model wastes context wandering. This issue is the one URL to paste into a fresh chat. The BODY carries the plan; comments form the audit trail.
How to maintain
Run /session-prime at session-end. The skill rewrites the body to reflect what changed in the roadmap and appends a comment with the audit trail.
If the prime prompt itself becomes stale (memory file renamed, project changed shape), edit the body's prime-prompt block directly.
Purpose: this issue is the durable roadmap for TortoiseWolfe/ScriptHammer. The body below is always-current — active arcs, next 3 sessions queued, backlog. Comments are the audit trail (what shipped + what changed in the roadmap, per session). The prime prompt at the top tells a fresh Claude session to read this body first.
Prime prompt (copy from here into a fresh Claude session)
Stop reading here when priming — everything below is the roadmap
Roadmap
Body regenerated 2026-08-22 (evening) against
992a1d58. #758 is CLOSED — not the ordering source.#945 MERGED at
992a1d58(2026-08-22 ~17:05Z) — 24/24 shards, all seven required green.#944 is closed. The stamp below is against
origin/main, not HEAD, which stays the right call.🚨 Owner-only blockers (8, and this is now the binding constraint)
The unblocked backlog is thinning faster than the owner-decision queue. Four of these are new
since yesterday.
#629 — the money steps only:
sk_live_+pk_live_+PAYMENT_MODE_EXPECTED=live+activate the 3 SKUs last + one real card. #621 — rotate three credentials with no API.
#714 — 3D Warehouse licensing, inherited by every fork. #769 — receipt address,
MCC 5734→7372.
NEW: #393 — the CSP now ships as a real header in report-only; flipping it to
enforcing is a dashboard change and an acceptance of risk. #822 — DMARC is
p=none, so thedomain that sends payment receipts can be spoofed. #462 — 181
/80and/85textinstances: needs a policy call (ban opacity for de-emphasis, or audit 94 files per surface),
not a sweep. #442 — the privacy policy must disclose Cloudflare Turnstile; that is legal
copy an agent should not author.
The arc that closed: CI reliability
Promoted on 2026-08-17 because every PR that day needed a re-run and not once for a failing
test. It is done. Since then 14 PRs landed, every one green on its first attempt, with a
single re-run spent all session — and that one was a genuine Azure apt-mirror outage, not a
defect.
Every fix up to 2026-08-18 hardened the install; it stayed fallible, so the arc stayed open.
#762 and #798 (a wall-clock timeout cannot tell slow from stalled — bound the silence instead),
#801/#802 (cache the apt debs; 17.7m → 1.0m), #809 (the idle bound, whose catch was then
demonstrated for real during the mirror outage), #766 (upload the traces the lane already pays to
capture), #726 (the budget guard can now see which backend it meters). Each lowered the odds or
the cost of a failure. None removed it: 24 network fetches per run, every one able to go dark on
its own.
2026-08-19 closed the arc by deleting the operation (#829, PRs #830 + #833) — a different kind
of change, and the reason this section moves from "active" to "closed" rather than just getting a
better number. There is no fetch to retry, no mirror to be down, no dpkg lock to orphan, no retry
loop to fall through. The failure class is unreachable rather than damped, which is what makes it
finishable at all.
The clearest illustration is a branch deleted the same night.
fix/795-install-failure-is-loud(
412fc86c, never merged) made a failed install fail its job loudly at all ten sites — correctwork, and instantly moot: nothing can fall through a step that no longer exists.
No workflow installs browsers on a runner any more — every lane runs from
mcr.microsoft.com/playwright:v1.55.0-noble, which already ships them. The payoff is measured ontwo consecutive commits of
main:6a4e8f57(installs present) failed 8 jobs, every one inInstall Playwright, with zero test failures;d45cfdc2ran 26 jobs green, 0 install steps.Before, each shard spent a median 11.6 min installing — ~158 runner-minutes per run across 24
independent network fetches.
Three of the four lanes are PROVEN:
e2e-local26/26 across chromium/firefox/webkit,signup-mailergreen with a real Mailpit round-trip, andsmokegreen against liveproduction.
e2e.yml's seven sites are migrated but unexercised — the cloud-quota guardskips every downstream job until the window reopens (~Sept 2). Treat it as unproven.
#795 and #819 closed against that evidence rather than being fixed.
The recurring theme, again: controls whose presence was asserted and whose effect never was
— a CSP delivered as
<meta name>and therefore inert, an email-health gate stuck inannotate, a keepalive listing two projects that could never answer, a guard reading an env var
nothing passed.
The arc that opened and mostly closed in one night: gates that could not fail (#396)
Started 2026-08-19 as routine backlog work and turned into the most productive thread of the
session. Nine PRs, and the mechanism matters more than the count.
#846added a reporter that names any test finishing with zero assertions.#847fixed it,because the first version printed nothing in CI:
playwright test --reporter=XREPLACES theconfig's reporter array rather than appending, so it ran in the two lanes that do not override
and in none of the 25 required-lane shards. That is the catalogue's own pattern, committed by
the tool built to detect it, and it was caught only by grepping a real run for the reporter's
own output.
Its first real output named 31 tests. Triage is at 15, recorded in #850, and the split
is the point: roughly half are not defects.
checkA11y()throws instead of callingexpect,so four names assert without a countable step; one says "if present" in its own title. What
the other half found:
/expect(alt).toBeDefined()—getAttributereturnsnull, and null IS defined<table>on any public routeconsole.warn; another that asserted nothing at allbutton[aria-label="Navigation menu"]— the hamburger is a<label>, so the whole body never ranAlso closed in the same thread:
validate:breakpointsran in CI and could not fail (#841 — alost coverage floor was a
warning, and the exit code keys off errors), and the required lane'sown header still called itself a PROTOTYPE (#840).
The rule that came out of it: measure what a selector finds on the route the test visits,
BEFORE reading the test. That turned eight candidates into two defects and six acquittals in a
single pass.
The arc that opened 2026-08-22: the fork path, proven by forking it
This did not exist yesterday because nobody had forked the template end-to-end before. Doing it
for real —
TortoiseWolfe/geolarp, fromrebrand.shto a live site on its own domain with its ownSupabase — surfaced ten defects in the one workflow this repo exists to provide. They are not
cosmetic and they are not hypothetical; each was found by the fork failing, not by reading code.
The keystone is #922.
rebrand.shdiscovers files withfindplus an extension allowlist.That single choice is upstream of most of the rest: the allowlist excludes
.mjs/.cjs, and thecomment in the script explains it cannot widen safely because
findhas no notion of what gittracks (adding three suffixes pulls in 1,746 files, 1,581 of them in caches and a vendored
virtualenv). Its own comment already proposes
git ls-files. Fix that and the widening becomessafe, which is what #939 and part of #926 actually need.
That comment also judged the residual
.mjsgap "cosmetic — ~8 doc strings". #939 is thecounter-example, and it is the most serious item here: a fork's service worker stamped
scripthammer-caches while purginggeolarp-ones, so every deploy left caches that could neverbe collected. Live on geolarp.com. Unbounded storage growth in visitors' browsers.
find→git ls-files— the keystoneScripthammer …package.jsononlymanifest.json/robots.txt— caught 3× in one sessionPROGRESS 2026-08-22 (evening). Three of these are fixed and in review, in the order the
keystone argument predicted:
find→git ls-filesE2E (local)git checkout --and silently discarded uncommitted workTwo NEW issues came out of the same work, both about the fork/parent split:
hosted lane this repo has blocked, against a Supabase project the guard is not metering. Its
BACKEND_EPOCH_PROJECT_REFis hardcoded to this repo's project, and the The E2E budget guard counts workflow runs, not Supabase — it cannot see a backend swap #726 mismatchdetector is inert here (the variable is unset) and merely advisory there.
e2e-localruns 26 jobs (16 firefox/webkit) on every PR and push, is nowqueue-bound (26–89 min wall, four runs stacked ~90 min today), and is a required check, so
it gates every merge. Its design reasoning is sound; the assumption that has expired is "all
24 can run at once".
geoLARP is now a live proving ground rather than a hypothetical. Every defect above came from
it; several are already fixed downstream with mutation-tested guards that can be ported straight
back (#929 was, as #930). Expect the next fork-shaped defect to be found the same way.
Active arc — #559, the payment-write lockdown (UNBLOCKED 2026-08-19)
Corrected the same day it was written: this issue was listed as blocked, and it is not. Both
stated blockers (#556, #558) are closed and Phase 2b shipped as #560.
T024 is already done — the recurring-price allowlist landed as #772 (Stripe) and #774
(PayPal); both refuse a client-sent identifier outright and resolve from the catalog.
Remaining, and ordering is load-bearing:
create-order:payment-service.ts:179(insert),:371(upsert),offline-queue/payment-adapter.ts:166(upsert). The queue drain is the awkward one — it replays with no live user gesture,
so
create-orderneeds an idempotency-keyed path. Do not discover this at the end..from('payment_intents'); confirm zero writes remain.INSERTfrom the GRANT.UPDATEisalready revoked (Of the three 050-audit findings, only the dead UPDATE grant was still real — now fixed and generalised into a cross-check #565), so this is one privilege, not two.
T027 must not run before T026 passes, or dropping the grant with an unmigrated write site
breaks payments silently rather than loudly.
Multi-session. The other unblocked items below are smaller and can interleave.
The other constraint is the owner-decision queue above, which now outnumbers the unblocked
engineering work.
Next 3 sessions queued
Session +1: #950 — stop
e2e-localgating every mergetoday waiting on it. Highest leverage available:
e2e.ymlalready gates firefox/webkit behinda
full-e2elabel, and applying the same gate here removes 16 of 26 jobs from the common pathat no backend cost, because the local lane has no shared backend to protect.
.github/workflows/e2e-local.yml:25-45before changing anything. The headerargues its shape deliberately and the argument is right — the caps in e2e.yml exist to protect
one shared cloud project. Do NOT copy those. The lever that applies here is the label gate,
which is about latency, not contention, and the header does not address it.
Session +2: the rest of the fork arc — #926, #933, #936, #938, #923
rebrand.sh rewrites the brand token inside GitHub URLs, so every fork's inherited doc links 404 #926 (URLs rewritten into 404s) and rebrand.sh misses case variants, so every fork's intro post is still titled 'Scripthammer - Opinionated Next.js PWA Template' #933 (case variants) are the two a reader of a fork sees.
deciding which URLs are citations rather than branding — a smaller decision once rebrand.sh misses case variants, so every fork's intro post is still titled 'Scripthammer - Opinionated Next.js PWA Template' #933 has
settled how substitution works.
Session +3: #850 — finish triaging the 31 zero-assertion tests
16 remain and the reporter cannot become a GATE until the list is empty.
reading the test.
Standing p1, not displaced: #559 T025–T028, the payment-write lockdown. It has not moved in
three sessions. That is worth noticing rather than repeating — if it keeps not moving, it should
be re-scoped or explicitly parked, not silently carried.
Backlog (no scheduled position)
nothing writes an IP. Split out of auth_audit_logs records no IP — but the 'nothing to rate-limit on' premise was our own test suite (#608) #609. The decision (capture vs delete) is an owner call and
touches the same privacy ground as Turnstile is live on the auth forms but the privacy policy never mentions Cloudflare, and invisible mode requires that reference #442.
cookie; the session lives in local/sessionStorage. Small, but needs a runnable auth lane.
docs/specs/,specs/,features/still carry theA production build kills the running dev server (.next clobbered despite NEXT_DIST_DIR) #293 build command. Live guidance is fixed; whether to rewrite historical spec records is a
separate decision.
reproduces and its missing PR trigger is deliberate — arguably close-as-documented.
@scripthammer.com.a paid, multi-day transfer at Squarespace, and both domains are transfer-locked. Practical
window closes ~mid-November (expiry 2026-12-06).
contribution policy).
3d/diorama arc, deferred by decision — but core for a geoLARP fork,where /twins/[slug]?ortho freezes the main thread for 4.7s (17.8s total blocking) — the E2E click timeout is real #717, Invisible glass walls: 43% of landmark faces are culled at render and still solid in collision #713, ?walk on a slug with no walkable terrain stalls in orbit silently #727 and Bake an offline address index so the twin can show real street addresses #708–Minimap, compass, and waypoints in the Directory panel #712 stop being backlog.
992a1d58): theynow refuse a non-local backend and seed an isolated admin instead of using the shared test user.
Steps 5–7 remain, and are sequenced behind the DECIDED (delete): the admin IP column and burst panel can never populate — no IP source exists and the architecture has nowhere to observe one #839 decision.
/scheduleshows a consent card before the calendar, on the front door for bookings.TortoiseWolfe/geolarp#25(the a11y job fails on a missingCI chromium, so the stale-HTML guard never runs) and
#26(24 E2E jobs inherited wholesale,14 failing, and no coverage of the one path that matters: signup). Not this repo's issues, but
they are the template's tests failing in a fork, so they are evidence about the template.
Next session should
Take #950.
e2e-localis a required check that is now queue-bound, so it gates every merge inthe repo — three PRs were stuck at 6/7 behind it today. The lever already exists in the other
lane: gate firefox and webkit behind a
full-e2elabel, chromium-only on PRs.State at hand-off:
mainis green at992a1d58(#945 merged). PRs #946, #947 and #948 areopen, each 6 of 7 required green, all waiting on the same
E2E (local) result. Nothing hasfailed. A local merge sequence was running them; if it is not still alive, merge them by hand
after verifying 7/7 — they are independent and touch no overlapping files.
Cautions carried forward, all learned by being wrong first:
gh run view <id> --logtruncates, and sometimes returns zero bytes. Fetch per job viagh api repos/OWNER/REPO/actions/jobs/<jid>/logs, or reproduce locally.wrong in the same direction: one counted an unreported check as passing, one printed "all 4
gates green" while listing a failure, and one fell through a timeout silently so nothing merged
and nothing said so. Make every probe fail on purpose before trusting a pass, and verify a
gate independently before merging on it.
pnpm test:coverageis ~4,700 tests; a blog-subset run passed locally andfailed CI. And never put a formatter and a test run in the same command — three false failures
came from prettier rewriting files while tests read them.
counts and was backwards: ScriptHammer's own
e2e-localis the 89-minute, 24-shard lane.Why this exists
Sessions end. The next session starts cold. Without a single canonical "what was the world like when we last stopped + here's what to do next" pointer, the new model wastes context wandering. This issue is the one URL to paste into a fresh chat. The BODY carries the plan; comments form the audit trail.
How to maintain
Run
/session-primeat session-end. The skill rewrites the body to reflect what changed in the roadmap and appends a comment with the audit trail.If the prime prompt itself becomes stale (memory file renamed, project changed shape), edit the body's prime-prompt block directly.