Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .agents/aw/environments.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# .agents/aw/environments.yml — where the verification suite runs. Committed; no secrets.
# Field reference: the aw-verify skill's rules/config.md. Written by /aw-verify setup.
# Read .agents/aw/notes.md before a run: local `web` talks to PRODUCTION Supabase,
# and the local Supabase stack runs on a developer machine only.
version: 1
environments:
local:
url: http://localhost:3000
stack:
driver: command
commands:
# Local Supabase (Postgres + Auth + Edge Functions) — LOCAL MACHINE ONLY.
# The script exits 78 when no Docker daemon is reachable (the Agent0 /
# cloud sandbox), and the run carries on with `web` against the hosted
# backend. Listed before `web` so it is ready when `web` picks its mode.
# No `port`: Docker owns 54321, and its listener would read as foreign.
- name: supabase
run: sh .agents/aw/scripts/stack/supabase-local.sh
ready_url: http://127.0.0.1:54321/functions/v1/health
# The dashboard. web.sh serves it against the local Supabase while
# supabase-local.sh runs, otherwise against the hosted project in
# .env.local (production).
- name: web
run: sh .agents/aw/scripts/stack/web.sh
ready_url: http://localhost:3000
port: 3000
component_map:
packages/web/: [web]
packages/schemas/: [web]
packages/feature-flags/: [web]
supabase/: [supabase, web]
scripts/seed/: [supabase, web]
default: [web]
hot_reload: [packages/web/src/]
config_files: [.env.local, packages/web/.env.local, supabase/config.toml]
ready_timeout_seconds: 300
auth:
strategy: storage-state
storage_state: .auth/local.json
# Present only when signed in (the Sidebar's user-settings link).
authed_check: '{role: "link", name: "User settings for"}'
refresh:
when: missing-or-expired
env: [LOREKIT_TESTING_USER_NAME, LOREKIT_TESTING_USER_PW]
# Reuses the repo's own headless login (scripts/auth-bootstrap-credentials.mjs).
# The runner exports PREVIEW_URL as this environment's url.
command: |
set -a
[ -f .env.local ] && . ./.env.local
[ -f packages/web/.env.local ] && . ./packages/web/.env.local
set +a
AUTH_LOGIN_URL="${PREVIEW_URL:-http://localhost:3000}/login" \
AUTH_STORAGE_STATE=./.auth/local.json \
AUTH_POST_LOGIN_URL_PATTERN='/lore' \
AUTH_AUTHED_ROLE='link' \
AUTH_AUTHED_NAME='User settings for' \
node scripts/auth-bootstrap-credentials.mjs
timeout_seconds: 120
# The email is the LOREKIT_TESTING_USER_NAME env var, never committed.
identity: { email: env:LOREKIT_TESTING_USER_NAME, role: user }
fixtures:
seed: null
preview:
url:
# GitHub deployments here are main's staging/production deploys only, so the
# PR preview comes from the sticky `lorekit-web-preview` comment that
# web-preview-deploy.yml posts (stable `pr-<n>` alias).
from: pr-deployment
comment:
authors: [github-actions]
host_pattern: 'lorekit-pr-\d+-[a-z0-9-]+\.vercel\.app'
template: 'https://lorekit-pr-{pr}-mads-thines-projects.vercel.app'
auth:
strategy: storage-state
# Outer wall: Vercel Deployment Protection on every preview request.
bypass_header: { name: x-vercel-protection-bypass, env: VERCEL_AUTOMATION_BYPASS_SECRET }
# Separate from local.json: the Supabase session cookie is host-scoped.
storage_state: .auth/preview.json
authed_check: '{role: "link", name: "User settings for"}'
refresh:
when: always
env: [LOREKIT_TESTING_USER_NAME, LOREKIT_TESTING_USER_PW, VERCEL_AUTOMATION_BYPASS_SECRET]
command: |
set -a
[ -f .env.local ] && . ./.env.local
[ -f packages/web/.env.local ] && . ./packages/web/.env.local
set +a
: "${PREVIEW_URL:?PREVIEW_URL is set by the runner to the resolved preview origin}"
AUTH_LOGIN_URL="$PREVIEW_URL/login" \
AUTH_STORAGE_STATE=./.auth/preview.json \
AUTH_POST_LOGIN_URL_PATTERN='/lore' \
AUTH_AUTHED_ROLE='link' \
AUTH_AUTHED_NAME='User settings for' \
node scripts/auth-bootstrap-credentials.mjs
timeout_seconds: 120
identity: { email: env:LOREKIT_TESTING_USER_NAME, role: user }
fixtures:
seed: null
48 changes: 48 additions & 0 deletions .agents/aw/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# LoreKit — repo facts for the verification suite

