From 0aecc3b6fbd6e81a7d45b4845857db7a4adb3923 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Sat, 18 Jul 2026 01:07:37 +0800 Subject: [PATCH] chore(agents): share guidance across Codex and Claude --- .agents/skills/ship-pr/SKILL.md | 84 +++++++++++++ .claude/hooks/post-tool-use.sh | 66 ----------- .claude/hooks/session-start.sh | 25 ---- .claude/hooks/stop.sh | 41 ------- .claude/settings.json | 20 ---- .claude/skills/ship-pr/SKILL.md | 1 + .gitignore | 7 +- AGENTS.md | 202 +++++++++++++++++++++++--------- CLAUDE.md | 91 +------------- docs/HARNESS.md | 28 ++--- eslint.config.js | 7 +- src/archtest/db-access.test.ts | 2 +- 12 files changed, 253 insertions(+), 321 deletions(-) create mode 100644 .agents/skills/ship-pr/SKILL.md delete mode 100755 .claude/hooks/post-tool-use.sh delete mode 100755 .claude/hooks/session-start.sh delete mode 100755 .claude/hooks/stop.sh delete mode 100644 .claude/settings.json create mode 120000 .claude/skills/ship-pr/SKILL.md diff --git a/.agents/skills/ship-pr/SKILL.md b/.agents/skills/ship-pr/SKILL.md new file mode 100644 index 0000000..d3490da --- /dev/null +++ b/.agents/skills/ship-pr/SKILL.md @@ -0,0 +1,84 @@ +--- +name: ship-pr +description: Ship a finished openboot.dev change through the canonical pull-request flow. Use when the user asks to open, submit, or ship a PR/MR for the current branch, including phrases such as "open a PR", "ship this", "提 PR", or "提个 MR". Push, open the PR, wait for CI, review the full diff, fix small findings, escalate product decisions, squash-merge a clean PR, and clean up locally. Do not use for status checks on an existing PR or for draft/WIP work. +--- + +# Ship a PR for openboot.dev + +Treat the PR review as the last reversible gate before `main` deploys. + +Do not use auto-merge. Complete CI and review before merging from the current +session. + +## Check the branch + +Run: + +```bash +git status -sb +git rev-parse --abbrev-ref HEAD +git log --oneline main..HEAD +``` + +Stop if the branch is `main` or has no commits ahead of `main`. Account for all +working-tree changes before shipping. + +For draft/WIP work, use a draft PR and stop. For a backwards-incompatible D1 +migration, verify it locally and ask for the rollout decision before proceeding. + +## Push and open the PR + +Push the current branch: + +```bash +git push -u origin "$(git rev-parse --abbrev-ref HEAD)" +``` + +The installed pre-push hook runs `npm run validate`; CI remains the gate when +the hook is not installed. + +Create a PR with a Conventional Commits title under 70 characters. Summarize +the change and list the actual local verification in the body. + +## Wait for CI + +Run: + +```bash +gh pr checks --watch +``` + +Require the checks listed in `.github/required-checks.txt`. If a required check +fails, inspect it with `gh run view --log-failed`, fix failures that have an +unambiguous solution, push, and wait again. Escalate failures that require a +product or rollout decision. Treat harness drift sensors as informational unless +repository policy changes. + +## Review the full diff + +After CI passes, review `git diff main...HEAD` for behavior, design, tests, +risk, and rollback. In particular, check: + +- D1 access boundaries and parameter binding. +- API contract alignment with `openboot-contract`. +- Forward-only D1 migrations. +- Round trips for fields that flow CLI -> server -> CLI. +- New warnings, missing tests, and unexpected generated files. + +Fix small, clear issues in the current session, commit them, push, and wait for +CI again. Escalate choices that depend on product intent or team convention. +Merge without asking for another confirmation when the PR is clean. + +## Merge and clean up + +Run: + +```bash +gh pr merge --squash --delete-branch +git checkout main +git pull --ff-only +``` + +Delete the local feature branch if it still exists. Do not roll back a failed +deployment, change the separate `openboot-contract` repository, or alter +production secrets without explicit direction. diff --git a/.claude/hooks/post-tool-use.sh b/.claude/hooks/post-tool-use.sh deleted file mode 100755 index 0387986..0000000 --- a/.claude/hooks/post-tool-use.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -# -# PostToolUse hook — fires after every Edit / Write / MultiEdit. -# -# For .ts / .svelte edits: runs `eslint` on the touched file so Claude -# gets immediate feedback if the edit introduced a lint error. ESLint -# is fast (~500ms on a single file warm) and catches the typical AI -# slip-ups: unused imports, `as any`, `@ts-ignore`, `console.log`. -# -# Exit code contract (per Claude Code hooks): -# 0 → success, silent -# 2 → block: stderr is fed back to Claude as feedback to self-correct -# -# Warnings DO NOT block — only errors do. Promote a rule from warn to -# error in eslint.config.js when its existing call sites are cleaned up. - -set -uo pipefail - -project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}" -cd "$project_dir" - -input=$(cat) -file_path=$(printf '%s' "$input" | node -e " -let d=''; process.stdin.on('data', c => d+=c); process.stdin.on('end', () => { - try { const j = JSON.parse(d); process.stdout.write(j.tool_input?.file_path || ''); } - catch { process.stdout.write(''); } -});" 2>/dev/null || true) - -[ -z "$file_path" ] && exit 0 - -# Skip non-source files. -case "$file_path" in - *.ts|*.svelte|*.js) ;; - *) exit 0 ;; -esac - -# Normalise to a path relative to the project root. Edits outside the -# project (e.g. ~/.claude/...) are ignored. -case "$file_path" in - "$project_dir"/*) rel_path="${file_path#"$project_dir"/}" ;; - /*) exit 0 ;; - *) rel_path="$file_path" ;; -esac - -# Skip directories that ESLint already ignores. -case "$rel_path" in - .svelte-kit/*|build/*|node_modules/*|coverage/*|.wrangler/*) exit 0 ;; -esac - -# Skip files outside src/ — config files, scripts, etc. don't need the -# strict treatment and would only slow the agent down. -case "$rel_path" in - src/*) ;; - *) exit 0 ;; -esac - -[ -f "$project_dir/$rel_path" ] || exit 0 - -# Run eslint on just this file. --no-warn-ignored prevents a warning when -# the file is excluded by config. -if ! out=$(npx eslint --no-warn-ignored "$rel_path" 2>&1); then - printf '[post-tool-use] eslint failed for %s:\n%s\n' "$rel_path" "$out" >&2 - exit 2 -fi - -exit 0 diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh deleted file mode 100755 index 9d20a33..0000000 --- a/.claude/hooks/session-start.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# SessionStart hook — warm caches so the first svelte-check / vitest in -# the session is fast and offline-safe. Only runs on Claude Code remote; -# local dev machines are expected to already have a working node_modules. -set -euo pipefail - -if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then - exit 0 -fi - -cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" - -echo "[openboot.dev session-start] Node version: $(node --version)" - -if [ ! -d node_modules ]; then - echo "[openboot.dev session-start] Installing deps..." - npm install --legacy-peer-deps --no-audit --no-fund -fi - -# Generates .svelte-kit/types/* so svelte-check / vitest don't fail on -# the first cold reference to $types or $app/* aliases. -echo "[openboot.dev session-start] svelte-kit sync..." -npx svelte-kit sync >/dev/null - -echo "[openboot.dev session-start] Done." diff --git a/.claude/hooks/stop.sh b/.claude/hooks/stop.sh deleted file mode 100755 index 50b137c..0000000 --- a/.claude/hooks/stop.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# -# Stop hook — fires when Claude finishes its turn. -# -# If any .ts / .svelte file in the working tree differs from HEAD, run -# the cheap end-of-turn sensors: -# - svelte-check (`npm run check`) — TypeScript across .ts and .svelte -# - vitest run src/archtest/ — architecture invariants -# -# Skips the full unit suite — that's pre-push's job. Goal: <15s warm. -# -# Exit codes: -# 0 → allow stop -# 2 → block stop; stderr is fed back to Claude so it can self-correct -# -# Skip via OPENBOOT_DEV_SKIP_STOP_HOOK=1 if iterating. - -set -uo pipefail - -[ "${OPENBOOT_DEV_SKIP_STOP_HOOK:-}" = "1" ] && exit 0 - -cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" - -# Cheap gate: only fire if any tracked or untracked .ts/.svelte file is -# dirty. Skip otherwise — most turns don't touch source. -dirty=$(git status --porcelain 2>/dev/null | grep -E '\.(ts|svelte)$' | head -1 || true) -[ -z "$dirty" ] && exit 0 - -echo "[stop] running end-of-turn sensors..." >&2 - -if ! check_out=$(npm run --silent check 2>&1); then - printf '[stop] svelte-check failed:\n%s\n' "$check_out" >&2 - exit 2 -fi - -if ! arch_out=$(npx vitest run src/archtest/ 2>&1); then - printf '[stop] archtest failed:\n%s\n' "$arch_out" >&2 - exit 2 -fi - -exit 0 diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index c3bfe63..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" }] - } - ], - "PostToolUse": [ - { - "matcher": "Edit|Write|MultiEdit", - "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use.sh" }] - } - ], - "Stop": [ - { - "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop.sh" }] - } - ] - } -} diff --git a/.claude/skills/ship-pr/SKILL.md b/.claude/skills/ship-pr/SKILL.md new file mode 120000 index 0000000..44f0db4 --- /dev/null +++ b/.claude/skills/ship-pr/SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/ship-pr/SKILL.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9b6a614..855fd0b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,12 +24,9 @@ coverage/ # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* -# .claude/ contains both committed project assets (settings.json, -# hooks/, skills/) and per-user noise (transcripts, local overrides). -# Ignore everything by default, allow the committed pieces explicitly. +# .claude/ contains the committed skill aliases plus per-user noise +# (transcripts, local overrides). Ignore everything except shared skills. .claude/* -!.claude/hooks/ -!.claude/settings.json !.claude/skills/ .claude/settings.local.json .dev.vars diff --git a/AGENTS.md b/AGENTS.md index 19c2a4a..fc3f937 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,80 +1,172 @@ # AGENTS.md -Canonical pointer for AI coding agents working on this repo. If you're a -human, you probably want [README.md](README.md) or [CLAUDE.md](CLAUDE.md). +Canonical instructions for coding agents working on this repository. Keep shared +project knowledge here rather than in tool-specific configuration. + +## Agent compatibility + +- Codex loads this `AGENTS.md` directly. +- Claude Code loads [CLAUDE.md](CLAUDE.md), which imports this file with + `@AGENTS.md`. +- Canonical project skills live in [`.agents/skills/`](.agents/skills/). + [`.claude/skills/`](.claude/skills/) contains relative `SKILL.md` symlinks so + Claude Code and agents that follow the open Agent Skills layout use the same + instructions. +- Quality enforcement is agent-independent: use the Git hooks, `npm run + validate`, and CI. Do not add tool-specific lifecycle hooks for checks that + belong in those shared gates. ## Read first -- **[CLAUDE.md](CLAUDE.md)** — project conventions, stack overview, - request flow, where-to-look table. Treat as authoritative. -- **[docs/HARNESS.md](docs/HARNESS.md)** — the steering meta-doc: when a - class of issue recurs, which file do you edit to prevent it next time. +- **[docs/HARNESS.md](docs/HARNESS.md)** — where repository controls live and + how to encode a recurring issue as a mechanical check. +- **[README.md](README.md)** — product-facing setup and usage. -## Project invariants (the things that must not drift) +## Overview -These are enforced mechanically. New violations fail `npm run validate` -or the `.claude/hooks/stop.sh` end-of-turn check. +Web dashboard and install API for OpenBoot, a CLI that bootstraps developer +machines. The app uses SvelteKit 5 on Cloudflare Workers with D1 (SQLite). It +serves configuration pages, curl-compatible install scripts, a dashboard, +OAuth login, and Markdown documentation. -| Invariant | Enforced by | -|---|---| -| D1 access (`.prepare` / `.exec` / `.batch`) only in `+server.ts` or `src/lib/server/db/` | `src/archtest/db-access.test.ts` | -| No `process.env` in `src/**` — Cloudflare Workers uses `platform.env` | `eslint` `no-restricted-globals` + `src/archtest/env.test.ts` | -| No `console.log` in `src/lib/server/**` — use `console.error` for real errors | `src/archtest/server-console.test.ts` | -| No `@ts-ignore` / `@ts-nocheck` | `eslint` `@typescript-eslint/ban-ts-comment` | -| No `eval` / `new Function` | `eslint` `no-eval` / `no-implied-eval` | -| Commit subjects follow Conventional Commits | `.github/workflows/conventional-commits.yml` | +The Go CLI lives at [openbootdotdev/openboot](https://github.com/openbootdotdev/openboot). -The full convention list lives in CLAUDE.md → "Conventions". - -## Run before committing +## Commands ```bash -npm run lint # eslint, ~2s warm -npm run check # svelte-check, ~10s -npx vitest run # unit tests, ~1s +npm run dev # Local development server +npm run build # Production build +npm run check # svelte-kit sync + svelte-check +npm run lint # ESLint +npm run validate # lint + check + tests + build (the harness gate) +npm test # All Vitest suites +npx vitest run src/lib/server/auth.test.ts +npx vitest run -t "test name" +npm run test:coverage +npm run install:hooks # Install shared Git pre-commit/pre-push hooks + +# Database +wrangler d1 migrations apply openboot --local +wrangler d1 migrations apply openboot --remote ``` -Or the umbrella: +Local development requires `.dev.vars` containing `GITHUB_CLIENT_ID`, +`GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, and `GOOGLE_CLIENT_SECRET`. + +## Architecture + +**Stack:** SvelteKit 5 (Svelte 5 runes), TypeScript, Cloudflare Workers, D1, +mdsvex, Shiki, and Fuse.js. + +`src/hooks.server.ts` intercepts requests, resolves short aliases such as +`/dev` to `/user/slug`, serves install scripts to curl/wget clients, and applies +security headers. + +| Path | Purpose | +|---|---| +| `src/hooks.server.ts` | Alias resolution, curl detection, security headers | +| `src/lib/server/` | Server-only auth, DB helpers, install scripts, rate limiting, validation | +| `src/lib/stores/` | Client-side auth and theme stores | +| `src/lib/presets.ts` | Package presets | +| `src/lib/package-metadata.ts` | Source of truth for package metadata; served at `/api/packages` | +| `src/routes/api/` | REST endpoints (`+server.ts`) | +| `src/routes/[username]/[slug]/` | Config page, install endpoint, config JSON, OG image | +| `src/docs/` | mdsvex documentation | +| `migrations/` | Sequential D1 migrations (`0001_*.sql`) | + +### Auth + +1. OAuth: `/api/auth/login` -> provider -> `/api/auth/callback/{github,google}` + -> JWT in the httpOnly `session` cookie. +2. Session: `getCurrentUser(request, cookies, db, secret)` verifies the JWT. +3. CLI device flow: `/api/auth/cli/start` -> approval at `/cli-auth` -> polling + at `/api/auth/cli/poll` -> `obt_`-prefixed API token. + +### Database + +D1 is used directly with parameterized SQL; there is no ORM. The tables are +`users`, `configs`, `config_revisions`, `api_tokens`, and `cli_auth_codes`. +`configs.packages` stores a JSON array of `{name, type}` objects and read paths +enrich each entry from `package-metadata.ts`. Visibility is `public`, +`unlisted`, or `private`. + +D1 cannot `ALTER TABLE DROP COLUMN`; remove columns with a new table and a data +migration. + +### Install scripts + +`src/lib/server/install-script.ts` provides `generateInstallScript()` for +public configs and `generatePrivateInstallScript()` for private configs that +require CLI authentication. + +## Conventions + +- Write code, comments, documentation, and commits in English. +- Use Svelte 5 runes (`$state`, `$derived`, `$props()`), not legacy `$:`. +- Return API data with `json({...})` and appropriate status codes. Return + errors as `json({ error: '...' }, { status: N })`. +- Use Conventional Commits: `type(scope): subject`. +- Put reusable D1 queries in `src/lib/server/db/`; endpoint-local access may + stay in `+server.ts`. Always bind query parameters. +- Do not use `as any`, `@ts-ignore`, or `@ts-nocheck`; model the type instead. +- Use `slugify()` from `$lib/server/auth` for lowercase alphanumeric/hyphen + slugs. +- Treat the in-memory sliding-window rate limiter as per-Worker-isolate, not + globally consistent. + +## Project invariants + +New violations fail `npm run validate`. + +| Invariant | Enforced by | +|---|---| +| D1 access (`.prepare` / `.exec` / `.batch`) stays in `+server.ts` or server-only helpers | `src/archtest/db-access.test.ts` | +| No `process.env` in `src/**`; Workers use `platform.env` | ESLint + `src/archtest/env.test.ts` | +| No `console.log` in server code; use `console.error` for real errors | `src/archtest/server-console.test.ts` | +| No `@ts-ignore` / `@ts-nocheck` | ESLint `@typescript-eslint/ban-ts-comment` | +| No `eval` / `new Function` | ESLint `no-eval` / `no-implied-eval` | +| Commit subjects use Conventional Commits | `.github/workflows/conventional-commits.yml` | + +When an architecture test fails, move accidental violations into an allowed +path. If the boundary is genuinely too narrow, update the test's allow-list and +explain why in the commit. The test is the audit trail; there is no baseline +file. + +## Verification + +Before committing, run: ```bash -npm run validate # lint + check + test + build, ~30s +npm run lint +npm run check +npx vitest run ``` -Install the git hooks once with `npm run install:hooks` and the -pre-commit (lint diff) + pre-push (full validate) run automatically. +Run `npm run validate` for the full gate. Install the shared Git hooks once with +`npm run install:hooks`; pre-commit checks the staged diff and pre-push runs the +full validation suite. -## When archtest fails +## CI/CD -The failure message names the violating file and the rule. Two options: +Pull requests run CI without deploying. A push to `main` runs CI and then the +deployment workflow. Deployment applies D1 migrations before deploying and +finishes with health and contract checks. -1. **You added a violation by accident** — move the call into an allowed - path (e.g. DB access into `src/lib/server/db/` or a `+server.ts`). -2. **The allowed-paths list is genuinely too narrow** — edit the - `allowedPaths` array in the test, and explain in your commit message - why the new path is justified. There is no baseline file; the test - itself is the audit trail. +## Actions that require confirmation -## Tools you may NOT use without asking +- Force-pushing `main`. +- Amending commits that have already been pushed. +- Hard-resetting away uncommitted work. +- Writing to production D1 with `wrangler d1 execute --remote`. +- Running `wrangler deploy` directly; CI deploys from `main`. +- Modifying production OAuth secrets or rotating JWTs. -- `git push --force` against `main`. -- `git commit --amend` on commits already pushed. -- `git reset --hard` discarding uncommitted work. -- `wrangler d1 execute --remote ...` against production D1 (read-only is - fine; writes are not). -- `wrangler deploy` directly — CI deploys on push to `main`. -- Anything that modifies production OAuth secrets or rotates JWTs. - -Everything else (Edit/Write/Read in repo, `npm run *`, `wrangler ... ---local`, vitest) is safe to run without confirmation. +Repository reads/writes, `npm run *`, local Wrangler commands, and Vitest are +otherwise safe without confirmation. ## Skills -Project-specific skills live under [`.claude/skills/`](.claude/skills/): - -- `ship-pr` — canonical post-edit flow: push → `gh pr create` → wait - for CI → review diff → triage (self-fix small / escalate decisions / - merge directly when clean) → `gh pr merge --squash` → local cleanup. - Use this instead of calling `gh pr create` / `gh pr merge` directly. - **Do not use `--auto`** — it skips the review gate. - **Do not ask for confirmation on a clean merge** — the loop closes - itself when nothing requires a decision. +- `ship-pr` — use the shared skill for the canonical push -> PR -> CI -> review + -> triage -> squash-merge -> cleanup flow. Never use `--auto`; merge directly + when the PR is clean, and escalate only decisions that need product or team + input. diff --git a/CLAUDE.md b/CLAUDE.md index 69bf750..f6aa6c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,92 +1,3 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -Web dashboard and install API for OpenBoot — a CLI tool that bootstraps developer machines. SvelteKit 5 on Cloudflare Workers with D1 (SQLite). Serves config pages, install scripts (curl-able), dashboard UI, OAuth login, and markdown docs. - -The CLI lives at [openbootdotdev/openboot](https://github.com/openbootdotdev/openboot) (Go). - -## Commands - -```bash -npm run dev # Local dev server -npm run build # Production build -npm run check # Type checking (svelte-kit sync + svelte-check) -npm run lint # ESLint -npm run validate # check + lint + test (the harness gate) -npm test # Run all tests (vitest) -npx vitest run src/lib/server/auth.test.ts # Run a single test file -npx vitest run -t "test name" # Run test by name -npm run test:coverage # Tests with coverage report -npm run install:hooks # Install git pre-commit/pre-push hooks - -# Database -wrangler d1 migrations apply openboot --local # Apply migrations locally -wrangler d1 migrations apply openboot --remote # Apply migrations to production -``` - -See [AGENTS.md](./AGENTS.md) for harness invariants (no `process.env`, no `console.log` in server code, D1 access scoping) and [docs/HARNESS.md](./docs/HARNESS.md) for the full enforcement model. - -Local dev requires `.dev.vars` with `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. - -## Architecture - -**Stack**: SvelteKit 5 (Svelte 5 runes) + TypeScript, Cloudflare Workers + D1 (SQLite), mdsvex for docs, shiki for syntax highlighting, fuse.js for doc search. - -### Request Flow - -`hooks.server.ts` intercepts all requests — it resolves aliases (short URLs like `/dev` → `/user/slug`), detects curl/wget user-agents to serve install scripts instead of HTML, and applies security headers globally (CSP, HSTS, X-Frame-Options). - -### Key Directories - -| Path | Purpose | -|------|---------| -| `src/hooks.server.ts` | Alias resolution, curl detection, security headers | -| `src/lib/server/` | Server-only: `auth.ts` (JWT + OAuth), `install-script.ts`, `rate-limit.ts`, `validation.ts` | -| `src/lib/stores/` | Svelte stores: `auth.ts` (user state), `theme.ts` (light/dark) | -| `src/lib/presets.ts` | Package presets (minimal, developer, full) | -| `src/lib/package-metadata.ts` | Source of truth for package metadata (100+ packages). Served via `/api/packages` for CLI consumption | -| `src/routes/api/` | REST API endpoints (`+server.ts` convention) | -| `src/routes/[username]/[slug]/` | Config page, install endpoint, config JSON, OG image | -| `src/docs/` | Markdown docs (mdsvex preprocessed) | -| `migrations/` | D1 SQL migrations (sequential `0001_*.sql` format) | - -### Auth System - -Three auth flows: -1. **OAuth** (GitHub + Google): `/api/auth/login` → provider → `/api/auth/callback/{github,google}` → JWT in httpOnly `session` cookie -2. **Session**: `getCurrentUser(request, cookies, db, secret)` from `$lib/server/auth` verifies JWT -3. **CLI device flow**: `/api/auth/cli/start` → user approves at `/cli-auth` → CLI polls `/api/auth/cli/poll` → gets `obt_` prefixed API token - -### Database - -D1 (SQLite), no ORM — direct parameterized SQL via `env.DB.prepare(sql).bind(...)`. Five tables: `users`, `configs`, `config_revisions`, `api_tokens`, `cli_auth_codes`. The `configs.packages` field is a JSON array of `{name, type}` objects; on read, endpoints enrich each entry with a `desc` filled from `package-metadata.ts`. Config visibility: `public` (discoverable), `unlisted` (accessible but not listed), `private` (owner-only, 403 on install). - -**D1 limitation**: No `ALTER TABLE DROP COLUMN`. Plan column removals via new table + data migration. - -### Install Script Generation - -`src/lib/server/install-script.ts` generates bash scripts: `generateInstallScript()` for public configs (installs packages, runs custom script, clones dotfiles), `generatePrivateInstallScript()` for private configs (requires CLI auth first). - -## Conventions - -- **Language**: All code, comments, docs, and commits in English -- **Svelte 5 runes**: Use `$state`, `$derived`, `$props()` — not legacy `$:` syntax -- **API responses**: Always `json({...})` with appropriate status codes. Errors: `json({ error: '...' }, { status: N })` -- **Commits**: Conventional Commits format: `type(scope): subject` (enforced by CI) -- **DB access**: Only in `+server.ts` files, always with parameterized `.bind()` queries -- **No `as any` or `@ts-ignore`** — type properly -- **Rate limiting**: In-memory sliding window per Worker isolate (not globally consistent) -- **Slug generation**: `slugify()` from `$lib/server/auth` — lowercase, alphanumeric + hyphens - -## CI/CD - -Push to `main` runs CI (type check + tests + build) then auto-deploys. PRs only run CI, no deploy. - -Deploy pipeline: check (type check → test → build) → deploy (build → D1 migrations → wrangler deploy → health check). Migrations run before deploy — safe for schema changes that new code depends on. - -## Testing - -Vitest with happy-dom. Test files: `src/**/*.{test,spec}.{js,ts}`. Setup: `src/lib/test/setup.ts`. Fixtures and DB mocks in `src/lib/test/`. +@AGENTS.md diff --git a/docs/HARNESS.md b/docs/HARNESS.md index bd56e7d..fd61f3d 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -21,7 +21,7 @@ the next agent (or the next refactor by a human) cannot drift the same way. Two execution flavors: - **Computational** — deterministic, fast, free: `eslint`, `svelte-check`, - `vitest`, `src/archtest/*`. Run on every change. + `vitest`, `src/archtest/*`. Run during local verification and in CI. - **Inferential** — non-deterministic, slower, paid: AI code review, `/security-review`, `/ultrareview`. Run on integration boundaries. @@ -29,7 +29,7 @@ Three regulation categories: 1. **Maintainability** — code style, complexity, dead code. 2. **Architecture fitness** — project-specific invariants (the "do X, not Y" - rules in CLAUDE.md). + rules in `AGENTS.md`). 3. **Behaviour** — does the code actually do the right thing. ## Where each control lives @@ -37,24 +37,21 @@ Three regulation categories: | Category | Control | Trigger | File | |---|---|---|---| | Maint. | `prettier --check` | save / `npm run format:check` | `.prettierrc` | -| Maint. | `eslint` (ts/svelte) — `no-explicit-any`, `no-unused-vars`, `no-console`, `no-restricted-globals` (banned: `process`), Svelte best practices | `npm run lint` / `.claude/hooks/post-tool-use.sh` / CI | `eslint.config.js` | +| Maint. | `eslint` (ts/svelte) — `no-explicit-any`, `no-unused-vars`, `no-console`, `no-restricted-globals` (banned: `process`), Svelte best practices | `npm run lint` / pre-commit / CI | `eslint.config.js` | | Maint. | `npm audit --audit-level=high` (drift) | informational CI | `.github/workflows/harness.yml` | | Maint. | `knip` dead-code (drift) | informational CI | `.github/workflows/harness.yml` | | Maint. | `required-checks alignment` (drift) — `.github/required-checks.txt` ↔ workflow job names | informational CI | `.github/workflows/harness.yml` | -| Arch. | `db-access-scoping` — `.prepare()` / `.exec()` / `.batch()` only in `+server.ts` or `src/lib/server/db/` | vitest (`src/archtest/`) | `src/archtest/db-access.test.ts` | +| Arch. | `db-access-scoping` — `.prepare()` / `.exec()` / `.batch()` only in `+server.ts` or server-only helpers | vitest (`src/archtest/`) | `src/archtest/db-access.test.ts` | | Arch. | `no-process-env` — Cloudflare Workers uses `platform.env`, not `process.env` | vitest + eslint `no-restricted-globals` | `src/archtest/env.test.ts` + `eslint.config.js` | | Arch. | `no-console-in-server` — server code uses `console.error` only (no `console.log`) | vitest | `src/archtest/server-console.test.ts` | -| Behav. | `svelte-check` (TypeScript across `.ts` and `.svelte`) | `npm run check` / `.claude/hooks/stop.sh` / CI | `tsconfig.json` | +| Behav. | `svelte-check` (TypeScript across `.ts` and `.svelte`) | `npm run check` / pre-push / CI | `tsconfig.json` | | Behav. | `vitest run` (unit + smoke) | `npm test` / pre-push / CI | `vitest.config.ts` | | Behav. | `vitest --coverage` → Codecov (informational) | CI | `.github/workflows/ci.yml` | | Behav. | Contract schema validation against `openboot-contract` | CI `check` job + post-deploy | `.github/workflows/ci.yml`, `.github/workflows/deploy.yml` | | Behav. | Post-deploy health check (`/api/health`) | CD `deploy` job | `.github/workflows/deploy.yml` | | Behav. | Post-deploy smoke test + contract round-trip | CD `deploy` job | `scripts/smoke-test-api.sh` | -| Feedfwd. | Agent conventions | every AI turn | `CLAUDE.md`, `AGENTS.md` | -| Feedfwd. | Session-start hook (warm `svelte-kit sync`) | every Claude session | `.claude/hooks/session-start.sh` | -| Feedfwd. | `ship-pr` skill — push → CI → review → triage → squash → cleanup; **no `--auto`** | model-loaded | `.claude/skills/ship-pr/SKILL.md` | -| Feedback (agent) | `eslint` on edited file | after every Edit/Write/MultiEdit | `.claude/hooks/post-tool-use.sh` | -| Feedback (agent) | `svelte-check` + `archtest` | end of every Claude turn (if ts/svelte dirty) | `.claude/hooks/stop.sh` | +| Feedfwd. | Shared agent conventions | Codex loads directly; Claude imports with `@AGENTS.md` | `AGENTS.md`, `CLAUDE.md` | +| Feedfwd. | `ship-pr` skill — push → CI → review → triage → squash → cleanup; **no `--auto`** | model-loaded | `.agents/skills/ship-pr/SKILL.md` (`.claude/skills/ship-pr/SKILL.md` is a symlink) | | Maint. | `eslint` on staged diff + `prettier --check` | local git pre-commit | `scripts/hooks/pre-commit` | | Behav. | `npm run validate` (lint + check + test + build) | local git pre-push | `scripts/hooks/pre-push` | | Drift loop | Failed harness sensor → open/update GitHub issue | on main / nightly | `.github/workflows/drift-to-issue.yml` | @@ -71,7 +68,7 @@ When you observe a recurring issue, decide where to encode the fix: | "Agent reaches for `process.env` on Cloudflare." | Already enforced by `eslint` `no-restricted-globals` + `src/archtest/env.test.ts`. | | "Agent introduces a new lint failure that ESLint should have caught." | Enable the relevant rule in `eslint.config.js`. | | "Agent breaks behaviour that has no test." | Write the test next to existing ones (`src/**/*.test.ts`). The pattern is vitest + happy-dom + the helpers in `src/lib/test/`. | -| "Agent missed a CLAUDE.md rule we keep restating." | Make it a lint rule or an archtest test. A docs rule that doesn't fail is a docs rule that drifts. | +| "Agent missed an `AGENTS.md` rule we keep restating." | Make it a lint rule or an archtest test. A docs rule that doesn't fail is a docs rule that drifts. | | "Agent guessed at an API contract." | Update `openboot-contract` fixtures + schemas. CI runs schema validation against them in the `check` job and after deploy. | | "Agent's PR description was off." | Tighten `.github/pull_request_template.md` (if added) or the `ship-pr` skill. | | "Drift sensor failed on main but nobody noticed." | Already handled: `.github/workflows/drift-to-issue.yml` opens/updates a tracking issue per failed sensor. | @@ -102,10 +99,13 @@ ideally one rule per PR so the diff is reviewable: test-shaped code without raising actual quality. - **No husky / lint-staged.** `scripts/hooks/` symlinked via `npm run install:hooks` does the same job in ~30 lines, no runtime dep. +- **No agent-specific lifecycle hooks.** Codex, Claude Code, and other tools + have different hook protocols. Shared Git hooks and CI enforce the same + checks for every contributor and agent. - **No baseline file for archtest rules.** Repo is small enough to clean up violations directly when a new rule is added. -- **No agent-driven changes to `main` without human review.** All AI - changes go through PR review and the existing CI matrix. +- **No agent-driven direct changes to `main`.** All changes go through PR + review and the existing CI matrix. - **No auto-release / tag automation.** Push to `main` triggers `ci.yml`; on success `deploy.yml` fires via `workflow_run` and ships to production. There is no separate release cadence to automate. @@ -115,7 +115,7 @@ ideally one rule per PR so the diff is reviewable: If you are reading this as an AI agent: this file tells you **where** to add a control, not what to check. The actual checks fire from `npm run -validate`, the `.claude/hooks/`, and the CI jobs. The most useful +validate`, the shared Git hooks, and the CI jobs. The most useful contribution you can make is, when a review reveals a recurring issue, proposing the row in the table above where the new control belongs — that is how the harness improves over time. diff --git a/eslint.config.js b/eslint.config.js index 6aa5095..d474109 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,5 @@ // Flat config — minimal rule set that mechanises the conventions -// already written in CLAUDE.md. See HARNESS.md for the steering loop +// already written in AGENTS.md. See HARNESS.md for the steering loop // (when a class of issue recurs, prefer encoding it here over re-stating // it in docs). @@ -29,7 +29,7 @@ export default [ parserOptions: { extraFileExtensions: ['.svelte'] } }, rules: { - // CLAUDE.md: "No `as any` or `@ts-ignore` — type properly". + // AGENTS.md: "Do not use `as any` or `@ts-ignore`; model the type instead". // Warn for now — full cleanup is tracked separately; `validate` // fails only on errors so existing call sites don't block. '@typescript-eslint/no-explicit-any': 'warn', @@ -37,8 +37,7 @@ export default [ // Stop merge of forgotten _-debugging without blocking intentional // throwaway names. Soft-landed (warn) so existing call sites - // don't block; new code stays clean via the .claude post-tool-use - // hook surfacing warnings during edit. + // don't block; `npm run lint`, Git hooks, and CI still surface them. 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', diff --git a/src/archtest/db-access.test.ts b/src/archtest/db-access.test.ts index ef1ce2b..a69d6d1 100644 --- a/src/archtest/db-access.test.ts +++ b/src/archtest/db-access.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { walkSourceFiles, collectViolations, formatViolations } from './walk'; -// CLAUDE.md: "DB access: Only in +server.ts files". +// AGENTS.md: keep D1 access in +server.ts or server-only helpers. // Extended in practice: server-only helpers under `src/lib/server/` // (auth, db query helpers) may access D1 too — concentrating SQL there // is the established pattern. Client-side code (Svelte components,