mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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:
1 parent
5a69bbc1a5
commit
01bb8225ae
125 files changed
+5078
-24529
No files matched your search
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SESSION="sauci-dev"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
SERVERS=(
|
||||
"functions|npx supabase functions serve --workdir apps"
|
||||
"web|scripts/with-local-supabase-env.sh web npm run dev -w @sauci/web -- --port 3000"
|
||||
"admin|scripts/with-local-supabase-env.sh admin npm run dev -w @sauci/admin -- --host 127.0.0.1 --port 3001"
|
||||
"mcp|scripts/with-local-supabase-env.sh mcp npm run dev -w sauci-mcp-server"
|
||||
"mobile|scripts/with-local-supabase-env.sh mobile npm run dev -w @sauci/mobile -- --port 8081 --localhost"
|
||||
)
|
||||
|
||||
PORTS=(
|
||||
"supabase-api:54321"
|
||||
"postgres:54322"
|
||||
"studio:54323"
|
||||
"web:3000"
|
||||
"admin:3001"
|
||||
"mcp:3002"
|
||||
"metro:8081"
|
||||
)
|
||||
|
||||
c_reset=$'\033[0m'; c_dim=$'\033[2m'; c_grn=$'\033[32m'; c_ylw=$'\033[33m'; c_red=$'\033[31m'; c_cyn=$'\033[36m'
|
||||
info() { printf "${c_cyn}▸ %s${c_reset}\n" "$*"; }
|
||||
ok() { printf "${c_grn}✓ %s${c_reset}\n" "$*"; }
|
||||
warn() { printf "${c_ylw}! %s${c_reset}\n" "$*"; }
|
||||
die() { printf "${c_red}✗ %s${c_reset}\n" "$*" >&2; exit 1; }
|
||||
port_up() { lsof -ti :"$1" -sTCP:LISTEN >/dev/null 2>&1; }
|
||||
|
||||
preflight() {
|
||||
command -v tmux >/dev/null 2>&1 || die "tmux not found. Install: brew install tmux"
|
||||
command -v npm >/dev/null 2>&1 || die "npm not found. Install Node $(cat "$ROOT/.nvmrc")."
|
||||
command -v docker >/dev/null 2>&1 || die "Docker-compatible CLI not found."
|
||||
docker info >/dev/null 2>&1 || die "Container runtime is not running."
|
||||
[ -d "$ROOT/node_modules" ] || die "Dependencies missing. Run: npm ci"
|
||||
local node_major
|
||||
node_major="$(node -p 'process.versions.node.split(`.`)[0]')"
|
||||
[ "$node_major" = "20" ] || die "Node 20 is required. Activate .nvmrc before starting the stack."
|
||||
}
|
||||
|
||||
ensure_infra() {
|
||||
if (cd "$ROOT" && npx supabase status --workdir apps >/dev/null 2>&1); then
|
||||
ok "Local Supabase already running"
|
||||
else
|
||||
info "Starting local Supabase"
|
||||
(cd "$ROOT" && GOOGLE_CLIENT_ID=local APPLE_CLIENT_ID=local \
|
||||
GOOGLE_CLIENT_SECRET=local APPLE_CLIENT_SECRET=local \
|
||||
npx supabase start --workdir apps)
|
||||
fi
|
||||
}
|
||||
|
||||
start_window() {
|
||||
local name="$1" cmd="$2"
|
||||
if tmux list-windows -t "$SESSION" -F '#{window_name}' 2>/dev/null | grep -qx "$name"; then
|
||||
warn "window '$name' already exists"
|
||||
return
|
||||
fi
|
||||
tmux new-window -t "$SESSION" -n "$name" -c "$ROOT"
|
||||
tmux send-keys -t "$SESSION:$name" "$cmd" C-m
|
||||
}
|
||||
|
||||
port_check() {
|
||||
printf " Port status (${c_dim}· = stopped or starting${c_reset}):\n"
|
||||
local entry name port
|
||||
for entry in "${PORTS[@]}"; do
|
||||
name="${entry%%:*}"; port="${entry##*:}"
|
||||
if port_up "$port"; then printf " ${c_grn}●${c_reset} %-14s :%s\n" "$name" "$port"
|
||||
else printf " ${c_dim}·${c_reset} %-14s :%s\n" "$name" "$port"; fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_up() {
|
||||
preflight
|
||||
ensure_infra
|
||||
tmux has-session -t "$SESSION" 2>/dev/null || tmux new-session -d -s "$SESSION" -n _bootstrap -c "$ROOT"
|
||||
local service
|
||||
for service in "${SERVERS[@]}"; do start_window "${service%%|*}" "${service#*|}"; done
|
||||
tmux kill-window -t "$SESSION:_bootstrap" 2>/dev/null || true
|
||||
ok "Stack starting in tmux session '$SESSION'"
|
||||
port_check
|
||||
printf "\n Web: http://127.0.0.1:3000\n Admin: http://127.0.0.1:3001\n MCP: http://127.0.0.1:3002/health\n Studio:http://127.0.0.1:54323\n"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
info "tmux '$SESSION' windows"
|
||||
tmux list-windows -t "$SESSION" -F ' #{window_index}: #{window_name}'
|
||||
else
|
||||
warn "session '$SESSION' not running"
|
||||
fi
|
||||
port_check
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
tmux has-session -t "$SESSION" 2>/dev/null || die "session not running"
|
||||
tmux capture-pane -p -S -400 -t "$SESSION:${1:?usage: logs <service>}"
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
tmux has-session -t "$SESSION" 2>/dev/null || die "session not running"
|
||||
local name="${1:?usage: restart <service>}" service
|
||||
tmux kill-window -t "$SESSION:$name" 2>/dev/null || true
|
||||
for service in "${SERVERS[@]}"; do
|
||||
if [ "${service%%|*}" = "$name" ]; then
|
||||
start_window "$name" "${service#*|}"
|
||||
ok "restarted $name"
|
||||
return
|
||||
fi
|
||||
done
|
||||
die "unknown service '$name'"
|
||||
}
|
||||
|
||||
cmd_down() {
|
||||
tmux kill-session -t "$SESSION" 2>/dev/null && ok "application services stopped" || warn "no session '$SESSION'"
|
||||
if [ "${1:-}" = "--all" ]; then
|
||||
(cd "$ROOT" && npx supabase stop --workdir apps)
|
||||
ok "local Supabase stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_reset() {
|
||||
preflight
|
||||
ensure_infra
|
||||
(cd "$ROOT" && npx supabase db reset --workdir apps)
|
||||
(cd "$ROOT" && scripts/with-local-supabase-env.sh tests node scripts/seed-e2e.mjs)
|
||||
ok "Local database reset and deterministic fixtures created"
|
||||
}
|
||||
|
||||
cmd_native() {
|
||||
local platform="$1"
|
||||
preflight
|
||||
ensure_infra
|
||||
(cd "$ROOT" && scripts/with-local-supabase-env.sh mobile npm run "$platform" -w @sauci/mobile)
|
||||
}
|
||||
|
||||
case "${1:-up}" in
|
||||
up) cmd_up ;;
|
||||
down) cmd_down "${2:-}" ;;
|
||||
status) cmd_status ;;
|
||||
logs) cmd_logs "${2:-}" ;;
|
||||
restart) cmd_restart "${2:-}" ;;
|
||||
attach) tmux attach -t "$SESSION" ;;
|
||||
reset|seed) cmd_reset ;;
|
||||
ios|android) cmd_native "$1" ;;
|
||||
help|-h|--help)
|
||||
echo "usage: scripts/dev-local.sh up|down [--all]|status|logs <service>|restart <service>|attach|reset|ios|android"
|
||||
;;
|
||||
*) die "unknown command '$1'" ;;
|
||||
esac
|
||||
@@ -0,0 +1,86 @@
|
||||
import { access, readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const errors = [];
|
||||
const scoped = process.argv.includes('--scope');
|
||||
|
||||
async function exists(file) {
|
||||
try { await access(file); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function walk(directory, files = []) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', 'evidence', '.ralph'].includes(entry.name)) continue;
|
||||
const target = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) await walk(target, files);
|
||||
else files.push(target);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fail(file, message, fix) {
|
||||
errors.push(`${path.relative(root, file)}: ${message}\n Fix: ${fix}`);
|
||||
}
|
||||
|
||||
const allFiles = await walk(root);
|
||||
for (const file of allFiles) {
|
||||
if (path.basename(file) === 'agents.md') {
|
||||
fail(file, 'lowercase agent context is not discoverable consistently', 'rename it to AGENTS.md or move maintained detail under docs/');
|
||||
}
|
||||
if (!/\.(md|sql)$/.test(file)) continue;
|
||||
const source = await readFile(file, 'utf8');
|
||||
if (/^\s*[-*]\s*Use\s+.*apply_migration/im.test(source)) {
|
||||
fail(file, 'instruction authorizes untracked remote migrations', 'create a local apps/supabase/migrations file instead');
|
||||
}
|
||||
}
|
||||
|
||||
if (!scoped) {
|
||||
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
||||
for (const app of (await readdir(path.join(root, 'apps'), { withFileTypes: true })).filter((entry) => entry.isDirectory())) {
|
||||
const packageFile = path.join(root, 'apps', app.name, 'package.json');
|
||||
if (!(await exists(packageFile))) continue;
|
||||
if (!packageJson.workspaces.includes(`apps/${app.name}`)) {
|
||||
fail(packageFile, 'application is outside the root workspace', `add apps/${app.name} to package.json workspaces`);
|
||||
}
|
||||
}
|
||||
|
||||
const claude = await readFile(path.join(root, 'CLAUDE.md'), 'utf8');
|
||||
if (claude.split('\n').length > 20 || !claude.includes('AGENTS.md')) {
|
||||
fail(path.join(root, 'CLAUDE.md'), 'runtime-specific context has drifted from the universal map', 'keep CLAUDE.md as a short pointer to AGENTS.md');
|
||||
}
|
||||
|
||||
const duplicateMigrations = path.join(root, 'supabase', 'migrations');
|
||||
if (await exists(duplicateMigrations)) {
|
||||
const entries = await readdir(duplicateMigrations);
|
||||
if (entries.length) fail(duplicateMigrations, 'second migration tree can bypass deployment', 'move migrations to apps/supabase/migrations and remove the duplicate');
|
||||
}
|
||||
|
||||
const cutoff = '20260201100806';
|
||||
const migrationDir = path.join(root, 'apps', 'supabase', 'migrations');
|
||||
for (const name of await readdir(migrationDir)) {
|
||||
if (name.slice(0, 14) <= cutoff) continue;
|
||||
const file = path.join(migrationDir, name);
|
||||
const source = await readFile(file, 'utf8');
|
||||
if (/https:\/\/[a-z0-9-]+\.supabase\.co/i.test(source)) {
|
||||
fail(file, 'new migration hardcodes a remote Supabase project', 'resolve the function URL from environment-specific app_config');
|
||||
}
|
||||
}
|
||||
|
||||
const docsIndex = await readFile(path.join(root, 'docs', 'index.md'), 'utf8');
|
||||
for (const match of docsIndex.matchAll(/\[[^\]]+\]\(([^)]+\.md)\)/g)) {
|
||||
const target = path.resolve(root, 'docs', match[1]);
|
||||
if (!(await exists(target))) fail(path.join(root, 'docs', 'index.md'), `broken documentation link ${match[1]}`, 'correct the link or restore the maintained document');
|
||||
}
|
||||
|
||||
const mcpAuth = await readFile(path.join(root, 'apps', 'mcp', 'src', 'lib', 'auth.ts'), 'utf8');
|
||||
if (/allowing all requests|query\(['"]apiKey/i.test(mcpAuth)) {
|
||||
fail(path.join(root, 'apps', 'mcp', 'src', 'lib', 'auth.ts'), 'MCP authentication can leak or fail open', 'require a configured Bearer token and fail closed');
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`Harness invariant failures (${errors.length}):\n\n${errors.join('\n\n')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Harness invariants passed');
|
||||
@@ -0,0 +1,80 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const url = process.env.SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!url || !serviceKey) throw new Error('Local Supabase environment is required');
|
||||
const host = new URL(url).hostname;
|
||||
if (host !== '127.0.0.1' && host !== 'localhost') {
|
||||
throw new Error(`Refusing to seed non-local Supabase host: ${host}`);
|
||||
}
|
||||
|
||||
const client = createClient(url, serviceKey, {
|
||||
auth: { autoRefreshToken: false, persistSession: false },
|
||||
});
|
||||
const password = 'Sauci-e2e-only-2026!';
|
||||
const emails = ['e2e.alex@example.test', 'e2e.sam@example.test'];
|
||||
|
||||
const { data: listed, error: listError } = await client.auth.admin.listUsers({ perPage: 1000 });
|
||||
if (listError) throw listError;
|
||||
for (const user of listed.users.filter((candidate) => emails.includes(candidate.email ?? ''))) {
|
||||
const { error } = await client.auth.admin.deleteUser(user.id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
const users = [];
|
||||
for (const email of emails) {
|
||||
const { data, error } = await client.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { display_name: email.includes('alex') ? 'Alex E2E' : 'Sam E2E' },
|
||||
});
|
||||
if (error || !data.user) throw error ?? new Error(`Could not create ${email}`);
|
||||
users.push(data.user);
|
||||
}
|
||||
|
||||
const coupleId = 'e2e00000-0000-4000-8000-000000000001';
|
||||
const packId = 'e2e00000-0000-4000-8000-000000000002';
|
||||
const questionId = 'e2e00000-0000-4000-8000-000000000003';
|
||||
await client.from('couples').delete().eq('id', coupleId);
|
||||
let result = await client.from('couples').insert({ id: coupleId, invite_code: 'E2E2026A' });
|
||||
if (result.error) throw result.error;
|
||||
result = await client.from('profiles').update({ couple_id: coupleId }).in('id', users.map((user) => user.id));
|
||||
if (result.error) throw result.error;
|
||||
result = await client.from('question_packs').upsert({
|
||||
id: packId,
|
||||
name: 'E2E Connection Pack',
|
||||
description: 'Deterministic local verification content',
|
||||
is_premium: false,
|
||||
is_public: true,
|
||||
sort_order: 999,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
result = await client.from('questions').upsert({
|
||||
id: questionId,
|
||||
pack_id: packId,
|
||||
text: 'Plan a technology-free evening together',
|
||||
intensity: 1,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
result = await client.from('redemption_codes').upsert({
|
||||
code: 'SAUCI-E2E-2026',
|
||||
max_uses: 100,
|
||||
current_uses: 0,
|
||||
is_active: true,
|
||||
expires_at: '2099-01-01T00:00:00.000Z',
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
|
||||
await mkdir(new URL('../e2e/.auth/', import.meta.url), { recursive: true });
|
||||
await writeFile(new URL('../e2e/.auth/fixtures.json', import.meta.url), JSON.stringify({
|
||||
users: emails.map((email, index) => ({ email, password, id: users[index].id })),
|
||||
coupleId,
|
||||
inviteCode: 'E2E2026A',
|
||||
packId,
|
||||
questionId,
|
||||
redemptionCode: 'SAUCI-E2E-2026',
|
||||
}, null, 2));
|
||||
|
||||
console.log('Created deterministic local E2E fixtures');
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
command -v deno >/dev/null 2>&1 || {
|
||||
echo "Deno is required for Supabase function tests: https://docs.deno.com/runtime/getting_started/installation/" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ! (cd "$ROOT" && npx supabase status --workdir apps >/dev/null 2>&1); then
|
||||
echo "Local Supabase is not running. Start it with scripts/dev-local.sh up." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$ROOT/apps/supabase/tests"
|
||||
"$ROOT/scripts/with-local-supabase-env.sh" tests deno task test
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const base = path.join(root, '.agents', 'learnings');
|
||||
const requiredSections = ['## Symptom', '## Reproduction', '## Cause', '## Remediation', '## Evidence'];
|
||||
const failures = [];
|
||||
|
||||
for (const state of ['inbox', 'promoted']) {
|
||||
const directory = path.join(base, state);
|
||||
for (const name of await readdir(directory)) {
|
||||
if (!name.endsWith('.md')) continue;
|
||||
const file = path.join(directory, name);
|
||||
const source = await readFile(file, 'utf8');
|
||||
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!frontmatter) { failures.push(`${file}: missing YAML frontmatter`); continue; }
|
||||
const fields = Object.fromEntries(frontmatter[1].split('\n').map((line) => {
|
||||
const index = line.indexOf(':');
|
||||
return index === -1 ? [line, ''] : [line.slice(0, index).trim(), line.slice(index + 1).trim()];
|
||||
}));
|
||||
for (const key of ['id', 'date', 'status', 'scope', 'evidence', 'review_after']) {
|
||||
if (!fields[key]) failures.push(`${file}: missing ${key}`);
|
||||
}
|
||||
if (fields.status !== state) failures.push(`${file}: status must be ${state}`);
|
||||
if (state === 'promoted' && !fields.destination) failures.push(`${file}: promoted lesson needs destination`);
|
||||
for (const section of requiredSections) if (!source.includes(section)) failures.push(`${file}: missing ${section}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`Learning record failures:\n${failures.join('\n')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Learning records are valid');
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PROFILE="${1:?usage: with-local-supabase-env.sh <web|admin|mobile|mcp|tests> <command...>}"
|
||||
shift
|
||||
[ "$#" -gt 0 ] || { echo "command is required" >&2; exit 2; }
|
||||
|
||||
STATUS_ENV="$(cd "$ROOT" && npx supabase status --workdir apps -o env 2>/dev/null)" || {
|
||||
echo "Local Supabase is not running. Run scripts/dev-local.sh up." >&2
|
||||
exit 1
|
||||
}
|
||||
eval "$STATUS_ENV"
|
||||
|
||||
case "${API_URL:-}" in
|
||||
http://127.0.0.1:*|http://localhost:*) ;;
|
||||
*) echo "Refusing non-local Supabase URL: ${API_URL:-unset}" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "$PROFILE" in
|
||||
web)
|
||||
exec env NEXT_PUBLIC_SUPABASE_URL="$API_URL" "$@"
|
||||
;;
|
||||
admin)
|
||||
exec env VITE_SUPABASE_URL="$API_URL" VITE_SUPABASE_ANON_KEY="$ANON_KEY" "$@"
|
||||
;;
|
||||
mobile)
|
||||
exec env EXPO_PUBLIC_SUPABASE_URL="$API_URL" EXPO_PUBLIC_SUPABASE_ANON_KEY="$ANON_KEY" "$@"
|
||||
;;
|
||||
mcp)
|
||||
exec env PORT=3002 SUPABASE_URL="$API_URL" \
|
||||
SUPABASE_SERVICE_ROLE_KEY="$SERVICE_ROLE_KEY" \
|
||||
SAUCI_MCP_API_KEY="local-sauci-mcp-only" "$@"
|
||||
;;
|
||||
tests)
|
||||
exec env SUPABASE_URL="$API_URL" SUPABASE_ANON_KEY="$ANON_KEY" \
|
||||
SUPABASE_SERVICE_ROLE_KEY="$SERVICE_ROLE_KEY" "$@"
|
||||
;;
|
||||
*) echo "unknown profile: $PROFILE" >&2; exit 2 ;;
|
||||
esac
|
||||
Reference in new issue
Block a user