Read by `aw-verify`, `ui-verify`, `telemetry-verify`, `aw-tester` and `aw` on every run.
Values live in the sibling `environments.yml`, `ui.yml` and `telemetry.yml`; this file says what they cannot.

## Local stack

- Two modes, one URL (`http://localhost:3000`). `web` alone serves the dashboard against the hosted Supabase project in `.env.local`. When the diff touches `supabase/**` or `scripts/seed/**`, the `supabase` component starts first and `web` switches to it (`nx serve-local web` → `http://127.0.0.1:54321`). `AW_WEB_BACKEND=local|hosted` forces a mode.
- **The hosted backend is production** (`pqokxlhvnosogizsjztg`). A probe that creates, edits or deletes a record in `web`-only mode writes real production rows under the shared test account. Name every record a run creates with `aw-verify` or `ui-verify-adv` in its key or title and delete it in the same run. Run write-heavy ACs (Concurrency rows, bulk edits) in local-Supabase mode or on the preview.
- **The local Supabase stack runs on a developer machine only.** `supabase-local.sh` exits `78` with an `aw: local Supabase skipped (…)` line when no Docker daemon or Supabase CLI is reachable. That happens in every Agent0 / cloud sandbox (no Docker socket). Treat it as `skipped`, not a failure: carry on with `web`, and mark ACs that need the PR's backend code `skipped (supabase runs locally only — verify on the preview)`.
- An Agent0 sandbox also starts without `.env.local`, so `web` has no Supabase URL and authed pages fail. There, prefer `run preview`.
- `supabase-local.sh` seeds the test account into the local auth from the same `LOREKIT_TESTING_USER_*` variables, so the login is identical in both modes. Stopping its process group stops the functions server only; `pnpm nx stop supabase` stops the containers. Studio: `http://127.0.0.1:54323`.
- `pnpm --filter @lorekit/web dev` runs `vercel env pull` first and needs a linked Vercel CLI; the stack uses `pnpm nx serve web` instead.

## Login

- One shared test account: email `LOREKIT_TESTING_USER_NAME`, password `LOREKIT_TESTING_USER_PW` (from `.env.local` locally, the shell elsewhere). Never commit either value.
- `/login` opens on a provider-choice screen; the email form appears after "Continue with email". `scripts/auth-bootstrap-credentials.mjs` handles that and lands on `/lore`. Never script the GitHub OAuth path.
- Signed in = the sidebar link "User settings for <name>". It shows the account's display name and has no `data-testid` to mask in screenshots.
- Sessions are host-scoped: `.auth/local.json` never authenticates the preview, and the preview session is recaptured every run.

## Preview

