Files
motovaultpro/.claude/skills/mvp-launch-readiness/SKILL.md
T
Eric GullicksonandClaude Fable 5 c239bb9347
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
chore: replace AI skill library with 16 verified mvp-* skills
Removes the old planner/decision-critic/role-agents/domain-agents system
(step-injector scripts, stale scopes, sprint-era workflow) and replaces it
with 16 ground-truth-verified skills under .claude/skills/: change control,
debugging playbook, failure archaeology, architecture contract, domain
reference, OCR/Gemini pipeline, config and secrets, build and env, run and
operate, diagnostics (with tested scripts), validation and QA, docs and
writing, launch readiness, deploy-safety campaign, proof and analysis
toolkit, and research frontier. RULE 0/1/2, the temporal-contamination rule,
and the decision stress-test protocol are carried forward into the new
skills; the retired content remains in git history.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-09 20:21:42 -05:00

21 KiB

name, description
name description
mvp-launch-readiness Load when assessing whether MotoVaultPro is ready to launch, take paying users, or make a public claim ("is this production ready", "can we ship", "launch checklist", "is billing working", "is this secure enough", "what is blocking launch", "can we say X is supported"). Also load before writing any user-facing statement about supported features, security posture, or reliability, and when triaging the known launch blockers - Stripe upgrade dead-end, Resend webhook secret, backup emails, fuel-log cache bug, Traefik/Grafana default credentials, stale AUDIT.md.

MVP Launch Readiness

External positioning for a PRE-LAUNCH product heading toward paying users. This skill defines what must be PROVEN before claiming readiness or shipping to humans, and lists the launch blockers with evidence. Its purpose is to prevent claiming readiness by eye.

When to use / When NOT to use

Use this skill when:

  • Deciding whether a feature can be called "supported" in any user-facing or public claim.
  • Building or reviewing a launch checklist, readiness report, or go/no-go decision.
  • Triaging any of the launch-blocking gaps listed below.

Do NOT use this skill for:

  • Fixing the CI gate hole itself — that is mvp-deploy-safety-campaign (the executable campaign).
  • How to deploy, roll back, or operate environments — mvp-run-and-operate.
  • What counts as test evidence and how to run suites — mvp-validation-and-qa.
  • Secrets/config mechanics (adding a secret, YAML config loader) — mvp-config-and-secrets.
  • Doc trust levels and doc-sync — mvp-docs-and-writing.
  • Issue/branch/PR mechanics for fixing any blocker — mvp-change-control.

Verdict labels (use these exact words)

Label Meaning
PROVEN Verified against code/config on 2026-07-07, with a test or a staging-verified procedure behind it
UNPROVEN Implemented in code but never verified end-to-end; no test, no recorded staging run
BROKEN Verified defective: the code path exists and does the wrong thing (or nothing)

A claim without a label is not a claim. "It compiles" and "staging boots" are not evidence: CI gates nothing beyond build + boot (canonical statement: mvp-validation-and-qa section 1).

1. Launch-blocking gaps (each verified 2026-07-07 — re-verify before acting)

