Created
Jun 21, 14:11
Started
Jun 22, 01:55
Completed
Jun 22, 03:01
DevOps handoff
Type
Feature
Shape
ui+db
Worktree Slug
standard-link-turf
Repositories
turf-monster
Release Train
—
Branch
feat/standard-link-turf
Pull Request
https://github.com/amcritchie/turf-monster/pull/160Acceptance Criteria
Expected Test Plan
Checks Run
Stage Timeline
Who handled each stage, the time it took (measured), and the model / tokens / cost reported (best-effort) — plus who's on it right now. — means the agent didn't report that metric.
No stage changes recorded yet.
Conversation
QA review feedback, agent handoffs, and follow-up notes for this task.
PR #160 (turf-monster). Adopts Studio::Link: shared model + /i referral cutover (legacy ?ref= kept as back-compat) + admin-managed magic-link banner. Magic link stays /magic_link (already short; /l is TM's landing namespace). Full minitest 1340 green + 10 e2e (magic_link + referrals) green + live-smoked. DEPENDS on engine PR #7 gem publish — Gemfile points at the engine branch (github); flip to ~> 0.8 + bundle on release.
QA BLOCK (rework). Strong adoption (draw_link_routes=false correctly set; in-change migrations; idempotent prefetch-safe referral, no double-credit) but 2 blockers + the engine-publish gate. (1) SECURITY — POST /i/:token (config/routes.rb:131-132) wires both /i verbs to the engine Studio::LinksController, whose #consume burns+signs-in ANY magic_link-kind token via the engine sign_up_new (bare User.new+save!): NO legal-age attestation, no reset_prior_session!, no reference cap. turf's compliance gate lives ONLY in MagicLinksController#sign_up_new:126-128. A user can request a magic link with the age box unchecked, then POST that token to /i/<token> and create an account bypassing the age gate — a second live login path the task forbids, on a mainnet-live underwriting-bound app. POST /i serves NO referral purpose (referral is GET-only by engine design). FIX: drop POST /i; constrain GET /i to referral-kind tokens only (404/redirect other kinds). (2) Carry-forward gate: expiry coverage was DELETED not replaced (old magic_link_test.rb held the only travel-past-TTL rejection; engine harness cannot host one). Add an integration test: mint token, travel past Studio.magic_link_ttl (15m), assert consume POST rejected. (3) Release-ordering hard gate: Gemfile pins studio-engine to unpublished branch feat/standard-link-model — cannot merge to main until engine #7 (task-f2ca3e97a730) merges + publishes 0.8.0 + re-pin to ~> 0.8. Sequence: fix 1+2 -> engine publish -> re-pin -> CI -> merge. Green CI only proves the engine BRANCH works.
QA rework done (re #160). (1) SECURITY FIXED — dropped POST /i; GET /i now -> InvitesController (referral-kind ONLY: cookie+redirect). Engine Studio::LinksController#consume no longer reachable at /i, so a magic-link token cannot create an account there bypassing the legal-age gate. Non-referral token at /i rejected; POST /i unrouted (404). Regression tests added. (2) Expiry coverage restored (mint -> travel past Studio.magic_link_ttl -> consume rejected). (3) Engine-publish gate acknowledged. Full minitest 1343/0F + e2e 10 green. Commit cca5161.
QA RE-REVIEW: APPROVE-PENDING-ENGINE-PUBLISH. SECURITY blocker RESOLVED — GET /i now routes to app-owned InvitesController#show (referral-kind + live only; magic_link tokens fall through to root redirect; NO sign-in, NO token burn, NO engine-consume delegation; draw_link_routes=false so no POST consume reachable outside MagicLinksController; POST /i returns 404). Tests prove every claim. Expiry test added (test/controllers/magic_links_controller_test.rb:266 — travel past Studio.magic_link_ttl -> rejected). NOT rework — held on DEPENDENCY: studio-engine 0.8.0 publish (Gemfile pins branch feat/standard-link-model). UNBLOCK RECIPE (one final commit once the gem ships): (1) operator publishes studio-engine 0.8.0; (2) repin Gemfile 'studio-engine','~> 0.8' + refresh lock; (3) FIX two stale comments still describing the REJECTED engine-controller-handles-/i design: config/initializers/studio.rb (draw_link_routes block) + test/integration/referral_link_test.rb header; (4) merge. Non-blocking follow-ups: orphaned magic_links table (no drop migration); referral_for lacks unique index on (linkable,kind,target).
Repin landed: Gemfile -> studio-engine ~> 0.8, lockfile resolves 0.8.0, 2 stale /i comments fixed (now describe InvitesController#show), full CI GREEN against the PUBLISHED 0.8.0 gem (lint/scan/test + 3 playwright shards). Avi pre-approved the logic; engine shipped -> reviewed. NOTE: turf-monster deploys via its OWN pipeline (turf-monster/bin/deploy), not mcritchie-studio bin/release.
PR #160 squash-merged to turf-monster main (2026-06-22) — code landed, repinned studio-engine ~> 0.8 (0.8.0). NOT deployed: turf-monster deploys via its own pipeline (turf-monster/bin/deploy → turf-monster-mainnet / app.turfmonster.media), which is a MAINNET prod deploy the operator runs explicitly. Task stays reviewed pending that deploy. (turf is NOT shippable via the mcritchie-studio bin/release conductor — single-app, mcr-only.)
SHIPPED to mainnet: turf-monster-mainnet v104 (app.turfmonster.media), 2026-06-22. PR #160 merged to turf main, deployed via turf-monster/bin/deploy. Preflight green (IDL hash already pinned — no on-chain change), migrations CreateStudioLinks + AllowNullImageCacheOwner ran in release phase, smoke /up 200. Studio::Link /l magic-link + referral + managed email image now LIVE on mainnet; studio-engine 0.8.0 in prod.
Sealed-bid sizing
Edit →Alex (PM)
—
Avi (PO)
—
Dev
—
Actual
—
We emailed a one-tap sign-in link to . It expires shortly and can only be used once.
No email? Check spam, or close this and try again.