- CI deploys the dashboard preview (`web-preview-deploy.yml`) only when web paths changed (`scripts/ci/web-preview-filter.mjs`). Comment `/web-preview` on the PR to force one.
- The URL is in the sticky `<!-- lorekit-web-preview` comment from `github-actions[bot]`: the stable alias `lorekit-pr-<n>-<vercel scope>.vercel.app` (scope `mads-thines-projects` unless a fork sets `VERCEL_SCOPE`). The GitHub deployments API holds only `main`'s staging (`preview` environment, ref `main`) and production deploys — never use them for a PR.
- Every preview request needs the `x-vercel-protection-bypass` header from `VERCEL_AUTOMATION_BYPASS_SECRET`; without it Vercel redirects to its SSO page before the app loads.
- An Agent0 thread in trusted-hosts network mode cannot reach `*.vercel.app` (connection reset, seen 2026-10-21). Preview runs from Agent0 need full network access.

## Data

- No package seed script; local `fixtures.seed` stays `null` and runs use the account's existing data.
- The preview Supabase project is seeded by `preview.yml` (`/preview --seed`) via `scripts/seed/seed-preview.mjs`: keys prefixed `preview-seed/`, tag `seed::preview`, org `preview-seed-org`. Locally: `node scripts/seed/seed-preview.mjs --psql postgresql://postgres:[email protected]:54322/postgres --user-email "$LOREKIT_TESTING_USER_NAME"` (needs `psql`). It refuses the production project.

## Telemetry

- Dash0 dev org (`app.dash0-dev.com`), dataset `default`. Use the Dash0 MCP server whose `listDatasets` contains it, and set its key as `mcp_server` in `~/.agents/aw/config.yml`.
- `service.namespace` is `lorekit` on every service in every environment, and production traffic sits in the same dataset and window. Separate a run's records by `deployment.environment.name` (`preview` on a PR preview, `local` or `development` locally) and, for web events, by the page host.
- `service.name`: `web` (browser RUM and the Next.js server), `api` (every Edge Function, told apart by `faas.name`), `cli`, `e2e`. Web events have been seen from `production` and `preview` only.
- Local export happens only when `.env.local` sets the `OTEL_*` variables; none arrived from local `web` in the 24 h before 2026-10-21. Without them, report `telemetry: skipped (no local export configured)` rather than a clean window.
- Attribute reference: `docs/otel.md`. Smoke and CI runs are tagged `deployment.environment.name=test`.

## Checks and commits

- Run `pnpm install --frozen-lockfile` before the first `pnpm nx` in a fresh checkout.
- Gate: `pnpm nx affected -t typecheck,test,lint`. Never `run-many … --all` in a sandbox — it stalls the box.
- Storybook interaction + visual tests: `cd packages/web && npx vitest run --config vitest.storybook.config.ts --changed=main` (use `npx`, not `pnpm exec`).
- Node's `fetch` ignores `HTTPS_PROXY` in a proxied sandbox; run Node scripts with `NODE_USE_ENV_PROXY=1`.
- Conventional Commits. A fix that changes what a user can do or see also updates the user-facing docs and regenerates `llms.txt` (CLAUDE.md → "User-facing docs"). PRs stay draft; undrafting is a human decision.
88 changes: 88 additions & 0 deletions .agents/aw/scripts/stack/supabase-local.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/bin/sh
# supabase-local.sh — aw-verify's `supabase` component. LOCAL MACHINE ONLY.
#
# 1. Refuses with exit 78 when no Docker daemon or Supabase CLI is reachable —
# the Agent0 / cloud sandbox has no Docker socket — or AW_LOCAL_SUPABASE=off.
# The run then carries on with `web` against the hosted backend.
# 2. `supabase start` (idempotent): Postgres, Auth, Studio on 54321-54324.
# 3. Seeds the LoreKit test account (LOREKIT_TESTING_USER_NAME / _PW, from the
# shell or .env.local) into the LOCAL auth, so the same login works here.
# 4. Serves the Edge Functions (`supabase functions serve`) and, once
# /functions/v1/health answers, marks the stack ready for web.sh.
#
# This process lives as long as the functions server. Stopping its process
# group stops the functions server and clears the marker; the containers keep
# running until `pnpm nx stop supabase`.
set -eu