# Gap Verdict Evidence
1 Upgrade path from tier-gated features dead-ends: the Upgrade button in UpgradeRequiredDialog just closes the dialog (// TODO: Navigate to upgrade page) BROKEN frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx:55-58. Nuance: a working checkout UI exists at route /garage/settings/subscription (frontend/src/App.tsx:1175), so users who find Settings can upgrade — but the upsell moment (hitting a gated feature) converts to nothing. Fix is a navigate call, not a Stripe build-out.
2 Resend inbound email webhook secret is unconfigured in every environment: resend-webhook-secret is absent from secrets/app/, from the backend secret mounts in docker-compose.yml:133-141, and from the 12-file list in scripts/inject-secrets.sh:30-43 BROKEN (feature), not open (security) POST /api/webhooks/resend/inbound is a PUBLIC endpoint (no JWT — backend/src/features/email-ingestion/api/email-ingestion.routes.ts:13). It fails CLOSED: verifyWebhookSignature throws when RESEND_WEBHOOK_SECRET is unset (resend-inbound.client.ts:39) and the handler returns 400 (email-ingestion.controller.ts:141-146). So no unsigned request is accepted — but email ingestion cannot work at all until the secret is provisioned end-to-end (secret file + compose mount + inject-secrets + Gitea secret). Config schema already treats it as optional (backend/src/core/config/config-loader.ts:121,185).
3 Backup failure emails are never sent — the notification path checks settings and the admin email, then only logs "Backup notification would be sent" BROKEN backend/src/features/backup/domain/backup.service.ts:441 (TODO: Integrate with email service). The settings UI implies emails that do not exist. A paying user's backup can fail silently.
4 Fuel-log delete invalidates only the imperial cache: deleteFuelLog hardcodes 'imperial' and invalidateCaches deletes only that unit-system's two keys BROKEN backend/src/features/fuel-logs/domain/fuel-logs.service.ts:223 and :266-271. The inline comment "simple sweep below" is false — there is no sweep. Metric-unit users see deleted logs for up to the cache TTL.
5 Traefik dashboard on prod has placeholder auth: basicauth hash is admin:$2y$10$foobar (not a valid bcrypt digest), while api.insecure: true with port 8080 published in the base compose BROKEN (relies on firewall only) docker-compose.prod.yml:23; config/traefik/traefik.yml:1-4. Generate a real htpasswd bcrypt hash before launch.
6 Grafana admin password defaults to admin unless the GRAFANA_ADMIN_PASSWORD Gitea secret is set UNPROVEN (mitigated) docker-compose.yml:350 (${GRAFANA_ADMIN_PASSWORD:-admin}). Mitigation exists: grafana-ipwhitelist middleware allows only RFC1918 source ranges (config/traefik/dynamic/grafana.yml:3-5), so it is not internet-reachable — but confirm the Gitea secret is actually set on staging and prod before launch.
7 CI gates nothing: no tests, no lint, no security scan on any PR; PR builds clobber :latest and the prod deploy input defaults to latest, so prod can ship an unmerged PR build BROKEN (process) .gitea/workflows/staging.yaml (PR-triggered, pushes :latest), .gitea/workflows/production.yaml (image_tag default latest). Do not fix here — route to mvp-deploy-safety-campaign. Launch gate: prod deploys by SHA, and at least one real test job on PR.
8 No account-deletion / data-retention policy document, despite the feature being live: a 30-day grace period is implemented (backend/src/features/user-profile/jobs/account-purge.job.ts) and mentioned once in docs/USER-GUIDE.md:613 UNPROVEN (code exists, policy does not) Paying users require a stated retention policy. docs/AUDIT.md flagged this MEDIUM in 2025-12 (AUDIT.md:303); still no doc. Write the policy doc; verify the purge job's behavior matches it before publishing.
9 docs/AUDIT.md (dated 2025-12-13, verdict "CONDITIONALLY READY") was never updated after remediation, so the real open findings are unknowable from the doc BROKEN (as a record) Several findings are demonstrably fixed in code (see section 3) while the doc presents them as open. Do a remediation-status pass: add a Status column (FIXED with file:line / OPEN / SUPERSEDED) so a reader can trust it. Until then, treat AUDIT.md as an archive, never as the current posture.

2. Claim discipline

Rule: nothing is "supported" until it has (a) a verified runbook in the repo and (b) a test or a staging-verified procedure. "The code exists" is UNPROVEN. When docs contradict code, code wins — say so in the claim.

Billing claims specifically

Do not claim billing works until the FULL cycle has been exercised on staging with Stripe test mode: checkout -> webhook -> tier sync -> downgrade. Current status, checked 2026-07-07:

  • Status: UNPROVEN. The subscriptions capsule (backend/src/features/subscriptions/) has ZERO test files (find backend/src/features/subscriptions -name "*.test.ts" returns nothing). Git history shows real-world debugging fixes on 2026-02-16 ("charge immediately on subscription", "attach payment method to customer" — commits 8a73352, 72e5573), which implies the flow was manually exercised around then, but there is no test and no recorded staging verification of the full cycle since. Honest label: implemented, manually poked in Feb 2026, never verified end-to-end as a procedure.

How to verify it (the moving parts):

  1. Price IDs: four env vars map plan -> Stripe price and back: STRIPE_PRO_MONTHLY_PRICE_ID, STRIPE_PRO_YEARLY_PRICE_ID, STRIPE_ENTERPRISE_MONTHLY_PRICE_ID, STRIPE_ENTERPRISE_YEARLY_PRICE_ID. Sandbox defaults are baked into docker-compose.yml:124-127 and .env.example:12-15; staging/prod override via .env generated from Gitea variables. Forward mapping in getPriceId, reverse tier inference in determineTierFromStripeSubscription (backend/src/features/subscriptions/domain/subscriptions.service.ts:831-900). An unknown price ID silently maps to tier free — a misconfigured price ID downgrades paying users without error.
  2. Secrets: stripe-secret-key and stripe-webhook-secret are file secrets read by getStripeConfig() (backend/src/core/config/config-loader.ts:258-263), mounted at docker-compose.yml:140-141, injected on deploy by scripts/inject-secrets.sh.
  3. Endpoints: POST /api/subscriptions/checkout (authenticated, subscriptions.routes.ts:28-29) and POST /api/webhooks/stripe (PUBLIC, signature- verified, webhooks.controller.ts:24). Tier lands in user_profiles.subscription_tier via syncTierToUserProfile and gates features through backend/src/core/config/feature-tiers.ts (limits and key catalog canonical in mvp-config-and-secrets section 3; unknown feature keys FAIL OPEN).
  4. Staging procedure: on staging with Stripe TEST-mode keys in the Gitea secrets, drive checkout from /garage/settings/subscription with card 4242 4242 4242 4242; confirm the webhook fired ({container="mvp-backend-staging"} | json in Grafana, and the subscription_events table); confirm user_profiles.subscription_tier flipped; then cancel/downgrade and confirm the tier reverts and the vehicle-selection flow triggers when over the free limit. The Stripe webhook endpoint for staging must be registered in the Stripe dashboard pointing at https://staging.motovaultpro.com/api/webhooks/stripe and its signing secret stored as the STRIPE_WEBHOOK_SECRET Gitea secret. Record the run (date, commit SHA, screenshots or log queries) in the issue that tracks the claim.

Remember owner non-negotiables while doing this: no destructive DB operations without a fresh backup, and never hand-edit files on staging/prod servers (rsync --delete reverts them; all changes flow through the repo and workflows).

3. Security posture — the honest list (verified 2026-07-07)

What IS solid (PROVEN in code):

Item Evidence
Auth0 JWT validation: JWKS-based verification with issuer and audience pinning backend/src/core/plugins/auth.plugin.ts:105-108 (allowedIss, allowedAud)
Network isolation: database network is internal: true; prod removes postgres/redis host ports docker-compose.yml:405-407; docker-compose.prod.yml sets ports: []
File-based secrets, never env, never committed (only .example siblings; google-wif-config.json is deliberately committed WIF config) backend/src/core/config/config-loader.ts:175-188; scripts/inject-secrets.sh
Upload magic-byte validation: content vs claimed MIME type checked before accepting documents backend/src/features/documents/api/documents.controller.ts:253-285
Storage keys from crypto.randomBytes(32) (the AUDIT.md CRITICAL, since fixed) backend/src/features/documents/api/documents.controller.ts:587
Stripe and Resend webhooks verify signatures and fail closed webhooks.controller.ts:36-46; resend-inbound.client.ts:37-52
Audit-log capsule and backend ESLint exist (both were AUDIT.md gaps) backend/src/features/audit-log/; backend/eslint.config.js

Open or unknown (do not claim these):

Item Status Truth
Content-Security-Policy on the served frontend OPEN frontend/nginx.conf contains zero add_header directives — the SPA is served with no CSP. Backend registers @fastify/helmet with defaults (backend/src/app.ts:49), which covers API responses only. AUDIT.md finding still open.
Google Maps API key exposure OPEN (by design, unmitigated) The key is injected at container startup into window.CONFIG served to every browser (frontend/scripts/load-config.sh -> /usr/share/nginx/html/config.js). Browser-side Maps keys are normal ONLY if referrer/API restrictions are set on the key in Google Cloud Console — that restriction is not verifiable from this repo. UNVERIFIED until checked in the console. AUDIT.md also flags key leakage via photo URLs.
PostgreSQL connection encryption RESOLVED CONTRADICTION — connections are UNENCRYPTED docs/ARCHITECTURE-OVERVIEW.md:511 claims "PostgreSQL SSL/TLS"; docs/SECURITY.md:30 says unencrypted, network-isolated. Code wins: no ssl option anywhere in backend/src/core/config/database.ts or the database: block of config/app/production.yml:12-18. SECURITY.md is correct. Acceptable for a single-host internal-network deployment; never claim encrypted-in-transit DB.
Tier guard fail-open OPEN (deliberate, document it) canAccessFeature returns true for unknown feature keys (backend/src/core/config/feature-tiers.ts). A typo in a feature key silently un-gates a paid feature.
Admin guard typing in backup routes OPEN backend/src/features/backup/api/backup.routes.ts uses (fastify as any).requireAdmin 16 times — a typo compiles clean and would ship an unguarded admin endpoint.

4. Reproducibility standard for launch

Launch bar: a new machine must be able to stand up the product from the repo plus Gitea secrets alone. No snowflake state on any server.

  • Docs to trust: docs/BUILD-SERVER-SETUP.md (VPS + act_runner provisioning) and docs/CICD-DEPLOY.md (deploy pipeline — the most code-accurate infra doc). Known defects in both, verified 2026-07-07:
    • BUILD-SERVER-SETUP.md installs act_runner v0.2.13 (line 84) but its "Update Runner" section downloads v0.2.12 (line 247) — following the update procedure downgrades.
    • CICD-DEPLOY.md says runner labels mvp-build/mvp-prod (lines 34, 59-60); actual labels are stage/prod (.gitea/workflows/staging.yaml:29, production.yaml:34).
  • Ansible caveat (the prune-cron gap): the staging runner's daily Docker prune cron was installed out-of-band and is deliberately NOT in the repo; worse, ansible/deploy-staging-runner.yml:317-324 REMOVES a legacy "Docker cleanup" cron. Rebuilding the staging box from ansible silently drops disk hygiene, and the 29G root disk then fills from per-commit image builds. This is the one standing exception to "everything flows through the repo" — a rebuilt box needs the prune cron reinstalled manually until it is codified.
  • Everything on-server is disposable by design: scripts/inject-secrets.sh regenerates all 12 secret files from Gitea Actions secrets on every deploy, and rsync --delete replaces config/ and scripts/. If standing up a new box requires copying anything from an old box other than data volumes, that is a launch-blocking reproducibility bug.
  • Trap: bind-mounting a missing secret file makes Docker create a DIRECTORY at that path; a new machine hitting "secret is a directory" errors is this failure mode (canonical description and fix: mvp-config-and-secrets section 2).

5. Ranked pre-launch checklist

Run every evidence command from the repo root. A row is done when the command output contradicts the gap, not when a PR merged.

Blocking (no paying users until green)

Item Evidence command
Wire UpgradeRequiredDialog to /garage/settings/subscription grep -n "TODO: Navigate" frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx (must return nothing)
Full billing cycle verified on staging, Stripe test mode, recorded in an issue See section 2 procedure; find backend/src/features/subscriptions -name "*.test.ts" should also stop returning empty
Real bcrypt hash on prod Traefik dashboard; kill api.insecure or unpublish 8080 grep -n "foobar" docker-compose.prod.yml (nothing); grep -n "insecure" config/traefik/traefik.yml
Backup failure emails actually send grep -n "would be sent" backend/src/features/backup/domain/backup.service.ts (nothing)
CI runs at least one real test job; prod deploys pinned by SHA (route to mvp-deploy-safety-campaign) grep -n "npm test|jest|pytest" .gitea/workflows/*.yaml (currently nothing); grep -n "default: 'latest'" .gitea/workflows/production.yaml
Account-deletion / data-retention policy doc exists and matches account-purge.job.ts behavior ls docs/ | grep -i -E "retention|deletion" (currently nothing)
GRAFANA_ADMIN_PASSWORD Gitea secret set for staging and prod Check Gitea repo secrets; on server: docker exec mvp-grafana env | grep GF_SECURITY must not show admin

Important (fix before public claims, not necessarily before first user)

Item Evidence command
Fix fuel-log delete cache invalidation for both unit systems grep -n "'imperial'" backend/src/features/fuel-logs/domain/fuel-logs.service.ts (line 223 hardcode gone)
Provision resend-webhook-secret end-to-end or disable/hide email ingestion grep -n "resend-webhook-secret" scripts/inject-secrets.sh docker-compose.yml (currently no hits in either)
Remediation-status pass on docs/AUDIT.md grep -n "Status" docs/AUDIT.md shows a per-finding FIXED/OPEN column
CSP header on the frontend (nginx or Traefik middleware) grep -n "add_header" frontend/nginx.conf or grep -rn "Content-Security" config/traefik/
Verify Google Maps key has referrer restrictions Google Cloud Console (not verifiable from repo — record the check in an issue)
Type the backup routes' admin guard grep -c "as any).requireAdmin" backend/src/features/backup/api/backup.routes.ts returns 0
Codify the staging-runner prune cron in ansible grep -n "prune" ansible/deploy-staging-runner.yml shows an install task, not just removal of the legacy cron

Nice-to-have

Item Evidence command
Tier guard fail-closed (or an explicit allowlist test) grep -n "fail open|return true" backend/src/core/config/feature-tiers.ts
Fix BUILD-SERVER-SETUP.md v0.2.12/v0.2.13 and CICD-DEPLOY.md label drift grep -n "0.2.12" docs/BUILD-SERVER-SETUP.md; grep -n "mvp-build" docs/CICD-DEPLOY.md
Silent-failure TODOs: fuel-log delete error toast, stations pull-to-refresh no-op grep -rn "TODO" frontend/src/features/fuel-logs/components/FuelLogsList.tsx frontend/src/features/stations/mobile/StationsMobileScreen.tsx
PostgreSQL in-transit encryption (only if topology ever spans hosts) grep -n "ssl" backend/src/core/config/database.ts config/app/production.yml

Provenance and maintenance

Authored 2026-07-07 from direct repo inspection at commit e729d42-era main. Every file:line above was verified on that date. Volatile facts and their re-verification commands:

  • UpgradeRequiredDialog dead-end: grep -n "TODO: Navigate" frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx
  • Resend webhook secret absent from deploy path: grep -n "resend-webhook-secret" scripts/inject-secrets.sh docker-compose.yml secrets/app/ -r
  • Backup email log-only: grep -n "would be sent" backend/src/features/backup/domain/backup.service.ts
  • Fuel-log cache hardcode: grep -n "invalidateCaches" backend/src/features/fuel-logs/domain/fuel-logs.service.ts
  • Traefik placeholder auth: grep -n "foobar" docker-compose.prod.yml && grep -n "insecure" config/traefik/traefik.yml
  • Grafana default password: grep -n "GRAFANA_ADMIN_PASSWORD" docker-compose.yml
  • CI runs no tests: grep -n "npm test\|jest\|pytest" .gitea/workflows/*.yaml (expect no hits until fixed)
  • Prod deploy default tag: grep -n "default: 'latest'" .gitea/workflows/production.yaml
  • Subscriptions capsule untested: find backend/src/features/subscriptions -name "*.test.ts"
  • Stripe price-ID mapping: grep -n "PRICE_ID" docker-compose.yml backend/src/features/subscriptions/domain/subscriptions.service.ts
  • Tier limits and fail-open: grep -n "VEHICLE_LIMITS\|return true" backend/src/core/config/feature-tiers.ts
  • pg unencrypted: grep -n "ssl" backend/src/core/config/database.ts config/app/production.yml (expect no hits)
  • No frontend CSP: grep -n "add_header" frontend/nginx.conf (expect no hits)
  • 30-day grace period: grep -rn "grace" backend/src/features/user-profile/jobs/account-purge.job.ts docs/USER-GUIDE.md
  • AUDIT.md staleness: grep -n "CONDITIONALLY READY" docs/AUDIT.md
  • Ansible prune-cron gap: grep -n "cron" ansible/deploy-staging-runner.yml
  • Runner label drift: grep -n "runs-on" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml && grep -n "mvp-build" docs/CICD-DEPLOY.md

Not verifiable from the repo (recheck externally): Google Maps key referrer restrictions (Google Cloud Console); whether the GRAFANA_ADMIN_PASSWORD and Stripe/Resend Gitea secrets are actually set (Gitea repo settings); whether a Stripe test-mode webhook endpoint is registered for staging (Stripe dashboard).