Closes the operator caveat from the `drop-public-setup` convoy: deployed
envs that ran `npm run setup-db` BEFORE `ff80753` (2026-05-22) still
carry the historical `admin123` bcrypt hash. The seed is idempotent
(`ON CONFLICT (email) DO NOTHING`), so re-running setup-db is a no-op
on existing rows.
## Design — D1: which option from the 3-option menu?
| Option | Picked? | Why |
|---|---|---|
| A. Close as no-op (defer rotation to manual app login) | No | Leaves a real-world residue if any pre-drop-public-setup env still exists — and an audit is harder than just shipping the script. |
| B. One-shot parameterized rotation script | **Yes** | Tightly scoped (~120 lines). Audit-trail-preserving (`updated_at` bump). Reusable for future rotations. No new auth surface in the app. |
| C. First-login forced password reset flow in the app | No | Right product answer, but heavier scope (new route, new flag column, UI work). Deferred as the queued `force-admin-password-reset-flow` convoy. |
## Script shape
`scripts/rotate-admin-password.js`:
- Reads `POSTGRES_URL` + `ADMIN_NEW_PASSWORD` from env (or `.env.local`).
- Optional `ADMIN_EMAIL` override; defaults to `admin@deckhearth.com`.
Pass `admin@tcgvault.com` for envs that pre-date `pick-a-name`
(squash `9abbab6`, 2026-05-24).
- Fail-loud-exits BEFORE opening any DB connection if:
- `POSTGRES_URL` is unset
- `ADMIN_NEW_PASSWORD` is unset or empty
- `ADMIN_NEW_PASSWORD` is shorter than 12 chars
- Validates the target row EXISTS AND has `role = 'admin'` before
touching it. Refuses to rotate non-admin rows even if `ADMIN_EMAIL`
points at one. Refuses to rotate when multiple rows match (impossible
given the UNIQUE(email) constraint, but checked anyway).
- Hashes with bcryptjs at 12 rounds — same as `setup-neon-db.js`.
- After UPDATE, re-fetches the row and runs `bcrypt.compare(newPassword,
row.password_hash)`; exits non-zero if the compare fails (extremely
unlikely, but catches silent UPDATE failures).
- NEVER echoes the password to stdout / stderr / shell history. The
only output is the row id, email, role, and updated_at.
Same import shape as the existing `scripts/migrations/2026-05-24-rename-admin-email.js`
(ESM, `dotenv.config({ path: '.env.local' })`, `import { neon } from
'@neondatabase/serverless'`, tagged-template SQL) — keeps the "11
scripts/* using neon() directly" graveyard from gaining new patterns;
fits the `purge-neondatabase-serverless-fully` follow-up convoy's
existing audit shape.
## Out of scope
- Sibling test users (alice / bob in `scripts/create-test-users.js`) —
dev fixtures, not real auth surfaces. Documented inline + in
AGENTS.md Gotcha #4.
- First-login forced password reset flow — deferred as the queued
`force-admin-password-reset-flow` convoy (it's the right product
answer, but heavier scope than this hygiene PR).
- Email rotation (already handled by
`scripts/migrations/2026-05-24-rename-admin-email.js`).
## Test plan
- [x] `node --check scripts/rotate-admin-password.js` — syntax OK
- [x] `npm run lint` — clean (1 pre-existing unrelated warning)
- [x] `npm run test:run` — 118 tests pass
- [ ] CI on this PR
- [ ] Operator-side smoke test (NOT covered by CI):
- Set `ADMIN_NEW_PASSWORD=test-rotation-12chars` against a throwaway
Neon branch DB, run the script, log in via the app with the new
password, run the script again with a different password, log in
again. Skip if there's no convenient throwaway DB.
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 | ||
| liquid-glass-design-tokens | ||
| pick-a-name | ||
| redesign-scanner-flow | ||
| unify-glass-panel-surfaces | ||
| .metrics.jsonl | ||
| add-rate-limiting.md | ||
| add-real-ocr-layer.md | ||
| adopt-playwright-smoke.md | ||
| bump-next-js.md | ||
| catalog-sync-vercel-cron.md | ||
| cleanup-card-item-list-and-share-modal-palette.md | ||
| cleanup-legacy-design-css.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 | ||
| harden-visual-diff-gate.md | ||
| lint-against-cjs-in-esm-scripts.md | ||
| liquid-glass-card-surfaces.md | ||
| liquid-glass-design-tokens.md | ||
| liquid-glass-form-primitives.md | ||
| liquid-glass-layout-shell.md | ||
| liquid-glass-modal-and-surface-primitive.md | ||
| liquid-glass-public-and-auth.md | ||
| liquid-glass-redesign.md | ||
| migrate-button-input-mobilenav-to-glass-primitive.md | ||
| migrate-ci-to-self-hosted.md | ||
| migration-tool.md | ||
| motion-system-pass.md | ||
| pick-a-name.md | ||
| purge-quick-login-from-loginpage.md | ||
| purge-weak-creds-from-helpers.md | ||
| README.md | ||
| redesign-scanner-flow.md | ||
| redesign-v2-from-mockups.md | ||
| rename-collections-vocabulary.md | ||
| scanner-correctness-polish.md | ||
| scanner-redesign-a11y-fixes.md | ||
| scanner-user-cards-quantity-guard.md | ||
| secure-scanner-gemini-key.md | ||
| server-side-scan-pipeline.md | ||
| ship-readiness.md | ||
| single-auth-provider.md | ||
| single-sql-client.md | ||
| test-scanner-redesign-surfaces.md | ||
| tighten-visual-diff-path-filter.md | ||
| unify-glass-panel-surfaces.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.