ROOT=$(git rev-parse --show-toplevel)
cd "$ROOT"
API=http://127.0.0.1:54321
MARKER="$ROOT/.supabase/aw-local-mode" # .supabase/ is gitignored

refuse() {
echo "aw: local Supabase skipped ($1). It runs on a developer machine only, never in the Agent0 sandbox; verify supabase/** changes on the PR preview instead." >&2
exit 78
}

[ "${AW_LOCAL_SUPABASE:-auto}" = off ] && refuse "AW_LOCAL_SUPABASE=off"
command -v docker >/dev/null 2>&1 || refuse "no docker CLI"
docker info >/dev/null 2>&1 || refuse "no reachable Docker daemon"
command -v supabase >/dev/null 2>&1 || refuse "no Supabase CLI: https://supabase.com/docs/guides/cli"

# Another run's server is still up: reuse it rather than start a second one.
if [ -f "$MARKER" ] && kill -0 "$(cut -d' ' -f1 "$MARKER")" 2>/dev/null; then
echo "aw: local Supabase already served by pid $(cut -d' ' -f1 "$MARKER") — reusing it"
exit 0
fi

mkdir -p "$(dirname "$MARKER")"
echo "$$ booting" > "$MARKER"
FN_PID=
cleanup() {
rm -f "$MARKER"
[ -n "$FN_PID" ] && kill "$FN_PID" 2>/dev/null || true
}
trap cleanup EXIT
trap 'exit 143' INT TERM

supabase start

# Seed the test account. The subshell keeps .env.local's production SUPABASE_*
# values away from everything else; the seeder gets the local API explicitly.
SERVICE_KEY=$(supabase status -o json 2>/dev/null | node -e '
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
try { const j = JSON.parse(s); process.stdout.write(j.SERVICE_ROLE_KEY || j.SECRET_KEY || j.service_role_key || j.Secret || ""); }
catch { /* no key: the seeder below reports it */ }
});')
(
set -a
[ -f .env.local ] && . ./.env.local
[ -f packages/web/.env.local ] && . ./packages/web/.env.local
set +a
if [ -z "${LOREKIT_TESTING_USER_NAME:-}" ] || [ -z "${LOREKIT_TESTING_USER_PW:-}" ]; then
echo "aw: LOREKIT_TESTING_USER_NAME / LOREKIT_TESTING_USER_PW unset — test account not seeded; authed specs cannot sign in locally" >&2
exit 0
fi
SUPABASE_URL="$API" \
SUPABASE_SERVICE_ROLE_KEY="$SERVICE_KEY" \
SUPABASE_ANON_KEY= \
LOREKIT_SMOKE_EMAIL="$LOREKIT_TESTING_USER_NAME" \
LOREKIT_SMOKE_PASSWORD="$LOREKIT_TESTING_USER_PW" \
node scripts/smoke/seed-smoke-user.mjs
) || echo "aw: seeding the test account into the local stack failed — authed specs cannot sign in locally" >&2

supabase functions serve &
FN_PID=$!

i=0
until curl -fsS -o /dev/null --max-time 3 "$API/functions/v1/health"; do
kill -0 "$FN_PID" 2>/dev/null || { echo "aw: supabase functions serve exited before it was healthy" >&2; exit 1; }
i=$((i + 1))
[ "$i" -gt 90 ] && { echo "aw: edge functions not healthy after 180s" >&2; exit 1; }
sleep 2
done

echo "$$ ready" > "$MARKER"
echo "aw: local Supabase ready — API $API, Studio http://127.0.0.1:54323"
wait "$FN_PID"
49 changes: 49 additions & 0 deletions .agents/aw/scripts/stack/web.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/sh
# web.sh — aw-verify's `web` component: the dashboard on http://localhost:3000.
#
# Backend, decided once at start:
# local supabase-local.sh is running (its marker names a live pid) → wait for
# it to report ready, then `nx serve-local web`
# (NEXT_PUBLIC_USE_LOCAL_SUPABASE=true → http://127.0.0.1:54321).
# hosted otherwise → `nx serve web` with the Supabase project in .env.local,
# which is PRODUCTION (pqokxlhvnosogizsjztg). See .agents/aw/notes.md.
# AW_WEB_BACKEND=local|hosted forces one.
set -eu

