Convoy: single-sql-client (P1 quality, launch sequence step 8) Addresses: AGENTS.md Gotcha #1, .convoys/ship-readiness.md P1 #8 ## Decisions - D1: Caller inventory = 2 files (1 source + 1 test), not "~3 based on graph". Only pages/api/auth-utils.js imports `db`; test/api/auth-utils.test.js mocks it purely to satisfy the import graph (the 5 tests exercise generateToken/verifyToken, not isAdmin/getUserById). - D2: Migrate both call sites (isAdmin, getUserById) to @vercel/postgres tagged-template SQL. Queries are SELECT-only, single-table, single-numeric-parameter — byte-equivalent translation; same result shape ({rows, rowCount}); no transaction or pool semantics differ. - D3: KEEP @neondatabase/serverless as a dep. 11 scripts/* files still use `neon()` directly (setup-neon-db.js, migrations/, reset-db.js, 8 historical add-*/fix-*/seed-* jobs). They are out of scope per the no-go-zones rule and the convoy spec; purging the dep entirely would be its own convoy (queued as `purge-neondatabase-serverless-fully`, blocked on migration-tool). - D4: sql.unsafe audit — NOT a real injection vector with current callers (userId comes from a verified JWT, is a numeric SERIAL id). Security finding: NO. Pure refactor + foot-gun removal that prevents the FUTURE caller that would have been the incident. - D5: Test mock cleanup — drop the now-unneeded `vi.mock('../../lib/database.js')` call + unused `vi` import. Test count + assertions unchanged (5/5). ## Per-file changes - pages/api/auth-utils.js: swap `import { db } from '../../lib/database.js'` for `import { sql } from '@vercel/postgres'`; rewrite isAdmin's `db.query(SELECT … WHERE id = $1, [userId])` and getUserById's same shape to `sql\`SELECT … WHERE id = ${userId}\``. Same try/catch, same result.rows[0] access, same error returns. - test/api/auth-utils.test.js: drop vi.mock for lib/database.js + the unused `vi` import. 5/5 tests still pass. - lib/database.js: DELETED (47 lines removed; manual-interpolation + sql.unsafe wrapper is gone). - .convoys/single-sql-client.md: NEW (the convoy file documenting all decisions + caller inventory + verification + risks + follow-ups). ## Verification - npm run lint → 128 problems (baseline preserved, no regression) - npm run test:run → 21/21 pass (vitest) - Grep "lib/database" --type js -l → 0 hits anywhere - Grep "@neondatabase/serverless" --type js -l → still matches the 11 scripts/* sites (expected; out of scope per D3) - node --check pages/api/auth-utils.js → exit 0 ## Scope note This convoy collapses the lib/database.js abstraction onto the canonical @vercel/postgres surface for pages/api/**. It does NOT eliminate @neondatabase/serverless from the dependency tree — that would require migrating the scripts/* helpers, which is out of scope here (no-go-zones rule + convoy spec). Queued as a follow-up. ## Live smoke Deferred. The two migrated functions (isAdmin, getUserById) are only reachable via pages/api/admin/index.js which requires an admin Bearer token and a populated users table in prod Neon. Byte-equivalent SQL + identical result shape gives high confidence; rollback is a single-commit revert if a post-merge admin action 500s. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .. | ||
| add-rate-limiting | ||
| adopt-playwright-smoke | ||
| bump-next-js | ||
| cors-tighten | ||
| drop-public-setup | ||
| fix-auth-bypass | ||
| fix-layout-default-user | ||
| fix-vercel-deployment-protection-in-ci | ||
| pick-a-name | ||
| add-rate-limiting.md | ||
| adopt-playwright-smoke.md | ||
| bump-next-js.md | ||
| cleanup-mobile-nav-dead-props.md | ||
| cors-tighten.md | ||
| drop-public-setup.md | ||
| fix-auth-bypass.md | ||
| fix-layout-default-user.md | ||
| fix-reset-db-script.md | ||
| fix-vercel-deployment-protection-in-ci.md | ||
| lint-against-cjs-in-esm-scripts.md | ||
| pick-a-name.md | ||
| purge-weak-creds-from-helpers.md | ||
| README.md | ||
| ship-readiness.md | ||
| single-sql-client.md | ||
| tighten-visual-diff-path-filter.md | ||
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):
---
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):
## Why(Conductor)## Scope(Conductor)## Roles invoked(Conductor)## Todos(Conductor → refined by Architect)## IA(IA Architect)## UX(UX Reviewer)## 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
- Open Cursor in this repo.
- Prompt: "Start a new convoy: . Success = ."
- The
role-conductorsubagent writes.convoys/<slug>.md. - Run subsequent roles in order per the convoy's
Roles invokedlist.
See .cursor/agents/role-conductor.md for the Conductor's full spec.
Multitask + worktrees (Cursor 3.2+)
Cursor 3.2 (Apr 24, 2026) 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 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:
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.