bump: next 15.4.3 → 16.2.6 + ESLint flat config (v9 fallback) + typescript devDep #4
35 changed files with 3213 additions and 0 deletions
176
.agent-context-manifest.yml
Normal file
176
.agent-context-manifest.yml
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
# .agent-context-manifest.yml
|
||||||
|
#
|
||||||
|
# Generated by agent-pipeline bootstrap-agent-context skill.
|
||||||
|
# Tracks which artifacts the bootstrap installed in this repo, where they
|
||||||
|
# came from, and what pipeline version they correspond to.
|
||||||
|
#
|
||||||
|
# Read by the `sync-agent-context` skill to detect drift and propose updates.
|
||||||
|
# Don't edit by hand — use the bootstrap or sync skill in Cursor.
|
||||||
|
#
|
||||||
|
# Schema: https://github.com/varutasu/agent-pipeline/blob/main/docs/manifest-schema.md
|
||||||
|
|
||||||
|
schema_version: 1
|
||||||
|
pipeline_version: "0.5.0"
|
||||||
|
pipeline_source: "https://github.com/varutasu/agent-pipeline"
|
||||||
|
installed_at: "2026-05-22T22:25:00Z"
|
||||||
|
last_synced_at: "2026-05-22T22:25:00Z"
|
||||||
|
|
||||||
|
layers:
|
||||||
|
- L1
|
||||||
|
- L2
|
||||||
|
- L3
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - AGENTS.md is hand-curated per-repo — NOT tracked (always shows drift)
|
||||||
|
# - docs/SCHEMA_MAP.md is hand-curated per-repo — NOT tracked
|
||||||
|
# - .convoys/<slug>.md files are runtime outputs — NOT tracked
|
||||||
|
artifacts:
|
||||||
|
- path: ".convoys/README.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/convoys-readme.md.template"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:a48548cd3f5d0c40fc179106890661c3be5fcdc13eb705af7cfe9233e0b8b209"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-a11y-auditor.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-a11y-auditor.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:a59938deceb0246ebd7e477f1f9a442102f9fcbb81b0364f0ddc5f86e95a7930"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-architect.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-architect.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:269bd62af1557c5d353a9f95a613960e3434be4ec6e0c0b5f6b099adf6872044"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-conductor.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-conductor.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:bc75a3e6646217a015f7bb60c3610afd9b57ae91c7d2fc7a7971f4709b19368a"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-design-system-auditor.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-design-system-auditor.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:d214cecb1e8482fc24f2815c8220c860191f08526614f89cf9a5797e4ee9110a"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-doc-writer.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-doc-writer.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:d4e8bf8cee93153506b7b742848462422dbe5cc7fd012c62f6ffd50460e344d4"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-ia-architect.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-ia-architect.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:69685a3a407c4ee25e2606d426c3107d6b917abee80f907e16ade4a16b439839"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-implementer.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-implementer.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:b4f4d8596068679b90ffc3a2b6d2e1b6548caf8c68a50f7ed640ba8f638c1c4c"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-reviewer.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-reviewer.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:1ff38349321402a0ac2be37878dc2c0bcab62e54caf74c422b919aa6d75f9b67"
|
||||||
|
|
||||||
|
- path: ".cursor/agents/role-ux-reviewer.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L2-roles/role-ux-reviewer.md"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:3a1d4b66981f469b15e23a1cd34ab41352759966179e126b3d56ddc1eca4a03e"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/api-routes.mdc"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L1-context/api-routes.mdc.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:54cd66d71f5a129a67d0f4b1797f5f63b7f9aae3eeabe67217861456ff4db59b"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/auth-and-permissions.mdc"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:9b7eb7bea0cad0e43d0e9442eb8b660945cb6935f9f7c82fd7d7d71d66b39a2d"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/db-and-schema.mdc"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:83df2cf7121722a092f85165b1a93c755ce57e5361e0f6b2ecc74e9f928c5015"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/no-go-zones.mdc"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L1-context/no-go-zones.mdc"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:aa7046bc3e0266cb3c9b0eb0ef8f68cc50d6837f65c96861804ff81b9c4afa64"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/schema-map.mdc"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:3429bad56384117dc81873b337a6d815bd53799389f7908dedb53dbb7642bced"
|
||||||
|
|
||||||
|
- path: ".cursor/rules/ui-and-theming.mdc"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:b841ddda5baa47a76c3726a3c92b2c82a45120fdd461b3243bf970479d1cf1df"
|
||||||
|
|
||||||
|
- path: ".cursor/skills/add-api-route/SKILL.md"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:0e29f7e994a51e5a40b8308edab08ee9c1f713e297d68e98029294d8ca568cc7"
|
||||||
|
|
||||||
|
- path: ".cursor/skills/add-page/SKILL.md"
|
||||||
|
source: "tcg-vault-local"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:318912077a6ced6a3a31f85dc15d069bf7627c161b6735e3fa259ca10766daa9"
|
||||||
|
|
||||||
|
- path: ".github/CODEOWNERS"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/CODEOWNERS.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:b714a0a011776300abeab92fe8969f150c273c37d0d6b37c1ad2eb67d47decda"
|
||||||
|
|
||||||
|
- path: ".github/PULL_REQUEST_TEMPLATE.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/PULL_REQUEST_TEMPLATE.md.template"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:89863e58b9ec194aef1c94d3596e892467833e8bc880a28994acca401b6d9635"
|
||||||
|
|
||||||
|
- path: ".github/workflows/agent-context-drift.yml"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/agent-context-drift.yml.template"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:5505c296c1b61d023ee2aca222103097e2b5ed2e0e38da3679cc4f9754457785"
|
||||||
|
|
||||||
|
- path: ".github/workflows/ci.yml"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/ci.yml.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:6aff7a1c9f2e42606580c241b6dadca7c2d8550aeb959bd69fdd843eb9097cac"
|
||||||
|
|
||||||
|
- path: ".github/workflows/pr-health-rollup.yml"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/pr-health-rollup.yml.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:8747674323807d84395fa027b25e7e27881c5e6b0cc87a138d9cb3f78fd88956"
|
||||||
|
|
||||||
|
- path: ".github/workflows/preview-smoke.yml"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/preview-smoke.yml.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:2e71026b09db8b2f32b6a868d705489600c875082d6320c2369bf2f5ebc315b8"
|
||||||
|
|
||||||
|
- path: ".github/workflows/visual-diff.yml"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/visual-diff.yml.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:88270b1fa59aba99591ec094764dd367deed956bcb746ac6bb195241b3a7dae1"
|
||||||
|
|
||||||
|
- path: "docs/agent-context/README.md"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L1-context/agent-context-readme.md.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:095b9cc6a30327114c9ddfb4ff57a5fde76b213e12b1c5574a1f96205d60dbad"
|
||||||
|
|
||||||
|
- path: "lib/flags/index.js"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/flags-index.ts.template"
|
||||||
|
version: "0.5.0-local"
|
||||||
|
installed_hash: "sha256:1a3cd1f900194eaf4ec86588dd1c3c2bff6a565e742061fc911abdd47bd5f3a5"
|
||||||
|
|
||||||
|
- path: "scripts/log-convoy-event.sh"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/log-convoy-event.sh"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:cd0413691066a177b6b4e6164a9a0978c20a853ad60222ae833b5d53b255818d"
|
||||||
|
|
||||||
|
- path: "scripts/wt.sh"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/wt.sh"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:2a4f44a159f80a8ea6fe53ac507c01a2f91a4e2118d997a98b051808ac35e9a5"
|
||||||
|
|
||||||
|
- path: "tests/smoke/app.smoke.spec.ts"
|
||||||
|
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/playwright-smoke.spec.ts.template"
|
||||||
|
version: "0.5.0"
|
||||||
|
installed_hash: "sha256:a62c10edb712a61f1cfece43705bfff75a5a66ad6bc8b53f7e69a43c3efb962c"
|
||||||
121
.convoys/README.md
Normal file
121
.convoys/README.md
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Convoys
|
||||||
|
|
||||||
|
A **convoy** is a multi-PR work-stream coordinated by an agent pipeline. One convoy = one feature, bug fix, or epic. Each convoy is a Markdown file in this directory plus an optional sub-directory of implementer briefs.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
.convoys/
|
||||||
|
├── README.md (this file)
|
||||||
|
├── <slug>.md (the convoy file — written by role-conductor)
|
||||||
|
└── <slug>/
|
||||||
|
├── brief-1-<kebab-title>.md (written by role-architect)
|
||||||
|
├── brief-2-<kebab-title>.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convoy file format
|
||||||
|
|
||||||
|
Frontmatter (set by `role-conductor`, then appended-to by other roles):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: <kebab-slug>
|
||||||
|
classification: feature | hotfix | docs-only | infra-only | server-only | config-only
|
||||||
|
success_metric: <one sentence>
|
||||||
|
skip:
|
||||||
|
- <flag1>
|
||||||
|
status: open | in-progress | merged | shipped | abandoned
|
||||||
|
created: <YYYY-MM-DD>
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Body sections (added in order by the pipeline roles):
|
||||||
|
|
||||||
|
1. `## Why` (Conductor)
|
||||||
|
2. `## Scope` (Conductor)
|
||||||
|
3. `## Roles invoked` (Conductor)
|
||||||
|
4. `## Todos` (Conductor → refined by Architect)
|
||||||
|
5. `## IA` (IA Architect)
|
||||||
|
6. `## UX` (UX Reviewer)
|
||||||
|
7. `## Architecture` (Architect)
|
||||||
|
|
||||||
|
After Architect, briefs live in `.convoys/<slug>/brief-N-*.md`. Implementers read only their brief, not the whole convoy.
|
||||||
|
|
||||||
|
## Skip flags
|
||||||
|
|
||||||
|
The Conductor sets `skip:` based on classification. These flags map to pipeline stages that no-op when set:
|
||||||
|
|
||||||
|
| Flag | Skips |
|
||||||
|
| --- | --- |
|
||||||
|
| `ia` | IA Architect |
|
||||||
|
| `ux` | UX Reviewer |
|
||||||
|
| `arch` | Architect |
|
||||||
|
| `test` | Component tests |
|
||||||
|
| `review` | Reviewer |
|
||||||
|
| `visual` | Visual diff |
|
||||||
|
| `a11y` | A11y auditor |
|
||||||
|
| `design` | Design-system auditor |
|
||||||
|
| `smoke` | Staging smoke |
|
||||||
|
| `qa` | Manual QA |
|
||||||
|
| `docs` | Doc Writer |
|
||||||
|
| `flag` | Flag rollout |
|
||||||
|
|
||||||
|
Never skipped (mandatory human gates): `plan-approval`, `pr-merge`, `prod-promote`.
|
||||||
|
|
||||||
|
## Status lifecycle
|
||||||
|
|
||||||
|
- `open` — Conductor created the convoy; no work started.
|
||||||
|
- `in-progress` — At least one brief has an open or merged PR.
|
||||||
|
- `merged` — All briefs merged to umbrella; release PR to develop pending.
|
||||||
|
- `shipped` — Release to main complete; flag rollout (if any) underway.
|
||||||
|
- `abandoned` — Convoy closed without shipping; reason in convoy body.
|
||||||
|
|
||||||
|
Update status by editing the convoy frontmatter as you progress.
|
||||||
|
|
||||||
|
## Adding a new convoy
|
||||||
|
|
||||||
|
1. Open Cursor in this repo.
|
||||||
|
2. Prompt: *"Start a new convoy: <one-paragraph idea>. Success = <metric>."*
|
||||||
|
3. The `role-conductor` subagent writes `.convoys/<slug>.md`.
|
||||||
|
4. Run subsequent roles in order per the convoy's `Roles invoked` list.
|
||||||
|
|
||||||
|
See `.cursor/agents/role-conductor.md` for the Conductor's full spec.
|
||||||
|
|
||||||
|
## Multitask + worktrees (Cursor 3.2+)
|
||||||
|
|
||||||
|
[Cursor 3.2 (Apr 24, 2026)](https://cursor.com/changelog/04-24-26) added `/multitask` async subagents and native worktree management in the Agents Window. The pipeline uses both:
|
||||||
|
|
||||||
|
**Audit fan-out** — after an implementer ships a PR draft:
|
||||||
|
|
||||||
|
```
|
||||||
|
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
|
||||||
|
```
|
||||||
|
|
||||||
|
All three read the same diff and emit independent comments. Use group id `audit-<convoy>-<pr>` so analytics can compute wall-clock savings.
|
||||||
|
|
||||||
|
**Implementer fleet** — after architect's plan is approved (gate 1), if `slice_dependencies:` declares parallel-safe briefs (`depends_on: []`, disjoint `files:`):
|
||||||
|
|
||||||
|
```
|
||||||
|
/multitask role-implementer briefs 1, 2, 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Use Cursor's Agents Window to create a worktree per brief — one click each. The legacy `scripts/wt.sh` is now a deprecation stub.
|
||||||
|
|
||||||
|
See the [multitask playbook](https://github.com/varutasu/agent-pipeline/blob/main/docs/multitask-playbook.md) for the full guardrail set.
|
||||||
|
|
||||||
|
## Self-analytics
|
||||||
|
|
||||||
|
Each L2 role appends one event to `.convoys/.metrics.jsonl` via `scripts/log-convoy-event.sh`. The file is gitignored by default — events stay local. To opt-in to commit team-shared metrics, remove `.convoys/.metrics.jsonl` from `.gitignore`.
|
||||||
|
|
||||||
|
Aggregate across repos and render a dashboard with the [agent-pipeline analytics scripts](https://github.com/varutasu/agent-pipeline/tree/main/analytics):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/code/agent-pipeline/analytics
|
||||||
|
npx tsx analyze-convoys.ts <repo-path> [<repo-path>...]
|
||||||
|
npx tsx render-dashboard.ts
|
||||||
|
open ~/agent-pipeline-data/dashboard.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Schema: [`analytics/schemas/convoy-event.json`](https://github.com/varutasu/agent-pipeline/blob/main/analytics/schemas/convoy-event.json).
|
||||||
|
|
||||||
232
.convoys/ship-readiness.md
Normal file
232
.convoys/ship-readiness.md
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
---
|
||||||
|
name: ship-readiness
|
||||||
|
classification: epic
|
||||||
|
success_metric: tcg-vault is safe to expose to anonymous internet traffic with a documented launch checklist green
|
||||||
|
skip: []
|
||||||
|
status: open
|
||||||
|
created: 2026-05-22
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ship-readiness convoy
|
||||||
|
|
||||||
|
Umbrella convoy capturing the full agent-pipeline review of tcg-vault as of 2026-05-22. Findings are grouped by L2 role lens (Reviewer / Architect / Design-system / A11y / IA / Doc-writer) and severity. Each item points to the convoy that will execute the fix.
|
||||||
|
|
||||||
|
Code graph: 122 files, 628 nodes, 5602 edges, 11 communities. Indexed by `user-code-review-graph` MCP.
|
||||||
|
|
||||||
|
## P0 — ship-blockers (security)
|
||||||
|
|
||||||
|
These MUST land before any anonymous traffic touches the production URL.
|
||||||
|
|
||||||
|
### 1. `getUserFromRequest` returns a hardcoded admin when no Bearer token is present
|
||||||
|
|
||||||
|
- **File:** `lib/permission-middleware.js` lines 13-17.
|
||||||
|
- **Impact:** Every API route that calls `getUserFromRequest` (30+ handlers — see `user-code-review-graph` cross-community edges from `api-handler` → `lib-admin`) accepts unauthenticated requests as admin user 1.
|
||||||
|
- **Repro:** `curl https://<host>/api/collections` with no `Authorization` header returns admin's collections.
|
||||||
|
- **Fix:** Delete lines 13-17. Return `null` when no Bearer token. Update every caller to handle `null` properly (most already do; the broken fallback was masking the right path).
|
||||||
|
- **Owns:** `role-architect` + `role-implementer` (one PR; small surface area in the helper, callers already check `!user`).
|
||||||
|
|
||||||
|
### 2. JWT_SECRET hardcoded fallback in 7 files
|
||||||
|
|
||||||
|
- **Files:**
|
||||||
|
- `pages/api/auth-utils.js` (`'your-secret-key'`)
|
||||||
|
- `pages/api/auth/login.js`, `pages/api/auth/register.js`, `pages/api/auth/verify.js`
|
||||||
|
- `pages/api/favorites.js`, `pages/api/users/search.js`
|
||||||
|
- `lib/permission-middleware.js`
|
||||||
|
- **Impact:** If `JWT_SECRET` env var is unset (e.g. preview/staging misconfig), tokens are signed with `'your-secret-key-change-in-production'` — an attacker can sign their own admin token in 5 seconds.
|
||||||
|
- **Fix:** Centralize JWT_SECRET access in one helper that `throw`s at module load if `process.env.JWT_SECRET` is unset. Every other file imports from there.
|
||||||
|
- **Bonus:** Token expiry is inconsistent (`/api/auth/login.js` uses 24h, `pages/api/auth-utils.js` uses 7d). Pick one.
|
||||||
|
- **Owns:** `role-architect` + `role-implementer`.
|
||||||
|
|
||||||
|
### 3. Default admin credentials in seed + README
|
||||||
|
|
||||||
|
- **Files:**
|
||||||
|
- `scripts/setup-neon-db.js` lines 130-138 — creates `admin@tcgvault.com` / `admin123`
|
||||||
|
- `README.md` documents the credentials
|
||||||
|
- `pages/api/setup-database.js` — duplicates the setup AND is an UNAUTHENTICATED public POST endpoint with `Access-Control-Allow-Origin: *`
|
||||||
|
- **Impact:** Anyone who hits `/api/setup-database` can re-trigger DDL. The `admin123` password is one Google away from public knowledge.
|
||||||
|
- **Fix:**
|
||||||
|
1. Delete `pages/api/setup-database.js`. Schema setup is a one-time job; it should not be a route.
|
||||||
|
2. Change `setup-neon-db.js` to require a `ADMIN_INITIAL_PASSWORD` env var (no default).
|
||||||
|
3. Strip the admin password from README — replace with "run `npm run setup-db` and follow the prompt".
|
||||||
|
- **Owns:** `role-implementer`.
|
||||||
|
|
||||||
|
### 4. Dev-only test endpoints shipped to production
|
||||||
|
|
||||||
|
- **Files:** `pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, `pages/api/setup-database.js`.
|
||||||
|
- **Impact:** Unknown — depends on what they expose. `/api/test-db` likely returns the DB connection string; `/api/test-auth` may leak token-handling details.
|
||||||
|
- **Fix:** Delete all four. Add a CI grep that fails the build if any file matching `pages/api/(test-|simple|setup-)*.js` exists.
|
||||||
|
- **Owns:** `role-implementer`.
|
||||||
|
|
||||||
|
### 5. CORS `Access-Control-Allow-Origin: *` on auth endpoints
|
||||||
|
|
||||||
|
- **Files:** at minimum `pages/api/auth/login.js`, `pages/api/auth/register.js`, `pages/api/setup-database.js` (verify others).
|
||||||
|
- **Impact:** Any origin can submit credentials. Combined with the no-rate-limit problem below, credential stuffing is wide open.
|
||||||
|
- **Fix:** Set `Access-Control-Allow-Origin` to the literal frontend origin (`https://tcgvault.com` / preview domain), or remove the header entirely if the API and the frontend are same-origin (they are, on Vercel).
|
||||||
|
- **Owns:** `role-implementer`.
|
||||||
|
|
||||||
|
### 6. No rate limiting anywhere
|
||||||
|
|
||||||
|
- **Impact:** Login endpoint accepts unlimited attempts; card-search endpoint can be hammered; image upload endpoints can be exhausted. The `pages/api/cards/import-*.js` endpoints externally hit Scryfall/Pokémon APIs with no caller throttling.
|
||||||
|
- **Fix:** Adopt `@upstash/ratelimit` (free tier covers a small launch) or Vercel's built-in middleware-based rate limiting. Apply to: `/api/auth/login`, `/api/auth/register`, `/api/users/search`, `/api/cards/search`, all `/api/cards/import-*`, and `/api/user/avatar*` (upload).
|
||||||
|
- **Owns:** `role-architect` (pattern) → `role-implementer` (per-route).
|
||||||
|
|
||||||
|
### 7. Layout default-prop leaks maintainer email
|
||||||
|
|
||||||
|
- **File:** `components/Layout.js` line 562: `function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, ... })`.
|
||||||
|
- **Impact:** Any page that renders Layout without passing a `user` prop displays your real email and impersonates you as the logged-in user.
|
||||||
|
- **Fix:** Default `user = null` and render a logged-out state branch. Verify every page passes `user` explicitly (the graph shows ~13 pages call `Layout`; audit each).
|
||||||
|
- **Owns:** `role-implementer`.
|
||||||
|
|
||||||
|
## P1 — pre-launch quality bar
|
||||||
|
|
||||||
|
### 8. Two SQL clients in parallel (`@neondatabase/serverless` + `@vercel/postgres`)
|
||||||
|
|
||||||
|
- **Impact:** Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus `lib/database.js`'s manual interpolation + `sql.unsafe(query)` is a SQL-injection vector if any caller passes user input through.
|
||||||
|
- **Fix:** Pick `@vercel/postgres` (tagged-template, no injection vector). Migrate every call site of `lib/database.js::db.query`. Delete `lib/database.js`.
|
||||||
|
- **Reviewer/Architect call:** small enough to fit in one convoy; touches ~3 files based on graph.
|
||||||
|
|
||||||
|
### 9. Three parallel client-side auth implementations
|
||||||
|
|
||||||
|
- **Files:** `lib/auth-context.js` (`AuthProvider` / `useAuth`), `lib/admin-auth.js` (`AdminProvider` / `useAdmin` / `useIsAdmin`), `lib/use-auth.js` (`useAuth`).
|
||||||
|
- **Impact:** Pages randomly import from one of three places. State is duplicated. Logout in one provider doesn't necessarily clear the others. Token-verify roundtrips happen 3× on initial page load if all three providers mount.
|
||||||
|
- **Fix:** Collapse to `lib/use-auth.js` as the canonical hook. Migrate every importer. Delete `auth-context.js` and `admin-auth.js`. Roll up `useIsAdmin` semantics into `useAuth().user?.role === 'admin'`.
|
||||||
|
- **Owns:** `role-architect` (decision) → `role-implementer` (per-page migration; ~30 importers).
|
||||||
|
|
||||||
|
### 10. No tests
|
||||||
|
|
||||||
|
- **Impact:** The first agent-driven refactor of `getUserFromRequest` (P0 #1) is high-blast-radius with no safety net.
|
||||||
|
- **Fix sequence:**
|
||||||
|
1. Install `vitest`. Add `npm run test:run` script.
|
||||||
|
2. Install `@playwright/test`. Wire up `tests/smoke/app.smoke.spec.ts` (already drafted; needs `playwright.config.ts`).
|
||||||
|
3. Re-enable the `test:` job in `.github/workflows/ci.yml` (commented out at install time).
|
||||||
|
4. Add unit tests for `lib/permission-middleware.js`, `lib/slug-utils.js`, `pages/api/auth-utils.js`.
|
||||||
|
5. Wire `preview-smoke.yml` to run against the Vercel preview URL.
|
||||||
|
- **Owns:** `role-architect` (test strategy) → `role-implementer` (initial suite).
|
||||||
|
|
||||||
|
### 11. No migration tool — `scripts/add-*.js` graveyard
|
||||||
|
|
||||||
|
- **Files:** 27 scripts in `scripts/` of the form `add-foo-column.js`, `fix-bar-constraint.js`, `seed-baz.js`. No idempotency tracking, no `schema_migrations` table, no rollback.
|
||||||
|
- **Impact:** Onboarding a new env requires re-running every script in the right order. No way to know what's been run on a given Neon branch. Every new column is at risk of being missed in prod.
|
||||||
|
- **Fix:** Adopt `node-pg-migrate` (lightweight, matches the existing pattern best) OR migrate to `drizzle-kit` if the team wants schema-as-code. Backfill a single "initial" migration matching current prod schema. From there, every new column ships as a migration file.
|
||||||
|
- **Owns:** `role-architect` (tool selection) → `role-implementer` (backfill + first new migration).
|
||||||
|
|
||||||
|
### 12. Branding mismatch — "TCG Vault" vs. "Deck Hearth"
|
||||||
|
|
||||||
|
- **Files:** README, `package.json`, seed data say "TCG Vault" / `admin@tcgvault.com`. `components/Layout.js` lines 596 + 689 render "Deck Hearth" + "DH" logo. The `.env.local` template, `vercel.json`, and Vercel project name should also be audited.
|
||||||
|
- **Impact:** Confusing for users. Confusing for marketing. Confusing for analytics. Pick one.
|
||||||
|
- **Fix:** Brand workshop → final name → global replace → update README, package.json `"name"`, every UI string, Vercel project name, email sender, support pages. Schedule a redirect from the old domain.
|
||||||
|
- **Owns:** `role-ia-architect` (which name? — needs human decision) → `role-implementer`.
|
||||||
|
|
||||||
|
## P2 — refactor priorities
|
||||||
|
|
||||||
|
### 13. God components (10 files over 500 lines)
|
||||||
|
|
||||||
|
| File | Lines | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pages/cards.js` | 1499 | `AuthenticatedCards` (886) + `Card3D` (502) live in one file. Split into `pages/cards/index.js` + `components/Card3D.js`. |
|
||||||
|
| `pages/collection/[identifier].js` | 1044 | `CollectionView` is one mega-component. Extract: header, card-grid, share-modal-wrapper, edit-form. |
|
||||||
|
| `pages/collections.js` | 989 | Similar structure to collection/[identifier]. Possibly share extracted pieces. |
|
||||||
|
| `pages/card/[id].js` | 913 | `CardDetail` — split into header, owned-badge, add-to-collection-flow. |
|
||||||
|
| `pages/deck-builder.js` | 823 | `DeckBuilder` — extract card-search, deck-list, mana-curve panels. |
|
||||||
|
| `components/CameraScanner.js` | 817 | Camera + AI-OCR + detection-loop — extract the detection loop into a hook. |
|
||||||
|
| `pages/admin/card-editor.js` | 778 | Form heavy. Use a `useFormState` pattern + separate the search-results subview. |
|
||||||
|
| `pages/scanner.js` | 776 | Mirror of CameraScanner concerns plus queue management. |
|
||||||
|
| `pages/settings.js` | 669 | One screen per settings section is the usual fix. |
|
||||||
|
| `pages/profile.js` | 625 | Avatar generation logic alone is ~150 lines — extract `useGeneratedAvatar` hook. |
|
||||||
|
|
||||||
|
Each is one convoy of its own. Use the architect role's `slice_dependencies:` to fan out implementers safely.
|
||||||
|
|
||||||
|
### 14. Schema-design smells (documented in `docs/SCHEMA_MAP.md`)
|
||||||
|
|
||||||
|
- `users` has two avatar columns (`profile_image_url` + `avatar_url`). Reconcile.
|
||||||
|
- `collections` has two visibility flags (`is_public BOOLEAN` + `visibility VARCHAR`). Reconcile.
|
||||||
|
- `cards.quantity` + `cards.favorited` are unused (they belong on `user_cards` / `user_favorites`). Drop.
|
||||||
|
- `user_settings` table duplicates several `users` columns. Reconcile.
|
||||||
|
- All enum-shaped VARCHARs (`role`, `condition`, `theme`, `game`, `visibility`) should be CHECK-constrained or proper Postgres ENUMs.
|
||||||
|
- `collections.tags` is `TEXT` (comma-separated). Migrate to `JSONB` or a join table.
|
||||||
|
|
||||||
|
### 15. Component coupling warning from graph
|
||||||
|
|
||||||
|
`user-code-review-graph` flagged:
|
||||||
|
- High coupling (44 edges) between `components-handle` and `pages-handle` (largely `Layout`, `CardItem`, `ManaCost` — expected for a shared UI surface).
|
||||||
|
- High coupling (34 edges) between `lib-admin` and `api-handler` — almost all via `getUserFromRequest`. After P0 #1 is fixed, this number stays high because the auth check is genuinely shared — that's fine.
|
||||||
|
|
||||||
|
### 16. Lots of inline SVG and emoji
|
||||||
|
|
||||||
|
The `getIcon` registry in `Layout.js` and `MobileNavigation.js` redefines the same SVG paths. Extract to `components/icons/` with named exports. Then audit the codebase for inline SVG that should be a named import. Bonus: lazy-load the larger icon families.
|
||||||
|
|
||||||
|
## P3 — UX, IA, design-system
|
||||||
|
|
||||||
|
### Role-ia-architect findings
|
||||||
|
|
||||||
|
- **URL structure** — solid. `/cards`, `/collections`, `/collection/[slug]`, `/deck-builder`, `/community/collections`. Coherent. One quirk: `/card/[id]` (singular) for detail vs. `/cards` (plural) for index — typical Next.js shape but worth a redirect rule so `/cards/[id]` also resolves.
|
||||||
|
- **Logged-out homepage** — current `pages/index.js` is 316 lines; needs an editorial pass. What's the value prop in one sentence? Right now it's mostly "we have cards".
|
||||||
|
- **Onboarding** — signup → profile setup → first collection → scan-or-import card. Currently each step is a separate page. Consider a multi-step wizard at `/onboarding` to keep the new user in flow.
|
||||||
|
- **Discoverability** — `/community/decks` and `/community/forums` are in the nav but flagged as placeholders. Either ship the MVP for each before launch (forums likely too big) or hide the nav items until they exist.
|
||||||
|
|
||||||
|
### Role-ux-reviewer findings
|
||||||
|
|
||||||
|
- **Loading states** — most data fetches set `loading: true` then re-render; very few show skeletons. Card grids should use shimmer placeholders; modals should disable submit while in flight.
|
||||||
|
- **Error states** — error messages bubble to `console.error` and toast nothing. Add a global toast system (e.g. `sonner`) and wire every catch block.
|
||||||
|
- **Empty states** — `/my-cards` and `/collections` when empty drop to "no cards yet". Replace with first-time CTA: "Scan your first card" or "Browse popular sets".
|
||||||
|
- **Mobile drawer** — `MobileNavigation` is solid (recent commit `442e906`). One thing: the bottom-bar's active state contrast looks low in light mode; verify against AA.
|
||||||
|
- **Camera scanner UX** — 817 lines of detection loop. Add a one-line "scanning…" status under the viewfinder and a single "captured N cards" badge. The current toolbar is busy.
|
||||||
|
|
||||||
|
### Role-design-system-auditor findings
|
||||||
|
|
||||||
|
- **Two visual languages mixing** — Tailwind classes AND CSS variables on the same elements. This is documented in `.cursor/rules/ui-and-theming.mdc`; the cleanup is to define which property goes where and enforce.
|
||||||
|
- **Hardcoded hex colors** — grep for `bg-\[#` and `style={{ backgroundColor: '#`. There are still a handful; convert to theme tokens.
|
||||||
|
- **Logo + brand** — see P1 #12. Then once the name is settled, the "DH" logo + AnimatedFireLogo need to be unified into one brand mark.
|
||||||
|
- **Modal patterns** — `CollectionSelectionModal`, `ShareModal`, `UploadImageModal` each have their own backdrop + focus-trap implementation. Extract `<Modal>` primitive. Use `headlessui` or `radix-ui`'s Dialog to get focus management for free.
|
||||||
|
- **Card grid spacing + density** — `pages/cards.js` (the 1499-line monster) does responsive grid math inline. Extract a `<CardGrid>` component that handles density (compact / comfortable / spacious) + sort + filter chrome.
|
||||||
|
|
||||||
|
### Role-a11y-auditor findings
|
||||||
|
|
||||||
|
- **Focus traps in modals** — none of the modals trap focus. Tab through `ShareModal` and you leave to the background. Critical for keyboard users + screen readers.
|
||||||
|
- **ESC to close modals** — inconsistent. Some have it, some don't.
|
||||||
|
- **Skip-to-content** — no `<a href="#main" class="sr-only focus:not-sr-only">`. Add to `_app.js`.
|
||||||
|
- **Image alts** — card images use `alt={card.name}` (good); avatar images sometimes have empty alts. Audit.
|
||||||
|
- **Color contrast** — verify the muted text colors (`var(--text-secondary)`) hit AA on both themes. The mobile bottom-bar inactive state is a likely fail.
|
||||||
|
- **Form errors** — login/signup form errors are visually red but not connected to inputs via `aria-describedby`. Screen readers don't know which field failed.
|
||||||
|
- **Keyboard ops on non-button elements** — most clickable `<div>`s already have `onKeyDown` but a few don't (audit with `rg "onClick" components pages | rg -v "<button"`).
|
||||||
|
|
||||||
|
### Role-doc-writer findings
|
||||||
|
|
||||||
|
- **README** — needs a public-facing rewrite. Currently mixes user docs + dev setup + admin credentials. Split into `README.md` (project landing) + `docs/DEVELOPMENT.md` (dev setup) + delete the admin credentials section entirely.
|
||||||
|
- **`docs/SCHEMA_MAP.md`** — installed at bootstrap (this convoy). Keep it fresh on every schema change.
|
||||||
|
- **CHANGELOG** — none yet. Adopt Keep-a-Changelog format. Backfill `[0.1.0] — initial private alpha` covering everything to date.
|
||||||
|
- **`TESTING_GUIDE.md`** — currently the only test doc; rename to `docs/MANUAL_QA.md` once `vitest` + `playwright` land.
|
||||||
|
- **`docs/API_REFERENCE.md`** — would help. Could be auto-generated by walking `pages/api/**/*.js` and extracting JSDoc; or hand-curated to start.
|
||||||
|
- **Privacy policy / Terms of service** — required before public launch. Use a template (Termly / Iubenda) and customize.
|
||||||
|
|
||||||
|
## Proposed launch sequence
|
||||||
|
|
||||||
|
Each phase is one Conductor-created convoy. Don't run more than two in parallel until tests exist.
|
||||||
|
|
||||||
|
1. **`fix-auth-bypass`** (P0 #1, #2, #4, #5, #6 partial). One PR. Highest risk; needs human review.
|
||||||
|
2. **`drop-public-setup`** (P0 #3, #4). One PR. Trivial; do as a hotfix.
|
||||||
|
3. **`fix-layout-default-user`** (P0 #7). One PR. Trivial.
|
||||||
|
4. **`add-rate-limiting`** (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes.
|
||||||
|
5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs.
|
||||||
|
6. **`adopt-vitest`** (P1 #10 step 1). One PR. Enables testing every future change.
|
||||||
|
7. **`migration-tool`** (P1 #11). One PR. Backfill + first new migration.
|
||||||
|
8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written.
|
||||||
|
9. **`single-auth-provider`** (P1 #9). 3-5 PRs via multitask.
|
||||||
|
10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR.
|
||||||
|
11. **`schema-cleanup`** (P2 #14). Multi-PR convoy via multitask.
|
||||||
|
12. **`god-component-split`** (P2 #13). One convoy per file; fan out via multitask once architect's `slice_dependencies` are written.
|
||||||
|
13. **`launch-polish`** (P3). UX/IA/a11y/docs convoy.
|
||||||
|
|
||||||
|
Total: ~13 convoys to get from current state to public-launch-ready. Estimate 4-8 weeks at one human-in-the-loop reviewer per convoy. Multitask + Cursor 3.2 worktrees compress steps 8-12 substantially.
|
||||||
|
|
||||||
|
## Self-analytics
|
||||||
|
|
||||||
|
After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules.
|
||||||
|
|
||||||
|
## How to start
|
||||||
|
|
||||||
|
Per `.cursor/agents/role-conductor.md`, start the next convoy with:
|
||||||
|
|
||||||
|
> *"Run role-conductor: start a new convoy `fix-auth-bypass` to address P0 #1, #2, #4, #5, #6 partial in `.convoys/ship-readiness.md`. Success = `getUserFromRequest` returns null for missing tokens; no API route accepts unauthenticated requests; CI green."*
|
||||||
|
|
||||||
|
The Conductor will set classification, skip flags, and hand off to subsequent roles.
|
||||||
105
.cursor/agents/role-a11y-auditor.md
Normal file
105
.cursor/agents/role-a11y-auditor.md
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
name: role-a11y-auditor
|
||||||
|
description: >-
|
||||||
|
Accessibility audit on a UI diff. Checks for missing labels, keyboard
|
||||||
|
navigation, focus management, color contrast, semantic HTML, and ARIA
|
||||||
|
correctness. Read-only. Use after the implementer's PR draft on PRs that
|
||||||
|
touch UI files. Does not require a browser MCP — works from the diff +
|
||||||
|
static analysis. Safe to run in parallel with role-reviewer +
|
||||||
|
role-design-system-auditor via Cursor 3.2 /multitask.
|
||||||
|
multitask: audit-fanout
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: A11y Auditor
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After `role-design-system-auditor` on UI-touching PRs. Skip when convoy frontmatter has `skip: a11y`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The PR diff (UI files only).
|
||||||
|
- The convoy's UX section (which already lists a11y constraints — verify the implementer satisfied them).
|
||||||
|
- Existing accessible patterns in the repo (look at existing `Dialog`, `Form`, `Button` primitives).
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
A structured comment for the PR Health rollup:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## A11y Audit
|
||||||
|
|
||||||
|
| Check | Status | Count |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Labels | ✅ / ❌ | <N> |
|
||||||
|
| Keyboard nav | ✅ / ❌ | <N> |
|
||||||
|
| Focus management | ✅ / ❌ | <N> |
|
||||||
|
| Color contrast | ✅ / ⚠️ | <N> |
|
||||||
|
| Semantic HTML | ✅ / ❌ | <N> |
|
||||||
|
| ARIA correctness | ✅ / ⚠️ | <N> |
|
||||||
|
| UX constraint match | ✅ / ❌ | <N> |
|
||||||
|
|
||||||
|
### Critical (must fix)
|
||||||
|
- <file:line> — <issue> — <fix>
|
||||||
|
...
|
||||||
|
|
||||||
|
### Warnings (recommended)
|
||||||
|
- <file:line> — <issue> — <fix>
|
||||||
|
...
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checklist (apply per file)
|
||||||
|
|
||||||
|
1. **Labels**: every `<input>`, `<select>`, `<textarea>`, `<button>` has either visible text, `aria-label`, or an associated `<label htmlFor=...>`.
|
||||||
|
2. **Icon-only buttons**: have `aria-label` or visually-hidden text.
|
||||||
|
3. **Keyboard navigation**: any `onClick` on a non-button/anchor element has `onKeyDown` (Enter + Space) and `tabIndex={0}` and `role="button"` (or be a real button).
|
||||||
|
4. **Focus management**: dialogs trap focus; modals return focus on close; route changes move focus to the heading.
|
||||||
|
5. **Color contrast**: text on backgrounds meets 4.5:1 (large text 3:1). Hardcoded colors that we can't measure → ⚠️.
|
||||||
|
6. **Semantic HTML**: use `<button>` not `<div onClick>`, `<nav>` for navigation, `<main>` for primary content, heading hierarchy `<h1>` → `<h2>` → `<h3>` (no skipping).
|
||||||
|
7. **ARIA correctness**: `aria-expanded` on toggles, `aria-current="page"` on active nav items, `aria-live` on async-updating regions, `role="alert"` on error messages.
|
||||||
|
8. **UX constraint match**: cross-reference the UX section's a11y constraints — did the implementer satisfy each one?
|
||||||
|
|
||||||
|
## Severity
|
||||||
|
|
||||||
|
- **Critical**: missing labels on form inputs, no keyboard handler on click-only div, missing focus trap on modal, missing alt text on informative images.
|
||||||
|
- **Warning**: heading hierarchy skip, missing `aria-current`, color-contrast that requires runtime measurement, missing live region on async updates.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Get UI diff.
|
||||||
|
2. Read the convoy's UX section once to know what was promised.
|
||||||
|
3. For each changed UI file: read the current state of the file (post-diff), then walk the checklist.
|
||||||
|
4. Build the comment. Cap at 8 critical + 8 warnings.
|
||||||
|
5. If clean: ✅ across the board with a one-line note.
|
||||||
|
|
||||||
|
## What this role does NOT do
|
||||||
|
|
||||||
|
- Run axe-core in a browser (that's a CI job, see `.github/workflows/preview-smoke.yml` if present).
|
||||||
|
- Test screen readers manually — beyond static analysis scope.
|
||||||
|
- Audit non-UI changes — server / API / config diffs are out of scope.
|
||||||
|
|
||||||
|
## Multitask (audit fan-out)
|
||||||
|
|
||||||
|
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — none modify code or the convoy. Safe to run in parallel via Cursor 3.2 `/multitask`.
|
||||||
|
|
||||||
|
When invoked as part of a cohort, pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After publishing the audit comment, emit one event:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-a11y-auditor convoy=<slug> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Demanding ARIA on already-semantic HTML (e.g. `aria-label` on a `<button>` that has visible text) → wrong, that's redundant.
|
||||||
|
- Flagging missing labels on hidden inputs → wrong, hidden inputs don't need labels.
|
||||||
|
- Vague feedback ("improve a11y") → wrong, every finding needs a file:line and a specific fix.
|
||||||
199
.cursor/agents/role-architect.md
Normal file
199
.cursor/agents/role-architect.md
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
---
|
||||||
|
name: role-architect
|
||||||
|
description: >-
|
||||||
|
Technical plan + decomposition. Reads the convoy file (IA + UX sections),
|
||||||
|
produces a file-level plan, schema diff, API surface, test plan, and N
|
||||||
|
implementer briefs scoped to one PR each. Read + Glob + Grep, no edits.
|
||||||
|
Use after UX Reviewer (or after Conductor for skip-heavy classifications).
|
||||||
|
Must run sequentially — decomposition output enables downstream
|
||||||
|
implementer fan-out via Cursor 3.2 /multitask.
|
||||||
|
multitask: single
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Architect
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After `role-ux-reviewer`, or directly after Conductor when `skip: ux` is set. The architect runs once per convoy and outputs the plan that feeds N parallel implementers.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The convoy file with IA + UX sections.
|
||||||
|
- AGENTS.md and `.cursor/rules/*.mdc` for the convention contract.
|
||||||
|
- Schema map at `docs/SCHEMA_MAP.md` (Prisma repos only).
|
||||||
|
- Existing similar code identified by IA / UX sections.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
Append a `## Architecture` section to the convoy file with:
|
||||||
|
|
||||||
|
1. **File plan** — table of `File | Action (new/modified) | Purpose`. One row per file the change touches.
|
||||||
|
2. **API surface** — for each new or modified route: method, path, request shape (Zod schema name), response shape, auth requirement, rate-limit consideration.
|
||||||
|
3. **Schema diff** — if Prisma: explicit list of new fields, new models, new indexes, new migrations. If no schema change: state that explicitly.
|
||||||
|
4. **Test plan** — what unit, integration, smoke tests are needed. Link existing test files for examples.
|
||||||
|
5. **Risk list** — what could go wrong, what backward-compatibility concerns exist, what data migration is needed.
|
||||||
|
6. **Decomposition** — table of `Brief # | Title | Files | Depends on | Estimated PR size`. One row per implementer brief.
|
||||||
|
7. **Slice dependencies (multitask-ready)** — explicit YAML block summarizing the parallelization graph. The conductor uses this to decide whether to dispatch parallel implementers via `/multitask`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
slice_dependencies:
|
||||||
|
- brief: 1
|
||||||
|
depends_on: []
|
||||||
|
files: [<exact list>]
|
||||||
|
- brief: 2
|
||||||
|
depends_on: []
|
||||||
|
files: [<exact list>]
|
||||||
|
- brief: 3
|
||||||
|
depends_on: [1]
|
||||||
|
files: [<exact list>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Any brief whose `files:` set overlaps with a sibling's MUST be sequenced via `depends_on` — never two parallel writers on the same file.
|
||||||
|
|
||||||
|
Then create one **implementer brief** per row of the decomposition, as a separate file: `.convoys/<slug>/brief-<N>-<kebab-title>.md`. Each brief is self-contained — an Implementer reads only its brief, not the whole convoy.
|
||||||
|
|
||||||
|
## Implementer brief format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
convoy: <slug>
|
||||||
|
brief_number: <N>
|
||||||
|
depends_on: [<other brief numbers>]
|
||||||
|
files:
|
||||||
|
- <path/to/file1>
|
||||||
|
- <path/to/file2>
|
||||||
|
# Optional: declare files this brief deletes.
|
||||||
|
deletes:
|
||||||
|
- <path/to/file3>
|
||||||
|
# Optional: cross-brief commitments. See "Cross-brief commitments" below.
|
||||||
|
cross_brief_commitments:
|
||||||
|
- brief: <other-brief>
|
||||||
|
description: |
|
||||||
|
<one-paragraph description of the commitment>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Brief <N>: <Title>
|
||||||
|
|
||||||
|
## Goal (1 sentence)
|
||||||
|
|
||||||
|
## Files in scope (do not edit anything else)
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Conventions to follow
|
||||||
|
- (cite rules + examples)
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
- [ ] ...
|
||||||
|
- [ ] tests added
|
||||||
|
- [ ] no scope expansion (do not edit files outside `files:` above)
|
||||||
|
|
||||||
|
## Rationale (≤3 sentences)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the convoy file in full (frontmatter + IA + UX).
|
||||||
|
2. Read AGENTS.md and any rule with globs that match the change's file patterns.
|
||||||
|
3. If Prisma: read `docs/SCHEMA_MAP.md` for the relevant model group.
|
||||||
|
4. Build the file plan. For each file, decide new vs modified.
|
||||||
|
5. Map out the API surface (if any).
|
||||||
|
6. Compute schema diff (if any).
|
||||||
|
7. Build the test plan, linking existing test files as examples.
|
||||||
|
8. Identify risks. Be specific (e.g. *"Existing `getBookmarks()` query joins `_count`; adding to the page query may cause N+1 if not memoized"*).
|
||||||
|
9. Decompose into briefs. Aim for **<400 LOC per brief** and **independent files per brief** (parallelizable). Sequence dependencies explicitly.
|
||||||
|
10. Write each brief file.
|
||||||
|
11. **Boot the brief** (see [Boot-the-brief check](#boot-the-brief-check) below) — verify each brief's verbatim code shapes against reality before declaring the architecture complete.
|
||||||
|
12. Append the Architecture section to the convoy file.
|
||||||
|
13. Print: *"Architecture complete. <N> briefs created. Estimated PRs: <N>. Awaiting human gate 1 (plan approval) before implementers run."*
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Stop. **Human gate 1.** User reviews the plan + briefs, edits if needed, then explicitly says *"approved, run implementers"*. Architect does not auto-spawn implementers.
|
||||||
|
|
||||||
|
## Boot-the-brief check
|
||||||
|
|
||||||
|
Adopted from a real production retro (`scaffold-nextjs-app`, recommendation #1) — a convoy lost ~1 hour to a Brief that paired HeroUI v3 + Tailwind 3 + a JS plugin recipe; the combination was specified plausibly but didn't actually compose. Briefs that *look* compileable rarely are without a verified-against-reality pass.
|
||||||
|
|
||||||
|
Before declaring the architecture complete, the Architect must verify each brief's verbatim code shapes against a fresh checkout. This is read-only verification — the Architect does not commit code:
|
||||||
|
|
||||||
|
1. **Dep set check.** For every package added in `files:` lists or implied by code shapes, run `pnpm view <pkg> peerDependencies` (or check `package.json` if it already exists). Confirm pinned versions resolve as a coherent dependency graph: no peer-dep conflicts, no transitive `client-only` imports landing in server-component pages, no missing peer deps. If any package was released in the last ~6 months, also read its CHANGELOG / migration guide for breaking changes from the prior major version (e.g. HeroUI v2 → v3 was a Tailwind-4 rewrite that dropped the JS plugin and `<HeroUIProvider>`; the migration guide called this out and would have been free to skim).
|
||||||
|
2. **Verbatim code shape check.** For each brief whose `files:` list includes more than 1 file with verbatim code shapes, identify any of the following that's present and verify it:
|
||||||
|
- **Middleware matchers** — Next.js route groups (`(auth)`, `(workspace)`) do NOT appear in URL paths; `'/(workspace)/(.*)'` matches zero real URLs. Negative matchers excluding the public surface area are the App-Router-idiomatic pattern.
|
||||||
|
- **Prisma schema directives** — `extensions = [...]`, `previewFeatures = [...]`, and other recently-added datasource flags often need both the preview-feature opt-in and a Prisma version that supports them.
|
||||||
|
- **Server-component / client-component boundaries** — any rich-a11y library (HeroUI, Mantine, Chakra, MUI) hits the `client-only` import boundary because it builds on React Aria. The pattern is `'use client'` wrapper components, not server-component imports.
|
||||||
|
- **Plugin / framework wrappers** — `next-intl` requires `createNextIntlPlugin('./i18n.ts')` wrapping the `next.config.ts` export. `prisma generate` requires `previewFeatures` opt-ins for any `Unsupported` types. These are easy to miss.
|
||||||
|
3. **Cross-brief commitments check.** For each brief that lands a stub or forward declaration that another brief will resolve (e.g. Brief 4's `types/auth.d.ts` ambient declaration of `@/auth` resolved by Brief 5; Brief 5's `app/layout.tsx` stub replaced by Brief 6), document the commitment in **both** briefs' frontmatter (see [Cross-brief commitments](#cross-brief-commitments) below). Implementers reading just the depended-on brief should know the commitment exists.
|
||||||
|
|
||||||
|
If any check surfaces a problem, **revise the brief in place** before declaring the architecture complete. Do not push the verification cost down to implementers.
|
||||||
|
|
||||||
|
## Cross-brief commitments
|
||||||
|
|
||||||
|
When Brief N ships a stub, forward declaration, or temporary placeholder that Brief M (M > N) is expected to resolve, both briefs must declare the commitment in their frontmatter so future agents reading either one in isolation can see it:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
convoy: <slug>
|
||||||
|
brief_number: 4
|
||||||
|
depends_on: [3]
|
||||||
|
files:
|
||||||
|
- lib/auth/require-auth.ts
|
||||||
|
- types/auth.d.ts
|
||||||
|
cross_brief_commitments:
|
||||||
|
- brief: 5
|
||||||
|
description: |
|
||||||
|
`types/auth.d.ts` is a temporary ambient declaration of `@/auth`
|
||||||
|
so `tsc --noEmit` passes before Brief 5 ships `auth.ts`. Brief 5
|
||||||
|
MUST delete this file when shipping the real `auth.ts`.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
convoy: <slug>
|
||||||
|
brief_number: 5
|
||||||
|
depends_on: [4]
|
||||||
|
files:
|
||||||
|
- auth.ts
|
||||||
|
- lib/auth-options.ts
|
||||||
|
deletes:
|
||||||
|
- types/auth.d.ts
|
||||||
|
cross_brief_commitments:
|
||||||
|
- brief: 4
|
||||||
|
description: |
|
||||||
|
Deletes `types/auth.d.ts` (Brief 4's temporary ambient declaration
|
||||||
|
of `@/auth`). The `lib/auth/require-auth.ts` stub comment block
|
||||||
|
is also removed in this brief.
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementer prompts can then automatically include the commitment text so the implementer knows what cross-brief debt is being paid off.
|
||||||
|
|
||||||
|
## Mid-convoy scope expansion
|
||||||
|
|
||||||
|
If the convoy's plan needs to change after `role-architect` has run (e.g. a user decision adds a new feature requirement, an implementer surfaces an inconsistency that requires re-planning), the scope-expansion PR must include:
|
||||||
|
|
||||||
|
1. The change to whichever brief(s) it affects (verbatim code shapes, `files:` list, acceptance criteria).
|
||||||
|
2. **An updated row in the convoy file's `### Decomposition` table** for every brief whose file list, LOC estimate, or dependency graph changed. Stale Decomposition tables are a documented retro finding (`scaffold-nextjs-app` retro recommendation #6) — they're how mid-convoy expansions become invisible.
|
||||||
|
3. A new dated entry in the `## Decisions (post-IA round)` section recording the user decision that drove the expansion (continue the A, B, C, ... letter sequence; reference the dated Decision in the matching `docs/04-architecture/*.md` file as the canonical authority).
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After writing the brief files, emit one event. Shell access is restricted to this single command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-architect convoy=<slug> duration_s=<seconds>
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Briefs >400 LOC → too big; decompose further.
|
||||||
|
- Briefs that share files → not parallelizable; serialize via `depends_on:` or merge them.
|
||||||
|
- Vague acceptance criteria ("looks right") → wrong, must be checkable.
|
||||||
|
- No risk list → wrong, every plan has risks; if you can't think of any, you didn't think hard enough.
|
||||||
|
- Auto-running implementers → forbidden, human gate is mandatory.
|
||||||
|
- Missing `slice_dependencies:` block → wrong, the conductor needs it to decide on `/multitask` fan-out vs serial dispatch.
|
||||||
|
- Skipping the Boot-the-brief check because the briefs "look obvious" → wrong, that's exactly when the dep-set or code-shape mismatches slip through.
|
||||||
|
- Cross-brief commitments declared in only one brief → wrong, the dependent brief MUST also declare it; otherwise an implementer reading the depended-on brief in isolation has no visibility into the commitment.
|
||||||
118
.cursor/agents/role-conductor.md
Normal file
118
.cursor/agents/role-conductor.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
---
|
||||||
|
name: role-conductor
|
||||||
|
description: >-
|
||||||
|
Routes a new idea through the agent-context pipeline. Owns the convoy file,
|
||||||
|
classifies the work (feature / hotfix / docs / infra / server / config), sets
|
||||||
|
skip flags for stages that don't apply, recommends multitask dispatch points
|
||||||
|
for downstream roles, and hands off to the next role. Use when a new feature,
|
||||||
|
bug fix, or epic is being kicked off and the work has not yet been scoped.
|
||||||
|
multitask: single
|
||||||
|
tools: [Read, Grep, Glob, Write, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Conductor
|
||||||
|
|
||||||
|
The Conductor is the entry point for every convoy. It does not write code. It writes one file (`.convoys/<slug>.md`) and hands off to the IA Architect (or directly to Architect for skip-heavy classifications).
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
User says any of:
|
||||||
|
|
||||||
|
- *"Start a new convoy for ..."*
|
||||||
|
- *"Run the pipeline on ..."*
|
||||||
|
- *"Scope this idea: ..."*
|
||||||
|
|
||||||
|
Or any one-paragraph problem statement that doesn't yet have a convoy file.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
1. **Idea**: one-paragraph problem statement.
|
||||||
|
2. **Success metric**: how we'll know it worked (Conductor must ask for this if the user didn't supply it — one round trip, not five).
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
A single file at `.convoys/<slug>.md` with this exact frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: <kebab-slug>
|
||||||
|
classification: feature | hotfix | docs-only | infra-only | server-only | config-only
|
||||||
|
success_metric: <one sentence>
|
||||||
|
skip:
|
||||||
|
- <flag1>
|
||||||
|
- <flag2>
|
||||||
|
status: open
|
||||||
|
created: <YYYY-MM-DD>
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Below the frontmatter, four sections (each a short paragraph or todo list):
|
||||||
|
|
||||||
|
1. `## Why` — the problem, in user-impact terms.
|
||||||
|
2. `## Scope` — what's in, what's out.
|
||||||
|
3. `## Roles invoked` — which roles will run, in order.
|
||||||
|
4. `## Todos` — high-level checkboxes the next role will refine.
|
||||||
|
|
||||||
|
## Classification → skip flags (defaults)
|
||||||
|
|
||||||
|
Use these as starting points; trust the obvious cases:
|
||||||
|
|
||||||
|
| Classification | Default skip flags | Reasoning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `feature` | (none) | Full pipeline |
|
||||||
|
| `hotfix` | `ia, ux, arch, review` | Speed over rigor; mandatory post-merge cleanup task |
|
||||||
|
| `docs-only` | `ia, ux, arch, test, visual, a11y, design, smoke, qa, flag` | Docs change docs; CI lint catches typos |
|
||||||
|
| `infra-only` | `ia, ux, arch, visual, a11y, design, smoke, qa, flag` | No UI; auditors no-op |
|
||||||
|
| `server-only` | `ia, ux, visual, a11y, design` | API or worker change; no UI |
|
||||||
|
| `config-only` | `ia, ux, arch, test, visual, a11y, design, smoke, qa, docs, flag` | env / CODEOWNERS / config file edit |
|
||||||
|
|
||||||
|
Never set: `plan-approval`, `pr-merge`, `prod-promote` (human gates are non-negotiable).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the idea. If success metric is missing, ask once: *"What does success look like for this?"*. Wait for answer.
|
||||||
|
2. Pick a classification. If ambiguous, default to `feature`.
|
||||||
|
3. Generate kebab-slug from the idea (3-5 words).
|
||||||
|
4. Write `.convoys/<slug>.md` with frontmatter + four sections.
|
||||||
|
5. Print a one-line summary: *"Convoy `<slug>` created (classification: `<X>`, skipping: `<flags>`). Next role: <role-X>."*
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Hand off by message to the user, not by spawning another role automatically. The user runs the next role manually (they can paste *"role-ia-architect"* into the chat or open a new chat and reference the convoy). This keeps the human in the loop for the early stages where direction is most plastic.
|
||||||
|
|
||||||
|
## Multitask dispatch recommendations
|
||||||
|
|
||||||
|
The Conductor doesn't run anything in parallel itself, but it **tells the user where parallelism is safe downstream** so they can use Cursor 3.2 `/multitask` when appropriate. Include these recommendations in the hand-off summary based on the classification:
|
||||||
|
|
||||||
|
| Classification | Recommended `/multitask` dispatch points |
|
||||||
|
| --- | --- |
|
||||||
|
| `feature` | After architect: dispatch implementers for all briefs with `depends_on: []` AND disjoint `files:` in parallel. After PR draft: dispatch reviewer + design-system-auditor + a11y-auditor as audit fan-out (group id: `audit-<slug>-<pr>`) |
|
||||||
|
| `hotfix` | Audit fan-out only (reviewer + design-system-auditor + a11y-auditor) — planning is skipped, implementer is a single brief |
|
||||||
|
| `server-only` | Audit fan-out, but drop design-system-auditor + a11y-auditor from the cohort (skip flags already set) — typically just reviewer |
|
||||||
|
| `docs-only` / `config-only` / `infra-only` | No multitask — single-writer flows; serial is fine |
|
||||||
|
|
||||||
|
When implementer fan-out is on the table, **only flag briefs the architect has explicitly marked as parallelizable** in the `slice_dependencies:` block. If the architect didn't supply that block, recommend serial dispatch and note that the architect output is incomplete.
|
||||||
|
|
||||||
|
See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) for the full guardrail set.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After writing the convoy file, emit one event for self-analytics. Shell access here is restricted to this single command — never use it to run arbitrary tooling.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh \
|
||||||
|
role=role-conductor \
|
||||||
|
convoy=<slug> \
|
||||||
|
classification=<feature|hotfix|docs-only|infra-only|server-only|config-only> \
|
||||||
|
skip_flags=<comma,separated> \
|
||||||
|
duration_s=<seconds-since-trigger>
|
||||||
|
```
|
||||||
|
|
||||||
|
If `scripts/log-convoy-event.sh` does not exist (L3 not installed), skip silently — analytics is opt-in.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Conductor writes code → wrong, that's Implementer.
|
||||||
|
- Conductor sets `skip: pr-merge` → forbidden, human gates are non-negotiable.
|
||||||
|
- Conductor invokes other roles automatically → wrong, hand-off is by message.
|
||||||
|
- Conductor produces more than one file → wrong, output is exactly `.convoys/<slug>.md`.
|
||||||
102
.cursor/agents/role-design-system-auditor.md
Normal file
102
.cursor/agents/role-design-system-auditor.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
name: role-design-system-auditor
|
||||||
|
description: >-
|
||||||
|
Audits a UI diff against the repo's design system. Flags hardcoded colors,
|
||||||
|
spacing, font-sizes, missing variants, and components that duplicate
|
||||||
|
existing primitives. Read-only. Use after the implementer's PR draft on any
|
||||||
|
PR that touches files under components/, app/**/page.tsx, or
|
||||||
|
app/**/layout.tsx. Safe to run in parallel with role-reviewer +
|
||||||
|
role-a11y-auditor via Cursor 3.2 /multitask.
|
||||||
|
multitask: audit-fanout
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Design System Auditor
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After `role-reviewer` on PRs that touch UI files. Skip when convoy frontmatter has `skip: design`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The PR diff.
|
||||||
|
- Design tokens: `tailwind.config.ts`, `app/globals.css` CSS variables (or `src/styles/`).
|
||||||
|
- Component primitives directory: `components/ui/` (or `src/components/ui/`).
|
||||||
|
- Any rule scoped to `components.mdc`, `styling.mdc`, `design-system.mdc`.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
A structured comment for the PR Health rollup:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Design System Audit
|
||||||
|
|
||||||
|
| Check | Status | Count |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Token violations | ✅ / ❌ | <N> |
|
||||||
|
| Duplicate primitives | ✅ / ❌ | <N> |
|
||||||
|
| Missing variants | ✅ / ❌ | <N> |
|
||||||
|
| Inline styles | ✅ / ❌ | <N> |
|
||||||
|
|
||||||
|
### Token violations
|
||||||
|
<file:line> — used `<value>` (use token `<name>` instead)
|
||||||
|
...
|
||||||
|
|
||||||
|
### Duplicate primitives
|
||||||
|
<NewComponent.tsx> duplicates <ExistingComponent.tsx>; consider reusing.
|
||||||
|
...
|
||||||
|
|
||||||
|
### Other findings
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## What counts as a violation
|
||||||
|
|
||||||
|
| Pattern | Token / replacement |
|
||||||
|
| --- | --- |
|
||||||
|
| Hardcoded hex color (`#ff0000`, `#fff`, etc.) | Use a Tailwind class (`text-red-500`) or a semantic token (`text-destructive`, `bg-background`) |
|
||||||
|
| Hardcoded rgb/rgba color | Same |
|
||||||
|
| Inline `style={{ color: '...' }}` | Same |
|
||||||
|
| Custom CSS for spacing values not on the Tailwind scale (e.g. `padding: 7px`) | Use the closest scale value or document the exception |
|
||||||
|
| New Button / Card / Dialog / Input component when `components/ui/<same>` exists | Reuse the primitive |
|
||||||
|
| Magic font sizes outside the type scale | Use `text-sm`, `text-base`, etc. |
|
||||||
|
| `className` strings >10 utility classes per element | Consider a component or a `cn()` extraction |
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Get the PR diff. Filter to UI files (`*.tsx`, `*.css`, `*.scss`).
|
||||||
|
2. Read `tailwind.config.ts` and `app/globals.css` (or equivalents) once to load the token vocabulary.
|
||||||
|
3. `Glob` `components/ui/**/*.tsx` to enumerate existing primitives.
|
||||||
|
4. For each changed UI file:
|
||||||
|
- `Grep` for hex/rgb literals → token violations.
|
||||||
|
- `Grep` for `style={{` → inline styles.
|
||||||
|
- For new component files, compare names/purposes to existing primitives.
|
||||||
|
5. Build the structured comment. Cap at 10 most-impactful findings.
|
||||||
|
6. If no violations: report ✅ across the board with a one-line note.
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Comment posted. Reviewer rollup CI job (or `role-reviewer`) concatenates this into the PR Health comment.
|
||||||
|
|
||||||
|
## Multitask (audit fan-out)
|
||||||
|
|
||||||
|
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — none modify code. Safe to run in parallel via Cursor 3.2 `/multitask`.
|
||||||
|
|
||||||
|
When invoked as part of a cohort, pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After publishing the audit comment, emit one event:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-design-system-auditor convoy=<slug> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Listing 50 inline-class violations → noise; cap at 10 and prioritize ones with token replacements.
|
||||||
|
- Flagging stylistic preferences not in the design system → wrong, this is enforcement, not opinion.
|
||||||
|
- Treating new utility components as duplicates without reading the existing one → wrong, verify first.
|
||||||
|
- Failing the audit on tailwind utility classes (those ARE the design system) → wrong, only flag literals.
|
||||||
83
.cursor/agents/role-doc-writer.md
Normal file
83
.cursor/agents/role-doc-writer.md
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
name: role-doc-writer
|
||||||
|
description: >-
|
||||||
|
Updates documentation after a feature merges to develop. Adds CHANGELOG
|
||||||
|
entries, updates AGENTS.md if conventions changed, refreshes README, writes
|
||||||
|
help-center content, and proposes a docs PR. Use after PR merge (gate 2)
|
||||||
|
before prod promote (gate 3). Skip when convoy frontmatter has skip: docs.
|
||||||
|
Must run sequentially — writes a single docs PR.
|
||||||
|
multitask: single
|
||||||
|
tools: [Read, Grep, Glob, Edit, Write, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Doc Writer
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After a convoy's PR(s) merge to `develop`, and before the release PR to `main`. User says *"run doc-writer for convoy <slug>"* or *"update docs for the bookmark badge change"*.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The merged convoy file (`.convoys/<slug>.md`).
|
||||||
|
- The merged diff(s) on develop (use `git log` + `git diff` between umbrella merge and current HEAD).
|
||||||
|
- Existing CHANGELOG.md, DEVELOPER_CHANGELOG.md, AGENTS.md, README.md, and `docs/help/` (or equivalent).
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
A docs-only PR that may touch:
|
||||||
|
|
||||||
|
| File | When to update |
|
||||||
|
| --- | --- |
|
||||||
|
| `CHANGELOG.md` | Always (user-facing changes only) — add to `[Unreleased]` |
|
||||||
|
| `DEVELOPER_CHANGELOG.md` | When API, schema, or breaking change happened |
|
||||||
|
| `AGENTS.md` | When a new convention emerged or an existing one shifted |
|
||||||
|
| `.cursor/rules/<topic>.mdc` | When a new convention belongs in a glob-scoped rule |
|
||||||
|
| `README.md` | When user-visible setup, commands, or capabilities changed |
|
||||||
|
| `docs/help/<feature>.md` | When end users need new help content |
|
||||||
|
| `docs/SCHEMA_MAP.md` (regenerate) | When Prisma schema changed — run `npm run schema:map` |
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the convoy file and the merged diff.
|
||||||
|
2. Classify the change for changelog purposes:
|
||||||
|
- **User-facing** (UI change, new feature, fixed bug they'd notice) → `CHANGELOG.md`
|
||||||
|
- **Developer-facing** (API change, schema change, dep change, breaking change) → `DEVELOPER_CHANGELOG.md`
|
||||||
|
- **Both** → both files, written for the right audience in each
|
||||||
|
3. Draft the CHANGELOG entry. Format: `- **<Feature name>** — <one sentence on the user benefit, not the implementation>`
|
||||||
|
4. Decide if AGENTS.md needs an update. Trigger conditions:
|
||||||
|
- New convention introduced (e.g. *"all bookmark queries now use _count.bookmarks"*)
|
||||||
|
- Existing convention shifted (e.g. *"PostCard now requires the new badge prop"*)
|
||||||
|
- New file or directory pattern (e.g. *"new lib/flags/ directory"*)
|
||||||
|
5. Decide if a new `.cursor/rules/` file is warranted. Threshold: the convention applies to >3 future PRs and is glob-scopeable.
|
||||||
|
6. Decide if README needs an update (rare).
|
||||||
|
7. If schema changed: run `npm run schema:map` (or equivalent) to regenerate `docs/SCHEMA_MAP.md`. Commit the regenerated file in the same PR.
|
||||||
|
8. Write all updates as a single docs-only PR. Use the existing PR template; add `<!-- pipeline: skip a11y, design-system, smoke -->` since it's docs-only.
|
||||||
|
9. Print: *"Docs PR drafted. Files changed: <list>. Awaiting human review."*
|
||||||
|
|
||||||
|
## Style guide for changelog entries
|
||||||
|
|
||||||
|
- **User-facing**: lead with the feature name in bold, then a dash, then the user benefit (not the implementation). Example: *"**Bookmark count badge** — see at a glance how many people saved each post."*
|
||||||
|
- **Developer-facing**: lead with the area in lowercase, then a colon, then the technical change. Example: *"posts API: `_count.bookmarks` now included in the default `select` for the home feed query."*
|
||||||
|
- Keep entries to one sentence. Link to the PR if the change needs more context.
|
||||||
|
- Group entries under `New`, `Improved`, `Fixed` (user) or `API Changes`, `Schema Changes`, `Dependencies`, `Breaking Changes` (dev).
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Docs PR opened. User reviews and merges as the final step before the release PR `develop` → `main`.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After producing the docs PR draft, emit one event with the convoy outcome:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-doc-writer convoy=<slug> duration_s=<seconds> outcome=complete
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Writing implementation-detail changelog entries to the user-facing file → wrong, audience matters.
|
||||||
|
- Updating AGENTS.md for one-off changes → wrong, AGENTS.md is for conventions, not history.
|
||||||
|
- Forgetting to regenerate SCHEMA_MAP.md after a Prisma change → wrong, schema docs drift fast.
|
||||||
|
- Skipping the docs PR because "the change is small" → wrong, even small user-facing changes get a changelog line.
|
||||||
68
.cursor/agents/role-ia-architect.md
Normal file
68
.cursor/agents/role-ia-architect.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
name: role-ia-architect
|
||||||
|
description: >-
|
||||||
|
Information architecture pass. Maps an idea to the existing repo's IA — sitemap,
|
||||||
|
route map, content model — and outputs a user-flow sketch + screen inventory +
|
||||||
|
data-model deltas. Read-only. Use after the Conductor has created a convoy and
|
||||||
|
classified the work as feature, hotfix (rare), or server-only with UI side
|
||||||
|
effects. Must run sequentially — output feeds role-ux-reviewer.
|
||||||
|
multitask: single
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: IA Architect
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
Conductor hands off to this role for any classification that includes UI work or new routes/pages. Skip when convoy frontmatter has `skip: ia`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The convoy file (`.convoys/<slug>.md`).
|
||||||
|
- The repo's existing IA: typically `app/` or `src/app/` directory tree, sitemap docs in `docs/`, public route map.
|
||||||
|
- Existing AGENTS.md and any rule scoped to navigation / routing.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
Append a `## IA` section to the convoy file. The section contains:
|
||||||
|
|
||||||
|
1. **Affected routes** — bullet list of paths created, modified, or impacted. Mark each as `[new]`, `[modified]`, or `[impacted]`.
|
||||||
|
2. **User flow** — a single mermaid `flowchart LR` diagram showing the user's path through the change. Keep to ≤8 nodes.
|
||||||
|
3. **Screen inventory** — table of `Screen | Path | New/modified | Notes`. One row per screen.
|
||||||
|
4. **Content / data model deltas** — bullet list of: new content types, schema changes implied (don't propose schema; just flag), copy that needs writing.
|
||||||
|
5. **Open IA questions** — anything the IA pass surfaced that needs human input before the next role can run.
|
||||||
|
|
||||||
|
Write the section — do **not** rewrite the convoy frontmatter, do **not** add code.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the convoy file in full.
|
||||||
|
2. Read the existing route map: `Glob` for `app/**/page.tsx`, `app/**/route.ts`, `src/pages/**/*.tsx`. Pick the matching one for this stack.
|
||||||
|
3. Identify which existing routes the change touches.
|
||||||
|
4. Sketch the user flow as mermaid. Prefer concrete page names over generic boxes.
|
||||||
|
5. Build the screen inventory. For each screen, note whether it's new or existing.
|
||||||
|
6. Identify content/data deltas. Don't design the schema; just say *"new field on Bookmark for ...?"*.
|
||||||
|
7. List open questions if any.
|
||||||
|
8. Append the IA section to the convoy file.
|
||||||
|
9. Print: *"IA pass complete. <N> screens, <M> routes affected. Next role: role-ux-reviewer (or role-architect if UX is skipped)."*
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Message the user. They run the next role.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After appending your IA section, emit one event. Shell access is restricted to this single command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-ia-architect convoy=<slug> duration_s=<seconds>
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Proposing a schema → wrong, that's Architect.
|
||||||
|
- Designing components → wrong, that's UX Reviewer + Architect.
|
||||||
|
- Writing code → wrong.
|
||||||
|
- Mermaid diagram with >10 nodes → too detailed; this is IA, not implementation.
|
||||||
99
.cursor/agents/role-implementer.md
Normal file
99
.cursor/agents/role-implementer.md
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
name: role-implementer
|
||||||
|
description: >-
|
||||||
|
Builds one PR worth of code from one architect brief. Strictly scoped to the
|
||||||
|
files listed in the brief; never widens scope. Writes code, writes tests,
|
||||||
|
runs lint, and proposes the PR (does not open it). Use after the architect's
|
||||||
|
plan is approved by human gate 1, once per brief. Multiple implementers can
|
||||||
|
run as a Cursor 3.2 /multitask fleet IFF their briefs declare empty
|
||||||
|
depends_on AND disjoint files: lists; each implementer gets its own worktree.
|
||||||
|
multitask: per-brief
|
||||||
|
tools: [Read, Grep, Glob, Edit, Write, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Implementer
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
User runs this role and references a specific brief: *"Run implementer on `.convoys/<slug>/brief-<N>-...md`"*. Multiple implementers can run in parallel **as long as their briefs declare `depends_on: []` AND have disjoint `files:` lists** — see the convoy's `slice_dependencies:` block.
|
||||||
|
|
||||||
|
Preferred parallel-dispatch path on Cursor 3.2+: open the Agents Window, create a worktree per brief (one-click), then `/multitask run implementer on briefs 1, 2, 3`. Cursor isolates each subagent in its own worktree automatically. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern B.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- Exactly one brief file (`.convoys/<slug>/brief-<N>-...md`).
|
||||||
|
- The convoy's IA / UX / Architecture sections (read once for context).
|
||||||
|
- AGENTS.md and matching `.cursor/rules/*.mdc`.
|
||||||
|
- Existing example files cited in the brief.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
1. Code changes to **only** the files listed in the brief's `files:` frontmatter.
|
||||||
|
2. Tests added per the brief's acceptance criteria.
|
||||||
|
3. A PR draft posted to chat (not opened on GitHub).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the brief in full. Confirm understanding of scope.
|
||||||
|
2. Read the convoy file's IA / UX / Architecture sections (one Read each).
|
||||||
|
3. Read each file in the brief's `files:` list (existing files only — new files have no content yet).
|
||||||
|
4. Read 1-2 example files cited in the brief.
|
||||||
|
5. Make the edits. Stay strictly inside `files:`.
|
||||||
|
6. Write the tests.
|
||||||
|
7. Run lint: `npm run lint` (or repo equivalent — check `package.json` scripts).
|
||||||
|
8. Run tests: `npm test` (or repo equivalent).
|
||||||
|
9. If lint or tests fail, fix and re-run. Three attempts max; if still failing, stop and report.
|
||||||
|
10. Produce a PR draft for the user:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## PR draft: <brief title>
|
||||||
|
|
||||||
|
<!-- pipeline: brief=<N>, convoy=<slug> -->
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
- 2-3 bullets on what changed and why
|
||||||
|
|
||||||
|
### Files changed
|
||||||
|
- (list)
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- [x] ...
|
||||||
|
- [x] tests added (link to test files)
|
||||||
|
- [x] no scope expansion
|
||||||
|
|
||||||
|
### Test plan
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Anything the reviewer should know
|
||||||
|
```
|
||||||
|
|
||||||
|
User copies the PR draft into the GitHub PR creation flow.
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
- **Never edit files outside the brief's `files:` list.** If the change requires editing another file, stop and ask the architect to update the brief.
|
||||||
|
- **Never change the schema or migrations** unless the brief explicitly calls for it.
|
||||||
|
- **Never disable tests** to make them pass. Fix the test or fix the code.
|
||||||
|
- **Never bypass auth, validation, or error helpers** to ship faster. Use the conventions in the rules.
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
The user reviews the PR draft, opens the PR via `gh` or Cursor's UI. Reviewer + auditors run on the open PR.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After producing the PR draft, emit one event:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-implementer convoy=<slug> brief=<N> duration_s=<seconds>
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Quietly editing a file not in `files:` because it "needed it" → forbidden, escalate to architect instead.
|
||||||
|
- Skipping tests because "it's obvious" → wrong.
|
||||||
|
- Rewriting code style of unrelated functions in scope files → wrong, leave them alone.
|
||||||
|
- Opening the PR yourself via `gh` → wrong, stop at PR draft.
|
||||||
101
.cursor/agents/role-reviewer.md
Normal file
101
.cursor/agents/role-reviewer.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
name: role-reviewer
|
||||||
|
description: >-
|
||||||
|
Self-review pass on a PR before requesting human review. Compares the diff
|
||||||
|
against the architect's brief, checks convention compliance, flags scope
|
||||||
|
expansion, security concerns, regression risk, and test coverage gaps.
|
||||||
|
Read-only. Outputs a structured PR comment. Use after the implementer's
|
||||||
|
PR draft and before the human merges. Safe to run in parallel with
|
||||||
|
role-design-system-auditor + role-a11y-auditor via Cursor 3.2 /multitask.
|
||||||
|
multitask: audit-fanout
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: Reviewer
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After `role-implementer` produces a PR draft, OR on any open PR when the user says *"run reviewer on PR #N"* or *"review this diff"*.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The PR diff (via `git diff` or `gh pr diff <N>`).
|
||||||
|
- The architect brief the implementer worked from (`.convoys/<slug>/brief-<N>-...md`).
|
||||||
|
- AGENTS.md and matching rules.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
A single Markdown comment ready to paste into the PR (or to the user). Use this exact format so the PR Health rollup CI job can parse it:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Reviewer Report
|
||||||
|
|
||||||
|
| Check | Status | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Scope match | ✅ / ⚠️ / ❌ | |
|
||||||
|
| Conventions | ✅ / ⚠️ / ❌ | |
|
||||||
|
| Security | ✅ / ⚠️ / ❌ | |
|
||||||
|
| Regression risk | low / medium / high | |
|
||||||
|
| Test coverage | ✅ / ⚠️ / ❌ | |
|
||||||
|
| Documentation | ✅ / ⚠️ / ❌ | |
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
|
||||||
|
- 🔴 **Critical** (must fix before merge): ...
|
||||||
|
- 🟡 **Suggestion** (consider): ...
|
||||||
|
- 🟢 **Nice to have** (optional): ...
|
||||||
|
|
||||||
|
### Approval recommendation
|
||||||
|
- approve / request-changes / comment-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the brief. Note the `files:` list and acceptance criteria.
|
||||||
|
2. Get the diff. Compare files-changed against `files:` — flag any expansion.
|
||||||
|
3. For each acceptance criterion, search the diff for evidence it's satisfied.
|
||||||
|
4. Check conventions against AGENTS.md and matching rules. Common gotchas:
|
||||||
|
- Auth/error helpers used vs. ad-hoc `NextResponse.json({ error: ... }, { status: ... })`
|
||||||
|
- Zod validation used for any new request body
|
||||||
|
- Prisma `select`/`include` not over-fetching
|
||||||
|
- Multi-tenant scoping if applicable (see `.cursor/rules/auth-tenancy.mdc` if present)
|
||||||
|
5. Security pass: any new endpoint without `requireAuth` / `requireAdmin`? Any user input flowing into a query without validation? Any secret in code?
|
||||||
|
6. Regression risk: does this change a function with many callers? Use `Grep -r "<function name>"` to estimate blast radius.
|
||||||
|
7. Test coverage: did the implementer add tests per the brief? Are they testing behavior or implementation?
|
||||||
|
8. Documentation: AGENTS.md or rule needs updating? Changelog entry needed under `[Unreleased]`?
|
||||||
|
9. Write the structured comment.
|
||||||
|
|
||||||
|
## Severity guidance
|
||||||
|
|
||||||
|
- 🔴 **Critical** is reserved for: security holes, broken builds, scope expansions outside the brief, missing auth on protected routes, breaking schema changes without migration.
|
||||||
|
- 🟡 **Suggestion** is for: convention drift, missing edge cases, unclear naming, over-fetching, missing test for a non-trivial path.
|
||||||
|
- 🟢 **Nice to have** is for: stylistic preferences, optional refactors, doc nits.
|
||||||
|
|
||||||
|
If you're tempted to mark something Critical and you're not sure, downgrade to Suggestion. The reviewer's credibility comes from sparing use of red.
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
User reads the report. If approve → human gate 2 (merge). If request-changes → user re-runs implementer with the findings.
|
||||||
|
|
||||||
|
## Multitask (audit fan-out)
|
||||||
|
|
||||||
|
This role is part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — they never modify code or the convoy file. Safe to run in parallel via Cursor 3.2 `/multitask`.
|
||||||
|
|
||||||
|
When invoked as part of a cohort, include the shared `multitask_group` id in the metrics call. The id convention is `audit-<convoy>-<pr>` (e.g. `audit-bookmark-badge-PR123`). See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After publishing the review comment, emit one event:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-reviewer convoy=<slug> brief=<N> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Suggestions list of 20 nits → noise; max 5 actionable items.
|
||||||
|
- Approving a PR with scope expansion → wrong, that's a Critical.
|
||||||
|
- Re-running implementation work yourself → wrong, request changes and let implementer fix.
|
||||||
|
- Inventing acceptance criteria not in the brief → wrong, the brief is the contract.
|
||||||
68
.cursor/agents/role-ux-reviewer.md
Normal file
68
.cursor/agents/role-ux-reviewer.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
name: role-ux-reviewer
|
||||||
|
description: >-
|
||||||
|
UX / IX review pass against the existing design system. Identifies which
|
||||||
|
existing components and patterns to reuse, calls out anti-patterns to avoid,
|
||||||
|
and lists a11y constraints that must be satisfied. Read-only. Use after
|
||||||
|
IA Architect on any feature with UI changes. Must run sequentially — refines
|
||||||
|
the IA section, feeds role-architect.
|
||||||
|
multitask: single
|
||||||
|
tools: [Read, Grep, Glob, Shell]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role: UX Reviewer
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
After `role-ia-architect` for any classification that includes UI work. Skip when convoy frontmatter has `skip: ux`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- The convoy file (with the IA section appended by the previous role).
|
||||||
|
- Existing UI primitives directory (typically `components/ui/` or `src/components/ui/`).
|
||||||
|
- Design tokens (typically `tailwind.config.ts`, `app/globals.css` CSS variables).
|
||||||
|
- Any rule scoped to `components.mdc`, `styling.mdc`, or `design-system.mdc`.
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
Append a `## UX` section to the convoy file with:
|
||||||
|
|
||||||
|
1. **Existing components to reuse** — bullet list of `<ComponentName>` (`path/to/file.tsx`) for each reusable primitive the screens need. Be specific — name the file.
|
||||||
|
2. **Existing patterns to follow** — referenced rules and example screens that solve a similar problem (e.g. *"PostCard.tsx is the canonical card pattern; use the same Badge primitive there"*).
|
||||||
|
3. **A11y constraints** — bullets enumerating: required ARIA labels, keyboard navigation paths, focus management, color-contrast requirements specific to this change.
|
||||||
|
4. **Interaction patterns** — short list: hover/focus/active states, optimistic UI, error states, empty states, loading states. Mark each as `required` or `nice-to-have`.
|
||||||
|
5. **Anti-patterns to avoid** — explicit list of what NOT to do (e.g. *"Don't add a new color outside the design tokens for the badge background"*).
|
||||||
|
6. **Mobile / responsive notes** — if the change has UI, this section is mandatory. If headless/server-only, note that.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Read the convoy file. Find the IA section.
|
||||||
|
2. For each screen in the IA inventory:
|
||||||
|
- `Glob` for relevant existing components in `components/ui/` (or equivalent).
|
||||||
|
- Identify the closest existing pattern by reading 1-3 example files.
|
||||||
|
3. Read the design tokens once (one Read of `tailwind.config.ts` or `app/globals.css`).
|
||||||
|
4. Author the UX section. Be opinionated. Pick one pattern, not three options.
|
||||||
|
5. Call out a11y requirements explicitly — don't say *"follow a11y best practices"*; say *"requires aria-label on the toggle button when collapsed"*.
|
||||||
|
6. Append section to convoy file.
|
||||||
|
7. Print: *"UX pass complete. Reuse: <N> primitives. A11y constraints: <M>. Next role: role-architect."*
|
||||||
|
|
||||||
|
## Hand-off
|
||||||
|
|
||||||
|
Message the user.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
After appending your UX section, emit one event. Shell access is restricted to this single command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/log-convoy-event.sh role=role-ux-reviewer convoy=<slug> duration_s=<seconds>
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Suggesting new components when an existing one fits → wrong, this role's job is reuse.
|
||||||
|
- Vague a11y guidance ("follow WCAG") → wrong, list specific requirements.
|
||||||
|
- Three alternatives — pick one → wrong, pick one with reasoning.
|
||||||
|
- Designing the schema or API → wrong, that's Architect.
|
||||||
106
.cursor/rules/api-routes.mdc
Normal file
106
.cursor/rules/api-routes.mdc
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
---
|
||||||
|
description: Conventions for Next.js Pages-router API route handlers in tcg-vault
|
||||||
|
globs: pages/api/**/*.js
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Route Conventions
|
||||||
|
|
||||||
|
Pages router handlers; `(req, res)` signature; Vercel serverless functions.
|
||||||
|
|
||||||
|
## Authentication & Authorization
|
||||||
|
|
||||||
|
Two paths are in use; both go through `lib/permission-middleware.js`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. Generic auth — every protected route
|
||||||
|
import { getUserFromRequest } from '../../lib/permission-middleware';
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const user = await getUserFromRequest(req);
|
||||||
|
if (!user) return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
// user = { userId, email, role }
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 2. Collection-scoped auth — when the route operates on a specific collection
|
||||||
|
import { withCollectionPermission } from '../../lib/permission-middleware';
|
||||||
|
|
||||||
|
async function handler(req, res) {
|
||||||
|
// req.user and req.permission are populated by the wrapper
|
||||||
|
const { userId, role } = req.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withCollectionPermission('viewer')(handler);
|
||||||
|
// 'viewer' | 'editor' | 'owner' — checks owner OR is_public OR explicit collection_permissions row
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL — known bug:** `getUserFromRequest` currently has a development-mode fallback that returns a hardcoded admin user when no Bearer token is present. Until that's fixed, callers MUST also assert `req.headers.authorization` exists when the route is sensitive (admin operations, deletes). See `AGENTS.md` Gotcha #2.
|
||||||
|
|
||||||
|
## Request validation
|
||||||
|
|
||||||
|
No schema validator is installed (no zod / yup / valibot). Validate manually:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { name, game } = req.body || {};
|
||||||
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
}
|
||||||
|
if (!['mtg', 'pokemon', 'lorcana'].includes(game)) {
|
||||||
|
return res.status(400).json({ error: 'invalid game' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When adding zod (planned in `.convoys/`), define schemas at the top of the file.
|
||||||
|
|
||||||
|
## Method gating
|
||||||
|
|
||||||
|
Reject unsupported methods explicitly — Next.js will otherwise call the handler for any method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Wrap the handler body in `try/catch`. Never throw unhandled — leaks stack traces in the Vercel response.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
// ...
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[POST /api/collections]', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database access
|
||||||
|
|
||||||
|
- **Prefer:** `import { sql } from '@vercel/postgres'` and tagged templates: `` await sql`SELECT … WHERE id = ${id}` ``.
|
||||||
|
- **Avoid:** `import { db } from '../../lib/database'`. Its `query(str, params)` API uses `sql.unsafe` after manual interpolation — SQL-injection vector. Slated for removal in a convoy.
|
||||||
|
- **Always select narrow columns** — don't `SELECT *` from `cards` (large `oracle_text`, `colors` JSONB).
|
||||||
|
|
||||||
|
## Response shape
|
||||||
|
|
||||||
|
- Success (GET / read): `res.status(200).json({ data: ... })` OR direct payload — codebase is inconsistent; match the surrounding route's existing shape.
|
||||||
|
- Created (POST): `res.status(201).json({ data: ... })`.
|
||||||
|
- Errors: `res.status(<4xx|5xx>).json({ error: string, details?: unknown })`.
|
||||||
|
|
||||||
|
## Activity logging
|
||||||
|
|
||||||
|
Any handler that mutates a collection must call `logCollectionActivity`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { logCollectionActivity } from '../../lib/permission-middleware';
|
||||||
|
|
||||||
|
await logCollectionActivity(collectionId, userId, 'card_added', { cardId, quantity });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev/test endpoints
|
||||||
|
|
||||||
|
`pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, `pages/api/setup-database.js` — these are dev-only endpoints currently shipped to prod. Don't add more. Existing ones should be deleted or admin-gated before public launch.
|
||||||
56
.cursor/rules/auth-and-permissions.mdc
Normal file
56
.cursor/rules/auth-and-permissions.mdc
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
description: Auth model + permission model for tcg-vault (JWT + collection roles)
|
||||||
|
globs: pages/api/**/*.js,lib/*.js,components/*.js,pages/*.js
|
||||||
|
---
|
||||||
|
|
||||||
|
# Auth + permissions
|
||||||
|
|
||||||
|
There are three parallel client-side auth implementations and one server-side helper. New code should use the canonical set listed below; don't proliferate variants.
|
||||||
|
|
||||||
|
## Canonical surface (use these)
|
||||||
|
|
||||||
|
| Concern | Module |
|
||||||
|
| --- | --- |
|
||||||
|
| Server: extract user from request | `lib/permission-middleware.js::getUserFromRequest` |
|
||||||
|
| Server: gate a collection route | `lib/permission-middleware.js::withCollectionPermission` |
|
||||||
|
| Server: log collection mutation | `lib/permission-middleware.js::logCollectionActivity` |
|
||||||
|
| Server: token / password primitives | `pages/api/auth-utils.js` (`generateToken`, `verifyToken`, `hashPassword`, `verifyPassword`) |
|
||||||
|
| Client: hook | `lib/use-auth.js::useAuth` |
|
||||||
|
| Client: route protection | `components/ProtectedRoute.js` |
|
||||||
|
| Client: admin route protection | `components/AdminProtected.js` |
|
||||||
|
|
||||||
|
## Legacy (do not extend)
|
||||||
|
|
||||||
|
- `lib/auth-context.js::AuthProvider` + `useAuth` — older context. Still wired in `pages/_app.js`; left in place for compatibility. Don't add new consumers.
|
||||||
|
- `lib/admin-auth.js::AdminProvider` + `useAdmin` + `useIsAdmin` — parallel admin context. Same story.
|
||||||
|
|
||||||
|
A convoy is planned to collapse these three into one provider + one hook.
|
||||||
|
|
||||||
|
## Token model
|
||||||
|
|
||||||
|
- JWT in localStorage under the key `auth_token`.
|
||||||
|
- Signed with `JWT_SECRET` (HS256), 7-day expiry, payload `{ userId, email, role }`.
|
||||||
|
- Sent on every authenticated fetch as `Authorization: Bearer <token>`.
|
||||||
|
- Verified server-side with `jsonwebtoken.verify(token, JWT_SECRET)`.
|
||||||
|
|
||||||
|
**JWT_SECRET MUST be set in the deploy environment.** Seven files default it to a string literal if unset; that defeats signing.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
Two role surfaces are in play:
|
||||||
|
|
||||||
|
1. **User role** — `users.role` column, values `'user'` or `'admin'`. Admin gates `/admin/*` pages and admin-only API endpoints.
|
||||||
|
2. **Collection role** — `collection_permissions.role` (`viewer` / `editor` / `owner`) + `collections.is_public` (anonymous viewer access). Resolved by `checkCollectionPermission` in priority order: owner → public-viewer → explicit row.
|
||||||
|
|
||||||
|
When introducing a new permission tier, update both `checkRolePermission`'s hierarchy AND every gate that reads `is_public`.
|
||||||
|
|
||||||
|
## Authentication state on the client
|
||||||
|
|
||||||
|
`useAuth()` returns `{ user, loading, login, logout, refresh }`. `user === null` means logged out; `loading === true` means token verification in flight. Always render against `loading === false` before deciding to redirect.
|
||||||
|
|
||||||
|
## Server-side authorization patterns
|
||||||
|
|
||||||
|
- **Owner-only** (delete, settings): inside the handler, `if (user.userId !== resource.user_id) return res.status(403)`.
|
||||||
|
- **Editor-or-owner**: use `withCollectionPermission('editor')`.
|
||||||
|
- **Public read**: use `withCollectionPermission('viewer')` — handles `is_public` and explicit-permission case.
|
||||||
|
- **Admin-only**: check `user.role === 'admin'` directly; consider extracting `withAdmin()` if a third call site appears.
|
||||||
59
.cursor/rules/db-and-schema.mdc
Normal file
59
.cursor/rules/db-and-schema.mdc
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
description: Neon Postgres conventions + the until-we-have-migrations workflow
|
||||||
|
globs: pages/api/**/*.js,lib/database.js,scripts/**/*.js
|
||||||
|
---
|
||||||
|
|
||||||
|
# DB + schema
|
||||||
|
|
||||||
|
## Clients
|
||||||
|
|
||||||
|
Two are installed (`@vercel/postgres` + `@neondatabase/serverless`); they point at the same Neon Postgres. New code uses `@vercel/postgres` (tagged-template SQL).
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { sql } from '@vercel/postgres';
|
||||||
|
|
||||||
|
const { rows } = await sql`
|
||||||
|
SELECT id, name, set_name
|
||||||
|
FROM cards
|
||||||
|
WHERE game = ${game}
|
||||||
|
AND set_code = ${setCode}
|
||||||
|
LIMIT 50
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Never** use `lib/database.js`'s `db.query(string, params)` API for new code — it interpolates params into a string and then calls `sql.unsafe()`, which is a SQL-injection vector. Marked for removal in `.convoys/`.
|
||||||
|
|
||||||
|
## Schema source of truth
|
||||||
|
|
||||||
|
`scripts/setup-neon-db.js` is the bootstrap DDL — idempotent (`CREATE TABLE IF NOT EXISTS`). Real schema state lives in Neon. Until a proper migration tool is adopted:
|
||||||
|
|
||||||
|
- **Adding a column**: new dated script under `scripts/migrations/YYYY-MM-DD-<slug>.js` (folder TBD; until then, top-level `scripts/add-*.js` named for the change).
|
||||||
|
- **Document** the change in `docs/SCHEMA_MAP.md`.
|
||||||
|
- **Never** edit a script that has already been run in prod.
|
||||||
|
|
||||||
|
## Tables (current)
|
||||||
|
|
||||||
|
| Table | Owner | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `users` | core | `(id, email UNIQUE, password, role, created_at, …)` |
|
||||||
|
| `cards` | core | Big — `oracle_text TEXT`, `colors JSONB`. Don't `SELECT *`. |
|
||||||
|
| `user_cards` | per-user | `(user_id, card_id, quantity, condition, is_foil)` — UNIQUE on tuple |
|
||||||
|
| `collections` | per-user | `is_public BOOLEAN`, `slug`, `tags`, `image_url`, `system_collection` |
|
||||||
|
| `collection_cards` | join | `(collection_id, card_id, quantity)` UNIQUE |
|
||||||
|
| `collection_permissions` | per-user | `(collection_id, user_id, role, status)` — `viewer\|editor\|owner` |
|
||||||
|
| `collection_activity` | log | `(collection_id, user_id, action, details JSONB, created_at)` |
|
||||||
|
| `decks` / `deck_cards` | per-user | Mirror of collections |
|
||||||
|
| `favorites` | per-user | `(user_id, card_id)` |
|
||||||
|
| `invitations` | per-collection | Pending share requests |
|
||||||
|
|
||||||
|
Full map: [`docs/SCHEMA_MAP.md`](../../docs/SCHEMA_MAP.md). Regenerate by re-reading `setup-neon-db.js` + every `add-*.js` script that's been run.
|
||||||
|
|
||||||
|
## Indexing reminders
|
||||||
|
|
||||||
|
- `cards.scryfall_id` is UNIQUE — use it for dedupe on import.
|
||||||
|
- `users.email` is UNIQUE — case-insensitive collation NOT set; lowercase before query/insert.
|
||||||
|
- `collections.slug` should be UNIQUE per user; verify with `lib/slug-utils.js::generateUniqueSlug` before insert.
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
Neon HTTP doesn't support multi-statement transactions across separate `sql` calls — each call is its own connection. For multi-table writes that need atomicity, use Neon's `sql.transaction([query1, query2])` array form OR refactor to a single SQL statement with CTEs. The current codebase has several non-atomic multi-step inserts that should be flagged.
|
||||||
37
.cursor/rules/no-go-zones.mdc
Normal file
37
.cursor/rules/no-go-zones.mdc
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
description: Files and directories agents must not edit, and should not use as context examples
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# No-go zones
|
||||||
|
|
||||||
|
Do not edit, refactor, or quote as context examples. If you think you need to change one of these, stop and ask.
|
||||||
|
|
||||||
|
## Generated / vendored
|
||||||
|
|
||||||
|
- `node_modules/` — generated dependency tree
|
||||||
|
- `.next/` — Next.js build output
|
||||||
|
- `.vercel/` — Vercel CLI local config + build cache
|
||||||
|
- `out/`, `build/` — build outputs if present
|
||||||
|
|
||||||
|
## Append-only / historical
|
||||||
|
|
||||||
|
- `components/Layout.js.backup` — legacy snapshot; delete with a real PR, never edit
|
||||||
|
- `scripts/add-*.js`, `scripts/fix-*.js`, `scripts/seed-*.js` — historical migration / seed jobs already executed. Write a NEW dated script (or a real migration) for further schema changes; never edit ones that already ran.
|
||||||
|
|
||||||
|
## Secrets / credentials
|
||||||
|
|
||||||
|
- `.env`, `.env.local`, `.env.development.local`, `.env.test.local`, `.env.production.local`
|
||||||
|
- Anything matching `.env*.local`
|
||||||
|
- Never commit `JWT_SECRET`, `POSTGRES_URL`, `RESEND_API_KEY`, `BLOB_READ_WRITE_TOKEN`, `GEMINI_API_KEY`, OpenAI/Anthropic keys.
|
||||||
|
|
||||||
|
## Local-only / per-developer
|
||||||
|
|
||||||
|
- `.code-review-graph/` — local MCP graph index (only if `user-code-review-graph` is installed)
|
||||||
|
- `.convoys/.metrics.jsonl` — per-developer convoy analytics (gitignored by default)
|
||||||
|
|
||||||
|
## Editing rules of thumb
|
||||||
|
|
||||||
|
- **Schema changes:** until a proper migration tool lands, document the change in a new dated script under `scripts/migrations/YYYY-MM-DD-<slug>.js` (folder TBD). Do NOT edit `scripts/setup-neon-db.js` in place — it's idempotent and meant for first-time setup only.
|
||||||
|
- **Auth refactors:** `lib/permission-middleware.js`, `pages/api/auth-utils.js`, `lib/auth-context.js`, `lib/admin-auth.js`, and `lib/use-auth.js` form a deliberately documented mess. Tighten them inside a single convoy; don't cherry-pick.
|
||||||
|
- **Card-import jobs:** `pages/api/cards/import-*.js` hit external APIs with rate limits. Don't run them ad-hoc against prod data; use staging.
|
||||||
30
.cursor/rules/schema-map.mdc
Normal file
30
.cursor/rules/schema-map.mdc
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
description: Schema map — when an agent needs to know what columns exist on which table
|
||||||
|
globs: pages/api/**/*.js,scripts/**/*.js,lib/database.js,lib/permission-middleware.js
|
||||||
|
---
|
||||||
|
|
||||||
|
# Use the schema map
|
||||||
|
|
||||||
|
Before writing any SQL that touches a column you haven't verified:
|
||||||
|
|
||||||
|
1. **Read [`docs/SCHEMA_MAP.md`](../../docs/SCHEMA_MAP.md)** — it's the curated reference.
|
||||||
|
2. If the column isn't documented there, search the migration scripts:
|
||||||
|
|
||||||
|
```
|
||||||
|
rg "<column_name>" scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If still no match, **stop and ask** — don't guess column names; Postgres won't be polite.
|
||||||
|
|
||||||
|
## When to update the schema map
|
||||||
|
|
||||||
|
After running any `scripts/add-*.js` / `scripts/fix-*.js` or any direct `ALTER TABLE`, update the matching table section in `docs/SCHEMA_MAP.md` in the same PR. The CI gate `schema-map-fresh` will fail if a `scripts/add-*.js` is added without a corresponding update.
|
||||||
|
|
||||||
|
## Common reference
|
||||||
|
|
||||||
|
- **Auth check inside an API route** → `users` (id, email, role)
|
||||||
|
- **Card lookup** → `cards` (id, name, scryfall_id UNIQUE, game)
|
||||||
|
- **What a user owns** → `user_cards` (user_id, card_id, quantity, is_foil)
|
||||||
|
- **Collection card list** → `collection_cards` JOIN `cards`
|
||||||
|
- **Permission check** → `collection_permissions` (collection_id, user_id, role, status='active')
|
||||||
|
- **Activity feed** → `collection_activity` (action, details JSONB)
|
||||||
61
.cursor/rules/ui-and-theming.mdc
Normal file
61
.cursor/rules/ui-and-theming.mdc
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
description: Tailwind + CSS-variable theming, component patterns, and a11y reminders
|
||||||
|
globs: components/**/*.js,pages/**/*.js
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI + theming
|
||||||
|
|
||||||
|
## Theming model
|
||||||
|
|
||||||
|
Two systems coexist:
|
||||||
|
|
||||||
|
1. **Tailwind utility classes** (`text-gray-700`, `bg-white`, `dark:bg-gray-800`) — used for layout, spacing, and structural styles.
|
||||||
|
2. **CSS variables** (`var(--bg-primary)`, `var(--text-primary)`, `var(--accent-ember)`, `var(--accent-flame)`, `var(--border)`) — used for colors that need to switch with theme (light/dark).
|
||||||
|
|
||||||
|
**Don't mix and match within a single style declaration.** Pick one source per property. Generally:
|
||||||
|
|
||||||
|
- Backgrounds + text colors: CSS variables (via `style={{ backgroundColor: 'var(--bg-primary)' }}`).
|
||||||
|
- Spacing, sizing, flex, grid: Tailwind classes.
|
||||||
|
- Focus rings: CSS variables for color, Tailwind for everything else (`focus:outline-none focus:ring-2 focus:ring-offset-2` + `'--tw-ring-color': 'var(--accent-ember)'`).
|
||||||
|
|
||||||
|
Theme switching: `useTheme()` from `lib/theme-context.js`. Provider is wired in `pages/_app.js`.
|
||||||
|
|
||||||
|
## Component conventions
|
||||||
|
|
||||||
|
- Functional components, default-exported by name (`export default function CardItem(...)`).
|
||||||
|
- Props destructured in the signature with defaults: `function Layout({ children, user = null, showSearch = false })`.
|
||||||
|
- **Avoid hardcoded default values for `user` props.** `Layout` currently defaults `user` to a real email address — every page passing through Layout should pass `user` explicitly. New components must default to `null` and render a logged-out state.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
Pages render inside `<Layout user={user} showSearch={...}>{children}</Layout>`. Layout owns:
|
||||||
|
|
||||||
|
- Desktop sidebar + mobile bottom-nav (`components/MobileNavigation.js`).
|
||||||
|
- Theme toggle.
|
||||||
|
- User profile dropdown.
|
||||||
|
|
||||||
|
Don't duplicate navigation in a page — extend `NavigationContent` inside Layout instead.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
- Every interactive element needs a label: `aria-label`, `aria-labelledby`, or visible text.
|
||||||
|
- Modals need `role="dialog"`, `aria-modal="true"`, and focus management (trap focus + restore on close).
|
||||||
|
- Color contrast: stick to the documented theme tokens — they're tuned for AA.
|
||||||
|
- Keyboard: every `onClick` on a non-`<button>` needs `tabIndex={0}` + `onKeyDown` for Enter/Space.
|
||||||
|
|
||||||
|
## Common UI patterns to reuse
|
||||||
|
|
||||||
|
| Need | Where |
|
||||||
|
| --- | --- |
|
||||||
|
| Card grid item | `components/CardItem.js` |
|
||||||
|
| Bulk-action toolbar | `components/BulkSelectionToolbar.js` |
|
||||||
|
| Modal | `components/CollectionSelectionModal.js`, `components/ShareModal.js` |
|
||||||
|
| Image upload | `components/UploadImageModal.js` |
|
||||||
|
| Camera scanner | `components/CameraScanner.js` |
|
||||||
|
| Auth-required wrapper | `components/ProtectedRoute.js` |
|
||||||
|
| Admin-only wrapper | `components/AdminProtected.js` |
|
||||||
|
| Public-or-auth wrapper | inline in `pages/cards.js` (`PublicCardsView` / `AuthenticatedCards`) — pattern to copy |
|
||||||
|
|
||||||
|
## Branding
|
||||||
|
|
||||||
|
The repo says "TCG Vault" everywhere except `components/Layout.js`, which renders "Deck Hearth" and "DH" logo. A naming convoy is open. Until resolved, **do not introduce a third name** in new copy.
|
||||||
113
.cursor/skills/add-api-route/SKILL.md
Normal file
113
.cursor/skills/add-api-route/SKILL.md
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
---
|
||||||
|
name: add-api-route
|
||||||
|
description: >-
|
||||||
|
Add a new authenticated API route under pages/api/. Use when you need to
|
||||||
|
expose a new server endpoint to the client, scaffold an admin-only route, or
|
||||||
|
add a CRUD method to an existing resource. Walks through file placement,
|
||||||
|
auth, validation, DB access, and error handling for the tcg-vault stack.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add an API route
|
||||||
|
|
||||||
|
`pages/api/<path>.js` becomes `/api/<path>`. Dynamic segments use `[name]` folder/file naming.
|
||||||
|
|
||||||
|
## Step 1: Decide the path
|
||||||
|
|
||||||
|
| Pattern | Example | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Resource collection | `pages/api/decks.js` → `/api/decks` | GET list, POST create |
|
||||||
|
| Single resource | `pages/api/decks/[id].js` → `/api/decks/:id` | GET, PUT, DELETE |
|
||||||
|
| Sub-resource | `pages/api/decks/[id]/cards.js` | GET, POST |
|
||||||
|
| Action | `pages/api/cards/find-or-create.js` | POST, RPC-style |
|
||||||
|
|
||||||
|
## Step 2: Pick the auth pattern
|
||||||
|
|
||||||
|
| Use case | Wrapper |
|
||||||
|
| --- | --- |
|
||||||
|
| Generic logged-in user | `getUserFromRequest(req)` inline |
|
||||||
|
| Collection-scoped op | `withCollectionPermission('viewer'\|'editor'\|'owner')` |
|
||||||
|
| Admin-only | inline `if (user.role !== 'admin') return res.status(403)` |
|
||||||
|
|
||||||
|
## Step 3: Skeleton
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { sql } from '@vercel/postgres';
|
||||||
|
import { getUserFromRequest } from '../../lib/permission-middleware';
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
// 1. Method gate
|
||||||
|
if (!['GET', 'POST'].includes(req.method)) {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Auth
|
||||||
|
const user = await getUserFromRequest(req);
|
||||||
|
if (!user || !req.headers.authorization) {
|
||||||
|
// Guard against the known dev-fallback bug; require real Bearer token.
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const { rows } = await sql`
|
||||||
|
SELECT id, name FROM example_table WHERE user_id = ${user.userId}
|
||||||
|
`;
|
||||||
|
return res.status(200).json({ items: rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate body
|
||||||
|
const { name } = req.body || {};
|
||||||
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Mutation
|
||||||
|
const { rows } = await sql`
|
||||||
|
INSERT INTO example_table (user_id, name)
|
||||||
|
VALUES (${user.userId}, ${name.trim()})
|
||||||
|
RETURNING id, name
|
||||||
|
`;
|
||||||
|
|
||||||
|
return res.status(201).json({ item: rows[0] });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[example handler]', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Wire the client
|
||||||
|
|
||||||
|
Use `fetch` with the auth header pattern from existing pages:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const res = await fetch('/api/example', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Check
|
||||||
|
|
||||||
|
- [ ] Method gate is the first thing in the handler.
|
||||||
|
- [ ] Auth check requires both `getUserFromRequest` AND `req.headers.authorization` (until the middleware bug is fixed).
|
||||||
|
- [ ] All inputs validated.
|
||||||
|
- [ ] Tagged-template SQL only (no `db.query(...)`).
|
||||||
|
- [ ] try/catch wraps the whole body.
|
||||||
|
- [ ] If the route mutates a collection: call `logCollectionActivity`.
|
||||||
|
- [ ] Add a Convoy entry if the route is new functionality (vs. a bug fix).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Don't | Do |
|
||||||
|
| --- | --- |
|
||||||
|
| `db.query(\`SELECT … \${userInput}\`)` | `await sql\`SELECT … \${userInput}\`` |
|
||||||
|
| Forget the method gate | Always declare which methods are allowed |
|
||||||
|
| Return `res.json(err.message)` in catch | Generic message; log details server-side |
|
||||||
|
| `SELECT *` from `cards` | Project narrow columns |
|
||||||
|
| Add another `/api/test-*` endpoint | Use a real test runner once one's adopted |
|
||||||
109
.cursor/skills/add-page/SKILL.md
Normal file
109
.cursor/skills/add-page/SKILL.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
---
|
||||||
|
name: add-page
|
||||||
|
description: >-
|
||||||
|
Add a new Next.js page under pages/. Use when you need a new route, a new
|
||||||
|
view for an existing resource, or an admin-only screen. Covers Layout
|
||||||
|
wiring, auth, theme tokens, and the public/authenticated split pattern.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add a page
|
||||||
|
|
||||||
|
`pages/<file>.js` becomes a route. Pages router conventions:
|
||||||
|
|
||||||
|
| File | Route |
|
||||||
|
| --- | --- |
|
||||||
|
| `pages/about.js` | `/about` |
|
||||||
|
| `pages/cards/[id].js` | `/cards/:id` (but use `pages/card/[id].js` per existing naming) |
|
||||||
|
| `pages/admin/index.js` | `/admin` |
|
||||||
|
|
||||||
|
## Step 1: Decide auth shape
|
||||||
|
|
||||||
|
| Mode | Template |
|
||||||
|
| --- | --- |
|
||||||
|
| Public-only | Render without `ProtectedRoute`; no auth check |
|
||||||
|
| Auth-required | Wrap top-level export with `<ProtectedRoute>` |
|
||||||
|
| Admin-only | Wrap with `<AdminProtected>` from `components/AdminProtected.js` |
|
||||||
|
| Public + auth-enhanced (e.g. `/cards`) | Inline split: render `<PublicView>` if not logged in, `<AuthedView>` if logged in. Copy the pattern from `pages/cards.js`. |
|
||||||
|
|
||||||
|
## Step 2: Skeleton
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Layout from '../components/Layout';
|
||||||
|
import ProtectedRoute from '../components/ProtectedRoute';
|
||||||
|
import { useAuth } from '../lib/use-auth';
|
||||||
|
|
||||||
|
export default function MyPage() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute>
|
||||||
|
<MyPageInner />
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MyPageInner() {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || !user) return;
|
||||||
|
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
setFetching(true);
|
||||||
|
fetch('/api/my-resource', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => setItems(data.items || []))
|
||||||
|
.catch((err) => console.error('fetch failed', err))
|
||||||
|
.finally(() => setFetching(false));
|
||||||
|
}, [user, loading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout user={user} showSearch={false}>
|
||||||
|
<div className="p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
My Page
|
||||||
|
</h1>
|
||||||
|
{fetching ? <p>Loading…</p> : items.map((i) => <div key={i.id}>{i.name}</div>)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Theme tokens (not hex)
|
||||||
|
|
||||||
|
- Backgrounds → `var(--bg-primary)`, `var(--bg-secondary)`, `var(--bg-tertiary)`
|
||||||
|
- Text → `var(--text-primary)`, `var(--text-secondary)`
|
||||||
|
- Accents → `var(--accent-ember)`, `var(--accent-flame)`
|
||||||
|
- Borders → `var(--border)`
|
||||||
|
|
||||||
|
Use Tailwind for layout, spacing, sizing, hover/focus states. Use CSS vars (inline `style={{ ... }}`) for colors that need to switch with theme.
|
||||||
|
|
||||||
|
## Step 4: Always pass `user` to Layout
|
||||||
|
|
||||||
|
`<Layout user={user}>` — never let the default kick in (it's a hardcoded maintainer email; see `AGENTS.md` Gotcha #8).
|
||||||
|
|
||||||
|
## Step 5: Mobile
|
||||||
|
|
||||||
|
`components/Layout.js` already handles the mobile drawer + bottom nav. To add the page to nav, edit `NavigationContent` in `Layout.js`. Use existing icon names from the `getIcon` registry; add new ones to that registry before referencing.
|
||||||
|
|
||||||
|
## Step 6: Check
|
||||||
|
|
||||||
|
- [ ] Auth wrapper chosen (ProtectedRoute / AdminProtected / public).
|
||||||
|
- [ ] `useAuth()` from `lib/use-auth.js` (not the legacy `lib/auth-context.js`).
|
||||||
|
- [ ] `user` passed to Layout explicitly.
|
||||||
|
- [ ] Colors come from theme tokens, not hex.
|
||||||
|
- [ ] All interactive elements have `aria-label` or visible text.
|
||||||
|
- [ ] Mobile: confirm the page renders in the mobile drawer.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Don't | Do |
|
||||||
|
| --- | --- |
|
||||||
|
| Hardcode hex colors | Use CSS variables |
|
||||||
|
| Default `user = { … }` to a real email | Default to `null` |
|
||||||
|
| Pull from `lib/auth-context` for new code | Use `lib/use-auth` |
|
||||||
|
| Render Layout twice on the same page | Single `<Layout>` at the top |
|
||||||
43
.github/CODEOWNERS
vendored
Normal file
43
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# CODEOWNERS — review routing for the agent pipeline.
|
||||||
|
# Replace @YOUR-GITHUB-HANDLE with the project owner's GitHub handle.
|
||||||
|
|
||||||
|
# Global default
|
||||||
|
* @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# High-risk: auth — every change here needs a maintainer review
|
||||||
|
pages/api/auth/** @YOUR-GITHUB-HANDLE
|
||||||
|
pages/api/auth-utils.js @YOUR-GITHUB-HANDLE
|
||||||
|
lib/permission-middleware.js @YOUR-GITHUB-HANDLE
|
||||||
|
lib/auth-context.js @YOUR-GITHUB-HANDLE
|
||||||
|
lib/admin-auth.js @YOUR-GITHUB-HANDLE
|
||||||
|
lib/use-auth.js @YOUR-GITHUB-HANDLE
|
||||||
|
components/ProtectedRoute.js @YOUR-GITHUB-HANDLE
|
||||||
|
components/AdminProtected.js @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# High-risk: admin operations + DB setup
|
||||||
|
pages/api/admin/** @YOUR-GITHUB-HANDLE
|
||||||
|
pages/api/setup-database.js @YOUR-GITHUB-HANDLE
|
||||||
|
scripts/setup-neon-db.js @YOUR-GITHUB-HANDLE
|
||||||
|
scripts/reset-db.js @YOUR-GITHUB-HANDLE
|
||||||
|
scripts/promote-user-to-admin.js @YOUR-GITHUB-HANDLE
|
||||||
|
scripts/demote-admin-to-user.js @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# Schema changes need extra eyes (until a real migration tool is adopted)
|
||||||
|
scripts/add-*.js @YOUR-GITHUB-HANDLE
|
||||||
|
scripts/fix-*.js @YOUR-GITHUB-HANDLE
|
||||||
|
docs/SCHEMA_MAP.md @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# Feature flags
|
||||||
|
lib/flags/** @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# CI / infra
|
||||||
|
.github/workflows/** @YOUR-GITHUB-HANDLE
|
||||||
|
next.config.js @YOUR-GITHUB-HANDLE
|
||||||
|
vercel.json @YOUR-GITHUB-HANDLE
|
||||||
|
|
||||||
|
# Agent context
|
||||||
|
.cursor/agents/** @YOUR-GITHUB-HANDLE
|
||||||
|
.cursor/rules/** @YOUR-GITHUB-HANDLE
|
||||||
|
.cursor/skills/** @YOUR-GITHUB-HANDLE
|
||||||
|
AGENTS.md @YOUR-GITHUB-HANDLE
|
||||||
|
.agent-context-manifest.yml @YOUR-GITHUB-HANDLE
|
||||||
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!--
|
||||||
|
pipeline: convoy=<slug>, brief=<N>
|
||||||
|
skip: <comma-separated flags or empty>
|
||||||
|
|
||||||
|
Skip flags (Conductor sets these — do not edit by hand):
|
||||||
|
ia, ux, arch, test, review, visual, a11y, design, smoke, qa, docs, flag
|
||||||
|
Never skip: plan-approval, pr-merge, prod-promote
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- 2-3 bullets: what changed and why. User-facing language preferred. -->
|
||||||
|
|
||||||
|
## Convoy + Brief
|
||||||
|
|
||||||
|
- Convoy: `.convoys/<slug>.md`
|
||||||
|
- Brief: `.convoys/<slug>/brief-<N>-...md`
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
<!-- Copy from the brief; check off as you complete. -->
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
- [ ] No scope expansion (only files listed in the brief's `files:` were edited)
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
<!-- What was tested, how, and what wasn't tested with rationale. -->
|
||||||
|
|
||||||
|
## Pipeline gates
|
||||||
|
|
||||||
|
<!-- Filled in by CI / role-reviewer. Don't edit. -->
|
||||||
|
|
||||||
|
- [ ] CI: lint, types, build, unit tests
|
||||||
|
- [ ] Visual diff (if UI change)
|
||||||
|
- [ ] A11y audit (if UI change)
|
||||||
|
- [ ] Design-system audit (if UI change)
|
||||||
|
- [ ] Reviewer report
|
||||||
|
- [ ] Smoke on staging (after merge to develop)
|
||||||
|
|
||||||
|
## Notes for reviewer
|
||||||
|
|
||||||
|
<!-- Anything unusual, intentional trade-offs, or follow-ups. -->
|
||||||
164
.github/workflows/agent-context-drift.yml
vendored
Normal file
164
.github/workflows/agent-context-drift.yml
vendored
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
# agent-context drift detection
|
||||||
|
#
|
||||||
|
# Weekly + on-demand check: does this repo's installed agent-pipeline
|
||||||
|
# artifacts match the latest upstream pipeline release?
|
||||||
|
#
|
||||||
|
# - Reads .agent-context-manifest.yml (committed at repo root)
|
||||||
|
# - Clones the pipeline repo at its latest tag
|
||||||
|
# - Compares each tracked artifact's installed_hash to the pipeline source hash
|
||||||
|
# - Compares manifest pipeline_version to pipeline version.txt
|
||||||
|
# - Opens (or updates) an issue titled "agent-context: N files behind v<X>"
|
||||||
|
# if drift is detected
|
||||||
|
#
|
||||||
|
# No auto-fix. The fix workflow is: a human runs `sync-agent-context` in
|
||||||
|
# Cursor and reviews per-file diffs.
|
||||||
|
|
||||||
|
name: agent-context-drift
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Mondays at 13:00 UTC. Adjust to taste.
|
||||||
|
- cron: "0 13 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
drift:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout consumer repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Read manifest
|
||||||
|
id: manifest
|
||||||
|
run: |
|
||||||
|
if [ ! -f .agent-context-manifest.yml ]; then
|
||||||
|
echo "::warning::No .agent-context-manifest.yml — agent-pipeline not installed or pre-v0.3.0. Skipping drift check."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
INSTALLED=$(grep -E '^pipeline_version:' .agent-context-manifest.yml | head -1 | sed -E 's/.*"(.*)".*/\1/')
|
||||||
|
SOURCE=$(grep -E '^pipeline_source:' .agent-context-manifest.yml | head -1 | sed -E 's/.*"(.*)".*/\1/')
|
||||||
|
echo "installed_version=$INSTALLED" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "pipeline_source=$SOURCE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Clone pipeline at latest tag
|
||||||
|
if: steps.manifest.outputs.skip != 'true'
|
||||||
|
id: pipeline
|
||||||
|
run: |
|
||||||
|
PIPELINE_URL="${{ steps.manifest.outputs.pipeline_source }}"
|
||||||
|
# Convert HTTPS URL → clone target. Already in HTTPS form.
|
||||||
|
mkdir -p /tmp/pipeline
|
||||||
|
git clone --depth 50 "$PIPELINE_URL" /tmp/pipeline
|
||||||
|
cd /tmp/pipeline
|
||||||
|
LATEST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||||
|
if [ -z "$LATEST_TAG" ]; then
|
||||||
|
echo "::warning::Pipeline repo has no tags. Comparing against main."
|
||||||
|
LATEST_TAG="main"
|
||||||
|
fi
|
||||||
|
git checkout "$LATEST_TAG"
|
||||||
|
PIPELINE_VER=$(cat version.txt | tr -d '[:space:]')
|
||||||
|
echo "tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$PIPELINE_VER" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Compute drift
|
||||||
|
if: steps.manifest.outputs.skip != 'true'
|
||||||
|
id: drift
|
||||||
|
run: |
|
||||||
|
INSTALLED="${{ steps.manifest.outputs.installed_version }}"
|
||||||
|
UPSTREAM="${{ steps.pipeline.outputs.version }}"
|
||||||
|
BEHIND=0
|
||||||
|
CUSTOMIZED=0
|
||||||
|
CONFLICT=0
|
||||||
|
|
||||||
|
# Walk manifest artifacts. For each, compare installed_hash to local
|
||||||
|
# current hash, and pipeline source hash to installed_hash.
|
||||||
|
# YAML parsing in bash is intentionally minimal — relies on the
|
||||||
|
# bootstrap skill emitting a predictable shape.
|
||||||
|
python3 - <<'PY' >> drift-report.md
|
||||||
|
import hashlib, sys, yaml, os
|
||||||
|
|
||||||
|
def sha(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(8192), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return "sha256:" + h.hexdigest()
|
||||||
|
|
||||||
|
with open(".agent-context-manifest.yml") as f:
|
||||||
|
m = yaml.safe_load(f)
|
||||||
|
|
||||||
|
counts = {"behind": [], "customized": [], "conflict": [], "deleted": []}
|
||||||
|
for art in m.get("artifacts", []):
|
||||||
|
local_hash = sha(art["path"])
|
||||||
|
pipe_hash = sha(os.path.join("/tmp/pipeline", art["source"]))
|
||||||
|
if local_hash is None:
|
||||||
|
counts["deleted"].append(art["path"])
|
||||||
|
continue
|
||||||
|
local_matches = local_hash == art["installed_hash"]
|
||||||
|
pipe_changed = pipe_hash is not None and pipe_hash != art["installed_hash"]
|
||||||
|
if local_matches and pipe_changed:
|
||||||
|
counts["behind"].append(art["path"])
|
||||||
|
elif not local_matches and pipe_changed:
|
||||||
|
counts["conflict"].append(art["path"])
|
||||||
|
elif not local_matches:
|
||||||
|
counts["customized"].append(art["path"])
|
||||||
|
|
||||||
|
print("# agent-context drift report")
|
||||||
|
print(f"\nInstalled: `{m.get('pipeline_version')}` · Upstream: `${{ steps.pipeline.outputs.version }}` (`${{ steps.pipeline.outputs.tag }}`)")
|
||||||
|
for kind in ("behind", "conflict", "customized", "deleted"):
|
||||||
|
files = counts[kind]
|
||||||
|
if files:
|
||||||
|
print(f"\n## {kind} ({len(files)})")
|
||||||
|
for f in files:
|
||||||
|
print(f"- `{f}`")
|
||||||
|
PY
|
||||||
|
|
||||||
|
BEHIND=$(grep -c '^## behind' drift-report.md || echo 0)
|
||||||
|
CONFLICT=$(grep -c '^## conflict' drift-report.md || echo 0)
|
||||||
|
NEED_ISSUE="false"
|
||||||
|
if [ "$INSTALLED" != "$UPSTREAM" ] || [ "$BEHIND" -gt 0 ] || [ "$CONFLICT" -gt 0 ]; then
|
||||||
|
NEED_ISSUE="true"
|
||||||
|
fi
|
||||||
|
echo "need_issue=$NEED_ISSUE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "upstream_version=$UPSTREAM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Open / update drift issue
|
||||||
|
if: steps.manifest.outputs.skip != 'true' && steps.drift.outputs.need_issue == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const body = fs.readFileSync('drift-report.md', 'utf8') +
|
||||||
|
'\n\n---\n\n_To resolve: open this repo in Cursor and ask **"Sync agent context for this repo"**. The sync skill walks the diff per file._';
|
||||||
|
const title = `agent-context: behind ${{ steps.drift.outputs.upstream_version }}`;
|
||||||
|
const existing = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
labels: 'agent-context-drift',
|
||||||
|
});
|
||||||
|
const found = existing.data.find(i => i.title.startsWith('agent-context: behind'));
|
||||||
|
if (found) {
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: found.number,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
labels: ['agent-context-drift'],
|
||||||
|
});
|
||||||
|
}
|
||||||
91
.github/workflows/ci.yml
vendored
Normal file
91
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
# Vercel variant: Vercel builds Preview deployments on every push and gates the
|
||||||
|
# PR via the Vercel GitHub integration check. Running `npm run build` here too
|
||||||
|
# would duplicate Vercel's work for ~3-5 minutes per PR with no added signal.
|
||||||
|
#
|
||||||
|
# What this CI covers (and Vercel does not):
|
||||||
|
# - Lint (cheap belt-and-suspenders)
|
||||||
|
# - Schema-map drift check (docs/SCHEMA_MAP.md updated when scripts/add-*.js changes)
|
||||||
|
#
|
||||||
|
# NOTE: tcg-vault has no test runner installed yet. Re-enable the `test:` job
|
||||||
|
# below once vitest (or equivalent) is adopted AND a `test:run` script exists
|
||||||
|
# in package.json. See .convoys/ for the testing convoy.
|
||||||
|
#
|
||||||
|
# NOTE: tcg-vault is JavaScript (not TypeScript). No `npx tsc --noEmit` step.
|
||||||
|
# Re-enable a type-check job if migrating to TypeScript.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '20'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint --if-present
|
||||||
|
|
||||||
|
schema-map-fresh:
|
||||||
|
name: Schema map up to date
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run when migration scripts or the schema map itself changed.
|
||||||
|
# If neither changed, nothing to verify.
|
||||||
|
if: |
|
||||||
|
contains(github.event.pull_request.changed_files, 'scripts/add-') ||
|
||||||
|
contains(github.event.pull_request.changed_files, 'scripts/fix-') ||
|
||||||
|
contains(github.event.pull_request.changed_files, 'scripts/setup-neon-db.js') ||
|
||||||
|
contains(github.event.pull_request.changed_files, 'docs/SCHEMA_MAP.md')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Verify schema map updated alongside migration scripts
|
||||||
|
run: |
|
||||||
|
MIGRATION_CHANGED=false
|
||||||
|
MAP_CHANGED=false
|
||||||
|
if git diff --name-only HEAD~1 | grep -qE '^scripts/(add-|fix-|setup-neon-db\.js)'; then
|
||||||
|
MIGRATION_CHANGED=true
|
||||||
|
fi
|
||||||
|
if git diff --name-only HEAD~1 | grep -q '^docs/SCHEMA_MAP\.md$'; then
|
||||||
|
MAP_CHANGED=true
|
||||||
|
fi
|
||||||
|
if [ "$MIGRATION_CHANGED" = "true" ] && [ "$MAP_CHANGED" = "false" ]; then
|
||||||
|
echo "::error::A migration script changed but docs/SCHEMA_MAP.md was not updated."
|
||||||
|
echo "Update docs/SCHEMA_MAP.md to reflect the schema change, then re-push."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: schema map and migration scripts are in sync."
|
||||||
|
|
||||||
|
# test:
|
||||||
|
# Disabled until a test runner is adopted. Re-enable as:
|
||||||
|
#
|
||||||
|
# test:
|
||||||
|
# name: Unit + integration tests
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - uses: actions/setup-node@v4
|
||||||
|
# with:
|
||||||
|
# node-version: ${{ env.NODE_VERSION }}
|
||||||
|
# cache: npm
|
||||||
|
# - run: npm ci
|
||||||
|
# - run: npm run test:run
|
||||||
|
# env:
|
||||||
|
# JWT_SECRET: ci-secret-only-for-tests
|
||||||
|
# POSTGRES_URL: postgres://ci:ci@localhost:5432/ci
|
||||||
97
.github/workflows/pr-health-rollup.yml
vendored
Normal file
97
.github/workflows/pr-health-rollup.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
name: PR Health rollup
|
||||||
|
|
||||||
|
# Rolls up CI gates AND the Vercel deployment status posted by the Vercel
|
||||||
|
# GitHub integration. Build status comes from Vercel, not from our own CI.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||||
|
workflow_run:
|
||||||
|
workflows: [CI, Preview smoke, Visual diff]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
checks: read
|
||||||
|
deployments: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rollup:
|
||||||
|
name: Aggregate gate status
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Compute status + post sticky comment
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pr_number = context.payload.pull_request?.number
|
||||||
|
?? context.payload.workflow_run?.pull_requests?.[0]?.number;
|
||||||
|
|
||||||
|
if (!pr_number) {
|
||||||
|
core.info('No PR context — skipping rollup.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = (await github.rest.pulls.get({ owner, repo, pull_number: pr_number })).data;
|
||||||
|
const sha = pr.head.sha;
|
||||||
|
|
||||||
|
const checks = (await github.rest.checks.listForRef({ owner, repo, ref: sha, per_page: 100 })).data.check_runs;
|
||||||
|
const find = (name) => checks.find(c => c.name === name);
|
||||||
|
|
||||||
|
const vercelCheck = checks.find(c => /^vercel/i.test(c.name));
|
||||||
|
|
||||||
|
const skip = (flag) =>
|
||||||
|
new RegExp(`pipeline:.*skip[^\\n]*\\b${flag}\\b`).test(pr.body || '');
|
||||||
|
|
||||||
|
const row = (label, run, opt = false) => {
|
||||||
|
if (!run) return `| ${label} | ${opt ? '⏭ skipped or pending' : '⏳ pending'} |`;
|
||||||
|
if (run.status !== 'completed') return `| ${label} | ⏳ in progress |`;
|
||||||
|
const ok = run.conclusion === 'success';
|
||||||
|
return `| ${label} | ${ok ? '✅ pass' : '❌ ' + run.conclusion} |`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
row('Vercel build (Preview)', vercelCheck),
|
||||||
|
row('CI: Lint', find('Lint')),
|
||||||
|
row('CI: Schema map fresh', find('Schema map up to date'), true),
|
||||||
|
skip('smoke') ? '| Preview smoke | ⏭ skipped (pipeline directive) |' : row('Preview smoke', find('Playwright smoke'), true),
|
||||||
|
skip('visual') ? '| Visual diff | ⏭ skipped (pipeline directive) |' : row('Visual diff', find('Screenshot diff'), true),
|
||||||
|
];
|
||||||
|
|
||||||
|
const reviewer_comment = (await github.rest.issues.listComments({
|
||||||
|
owner, repo, issue_number: pr_number, per_page: 100,
|
||||||
|
})).data.find(c => c.body?.startsWith('## Reviewer Report'));
|
||||||
|
|
||||||
|
const a11y_comment = (await github.rest.issues.listComments({
|
||||||
|
owner, repo, issue_number: pr_number, per_page: 100,
|
||||||
|
})).data.find(c => c.body?.startsWith('## A11y Audit'));
|
||||||
|
|
||||||
|
const ds_comment = (await github.rest.issues.listComments({
|
||||||
|
owner, repo, issue_number: pr_number, per_page: 100,
|
||||||
|
})).data.find(c => c.body?.startsWith('## Design System Audit'));
|
||||||
|
|
||||||
|
const role_row = (label, c, skipped) =>
|
||||||
|
skipped ? `| ${label} | ⏭ skipped |` : c ? `| ${label} | ✅ posted |` : `| ${label} | ⏳ pending |`;
|
||||||
|
|
||||||
|
const role_rows = [
|
||||||
|
role_row('Reviewer report', reviewer_comment, skip('review')),
|
||||||
|
role_row('A11y audit', a11y_comment, skip('a11y')),
|
||||||
|
role_row('Design system audit', ds_comment, skip('design')),
|
||||||
|
];
|
||||||
|
|
||||||
|
const marker = '<!-- pipeline-rollup -->';
|
||||||
|
const body = `${marker}\n## Pipeline Health\n\n### Build + CI gates\n\n| Gate | Status |\n| --- | --- |\n${rows.join('\n')}\n\n_Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build)._\n\n### Role reports\n\n| Role | Status |\n| --- | --- |\n${role_rows.join('\n')}\n\nSee individual comments above for details. This rollup updates automatically.`;
|
||||||
|
|
||||||
|
const comments = (await github.rest.issues.listComments({
|
||||||
|
owner, repo, issue_number: pr_number, per_page: 100,
|
||||||
|
})).data;
|
||||||
|
const existing = comments.find(c => c.body?.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: pr_number, body });
|
||||||
|
}
|
||||||
76
.github/workflows/preview-smoke.yml
vendored
Normal file
76
.github/workflows/preview-smoke.yml
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
name: Preview smoke
|
||||||
|
|
||||||
|
# Waits for Vercel's per-PR Preview deployment to be ready, then runs
|
||||||
|
# Playwright smoke against its URL.
|
||||||
|
#
|
||||||
|
# Vercel's GitHub integration auto-deploys every push and posts a Deployment
|
||||||
|
# to the GitHub API once ready. We wait on that Deployment so we always hit
|
||||||
|
# the canonical preview URL Vercel just published.
|
||||||
|
#
|
||||||
|
# REQUIRES: @playwright/test installed. Until then, this workflow will fail
|
||||||
|
# on `npx playwright install`. See .convoys/ for the testing convoy.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: preview-smoke-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gate:
|
||||||
|
name: Should run?
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
|
steps:
|
||||||
|
- name: Decide
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if echo "${{ github.event.pull_request.body }}" | grep -qE 'pipeline:.*skip.*\bsmoke\b'; then
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "::notice::Smoke skipped via pipeline directive"
|
||||||
|
else
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
smoke:
|
||||||
|
name: Playwright smoke
|
||||||
|
needs: gate
|
||||||
|
if: needs.gate.outputs.should_run == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Wait for Vercel Preview deployment
|
||||||
|
id: vercel
|
||||||
|
uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
max_timeout: 600
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run smoke tests
|
||||||
|
run: npx playwright test --project=smoke
|
||||||
|
env:
|
||||||
|
BASE_URL: ${{ steps.vercel.outputs.url }}
|
||||||
|
|
||||||
|
- name: Upload Playwright report on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 7
|
||||||
88
.github/workflows/visual-diff.yml
vendored
Normal file
88
.github/workflows/visual-diff.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
name: Visual diff
|
||||||
|
|
||||||
|
# Same as preview-smoke — waits for Vercel's Preview deployment, then captures
|
||||||
|
# Playwright screenshots against it. UI-paths-only trigger to keep cost down.
|
||||||
|
# Paths are tcg-vault-specific (pages router, JS).
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'pages/**'
|
||||||
|
- 'components/**'
|
||||||
|
- 'styles/**'
|
||||||
|
- 'tailwind.config.js'
|
||||||
|
- 'postcss.config.js'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: visual-diff-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gate:
|
||||||
|
name: Should run?
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
|
steps:
|
||||||
|
- id: check
|
||||||
|
run: |
|
||||||
|
if echo "${{ github.event.pull_request.body }}" | grep -qE 'pipeline:.*skip.*\bvisual\b'; then
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "::notice::Visual diff skipped via pipeline directive"
|
||||||
|
else
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
visual:
|
||||||
|
name: Screenshot diff
|
||||||
|
needs: gate
|
||||||
|
if: needs.gate.outputs.should_run == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Wait for Vercel Preview deployment
|
||||||
|
id: vercel
|
||||||
|
uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
max_timeout: 600
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Capture screenshots (PR)
|
||||||
|
run: npx playwright test --project=visual --update-snapshots=none
|
||||||
|
env:
|
||||||
|
BASE_URL: ${{ steps.vercel.outputs.url }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Upload screenshots + diffs
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: visual-diff
|
||||||
|
path: |
|
||||||
|
tests/visual/__screenshots__/
|
||||||
|
test-results/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Comment on PR with diff link
|
||||||
|
if: always()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `## Visual Diff\n\nScreenshots and diffs uploaded as artifacts: [view run](${run})\n\nIf intentional changes: update snapshots locally with \`npx playwright test --project=visual --update-snapshots\` and commit.`
|
||||||
|
});
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -29,3 +29,10 @@ __pycache__/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
# Agent pipeline self-analytics (per-developer; opt-out by default).
|
||||||
|
# Remove this line if you want team-shared convoy metrics committed.
|
||||||
|
.convoys/.metrics.jsonl
|
||||||
|
|
||||||
|
# Local code-knowledge-graph (per-developer; user-code-review-graph MCP)
|
||||||
|
.code-review-graph/
|
||||||
|
|
|
||||||
72
AGENTS.md
Normal file
72
AGENTS.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# AGENTS.md — AI collaboration (tcg-vault)
|
||||||
|
|
||||||
|
Guidance for agents and humans working in this repo. Prefer existing patterns over new abstractions.
|
||||||
|
|
||||||
|
> Branding note: the repo, README, and seed data say "TCG Vault" and `admin@tcgvault.com`, but the Layout component renders "Deck Hearth". Pick one before launch — see `.convoys/` for tracking.
|
||||||
|
|
||||||
|
## 1. Project overview
|
||||||
|
|
||||||
|
A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database.
|
||||||
|
|
||||||
|
- **Framework:** Next.js 15 (Pages router) + React 18, JavaScript (not TypeScript)
|
||||||
|
- **Data:** Neon Postgres, accessed two different ways — `@neondatabase/serverless` (`lib/database.js`) AND raw `@vercel/postgres` (`pages/api/**`). Pick ONE; see Gotcha #1.
|
||||||
|
- **Auth:** Custom JWT (jsonwebtoken + bcryptjs), token stored in `localStorage`, sent as `Authorization: Bearer …`. No NextAuth.
|
||||||
|
- **UI:** Tailwind CSS + custom CSS variables for theming (light/dark via `lib/theme-context.js`)
|
||||||
|
- **Hosting:** Vercel (`vercel.json`, `.vercel/` present)
|
||||||
|
|
||||||
|
## 2. Architecture quick reference
|
||||||
|
|
||||||
|
| Area | Path | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Pages router views | `pages/*.js` | Public + auth views; uses `components/Layout.js` |
|
||||||
|
| API routes | `pages/api/**/*.js` | Express-style `handler(req, res)`. **30+ handlers depend on `lib/permission-middleware.js::getUserFromRequest`** |
|
||||||
|
| Shared UI | `components/*.js` | `Layout`, `CardItem`, `CameraScanner`, modal family |
|
||||||
|
| Auth + DB libs | `lib/*.js` | `auth-context`, `admin-auth`, `use-auth` (three parallel auth surfaces), `database`, `permission-middleware` |
|
||||||
|
| Migration scripts | `scripts/*.js` | 27+ one-off "add column" / "seed" scripts. No formal migration tool |
|
||||||
|
| Card-import jobs | `pages/api/cards/import-*.js`, `scripts/import-*.js` | Scryfall / Lorcana / Pokémon TCG APIs |
|
||||||
|
| Database schema | `scripts/setup-neon-db.js` | Bootstrap SQL DDL — the source of truth until a real migration tool lands |
|
||||||
|
| Schema map | `docs/SCHEMA_MAP.md` | Hand-curated; regenerate after schema changes |
|
||||||
|
|
||||||
|
Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 5602 edges). Ask: *"what calls `getUserFromRequest`?"* before refactoring auth.
|
||||||
|
|
||||||
|
## 3. Key conventions
|
||||||
|
|
||||||
|
- **Auth (server):** `import { getUserFromRequest } from '../../lib/permission-middleware'` → returns `{ userId, email, role }` or `null`. **IMPORTANT: the current implementation returns a hardcoded admin user when no Bearer token is present — treat that as a known prod bug, do NOT copy the pattern.**
|
||||||
|
- **Auth (client):** `import { useAuth } from '../lib/use-auth'`. Avoid `lib/auth-context.js` and `lib/admin-auth.js` for new code — they are legacy parallel implementations.
|
||||||
|
- **Auth helper (JWT only):** `import { ... } from '../../lib/api/auth-utils'` (`generateToken`, `verifyToken`, `hashPassword`, `verifyPassword`).
|
||||||
|
- **Permission gate for collection routes:** wrap handlers with `withCollectionPermission('viewer' | 'editor' | 'owner')` from `lib/permission-middleware.js`.
|
||||||
|
- **DB access:** Use **tagged-template** style — `import { sql } from '@vercel/postgres'`. Avoid the legacy `lib/database.js` `db.query(string, params)` API; its parameter interpolation uses `sql.unsafe` and is a SQL-injection vector.
|
||||||
|
- **Activity logging:** `logCollectionActivity(collectionId, userId, action, details)` — call it from any handler that mutates a collection.
|
||||||
|
- **File names:** `kebab-case.js` for libs/scripts; `PascalCase.js` for React components.
|
||||||
|
- **Imports:** No path aliases configured; use relative imports.
|
||||||
|
- **Slugs:** `lib/slug-utils.js::generateUniqueSlug` for any user-facing identifier (collections, decks).
|
||||||
|
- **CSS theme tokens:** Components read `var(--bg-primary)`, `var(--text-primary)`, `var(--accent-ember)`, etc. — defined in `styles/`. Don't hardcode hex colors.
|
||||||
|
|
||||||
|
## 4. Common gotchas
|
||||||
|
|
||||||
|
- **#1 — Two SQL clients live in parallel.** `@neondatabase/serverless` (used by `lib/database.js`) and `@vercel/postgres` (used by most `pages/api/**` handlers). New code: prefer `@vercel/postgres` tagged templates. Migration to a single client is tracked in `.convoys/`.
|
||||||
|
- **#2 — `getUserFromRequest` has a dev fallback shipped to prod.** When no Bearer token is present it returns user 1 as admin. This is a critical security issue, NOT a feature. Don't rely on it; treat unauthenticated requests as 401.
|
||||||
|
- **#3 — JWT_SECRET default is hardcoded across 7 files.** If `process.env.JWT_SECRET` is unset, tokens are signed with `'your-secret-key-change-in-production'`. The Vercel project MUST set `JWT_SECRET`; CI/staging too.
|
||||||
|
- **#4 — Default admin credentials are in the seed.** `admin@tcgvault.com` / `admin123` from `scripts/setup-neon-db.js`. Change the password immediately after running setup.
|
||||||
|
- **#5 — `pages/api/setup-database.js` is a public endpoint.** Anyone hitting it triggers DB DDL. Either delete or gate behind admin auth before public launch.
|
||||||
|
- **#6 — Migrations are bare scripts.** `scripts/add-*.js` and `scripts/fix-*.js` are run-once jobs with no idempotency tracking. Adopt `node-pg-migrate`, `kysely`, or `drizzle-kit` before more schema changes.
|
||||||
|
- **#7 — Dual `is_public` semantics.** Collections and decks both have `is_public` columns; check which controls discovery vs. anonymous read in the relevant route.
|
||||||
|
- **#8 — Layout has hardcoded default user.** `Layout({ user = { email: 'me@randallstillwell.com', role: 'user' } })`. Anything rendering Layout without passing `user` will impersonate the maintainer. Pass `user` explicitly from every page.
|
||||||
|
|
||||||
|
## 5. Running locally
|
||||||
|
|
||||||
|
- **Runtime:** Node 20 (Vercel default).
|
||||||
|
- **Setup:** `npm install`, copy `.env.local` template (POSTGRES_URL + JWT_SECRET + RESEND_API_KEY + BLOB_READ_WRITE_TOKEN), then `npm run setup-db` once.
|
||||||
|
- **Dev server:** `npm run dev` → http://localhost:3000.
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
- **Runner:** None yet. Adding `vitest` + `@playwright/test` is in `.convoys/`. Until then: manual smoke per `TESTING_GUIDE.md`.
|
||||||
|
|
||||||
|
## 7. Deployment
|
||||||
|
|
||||||
|
- **Vercel** auto-deploys `main` and creates Preview deployments for every PR. `vercel.json` and `.vercel/` are committed. CI in `.github/workflows/` runs lint + types (no duplicate build — Vercel handles it).
|
||||||
|
|
||||||
|
## 8. Code graph
|
||||||
|
|
||||||
|
A local code-knowledge-graph MCP server (`user-code-review-graph`) is set up for this repo. Ask "what calls X?" or "show me the flow from /api/auth/login" instead of grepping. See [`docs/agent-context/README.md`](docs/agent-context/README.md).
|
||||||
181
docs/SCHEMA_MAP.md
Normal file
181
docs/SCHEMA_MAP.md
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
# SCHEMA_MAP.md
|
||||||
|
|
||||||
|
> Hand-curated reference for the Neon Postgres schema. The actual schema is the union of `scripts/setup-neon-db.js` (initial DDL) plus every `scripts/add-*.js` / `scripts/fix-*.js` that has been run. Until a real migration tool is adopted, this file is the source of truth for agents and humans.
|
||||||
|
>
|
||||||
|
> **Last reviewed:** 2026-05-22 against `scripts/setup-neon-db.js` + every `scripts/add-*.js` and `scripts/fix-*.js` in repo HEAD.
|
||||||
|
|
||||||
|
## Quick model groups
|
||||||
|
|
||||||
|
| Group | Tables | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Identity** | `users`, `user_settings`, `user_avatars` | Accounts, profile, preferences |
|
||||||
|
| **Catalog** | `cards` | Master card list across MTG / Pokémon / Lorcana |
|
||||||
|
| **Ownership** | `user_cards`, `user_favorites` | What a user owns / has favorited |
|
||||||
|
| **Collections** | `collections`, `collection_cards`, `collection_permissions`, `collection_activity` | Curated card lists with sharing |
|
||||||
|
| **Decks** | `decks`, `deck_cards` | Playable deck definitions |
|
||||||
|
| **Invitations** | `invitations` (referenced; verify) | Pending share requests |
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
### users
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `email` | `VARCHAR(255) UNIQUE NOT NULL` | Lowercase before query/insert (no CI collation set) |
|
||||||
|
| `password` | `VARCHAR(255) NOT NULL` | bcrypt hash, cost 12 |
|
||||||
|
| `role` | `VARCHAR(50)` default `'user'` | `'user' \| 'admin'` |
|
||||||
|
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
|
||||||
|
| `first_name`, `last_name` | `VARCHAR(255)` | From `add-user-profile-columns.js` |
|
||||||
|
| `username` | `VARCHAR(255) UNIQUE` | |
|
||||||
|
| `profile_image_url`, `avatar_url` | `TEXT` | Two redundant columns; verify which is canonical |
|
||||||
|
| `bio` | `TEXT` | |
|
||||||
|
| `favorite_games` | `JSONB` default `'["MTG"]'` | Per-user game preference array |
|
||||||
|
| `collection_visibility` | `VARCHAR(20)` default `'private'` | |
|
||||||
|
| `preferred_currency` | `VARCHAR(3)` default `'USD'` | |
|
||||||
|
| `cards_per_page` | `INTEGER` default `50` | |
|
||||||
|
| `default_view` | `VARCHAR(10)` default `'grid'` | |
|
||||||
|
| `notifications_email` | `BOOLEAN` default `true` | |
|
||||||
|
| `notifications_marketing` | `BOOLEAN` default `false` | |
|
||||||
|
| `two_factor_enabled` | `BOOLEAN` default `false` | Not implemented yet |
|
||||||
|
| `theme` | `VARCHAR(10)` default `'system'` | `'light' \| 'dark' \| 'system'` |
|
||||||
|
| `language` | `VARCHAR(5)` default `'en'` | |
|
||||||
|
| `is_pending` | `BOOLEAN` default `false` | Set by invitation flow before signup completes |
|
||||||
|
|
||||||
|
### cards
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `name` | `VARCHAR(255) NOT NULL` | |
|
||||||
|
| `set_name`, `set_code`, `card_number` | `VARCHAR` | |
|
||||||
|
| `rarity` | `VARCHAR(50)` | |
|
||||||
|
| `game` | `VARCHAR(50) NOT NULL` | `'mtg' \| 'pokemon' \| 'lorcana'` |
|
||||||
|
| `mana_cost` | `VARCHAR(50)` | MTG only |
|
||||||
|
| `cmc` | `INTEGER` | MTG only |
|
||||||
|
| `card_type` | `VARCHAR(255)` | |
|
||||||
|
| `colors` | `JSONB` | MTG color array |
|
||||||
|
| `oracle_text` | `TEXT` | LARGE — never `SELECT *` |
|
||||||
|
| `power`, `toughness` | `VARCHAR(10)` | MTG creatures |
|
||||||
|
| `image_url`, `stock_image_url` | `TEXT` | |
|
||||||
|
| `current_price`, `market_price` | `DECIMAL(10,2)` | |
|
||||||
|
| `scryfall_id` | `VARCHAR(255) UNIQUE` | Use for dedupe on MTG import |
|
||||||
|
| `verified` | `BOOLEAN` default `false` | Admin-edited cards |
|
||||||
|
| `quantity` | `INTEGER` default `0` | **Unused; consider dropping — quantity lives in `user_cards`** |
|
||||||
|
| `favorited` | `BOOLEAN` default `false` | **Unused; favorites live in `user_favorites`** |
|
||||||
|
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
|
||||||
|
|
||||||
|
### user_cards
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
|
||||||
|
| `card_id` | `INTEGER FK cards(id) ON DELETE CASCADE` | |
|
||||||
|
| `quantity` | `INTEGER` default `1` | |
|
||||||
|
| `condition` | `VARCHAR(50)` default `'NM'` | NM / LP / MP / HP / DMG |
|
||||||
|
| `is_foil` | `BOOLEAN` default `false` | |
|
||||||
|
| `notes` | `TEXT` | |
|
||||||
|
| | | **UNIQUE(user_id, card_id, is_foil)** |
|
||||||
|
|
||||||
|
### user_favorites
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `user_id`, `card_id` | FKs cascade | |
|
||||||
|
| `created_at` | `TIMESTAMP` | |
|
||||||
|
| | | **UNIQUE(user_id, card_id)** (verify constraint exists) |
|
||||||
|
|
||||||
|
### collections
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
|
||||||
|
| `name` | `VARCHAR(255) NOT NULL` | |
|
||||||
|
| `description` | `TEXT` | |
|
||||||
|
| `is_public` | `BOOLEAN` default `false` | Used by `withCollectionPermission('viewer')` |
|
||||||
|
| `slug` | `VARCHAR(100) UNIQUE` | Format constraint: lowercase kebab, ≤50 chars |
|
||||||
|
| `image` | `TEXT` | Cover image |
|
||||||
|
| `visibility` | `VARCHAR(20)` default `'private'` | **Coexists with `is_public`; verify single source of truth** |
|
||||||
|
| `tcg` | `VARCHAR(50)` default `'MTG'` | |
|
||||||
|
| `tags` | `TEXT` | Comma-separated; consider migrating to JSONB array |
|
||||||
|
| `is_system_collection` | `BOOLEAN` default `false` | E.g. "All My Cards" auto-collection |
|
||||||
|
| `created_at`, `updated_at` | `TIMESTAMP` | |
|
||||||
|
|
||||||
|
### collection_cards
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `collection_id`, `card_id` | FKs cascade | |
|
||||||
|
| `quantity` | `INTEGER` default `1` | |
|
||||||
|
| | | **UNIQUE(collection_id, card_id)** |
|
||||||
|
|
||||||
|
### collection_permissions
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `collection_id`, `user_id` | FKs cascade | |
|
||||||
|
| `role` | `VARCHAR` | `'viewer' \| 'editor' \| 'owner'` |
|
||||||
|
| `status` | `VARCHAR` | `'pending' \| 'active' \| 'declined'` — used by invite flow |
|
||||||
|
| `created_at` | `TIMESTAMP` | |
|
||||||
|
|
||||||
|
### collection_activity
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `collection_id`, `user_id` | FKs | |
|
||||||
|
| `action` | `VARCHAR` | e.g. `'card_added'`, `'permission_granted'` |
|
||||||
|
| `details` | `JSONB` | |
|
||||||
|
| `created_at` | `TIMESTAMP` | |
|
||||||
|
|
||||||
|
### decks
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `user_id` | FK | |
|
||||||
|
| `name`, `description` | text | |
|
||||||
|
| `game` | `VARCHAR(50)` | |
|
||||||
|
| `is_public` | `BOOLEAN` default `false` | |
|
||||||
|
| `created_at`, `updated_at` | `TIMESTAMP` | |
|
||||||
|
|
||||||
|
### deck_cards
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `SERIAL PK` | |
|
||||||
|
| `deck_id`, `card_id` | FKs cascade | |
|
||||||
|
| `quantity` | `INTEGER` default `1` | |
|
||||||
|
| | | **UNIQUE(deck_id, card_id)** |
|
||||||
|
|
||||||
|
### user_settings (split from users.*; verify which is canonical)
|
||||||
|
|
||||||
|
Defined in `add-user-profile-fields.js`. Mirrors several `users.*` columns — there's redundancy that needs to be reconciled.
|
||||||
|
|
||||||
|
### user_avatars
|
||||||
|
|
||||||
|
Tracks uploaded avatar history. Older avatars are typically deleted from blob storage; verify the cleanup job runs.
|
||||||
|
|
||||||
|
## Known schema smells
|
||||||
|
|
||||||
|
1. **Two `users` avatar columns** — `profile_image_url` and `avatar_url`. Pick one.
|
||||||
|
2. **Two collection-visibility flags** — `collections.is_public` (BOOLEAN) and `collections.visibility` (VARCHAR). Pick one.
|
||||||
|
3. **`cards.quantity` + `cards.favorited`** — these belong on `user_cards` / `user_favorites`, not the catalog. Drop them.
|
||||||
|
4. **`user_settings` ↔ `users.*`** — split-brain. Reconcile.
|
||||||
|
5. **No formal constraints on enums** — `role`, `condition`, `theme`, `language`, `game`, `visibility` are all `VARCHAR`. Consider CHECK constraints or proper ENUMs.
|
||||||
|
6. **`collections.tags` is `TEXT`** — should be `JSONB` or a join table.
|
||||||
|
|
||||||
|
## Regeneration
|
||||||
|
|
||||||
|
Until a migration tool lands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg "ALTER TABLE|CREATE TABLE|ADD COLUMN" scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
…then update this file by hand. A `npm run schema:map` script regenerated from the migration history is in `.convoys/`.
|
||||||
78
docs/agent-context/README.md
Normal file
78
docs/agent-context/README.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Agent Context System
|
||||||
|
|
||||||
|
A three-layer system that gives AI coding agents fast, accurate orientation in this repo so they can start coding immediately instead of grepping a thousand files.
|
||||||
|
|
||||||
|
## The three layers
|
||||||
|
|
||||||
|
| Layer | Location | What it does | Token cost |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Intent** | [`AGENTS.md`](../../AGENTS.md) | High-level architecture, conventions, gotchas — always loaded | Always on |
|
||||||
|
| **Per-context guidance** | [`.cursor/rules/*.mdc`](../../.cursor/rules) | Glob-scoped rules (e.g. only loaded when editing matching files) | Loaded only when matching files are open |
|
||||||
|
| **On-demand recipes** | [`.cursor/skills/*`](../../.cursor/skills) | Step-by-step skills the agent reads when its description matches | Loaded only when invoked |
|
||||||
|
| **Schema map** | [`docs/SCHEMA_MAP.md`](../SCHEMA_MAP.md) | Hand-curated DB table reference for token-efficient agent queries | Loaded only when read |
|
||||||
|
|
||||||
|
## Plus a fourth: the structural brain
|
||||||
|
|
||||||
|
This repo also indexes itself with the [`user-code-review-graph`](https://github.com/varutasu/code-review-graph) MCP server. It runs locally per-developer and parses the codebase with Tree-sitter into a queryable graph of files, functions, classes, calls, and imports.
|
||||||
|
|
||||||
|
| You can ask… | …and get |
|
||||||
|
| --- | --- |
|
||||||
|
| "What calls `getUserFromRequest`?" | A list of every handler that depends on the auth fallback bug |
|
||||||
|
| "Where is `withCollectionPermission` used?" | Every collection-scoped route |
|
||||||
|
| "Show me the flow from `/api/auth/login`" | Call graph with imports and DB writes |
|
||||||
|
| "Find functions over 100 lines" | Refactor targets |
|
||||||
|
| "Show me bridge nodes between communities" | High-coupling functions to test carefully |
|
||||||
|
|
||||||
|
To use it in this repo:
|
||||||
|
|
||||||
|
1. Install + start the MCP server (`brew install pipx && pipx install code-review-graph` or per the project's README).
|
||||||
|
2. Open this repo in Cursor.
|
||||||
|
3. The MCP picks up the repo root automatically.
|
||||||
|
4. Ask the agent natural questions — the graph is the data source.
|
||||||
|
|
||||||
|
The graph is per-developer; no `.code-review-graph/` directory ever gets committed (it's already in `.gitignore`).
|
||||||
|
|
||||||
|
## Quickstart for a new task
|
||||||
|
|
||||||
|
1. **Open the file you'll edit.** Cursor automatically loads `AGENTS.md` and any `.cursor/rules/*.mdc` whose `globs:` match.
|
||||||
|
2. **Describe the task.** The agent has the conventions in scope; it does not need to grep for them.
|
||||||
|
3. **Drafting.** Agent proposes the change against the relevant rule's conventions.
|
||||||
|
4. **Verify.** `npm run lint` (no test runner yet — adding `vitest` is in `.convoys/`).
|
||||||
|
|
||||||
|
## How to extend
|
||||||
|
|
||||||
|
| You want to... | Do this |
|
||||||
|
| --- | --- |
|
||||||
|
| Add a new convention scoped to a folder | Create `.cursor/rules/<topic>.mdc` with frontmatter `description` + `globs:` |
|
||||||
|
| Add a step-by-step recipe agents can invoke | Create `.cursor/skills/<name>/SKILL.md` with frontmatter `description` |
|
||||||
|
| Mark a path as "do not touch" | Add it to [`.cursor/rules/no-go-zones.mdc`](../../.cursor/rules/no-go-zones.mdc) |
|
||||||
|
| Regenerate the schema map | Re-read `scripts/setup-neon-db.js` + any new `scripts/add-*.js`, then update `docs/SCHEMA_MAP.md` |
|
||||||
|
| Refresh the code graph | Ask Cursor to call the `build_or_update_graph_tool` MCP tool |
|
||||||
|
|
||||||
|
Always-on rules cost from a shared instruction budget — keep them tight. The "would removing this line cause a mistake the agent wouldn't otherwise make?" test is the gate.
|
||||||
|
|
||||||
|
## Subagent pipeline (L2 + L3)
|
||||||
|
|
||||||
|
In addition to L1 context, this repo has:
|
||||||
|
|
||||||
|
- **L2 — 9 subagent roles** in [`.cursor/agents/`](../../.cursor/agents/) covering the idea → IA → UX → architecture → implementation → review → docs flow.
|
||||||
|
- **L3 — pipeline scaffolding** in `.github/workflows/`, `.github/PULL_REQUEST_TEMPLATE.md`, `.github/CODEOWNERS`, [`.convoys/`](../../.convoys/), `lib/flags/`, and `scripts/log-convoy-event.sh`.
|
||||||
|
|
||||||
|
See [`.convoys/README.md`](../../.convoys/README.md) for how convoys work and how to start one. The full role reference is in the [agent-pipeline repo](https://github.com/varutasu/agent-pipeline).
|
||||||
|
|
||||||
|
## Multitask + worktrees (Cursor 3.2+)
|
||||||
|
|
||||||
|
After role-implementer ships a PR draft:
|
||||||
|
|
||||||
|
```
|
||||||
|
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
|
||||||
|
```
|
||||||
|
|
||||||
|
All three read the same diff and emit independent comments. Use group id `audit-<convoy>-<pr>` so analytics can compute wall-clock savings.
|
||||||
|
|
||||||
|
For parallel implementer briefs (when `slice_dependencies` allows it), use Cursor's native Agents Window worktrees. Full playbook: [agent-pipeline docs/multitask-playbook.md](https://github.com/varutasu/agent-pipeline/blob/main/docs/multitask-playbook.md).
|
||||||
|
|
||||||
|
## What's intentionally NOT here
|
||||||
|
|
||||||
|
- No commits of `.cursor/mcp.json` to this repo (any MCP install is personal-only, in `~/.cursor/mcp.json`).
|
||||||
|
- No measurement protocol yet — the colab repo's -53% token result is documented upstream; we'll measure tcg-vault's number after a few convoys.
|
||||||
71
lib/flags/index.js
Normal file
71
lib/flags/index.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Feature flag wrapper. Lightweight, dependency-free, env-var driven.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { isEnabled } from '../../lib/flags';
|
||||||
|
* if (isEnabled('bookmark_count_badge', { userId: user?.userId })) {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Flag values resolve from env vars: FLAG_<UPPER_SNAKE_NAME>=on|off|<percent>|<comma list of user ids>
|
||||||
|
* Examples:
|
||||||
|
* FLAG_BOOKMARK_COUNT_BADGE=on # everyone
|
||||||
|
* FLAG_BOOKMARK_COUNT_BADGE=off # nobody
|
||||||
|
* FLAG_BOOKMARK_COUNT_BADGE=10 # 10% of users (deterministic per userId)
|
||||||
|
* FLAG_BOOKMARK_COUNT_BADGE=u1,u2,u3 # specific user ids
|
||||||
|
*
|
||||||
|
* For a richer flag system (LaunchDarkly, Statsig, Unleash) replace the
|
||||||
|
* resolver below with an SDK call. The public API (isEnabled) stays the same.
|
||||||
|
*
|
||||||
|
* Pipeline integration: convoys with `skip: flag` in their frontmatter ship
|
||||||
|
* without flag-gating. Convoys without `skip: flag` MUST gate the new code
|
||||||
|
* behind a flag and document the rollout plan in the convoy file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const KNOWN_FLAGS = new Set([
|
||||||
|
// Add flags here as they're created. Helps catch typos.
|
||||||
|
// 'bookmark_count_badge',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function envName(flag) {
|
||||||
|
return `FLAG_${flag.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashUserId(userId, salt) {
|
||||||
|
let h = 0;
|
||||||
|
const s = `${salt}::${userId}`;
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
h = (h * 31 + s.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return Math.abs(h) % 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnabled(flag, ctx = {}) {
|
||||||
|
if (process.env.NODE_ENV !== 'test' && !KNOWN_FLAGS.has(flag)) {
|
||||||
|
if (typeof console !== 'undefined') {
|
||||||
|
console.warn(`[flags] unknown flag '${flag}'. Add it to KNOWN_FLAGS in lib/flags/index.js.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = (ctx.envValue ?? process.env[envName(flag)] ?? 'off').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (raw === 'on' || raw === 'true' || raw === '1') return true;
|
||||||
|
if (raw === 'off' || raw === 'false' || raw === '0' || raw === '') return false;
|
||||||
|
|
||||||
|
const pct = Number(raw);
|
||||||
|
if (!Number.isNaN(pct) && pct >= 0 && pct <= 100) {
|
||||||
|
if (!ctx.userId) return false;
|
||||||
|
return hashUserId(String(ctx.userId), flag) < pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.includes(',') || raw.length > 0) {
|
||||||
|
const ids = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
return Boolean(ctx.userId && ids.includes(String(ctx.userId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listKnownFlags() {
|
||||||
|
return [...KNOWN_FLAGS].sort();
|
||||||
|
}
|
||||||
89
scripts/log-convoy-event.sh
Executable file
89
scripts/log-convoy-event.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# log-convoy-event.sh — append one convoy event to .convoys/.metrics.jsonl.
|
||||||
|
#
|
||||||
|
# Used by L2 roles to emit lightweight metrics for self-analytics.
|
||||||
|
# Schema: github.com/varutasu/agent-pipeline/analytics/schemas/convoy-event.json
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/log-convoy-event.sh role=role-conductor convoy=bookmark-badge \
|
||||||
|
# classification=feature 'skip_flags=visual,smoke' duration_s=42
|
||||||
|
#
|
||||||
|
# All args are key=value. Required: role, convoy.
|
||||||
|
# Optional: brief, classification, skip_flags (comma-separated), duration_s,
|
||||||
|
# stack_class, outcome, multitask_group.
|
||||||
|
#
|
||||||
|
# multitask_group: cohort id when this role ran as part of a Cursor 3.2
|
||||||
|
# /multitask fan-out (e.g. 'audit-bookmark-badge-PR123'). Events sharing
|
||||||
|
# this id should be aggregated with max(duration_s), not sum, for wall-clock.
|
||||||
|
# See docs/multitask-playbook.md.
|
||||||
|
#
|
||||||
|
# Privacy: this file is gitignored by default; events contain only metadata,
|
||||||
|
# no code or prompts. To opt-in to commit, remove `.convoys/.metrics.jsonl`
|
||||||
|
# from your `.gitignore`.
|
||||||
|
#
|
||||||
|
# Atomicity: concurrent invocations append safely because each python3
|
||||||
|
# subprocess writes one short JSON line via O_APPEND. POSIX guarantees
|
||||||
|
# writes <= PIPE_BUF are atomic on regular files opened with O_APPEND.
|
||||||
|
# Typical line size is 200-400 bytes; PIPE_BUF is 4096 on Linux and
|
||||||
|
# 512+ on macOS. Larger custom fields could break this — keep
|
||||||
|
# multitask_group <= 64 chars (matches the JSON schema).
|
||||||
|
#
|
||||||
|
# Portable across macOS bash 3.2 and Linux bash 4+; uses python3 (always
|
||||||
|
# present on macOS + most Linux) for safe JSON encoding.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
REPO_NAME="$(basename "$REPO_ROOT")"
|
||||||
|
METRICS_FILE="$REPO_ROOT/.convoys/.metrics.jsonl"
|
||||||
|
mkdir -p "$REPO_ROOT/.convoys"
|
||||||
|
|
||||||
|
# Pull values out of args without using associative arrays (bash 3.2 compat)
|
||||||
|
ROLE=""; CONVOY=""; BRIEF=""; CLASSIFICATION=""
|
||||||
|
SKIP_FLAGS=""; DURATION_S=""; STACK_CLASS=""; OUTCOME=""; MULTITASK_GROUP=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
k="${arg%%=*}"
|
||||||
|
v="${arg#*=}"
|
||||||
|
case "$k" in
|
||||||
|
role) ROLE="$v" ;;
|
||||||
|
convoy) CONVOY="$v" ;;
|
||||||
|
brief) BRIEF="$v" ;;
|
||||||
|
classification) CLASSIFICATION="$v" ;;
|
||||||
|
skip_flags) SKIP_FLAGS="$v" ;;
|
||||||
|
duration_s) DURATION_S="$v" ;;
|
||||||
|
stack_class) STACK_CLASS="$v" ;;
|
||||||
|
outcome) OUTCOME="$v" ;;
|
||||||
|
multitask_group) MULTITASK_GROUP="$v" ;;
|
||||||
|
*) echo "log-convoy-event: ignoring unknown arg '$k'" >&2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$ROLE" ] || [ -z "$CONVOY" ]; then
|
||||||
|
echo "log-convoy-event: role and convoy are required" >&2
|
||||||
|
echo "Usage: $0 role=<role> convoy=<slug> [classification=...] [skip_flags=a,b] [duration_s=N] [brief=N] [stack_class=...] [outcome=...]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
# Build the event with python3 — handles all string escaping and array encoding
|
||||||
|
python3 - <<PY >> "$METRICS_FILE"
|
||||||
|
import json, sys
|
||||||
|
ev = {
|
||||||
|
"ts": "$ts",
|
||||||
|
"role": $(printf '%s' "$ROLE" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
|
||||||
|
"convoy": $(printf '%s' "$CONVOY" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
|
||||||
|
"repo": $(printf '%s' "$REPO_NAME" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))'),
|
||||||
|
"skip_flags": [s for s in "$SKIP_FLAGS".split(",") if s],
|
||||||
|
}
|
||||||
|
if "$BRIEF": ev["brief"] = int("$BRIEF")
|
||||||
|
if "$CLASSIFICATION": ev["classification"] = "$CLASSIFICATION"
|
||||||
|
if "$DURATION_S": ev["duration_s"] = int("$DURATION_S")
|
||||||
|
if "$STACK_CLASS": ev["stack_class"] = "$STACK_CLASS"
|
||||||
|
if "$OUTCOME": ev["outcome"] = "$OUTCOME"
|
||||||
|
if "$MULTITASK_GROUP": ev["multitask_group"] = "$MULTITASK_GROUP"
|
||||||
|
print(json.dumps(ev))
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "Logged: role=$ROLE convoy=$CONVOY → .convoys/.metrics.jsonl"
|
||||||
37
scripts/wt.sh
Executable file
37
scripts/wt.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# wt.sh — DEPRECATED in Cursor 3.2+.
|
||||||
|
#
|
||||||
|
# Cursor 3.2 (Apr 24, 2026) added native worktree management to the
|
||||||
|
# Agents Window with one-click foregrounding. Use that instead:
|
||||||
|
# https://cursor.com/docs/configuration/worktrees
|
||||||
|
#
|
||||||
|
# This stub is kept for two reasons:
|
||||||
|
# 1. Pre-3.2 users who haven't upgraded yet.
|
||||||
|
# 2. Scripted / CI worktree creation outside the IDE.
|
||||||
|
#
|
||||||
|
# To create a worktree manually:
|
||||||
|
# git worktree add -b brief/<convoy>/<N>-<title> \
|
||||||
|
# "$(dirname "$(git rev-parse --show-toplevel)")/$(basename "$(git rev-parse --show-toplevel)")-worktrees/brief-<N>-<title>" \
|
||||||
|
# develop
|
||||||
|
#
|
||||||
|
# See docs/multitask-playbook.md for when to spin up worktrees vs.
|
||||||
|
# running implementers in the same checkout.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cat <<'EOF' >&2
|
||||||
|
wt.sh: deprecated. In Cursor 3.2+ use the Agents Window worktree UI.
|
||||||
|
|
||||||
|
Why this is deprecated:
|
||||||
|
- Cursor 3.2 worktrees integrate with subagent runs and one-click foreground.
|
||||||
|
- The legacy script duplicates that feature without the integration.
|
||||||
|
|
||||||
|
What to do instead:
|
||||||
|
- In Cursor: open Agents Window → "New worktree" → pick brief branch.
|
||||||
|
- For CI / scripted use: run `git worktree add` directly.
|
||||||
|
|
||||||
|
Reference: docs/multitask-playbook.md (worktrees section)
|
||||||
|
https://cursor.com/changelog/04-24-26
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit 0
|
||||||
32
tests/smoke/app.smoke.spec.ts
Normal file
32
tests/smoke/app.smoke.spec.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke tests run against a deployed preview URL.
|
||||||
|
* BASE_URL is injected by the GitHub Action (PREVIEW_URL).
|
||||||
|
*
|
||||||
|
* Add or replace tests here for each critical user path you ship.
|
||||||
|
* Keep this file fast (<60s total). For deeper E2E, use a separate suite.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE = process.env.BASE_URL ?? 'http://localhost:3000';
|
||||||
|
|
||||||
|
test.describe('smoke: app boots and core pages render', () => {
|
||||||
|
test('home redirects or renders without 5xx', async ({ page }) => {
|
||||||
|
const response = await page.goto(BASE);
|
||||||
|
expect(response?.status(), 'home should not 5xx').toBeLessThan(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign-in page renders', async ({ page }) => {
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('public health endpoint responds', async ({ request }) => {
|
||||||
|
const res = await request.get(`${BASE}/api/health`);
|
||||||
|
expect(res.ok(), `${BASE}/api/health should respond 2xx`).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add convoy-specific smoke tests below as features ship. Each new flag-gated
|
||||||
|
// feature should add a smoke test that exercises the happy path with the flag
|
||||||
|
// forced on (if your flag wrapper supports query-string overrides).
|
||||||
Loading…
Reference in a new issue