chore(harness): make repository agent-ready

Add concise routed context, task-aware agent roles, focused skills, and structured learning promotion.\n\nUnify local development and workspace tooling, add layered test and E2E verification, harden MCP authentication, and gate deployment behind verification and explicit production promotion.
This commit is contained in:
jjbrunton committed 2026-08-27 20:58:48 +01:00
1 parent 5a69bbc1a5
commit 01bb8225ae
125 files changed
+5078 -24529

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# Admin application
Vite/React administration UI. Use existing layout, route, and Supabase client
patterns before introducing abstractions.
- `pack_creator` manages permitted content; `super_admin` can access privileged
user/admin/audit surfaces.
- Enforce role checks in both route visibility and the backend/RLS boundary.
- Administrative mutations must remain auditable.
- Use `.maybeSingle()` when zero rows are valid.
- Never expose credentials, tokens, or private user content unnecessarily.
- Add accessible labels and a focused test for changed behavior.
- Verify with `npm run lint -w @sauci/admin`, `npm run typecheck -w
@sauci/admin`, and `npm run build -w @sauci/admin`.
-84
View File
@@ -1,84 +0,0 @@
# Admin Dashboard - Agent Guidelines
**STRICT RULES - DO NOT VIOLATE**
## Architecture
This is a React Admin dashboard using:
- **Build**: Vite
- **Routing**: react-router-dom with component-based routing in `App.tsx`
- **Styling**: Tailwind CSS
- **State**: React Context (`AuthContext`)
- **Backend**: Supabase client in `src/lib/supabase.ts`
## Directory Structure
```
src/
components/ # Reusable components
layout/ # AppLayout, Sidebar, etc.
ProtectedRoute.tsx # Route guard
contexts/ # React contexts (AuthContext)
hooks/ # Custom hooks
lib/ # Utilities (supabase)
pages/ # Page components
content/ # Categories, Packs, Questions
users/ # User management (super_admin only)
admins/ # Admin management (super_admin only)
```
## Critical Rules
### Authentication & Authorization
Two admin roles exist:
- `admin` - Content management only (categories, packs, questions)
- `super_admin` - Full access including user data, admins, audit logs
Use `<ProtectedRoute requireSuperAdmin>` for super_admin-only routes.
### Route Structure
```typescript
// Public
/login
// Protected (all admins)
/ # Dashboard
/categories # Category management
/categories/:id/packs # Packs in category
/packs/:id/questions # Questions in pack
// Protected (super_admin only)
/users # User list
/users/:id # User detail
/users/:id/matches/:id # Match chat view
/admins # Admin management
/audit-logs # Audit log viewer
```
### Supabase Queries
- **Always use `.maybeSingle()` instead of `.single()`** when the row might not exist
- All admin operations are audited - check `audit_logs` table structure
### UI Patterns
- Use Tailwind CSS for styling
- Follow existing component patterns in `src/components/`
- Use `AppLayout` wrapper for all protected pages
### Security
- Never expose user passwords or sensitive data in admin views
- Audit log all administrative actions
- Verify admin role before performing operations
## Forbidden Actions
1. **DO NOT** expose user passwords or auth tokens
2. **DO NOT** allow content admins to access user data
3. **DO NOT** bypass role checks in protected routes
4. **DO NOT** perform destructive operations without confirmation
5. **DO NOT** use `.single()` for queries that might return 0 rows
6. **DO NOT** skip audit logging for administrative actions
+5 -3
View File
@@ -4,9 +4,11 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"typecheck": "tsc --noEmit",
"dev": "vite",
"build": "vite build",
"lint": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
+11
View File
@@ -0,0 +1,11 @@
# Internal MCP service
Hono service exposing privileged administrative tools.
- Treat the service-role key and `SAUCI_MCP_API_KEY` as secrets.
- Fail closed when MCP authentication is missing outside explicit local mode.
- Every mutation validates inputs, authorizes the operation, and writes audit
evidence where the product contract requires it.
- Read operations preserve least privilege and avoid excessive user data.
- Add unit/API coverage for tool contract or auth changes.
- Verify lint, tests, typecheck, build, and `/health` locally.
-2785
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -5,10 +5,13 @@
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsup src/index.ts src/bridge.ts --format esm --clean",
"lint": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"start": "node dist/index.mjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hono/node-server": "^1.13.7",
"@hono/mcp": "^0.1.1",
"@modelcontextprotocol/sdk": "^1.0.1",
"@supabase/supabase-js": "^2.45.0",
+32
View File
@@ -0,0 +1,32 @@
import { Hono } from 'hono';
import { afterEach, describe, expect, it } from 'vitest';
import { authMiddleware } from './auth.js';
function app() {
const instance = new Hono();
instance.use('/mcp', authMiddleware);
instance.get('/mcp', (c) => c.json({ ok: true }));
return instance;
}
afterEach(() => {
delete process.env.SAUCI_MCP_API_KEY;
});
describe('MCP authentication', () => {
it('fails closed when the server key is missing', async () => {
const response = await app().request('/mcp');
expect(response.status).toBe(503);
});
it('requires and validates a bearer token', async () => {
process.env.SAUCI_MCP_API_KEY = 'local-test-key';
expect((await app().request('/mcp')).status).toBe(401);
expect((await app().request('/mcp', {
headers: { Authorization: 'Bearer wrong' },
})).status).toBe(401);
expect((await app().request('/mcp', {
headers: { Authorization: 'Bearer local-test-key' },
})).status).toBe(200);
});
});
+7 -11
View File
@@ -1,24 +1,20 @@
import { Context, Next } from 'hono';
export const authMiddleware = async (c: Context, next: Next) => {
const authHeader = c.req.header('Authorization');
const apiKey = process.env.SAUCI_MCP_API_KEY;
if (!apiKey) {
// If no API key is set in env, allow access (warn in logs)
// Ideally this should default to deny, but for dev ease we might want to warn
console.warn('SAUCI_MCP_API_KEY not set in environment - allowing all requests');
return next();
console.error('SAUCI_MCP_API_KEY is not configured; refusing MCP access');
return c.json({ error: 'Service authentication is not configured' }, 503);
}
let token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
token = c.req.query('apiKey');
}
const authHeader = c.req.header('Authorization');
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice('Bearer '.length)
: undefined;
if (!token) {
return c.json({ error: 'Unauthorized: Missing or invalid Authorization header or apiKey query param' }, 401);
return c.json({ error: 'Unauthorized: Bearer token required' }, 401);
}
if (token !== apiKey) {
+6
View File
@@ -2,4 +2,10 @@
module.exports = {
extends: 'expo',
ignorePatterns: ['/dist/*'],
overrides: [
{
files: ['plugins/**/*.js'],
env: { node: true },
},
],
};
+15
View File
@@ -0,0 +1,15 @@
# Mobile application
Expo React Native with Expo Router, Zustand, Supabase, RevenueCat, Firebase, and
native iOS/Android targets.
- Read `DESIGN.md` before UI changes.
- Routes belong under `app`; feature logic belongs under `src/features`.
- Reuse existing Zustand stores and clear user-scoped state during sign-out.
- Check RevenueCat availability before native purchase operations.
- Use `.maybeSingle()` when zero rows are valid and handle nullable profiles.
- Keep route files thin; extract focused components/hooks when responsibilities
diverge rather than enforcing a mechanical line limit.
- Add stable accessibility labels or `testID` values for critical interactions.
- Verify focused Jest tests, typecheck, lint, then native/Expo behavior as risk
requires. UI work needs observable simulator evidence.
-73
View File
@@ -1,73 +0,0 @@
# Mobile App - Agent Guidelines
**STRICT RULES - DO NOT VIOLATE**
## Architecture
This is an Expo React Native app using:
- **Routing**: expo-router with file-based routing in `app/` directory
- **State**: Zustand stores in `src/store/index.ts`
- **Backend**: Supabase client in `src/lib/supabase.ts`
Design guidance lives in `apps/mobile/DESIGN.md`.
## Directory Structure
```
app/ # Expo Router routes
(auth)/ # Authentication screens
(app)/ # Protected app screens (tabs)
src/
components/ # Reusable components
ui/
lib/ # Utilities (supabase, analytics, revenuecat)
store/ # Zustand stores (index.ts)
theme/
types/ # TypeScript types
```
## Critical Rules
### File Size
- Keep files small: aim for 200-300 lines max per file.
### State Management
- Use existing Zustand stores: `useAuthStore`, `useMatchStore`, `usePacksStore`, `useMessageStore`, `useSubscriptionStore`
- Access cross-store state via `getState()` (e.g., `useAuthStore.getState().user`)
- Clear related stores on sign out (see `signOut` in `useAuthStore`)
### Supabase Queries
- **Always use `.maybeSingle()` instead of `.single()`** when the row might not exist
- `.single()` throws an error if 0 or >1 rows are returned
- Check for null before performing operations
### Response Handling
- Responses use UPSERT with `onConflict: "user_id,question_id"`
- Match detection happens in `submit-response` edge function
### Typing
- Import shared types from `@/types`
- Import Supabase types from `@/types/supabase`
### RevenueCat
- RevenueCat service in `src/lib/revenuecat.ts`
- Does not work in Expo Go - check `revenueCatService.isAvailable()` first
- Initialize with user ID after auth
### Analytics
- Use `Events` from `src/lib/analytics` for tracking
- Track: `packEnabled`, `packDisabled`, `signOut`, etc.
## Forbidden Actions
1. **DO NOT** use `.single()` for queries that might return 0 rows
2. **DO NOT** create new stores without integrating with auth sign-out cleanup
3. **DO NOT** bypass RevenueCat availability checks
4. **DO NOT** add navigation logic outside of expo-router
-16696
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
# Supabase backend
This directory owns local configuration, migrations, Edge Functions, seeds, and
integration tests.
## Schema safety
- Create migrations only with `npm run db:migration:new -- <name>`.
- Edit only `apps/supabase/migrations/`; this is the authoritative migration tree.
- Never use any remote `apply_migration` tool.
- Never execute DDL through MCP SQL tools; those tools are `SELECT`-only.
- Never link or reset a remote project during local verification.
- Cron/function URLs must resolve per environment, never hardcode production.
## Function boundaries
- Set deployment JWT configuration deliberately per function. A function using
`verify_jwt = false` must implement the correct manual, webhook, cron, or internal
authentication boundary; it is not permission to omit authorization.
- Service-role clients never run in customer clients.
- Preserve CORS handling and return non-sensitive errors.
- Test RLS, authorization, and important side effects, not only HTTP status.
Verify with local reset, `npm run test:supabase`, invariant lint, and affected
client tests. See `docs/operations/database-migrations.md`.
-136
View File
@@ -1,136 +0,0 @@
# Supabase - Agent Guidelines
**STRICT RULES - DO NOT VIOLATE**
## Architecture
This contains Supabase configuration:
- **Migrations**: SQL migrations in `migrations/`
- **Edge Functions**: Deno functions in `functions/`
- **Email Templates**: Auth email templates in `email-templates/`
- **Config**: Local development config in `config.toml`
## Directory Structure
```
functions/
delete-relationship/ # Delete couple relationship
manage-couple/ # Join/leave couple
revenuecat-webhook/ # Subscription webhook handler
send-message-notification/ # Push notification for messages
send-notification/ # Push notification for matches
submit-response/ # Save response, check matches
sync-subscription/ # Sync RevenueCat status
migrations/ # SQL migrations
email-templates/ # Auth email templates
config.toml # Local dev configuration
```
## Critical Rules
### Edge Function Deployment
**ALWAYS use `verify_jwt: false`** when deploying via MCP tools.
Supabase's built-in JWT verification causes 401 errors. Handle auth manually:
```typescript
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace("Bearer ", "")
);
```
### Edge Function Pattern
All edge functions MUST follow this structure:
```typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
Deno.serve(async (req) => {
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Manual auth verification
const authHeader = req.headers.get("Authorization");
if (!authHeader) {
return new Response(
JSON.stringify({ error: "Missing authorization header" }),
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace("Bearer ", "")
);
// ... function logic
} catch (error) {
return new Response(
JSON.stringify({ error: "Internal server error" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
```
### Function Auth Methods
| Function | Auth Method |
|----------|-------------|
| `manage-couple` | Manual via getUser() |
| `submit-response` | Manual via getUser() |
| `sync-subscription` | Manual via getUser() |
| `delete-relationship` | Manual via getUser() |
| `revenuecat-webhook` | Webhook signature validation |
| `send-notification` | Internal trigger (no auth) |
| `send-message-notification` | Internal trigger (no auth) |
### Migration Rules
- Use `apply_migration` MCP tool for DDL operations
- Never hardcode generated IDs in data migrations
- Always verify target environment (prod vs non-prod) before running
### Database Patterns
- Use UPSERT with `onConflict` for responses: `onConflict: "user_id,question_id"`
- Use UPSERT for matches: `onConflict: "couple_id,question_id"`
- RLS policies enforce couple-level data isolation
### Profile Creation
The `on_auth_user_created` trigger automatically creates a profile. Never assume a profile exists without checking - check for null before operations.
## MCP Environments
| Environment | MCP Server |
|-------------|------------|
| Production | `mcp__sauci-prod__*` |
| Non-Production | `mcp__sauci-non-prod__*` |
**ALWAYS verify environment before destructive operations.**
## Forbidden Actions
1. **DO NOT** use `verify_jwt: true` when deploying functions via MCP
2. **DO NOT** skip CORS headers in responses
3. **DO NOT** use service role key on client-side
4. **DO NOT** hardcode IDs in migrations
5. **DO NOT** assume profiles exist - always check for null
6. **DO NOT** run migrations on production without explicit confirmation
7. **DO NOT** expose internal error details in responses
+8 -2
View File
@@ -1,5 +1,11 @@
{
"name": "@sauci/supabase",
"version": "1.0.0",
"private": true
"version": "1.0.0",
"private": true,
"scripts": {
"lint": "node ../../scripts/lint-harness.mjs --scope supabase",
"test": "node -e \"console.log('Supabase integration tests run through npm run test:supabase')\"",
"typecheck": "node -e \"console.log('Edge Functions are checked by Deno during test:supabase')\"",
"build": "node -e \"console.log('Supabase deployables do not require a build step')\""
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ export async function createRedemptionCode() {
const { data, error } = await adminClient.from("redemption_codes").insert({
code,
max_uses: 1,
active: true,
is_active: true,
expires_at: new Date(Date.now() + 3600000).toISOString() // 1 hour
}).select().single();
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
+10
View File
@@ -0,0 +1,10 @@
# Web application
Next.js App Router marketing, legal, and redemption UI.
- Prefer server components; add `use client` only when browser state is needed.
- Preserve PostHog provider/page-view integration unless the task changes it.
- Keep metadata current for public pages.
- Never expose server credentials through `NEXT_PUBLIC_*` variables.
- Public redemption must use sandbox/local Supabase during automated tests.
- Verify lint, typecheck, build, and the relevant Playwright flow.
-86
View File
@@ -1,86 +0,0 @@
# Web App - Agent Guidelines
**STRICT RULES - DO NOT VIOLATE**
## Architecture
This is a Next.js 14+ marketing/landing website using:
- **Framework**: Next.js with App Router
- **Styling**: Tailwind CSS
- **Analytics**: PostHog
- **Routing**: File-based routing in `app/` directory
## Directory Structure
```
app/
layout.tsx # Root layout with PostHog provider
page.tsx # Home/landing page
globals.css # Global styles
providers.tsx # PostHog provider wrapper
PostHogPageView.tsx # Page view tracking component
privacy/ # Privacy policy
terms/ # Terms of service
redeem/ # Code redemption page
components/ # Reusable components
public/ # Static assets (images, fonts)
```
## Critical Rules
### App Router
This uses Next.js App Router (not Pages Router):
- Server Components by default
- Use `'use client'` directive for client components
- Metadata exported from layout.tsx and page.tsx
### PostHog Integration
PostHog is configured at root level:
- Provider in `providers.tsx`
- Page view tracking in `PostHogPageView.tsx`
- Wrap all pages in `<PostHogProvider>`
### SEO & Metadata
Metadata is defined in `layout.tsx`:
```typescript
export const metadata: Metadata = {
title: 'Sauci - Ignite Your Connection',
description: '...',
openGraph: { ... },
twitter: { ... },
};
```
Update metadata when adding new pages.
### Styling
- Use Tailwind CSS classes
- Global styles in `globals.css`
- Follow mobile-first responsive design
### Static Pages
Privacy and Terms pages are static content:
- `/privacy` - Privacy policy
- `/terms` - Terms of service
Keep these updated with legal requirements.
### Environment Variables
Required environment variables:
- `NEXT_PUBLIC_POSTHOG_KEY` - PostHog API key
- `NEXT_PUBLIC_POSTHOG_HOST` - PostHog host
## Forbidden Actions
1. **DO NOT** remove PostHog tracking without approval
2. **DO NOT** use Pages Router patterns (use App Router)
3. **DO NOT** add server-side API routes that expose sensitive data
4. **DO NOT** forget to update metadata for new pages
5. **DO NOT** skip mobile responsiveness
6. **DO NOT** commit .env files with real API keys
+2 -2
View File
@@ -120,7 +120,7 @@ export default function Pricing() {
<p className="text-sm text-white/80">
<span className="font-semibold text-white">Pay once per couple.</span>{' '}
Whether monthly or yearly, one subscription unlocks Pro for both you and your partner.
That's two accounts for the price of one.
That&apos;s two accounts for the price of one.
</p>
</div>
</div>
@@ -153,7 +153,7 @@ export default function Pricing() {
transition={{ delay: 0.3 }}
className="text-center text-white/40 text-sm mt-8 max-w-lg mx-auto"
>
Start free with access to selected packs. Upgrade to Pro when you're ready to unlock everything.
Start free with access to selected packs. Upgrade to Pro when you&apos;re ready to unlock everything.
</motion.p>
</div>
</section>
-1798
View File
File diff suppressed because it is too large. Load diff
+9 -6
View File
@@ -5,9 +5,10 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
"start": "next start",
"lint": "next lint",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"framer-motion": "^11.0.0",
@@ -20,9 +21,11 @@
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.0",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.35",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0"
}