Status (reference, not a checklist). This was the original design intent for the dev/team experience; parts shipped, parts didn't. The authoritative contributor on-ramp is
CONTRIBUTING.md
- the README Quickstart; trust those and the code over this doc. Specifically not built as described below: a
bun run setupwizard (copy.dev.vars.exampleyourself), theenv:checkSessionStart step (the hook only runsbun install --frozen-lockfile), and the GitHub Actions CI in E4 — there is no hosted CI; runbun run verifyyourself before a PR.
The repo is built to be operated primarily from cloud Claude sessions (including mobile) with local dev as a fallback. Every workflow that a human would do via VS Code must also be doable by typing into a chat box.
.claude/settings.jsonat the repo root with:permissions.allowallowlist tailored for this project's common commands (bun *,bunx *,wrangler d1 *,wrangler tail,git status|log|diff|add|commit|push,gh pr *).- A SessionStart hook that runs
bun install --frozen-lockfileandbun run env:checkso every new web/mobile session is ready to run/test in ≤30s. - Environment variables setting
WRANGLER_DEV_REGISTRYandWRANGLER_LOG=warnto keep output mobile-readable.
CLAUDE.mdat root: 1-page architecture briefing + pointers to key files. Future Claude sessions land here first..claude/commands/*custom slash commands tuned for this project:/migrate— apply pending D1 migrations to the local + dev environment, print diff./seed— load fixture upstreams + docs into local D1 for demos./deploy:preview— wrangler versions deploy + post preview URL to the conversation./smoke— runs the cross-cutting smoke harness (see E5) and prints a status table.
bun run devstarts:- Vite dev server for the SPA on
:5173. wrangler dev --persist-to .wrangler/statefor the Worker on:8787with Miniflare local emulation: D1 (sqlite file), KV (sqlite), R2 (filesystem), Queues (in-memory), Durable Objects.
- Vite dev server for the SPA on
.dev.vars.examplechecked in with placeholders;.dev.varsgitignored. Copy it (cp .dev.vars.example .dev.vars) and fill in the secrets — see the README's "Filling in.dev.vars" for which ones you actually need to sign in locally. (There is nobun run setupwizard; the predev hooks provision TLS +apps/web/dist.)
Three layers, all runnable as bun run test, bun run test:int, bun run test:e2e:
| Layer | Runner | Scope | When |
|---|---|---|---|
| Unit | Vitest (node env) | Pure functions (chunker, token estimator, allowlist, namespacing, AES-GCM wrapper). 106 tests, ≤500ms total. Live under apps/worker/src/**/*.test.ts. |
Every change, every PR (bun run test). |
| Integration | Vitest + @cloudflare/vitest-pool-workers against a real D1 (miniflare-backed) |
Query-layer coverage: usage rollup math, doc-ACL gates, audit-log pagination. Migrations applied via applyD1Migrations in a setupFiles hook; per-test isolated storage rolls back inserts. 23 tests, ~600ms. Live under apps/worker/test/integration/. |
Locally + nightly (bun run test:int). Not in bun run verify to keep that fast. |
| End-to-end | Playwright | SPA sign-in, doc edit (two browsers), upstream connect, MCP setup, admin CRUD. Runs against a wrangler versions deploy preview URL. |
🚧 not yet wired; the test:e2e script is a stub. |
Special harnesses (planned, not all shipped):
tests/fixtures/fake-idp/— minimal OIDC issuer + GitHub-shaped API for Google and GitHub allowlist tests. No external dependency. 🚧 not yet built.tests/fixtures/fake-upstream-mcp/— a tiny in-process MCP server (Streamable HTTP) that the integration tests register as an upstream. Verifies proxy + namespacing + error surfacing end-to-end. 🚧 not yet built.
🚧 Not implemented. There is no hosted CI today — no
.github/workflows.bun run verifyis the gate and contributors run it locally before a PR; deploys are manual (bun run deploy/deploy:preview). The design below is kept as intent.
- GitHub Actions workflows:
pr.yml: install → typecheck → lint → unit + integration tests →wrangler versions deploy --preview→ post preview URL as PR comment.main.yml: same as PR + E2E against preview → on green,wrangler deployto production environment.prune.yml(cron): clears oldusage_events, archives staledoc_revisionsto R2.
- Branch model: trunk-based on
main, every change goes through a PR with preview deploy. Wrangler "environments" (preview,production) bind to different D1 databases.
Optimisations specifically for typing into Claude on a phone:
/smokeslash command — single command that:- Deploys a preview.
- Hits a hard-coded set of endpoints (
/api/health,/api/mewith a baked-in test token,/mcpinitialize+tools/list). - Returns a compact text status table — no screenshots required.
- Verbose-by-default scripts: every
bun runscript prints what it's about to do and a single-line summary on completion. No spinners (mobile transcripts hate them). bun run verify— composite command: typecheck + lint + unit + integration (fully offline;bun run verify:fulladds smoke). Returns a final pass/fail table. Designed to fit on one phone screen.wrangler tailaliases —bun run logs(errors only),bun run logs:all,bun run logs:mcp(filtered to /mcp routes). All print as plain text.- Curl-bot test tokens — a long-lived non-prod OAuth client whose secret is in CI secret env vars, used by smoke scripts. Scoped to a "test" user that doesn't appear in real usage rollups.
AGENTS.md— opinionated "how a Claude agent should make changes in this repo" file alongsideCLAUDE.md: where types live, what to run before pushing, the one-concern-per-file rule (~300 lines as a review trigger, not a cap), the test-first cadence. Reduces token cost of every future session.- Repository-level
.claude/output-style.jsonsets terse, mobile-friendly defaults for AI replies in this repo.
To keep AI agents (and humans) productive at scale:
- One file = one concern. This is the actual rule. Split a file when it holds separable concerns — two things a reader could name apart, that change for different reasons.
- ~300 LoC is a review trigger, not a hard cap. Its only job is to prompt the
question "is this still one thing?". It is a proxy for agent read-cost (~8.8
tokens/line here, so ~300 LoC ≈ 2.6k tokens — cheap to read whole; the 1100-line
tools-proxy.tsis ~10k, which forces an agent to grep and reason about a fragment). It is a weak proxy: dense JSX costs far more per line than type declarations, and this repo's comment style inflates counts on files that are conceptually small. Do not let it accrete more authority than that — in the 2026-07 review it predicted none of the findings that mattered (collab/doc-room-do.tsat 375 is fine; the worst authorization hole was in a ~180-line file). - Cohesive units are exempt at any length. A standalone component with its own
markup and handlers, a single Durable Object's lifecycle, one linear pipeline —
each is one concern however long it runs. Slicing them to hit a number buys
prop-drilling, artificial modules, and indirection across a boundary that isn't
there. Keep:
collab/doc-room-do.ts(one DO, one Y.Doc lifecycle),queues/reindex-consumer.ts(one pipeline). Split:db/queries/docs.ts(three tables + ACL predicates),mcp/tools-proxy.ts(registry + catalogue read-models- call runner),
routes/docs-editor/index.tsx(six independent hooks).
- call runner),
- Nothing enforces the number in
verify, and that is deliberate — a lint rule on line count would mechanise exactly the OCD-splitting this section warns against. - One folder = one concern. No circular imports across
apps/worker/src/*directories. - Every Hono route handler lives in
api/*with a one-line export; route-mounting happens centrally inapp.ts(index.tsonly wraps the composed app in the OAuthProvider + queue/scheduled handlers). - Every DO class has the file pattern
*-do.tsand the only export is the class. - D1 queries live in
db/queries/*.tsand never leak SQL into route files; queries return typed objects matchingpackages/shared. - All env access goes through
env.tstyped bindings —process.envis forbidden.
- Logpush to R2 (or to a third party if the org has one) for
wranglerlogs, retained 7 days. - Sentry (free tier or self-hosted) for unhandled exceptions in Worker + SPA. DSN in vars.
- Cloudflare Analytics Engine binding for high-cardinality custom metrics (per-tool latency, error rates) — cheaper than D1 for write-heavy series. Powers the admin "system health" sub-page.
- Cron health check — every cron run records its outcome in
audit_logso silent cron failures are visible.
Added by Section E:
- Vars:
SENTRY_DSN_WORKER,SENTRY_DSN_WEB,LOGPUSH_ENABLED. - Secrets:
CI_SMOKE_OAUTH_CLIENT_ID,CI_SMOKE_OAUTH_CLIENT_SECRET.
- Sign in to claude.ai/code, open the ctxlayer repo as a web session.
- Run
/smoketo confirm the preview deploy works. - Read
CLAUDE.md(5min). - Run
bun run verifylocally OR in the cloud session. - Skim
CONTRIBUTING.mdfor the change loop, pick something small, and keep the PR focused (one concern per file keeps changes reviewable).