Imported from zllovesuki/git-on-cloudflare (
AGENTS.md). Install upstream withnpx skills add zllovesuki/git-on-cloudflare. Copyright stays with the author.
AGENTS.md
Purpose
This repository is a Git Smart HTTP v2 server implemented on Cloudflare Workers with Durable Objects, R2, KV, React SSR, and a small amount of client-side hydration.
Write changes against the current source tree, not the docs alone. Some documentation is slightly behind the live layout.
Rules from the user
- Reuse existing types/helpers/methods. Do not invent new types/helpers/method unnecessarily, especially do not use any or casting.
- Prefer clarity over cleverness. Favor explicit names and intermediate types over dense inline expressions. Small duplication is acceptable when it materially improves readability and maintenance.
- Comment the code, especially around nuanced behaviors and footguns. This is also not an excuse to be lean on comments.
- Prioritize lookup on
cloudflare-docsmcp for up-to-date Cloudflare developer docs (if available). Fallback to searching the web if you cannot find the relevant information on this mcp
Avoid transitive boundary crossings
- Try to keep cross-runtime boundaries to a single hop from the caller. Workers -> DO, Workers -> R2, or DO -> R2 are allowed, but do not chain boundaries transitively. In particular, Workers -> DO -> R2 is not allowed in most circumstances.
- Retain strict mutation boundaries between R2, Workers, and Durable Object: Durable Objects can hold a transaction against its object. Do not attempt to propose a design that resembles a distributed transaction (e.g. Workers reads a row, decide on resolutions, then invoke Durable Object RPC to mutate)
- If there needs to be resolution, Durable Object can resolve that conflict in a transaction and return a tagged union for Workers to decide
- If you need to reach for
blockConcurrencyWhilein an Durable Object RPC, your design is probably wrong. Try again. - In general: Workers stays stateless, no mutations; Durable Objects are stateful, transactional.
Error handling
- Durable Objects do not throw. Use tagged union between Workers and Durable Objects to communicate outcome.
- Reserve
throwin Durable Object for FUBAR
No ad-hoc or duplicated types
- Before introducing a type, check whether a canonical one already exists. Key hubs:
src/worker/git/core/objects.ts(GitObjectType),src/worker/common/hex.ts(OID helpers, zeroOid),src/worker/git/object-store/support.ts(typeCodeToObjectType),src/worker/git/operations/limits.ts(Limiter). - Do not use
ReturnType<>,Awaited<ReturnType<>>, or inline union literals when a named type already covers the shape. Extract a named type alias if one doesn't exist yet. - Do not duplicate helpers. If you need
isZeroOidortypeCodeToObjectType, import the existing one—don't redefine it locally. - Watch for unused imports left behind after refactors; remove them.
Keep comments in sync with code
- When modifying behavior, update every comment, JSDoc, and inline note that describes the old behavior in the same change. Outdated comments are worse than no comments.
- Review neighboring comments when editing a function—if the surrounding prose no longer matches the logic, fix it.
Remove implementation-phase language
- Do not leave wording like "Phase 1/2/3", "TODO: next phase", "streaming-push WIP", or similar milestone markers in code, comments, or filenames once the feature is merged or the phase boundary is no longer meaningful. These create confusion for future readers.
- If a prior change left such references and you're editing the same area, clean them up.
Visibility logging on new code paths
- Every non-trivial code path that touches R2, DO RPC, or background work must include
structured logging using
createLoggerfromsrc/worker/common/logger.ts(or the DO'sthis.logger). - Follow the established conventions: kebab-case message identifiers scoped by component
(e.g.
"receive:finalize-committed"), appropriate log level (debug for flow, info for state changes, warn for recoverable errors, error for hard failures), and structured extra fields with relevant context (oid, packKey, counts, etc.). - When adding a branch or error path to an existing function that already logs, add matching visibility for the new path—don't leave silent gaps.
Limiter usage on platform-bound calls
- Every R2 read/write and outbound DO RPC in a request-scoped code path must go through
the
Limiterfromsrc/worker/git/operations/limits.tsvialimiter.run(label, fn). Obtain the limiter withgetLimiter(cacheCtx)or pass it through options. - Use a descriptive label prefixed by target (e.g.
"r2:get-pack","do:get-object-compat"). - Respect the subrequest budget (
DEFAULT_SUBREQUEST_BUDGET = 900). If the code path has its own budget (likeRECEIVE_SUBREQUEST_BUDGET), usecountSubrequest()to track it. - Never bypass the limiter for "just one call"—the hard 1000-subrequest and 6-concurrent-connection ceilings apply to the entire request, not individual call sites.
Stack At A Glance
- Runtime: Cloudflare Workers with
nodejs_als - Language: TypeScript ESM, strict mode
- UI: React 19 SSR via
react-dom/server, client islands, Tailwind CSS v4, Vite - Storage: Durable Object storage, Durable Object SQLite via
drizzle-orm/durable-sqlite, R2, KV - Routing:
itty-router - Path alias:
@/*maps tosrc/* - Formatting: Prettier only; no ESLint config is present
First Files To Read
src/worker/index.ts: top-level router registration and route orderingsrc/worker/routes/git.ts: Git Smart HTTP endpoints, upload-pack/receive-pack behaviorsrc/worker/routes/admin.ts: owner-authenticated admin JSON endpointssrc/worker/routes/auth.ts: auth UI and auth API endpointssrc/worker/do/repo/repoDO.ts: repository Durable Object — metadata authority, receive leases, compaction, and background worksrc/worker/do/repo/db/dal.ts: the required access layer for SQLite-backed repo metadatasrc/client/server/render.tsxandsrc/client/server/registry.tsx: SSR view registration and renderingwrangler.jsonc: bindings, vars, assets handling, compatibility date
Directory Map
src/worker/routes/: HTTP route registration and route handlerssrc/worker/do/repo/: repository Durable Object, receive, compaction, idle cleanup, storage, DBsrc/worker/do/auth/: authentication Durable Objectsrc/worker/git/core/: low-level Git protocol parsing and object helperssrc/worker/git/operations/: fetch, read-path logic, streaming upload-pack implementationsrc/worker/git/pack/: pack assembly, pack indexing, pack metadata helperssrc/client/pages/: route-level React pagessrc/client/components/: shared SSR componentssrc/client/islands/: client-only interactive modulessrc/client/entries/: Vite client entrypoints used by SSR pagessrc/shared/web/: browser-safe request parsing, formatting, MIME/JSON helperssrc/worker/common/: Worker response, logging, compression, stubs, progress helperstest/: Vitest worker integration tests and Node unit testsdocs/: architecture and API notes; useful, but verify against source before relying on path details
Core Invariants
- Route order matters.
registerAuthRoutes(router)must stay beforeregisterUiRoutes(router)so/authis not shadowed by/:owner. - The Worker owns HTML routing.
wrangler.jsoncsetsassets.html_handlingto"none"; do not move page ownership into the assets layer by accident. - The repo Durable Object is the source of truth for a single repository. Keep refs/HEAD authority there.
- SQLite access for repo metadata must go through
src/worker/do/repo/db/dal.ts. Do not add ad hoc raw Drizzle queries in unrelated files. RepoDurableObject.fetch()intentionally exposes only a small HTTP surface. Keep typed RPC methods as the default internal interface.- Receive uses a lease model: one active receive lease at a time, acquired via
beginReceive()and committed viafinalizeReceive(). Concurrent pushes receive503 Retry-After: 10. - Git fetch paths are streaming-sensitive. Avoid unnecessary buffering on upload-pack and pack assembly paths.
- Git pushes require a D1-backed PAT with
level = "push"; HTTP Basic username must match the route namespace slug. - UI rendering goes through
renderUiView()and the view registry insrc/client/server/registry.tsx. New pages should plug into that system rather than inventing a parallel renderer.
Normal Workflow For Agents
- Check repo state first with
git status --short. - Read the smallest relevant slice of the codebase before editing.
- Prefer narrow changes in the subsystem that owns the behavior.
- Run the smallest useful validation commands before finishing.
- Update tests when behavior changes.
Dirty worktrees are normal here. Do not overwrite or revert unrelated user changes.
Commands
Install and run
npm install
npm run dev
Build and static validation
npm run build
npm run typecheck
npm run format:check
Formatting
npm run format
Tests
npm run test runs Node Vitest tests for non-worker units in test/**/*.test.ts excluding *.worker.test.ts.
npm run test:workers runs Vitest against Cloudflare worker integration tests.
npm run test:auth runs only test/auth.worker.test.ts.
The 42 MiB pack-indexer fixture test is opt-in. Use PACK_INDEXER_FIXTURE=1 npx vitest run --config vitest.config.ts test/pack-indexer-fixture.worker.test.ts when you intentionally want to run it.
Targeted commands:
npx vitest run --config vitest.unit.config.ts test/object-parse.test.ts
npx vitest run --config vitest.config.ts test/streaming-receive.worker.test.ts
npx vitest run --config vitest.config.ts test/auth.worker.test.ts
Cloudflare and schema maintenance
npm run cf-typegen
npm run db:generate
Do not edit generated migrations under drizzle/repo-do/ manually. Treat src/worker/do/repo/db/schema.ts as the source of truth: make the schema change there first, then run npm run db:generate to generate the migration.
Validation By Change Type
- Git protocol, DO, pack, compaction, caching, or routing changes:
run
npm run typecheckand the relevant worker tests intest/*.worker.test.ts - Auth changes:
run
npm run test:auth - Pure parsing or helper changes:
run the targeted Node Vitest test plus
npm run typecheck - UI-only SSR/component changes:
run
npm run typecheck; if route behavior changed, add relevant worker coverage - SQLite schema or DAL changes:
run
npm run db:generate,npm run typecheck, and the worker tests that cover the affected flow
UI Notes
- Design context (audience, brand personality, aesthetic direction, color/typography choices) lives in
PRODUCT.mdat the project root. Read it before making visual or UX decisions. - SSR pages live in
src/client/pages/. - Shared shell/document logic lives in
src/client/server/. - Client interactivity should stay in focused islands under
src/client/islands/. - If a page needs client code, wire it through
src/client/entrypoints.tsandsrc/client/server/registry.tsx. - Shared CSS starts at
src/client/styles.css, which importssrc/client/styles/app.css.
Testing Notes
- Worker Vitest uses
@cloudflare/vitest-pool-workersand points atsrc/worker/index.ts. - Node unit tests use
vitest.unit.config.ts. - The Vitest pool compatibility date should stay aligned with
wrangler.jsonc. - Stable test env vars are defined in
test/vitest.bindings.ts.
Good Change Patterns
- When adding a route, modify the owning module under
src/worker/routes/and keep registration order safe. - When adding repo metadata, decide whether it belongs in DO storage or SQLite; if SQLite, add schema and DAL changes together.
- When adding a new page, register it in
src/client/server/registry.tsxand add a client entrypoint only if hydration is actually needed. - When changing pack or fetch behavior, look for existing worker tests before writing new code; the repo already has strong coverage for those paths.
Avoid
- Broad refactors across route, DO, and UI layers unless the task truly needs it
- Raw SQL/Drizzle access outside the repo DB DAL
- Accidental route shadowing with
/:ownerand/:owner/:repo - Re-introducing loose objects as a correctness dependency
- Assuming README or docs reflect every current file path without checking the source tree