ROOT=$(git rev-parse --show-toplevel)
cd "$ROOT"
MARKER="$ROOT/.supabase/aw-local-mode"

marker_live() {
[ -f "$MARKER" ] && kill -0 "$(cut -d' ' -f1 "$MARKER")" 2>/dev/null
}

mode=${AW_WEB_BACKEND:-auto}
if [ "$mode" = auto ]; then
mode=hosted
if marker_live; then
i=0
# supabase-local.sh may still be seeding or booting the functions server.
while marker_live && [ "$(cut -d' ' -f2 "$MARKER" 2>/dev/null)" != ready ]; do
i=$((i + 1))
[ "$i" -gt 120 ] && break
sleep 2
done
marker_live && [ "$(cut -d' ' -f2 "$MARKER")" = ready ] && mode=local
fi
fi

case "$mode" in
local)
echo "aw: web → local Supabase (http://127.0.0.1:54321)"
exec pnpm nx serve-local web
;;
hosted)
echo "aw: web → hosted Supabase from .env.local (production)"
exec pnpm nx serve web
;;
*)
echo "aw: AW_WEB_BACKEND must be local or hosted, got '$mode'" >&2
exit 64
;;
esac
20 changes: 20 additions & 0 deletions .agents/aw/telemetry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# .agents/aw/telemetry.yml — the telemetry witness (telemetry-verify). Committed.
# Personal values (your own local dataset, the Dash0 MCP server key) go in
# ~/.agents/aw/config.yml under repos.mthines/lorekit.
version: 1
backend: dash0-mcp
environments:
# Local export goes wherever your .env.local OTEL_* variables point; set the
# dataset in your personal overlay. Every LoreKit service shares this namespace.
local: { namespace: lorekit }
# PR previews (web on Vercel, api on the preview Supabase project) export to the
# shared org's `default` dataset with deployment.environment.name=preview.
preview: { dataset: default, namespace: lorekit }
web:
service_name: web # SERVICE_NAME in packages/web/src/lib/dash0-rum.ts
event_patterns:
# track({ name: 'lore.sort_changed', … }) — packages/web/src/lib/analytics/track.ts
- "track\\(\\{\\s*name:\\s*['\"]([^'\"]+)"
# export const AUTH_ATTEMPT_EVENT = 'auth.attempt' — packages/web/src/lib/auth-telemetry.ts
- "_EVENT\\s*=\\s*['\"]([a-z_]+\\.[a-z_.]+)"
ingest_wait_seconds: 45
19 changes: 19 additions & 0 deletions .agents/aw/ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# .agents/aw/ui.yml — the browser witness (ui-verify, aw-tester). Committed.
# Only keys that differ from the defaults are set; see the aw-verify skill's rules/config.md.
version: 1
# The default classifier misses packages/web/src/lib/** and middleware.ts (plain .ts
# outside a UI directory) and counts the app/api route handlers as UI.
surface:
include: ['packages/web/src/**']
exclude:
- 'packages/web/src/app/api/**'
- 'packages/web/src/mocks/**'
- '**/__screenshots__/**'
- '**/*.md'
adversarial:
# Mutations may reach the local Supabase stack. The hosted production project
# (pqokxlhvnosogizsjztg) is deliberately absent — see notes.md.
allowed_origins: ['http://127.0.0.1:54321']
# Added to the built-in destructive pattern: never touch the shared test
# account's credentials.
deny_actions: ['/reset (your )?password/i', '/change password/i']
50 changes: 0 additions & 50 deletions .claude/aw-targets/local.yml

This file was deleted.

Loading
Loading