From 3c93d98806288ed58fef297f58c77282248ffbb0 Mon Sep 17 00:00:00 2001 From: Tim Disney Date: Thu, 30 Jul 2026 09:44:01 -0700 Subject: [PATCH] add agents documentation --- CLAUDE.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f423106 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Radial coordinates humans and coding agents on software goals. A human writes a goal and requests a +typed **artifact** against it (plan, implementation, review, ADR); a daemon dispatches an agent in a +container and the result lands as a signed atproto record. There is **no central server** — the web +UI and the daemon each ingest the same records from members' PDSes and independently fold them into +the same view. + +`docs/design.md` is the authority on the protocol (trust, data model, turn model, forges, security, +and rejected alternatives) and its section numbers are cited throughout the source. Read the +relevant section before changing anything in `core`'s fold or the lexicons. + +## Commands + +Node 24, pnpm 10. Run from the repo root. + +```sh +pnpm install --offline +pnpm codegen # regenerate core/src/generated/records.ts from packages/lexicons/lexicons/*.json +pnpm typecheck # tsc --noEmit in every package +pnpm lint # typecheck + core's type-strip lint + `generate.mjs --check` (generated file is current) +pnpm test # builds each package, then runs its tests +pnpm build +pnpm images # build the agent turn container image (docker/Dockerfile) +``` + +CI (`.github/workflows/ci.yml`) runs `codegen` then `git diff --exit-code`, so a lexicon change with +stale generated output fails the build. + +### Running one test + +**Tests import from `dist/`, not `src/`** — build the package first: + +```sh +pnpm --filter @radial/core build +node --test packages/core/test/materializer.test.mjs +node --test --test-name-pattern 'claim' packages/core/test/materializer.test.mjs +pnpm test:watch # core only: rebuild + re-run on change +``` + +Per-package: `pnpm --filter @radial/daemon test`, `--filter @radial/ui dev`, etc. + +`@radial/ui` is the exception with two runners: `node --test test/*.test.mjs` (bundle/serve +assertions) plus `vitest run` over `src/**/*.test.ts`. + +## Package graph + +Dependencies point one way; nothing depends on `daemon` or `ui`. + +| Package | Role | +|---|---| +| `lexicons` | `com.disnetdev.radial.*` JSON schemas — the wire contract, source of truth | +| `core` | The fold: `materialize()` records → index, plus `RecordStore`, `timeline()`, `bundle`, validation, claim-lease rules. **Isomorphic** | +| `atproto` | Zero-dependency XRPC/repo-read client | +| `ingest` | Repo polling and Jetstream subscription, per-space ingestion into a `RecordStore` | +| `sidecar` | `radial` CLI. `runCli()` is the single write path for *both* the CLI and the web app | +| `daemon` | `radiald`: sync runtime, claims, dispatch, containers, forge adapters, checks, merge polling | +| `ui` | SvelteKit SPA — a second full implementation of Radial, not a client of one | + +### `core` is isomorphic; `core/node` is not + +The main entry must stay free of `node:*` so a browser tab runs the identical `materialize()` the +daemon runs. Node-only implementations (`SqliteRecordStore`) live behind the `./node` export. +`packages/ui/test/browser-bundle.test.mjs` fails if a `node:*` import creeps onto the browser path. +`ingest`, `sidecar`, and `atproto` follow the same `./node` split. + +### Generated types + +`packages/core/src/generated/records.ts` is written by `packages/lexicons/scripts/generate.mjs`. +Never hand-edit it; change the lexicon JSON and run `pnpm codegen`. `packages/core/src/records.ts` +re-exports it and adds the hand-written record helpers. + +## The daemon loop + +`SpaceSyncRuntime` (`runtime.ts`) keeps SQLite record + checkpoint stores open, syncs, and +materializes; `TurnDispatcher.pump()` (`dispatch.ts`) then launches eligible turns with bounded +concurrency and a durable turn ledger. An **assigned** request dispatches directly; an **open** one +requires writing a `claim`, ingesting it back, and winning the deterministic tie-break first +(`claims.ts`, `claim-ledger.ts`) — that confirmation wait is what bounds duplicated work between two +operators to one ingestion cycle. + +A turn is one container run producing exactly one terminal record, fixed before the container +starts by the request's type: an `artifact`, a `review` verdict, or a thread `message`. The +container holds a forge token and a model key and **never a protocol credential** — the in-container +`radial` sidecar forwards over a socket to `turn-socket.ts`, and the daemon does the validation, +anchoring, signing, and PDS write. `packages/daemon/README.md` covers claims, forges, credentials, +transports, and turn types. + +Forges are a **registry keyed by the project's `gitUrl` host** (`forge.ts`), not one global switch: +a space may hold a GitHub project and a tangled project at once. GitHub is observation-only (turns +push and open PRs themselves with `gh`); tangled pushes over ssh and the *daemon* opens the pull +request, because tangled ships no user-facing CLI. + +## Invariants worth knowing before you edit + +- **Lexicon changes are additive-only.** Records are already live in people's repos. A field that + cannot be added to already-signed records must not become `required` — see the `artifact.title` + discussion in `packages/lexicons/README.md`: the obligation lives in the writers instead. +- **Any new record type or index rule lands in `core` with permutation coverage** — every arrival + order must produce the same view — before the daemon or the UI consumes it. The shared fixture is + `packages/core/test/scenario.ts` (`goldenScenario()`); `materializer.test.mjs` shuffles it. +- **The fold cannot read observer-local state.** Repo revisions and first-sighting stamps are + ingestion metadata, not provenance. Claims are the one deliberate exception (a lease is a duration + measured from first sighting — `claim-lease.ts`), and nothing convergent, including the digest, + reads it. +- **Primary record + typed overlay records.** Nothing edits a primary record: a goal's ending is a + `closeGoal`, a project's rename an `editProject`, an auto-review toggle a `setAutoReview`. In-place + edits are flagged as edit annotations, not adopted (the sanctioned exceptions are claim renewal + and an agent profile's republish). +- **The daemon never authors an `answer` request.** Auto-review is the only daemon-authored hop. + `daemon/test/auto-review.test.mjs` asserts this over the source: exactly one + `create(COLLECTIONS.artifactRequest)` exists under `src/`, and it builds a `review`. +- **Reviews annotate, never gate**, and Radial never merges. Anything the UI or daemon acts on + mechanically must be a typed record — never a parsed message body. +- **No third-party runtime or test dependencies** outside `@radial/ui` (and `esbuild` for builds). + Tests are `node --test` `.mjs` files. Packages compile with `"types": []` and hand-written + `src/node-shims.d.ts` declarations rather than `@types/node` — extend the shim when you use a new + Node API. `tsconfig.base.json` is strict, including `noUncheckedIndexedAccess` and + `exactOptionalPropertyTypes`. +- **`127.0.0.1`, never `localhost`** for anything serving the UI: atproto's loopback OAuth redirect + must be an IP, and the other name is a different origin holding a different IndexedDB. + +## Docs + +| | | +|---|---| +| `docs/design.md` | The protocol design; source comments cite its section numbers | +| `docs/plan.md` | Phased implementation plan and what is out of scope | +| `docs/running-an-agent.md`, `docs/operators.md`, `docs/radial-json.md` | Operator walkthrough, multi-operator concerns, full `radial.json` reference | +| `docs/adr-*.md` | Decision records (tangled forge, claim leases as durations) | +| `PRODUCT.md`, `DESIGN.md` | Who this is for; the UI design system as shipped | +| `packages/*/README.md` | Per-package detail — `ui`'s maps every `src/lib` module | -- 2.51.2