diff --git a/readme.md b/readme.md --- a/readme.md +++ b/readme.md @@ -1,326 +1,148 @@ # Radial Radial is a coding-agent orchestration system built on -[atproto](https://atproto.com/). Humans commission typed artifacts, agents -produce them, and reviews annotate exact artifact versions. Coordination lives -in each actor's atproto repo; code stays in git. +[atproto](https://atproto.com/). A human writes a goal and asks for a typed +**artifact** against it — a plan, an implementation, a review, an ADR. A daemon +watching the network runs an agent in a container to produce it, and the result +lands as a signed record pinned to an exact version that reviews can annotate. -Phases 0 through 2 are implemented: +Coordination state lives in each actor's own atproto repo. Code stays in git. +There is no privileged server in between: the web app and the daemon are two +independent folds of the same record stream, and either one alone is enough to +see the whole space. -- pnpm TypeScript monorepo and offline CI/test harness -- all 13 `com.disnetdev.radial.*` lexicons and generated record interfaces -- strict runtime validation plus `plan` and `implementation` seed records -- in-memory and SQLite record stores with immutable-version annotations -- deterministic membership computation with retain-nothing grant revocation -- goal/project materialization, version chains, reviews, checkruns, request - fulfillment, declines, claims, and current system artifacts -- golden scenarios and an arrival-permutation convergence test -- zero-dependency atproto XRPC, app-password sessions, token refresh, and - protected session persistence -- `radiald init` for existing agent identities and published agent profiles -- stable, paginated repo polling with per-author ordering and member discovery -- human `radial` commands for the full request → v1 → changes → v2 → approval - cycle, covered by a two-DID/two-PDS convergence test +The model in one line: **humans initiate every step; agents produce artifacts; +reviews annotate them.** -## Development +## What that buys -Node 24 and pnpm 10 are required. The workspace has no third-party runtime or -test dependencies; TypeScript is the only development dependency. +- **Attribution is structural.** Every actor — human or agent — is a DID, and + every action is a record they signed. A review of plan v2 stays a review of + plan v2 after v3 exists, because references pin the CID. +- **A space is the trust boundary.** Its creator is the root of trust and + publishes membership as an append-only grant log. Removing a member revokes + one exact grant and retains nothing, so every materializer ignores the same + records regardless of what it ingested when. +- **Artifact types are data, not code.** An admin adds `security-review` by + writing a registry record; every client grows a button for it and a turn + dispatches with that brief, with nothing deployed. +- **Reviews annotate, they don't gate.** The protocol enforces no pipeline, so + the UI surfaces soft signals (an implementation requested off an unapproved + plan, a red check) as information rather than as a block. +- **Agents from different operators share a space** without shared + infrastructure — the atproto network is the bus. + +## The pieces + +| | | +|---|---| +| Web UI — [`packages/ui`](packages/ui/README.md) | A static SvelteKit SPA. Folds records in the browser over IndexedDB, writes to the human's own PDS under their own OAuth session. No view server. | +| Daemon — [`packages/daemon`](packages/daemon/README.md) | `radiald`: polls the spaces it serves, dispatches requests assigned to its agents into isolated containers, runs project checks, observes merges. | +| Agent container | One turn: a harness against a checkout with a request-specific brief. Claude Code is the harness implemented today. Holds a forge token and a model key, never a protocol credential. | +| Sidecar — [`packages/sidecar`](packages/sidecar/README.md) | `radial`: the human CLI, and the only path from a harness to the protocol. Validation, signing, and repo writes happen daemon-side, so a turn physically cannot emit anything but what its request asked for. | +| [`core`](packages/core), [`ingest`](packages/ingest), [`lexicons`](packages/lexicons/README.md), [`atproto`](packages/atproto) | The materializer and its record store, repo polling, the `com.disnetdev.radial.*` schemas, and a zero-dependency XRPC client. | + +## Quick start + +Node 24 and pnpm 10. The workspace has no third-party runtime or test +dependencies; TypeScript is the only development dependency. ```sh pnpm install --offline pnpm codegen -pnpm lint pnpm test pnpm build ``` -## Run an agent - -This walkthrough runs a long-lived `radiald` process that watches a space and -fulfills requests assigned to its agent profiles. It uses an existing atproto -account for the agent and a separate existing account for the human operator. -The daemon currently launches the Claude Code CLI inside Docker, so the agent -profile below uses `claude` and an Anthropic model. - -Before starting, install Docker and authenticate the GitHub CLI if implementation -turns will push branches and open PRs on GitHub. (For a project hosted on -[tangled](https://tangled.org) the setup is different — see "A tangled project" -below — and `gh` is not involved at all.) For Claude, provide either a dedicated, -spend-capped `ANTHROPIC_API_KEY` or a `CLAUDE_CODE_OAUTH_TOKEN`; the daemon -forwards either credential only to turn containers. Build the turn image after -building the workspace: +### Look at the app first ```sh -pnpm build -pnpm images -gh auth login # needed for implementation turns -export ANTHROPIC_API_KEY='...' -# Or, if your Claude Code subscription is authenticated with OAuth: -export CLAUDE_CODE_OAUTH_TOKEN='...' +pnpm --filter @radial/ui dev # http://127.0.0.1:5173 ``` -Create the agent configuration. This prompts for an existing agent handle (or -accepts one as an argument): +The space picker's second small door opens the **fixture** — a complete +synthetic space of real `com.disnetdev.radial.*` records, no account and no +network involved. It is the fastest way to see what a goal timeline, a version +chain, a verdict and the System section actually look like. + +Signing in with an atproto account reads and writes live spaces from the +browser. Reading one needs no account at all; paste a space URI into the other +door. [`packages/ui/README.md`](packages/ui/README.md) covers the keyboard map, +deployment, and how the fold stays honest. + +### Drive the protocol by hand + +Every capability exists as a CLI command before it exists as a button. The +human-only loop — space, member, project, goal, request, artifact, review, v2 — +runs entirely through `radial` with no daemon and no agents, which is the +clearest way to see the record model: ```sh -radiald config init # or: radiald config init agent.example +printf '%s\n' "$APP_PASSWORD" | radial auth login \ + --profile alice --identifier alice.example --password-stdin + +SPACE=$(radial space create --profile alice --name radial --description 'Open development') +PROJECT=$(radial project create --profile alice --space "$SPACE" \ + --name radial --git-url https://github.com/OWNER/REPOSITORY.git) +GOAL=$(radial goal create --profile alice --project "$PROJECT" \ + --title 'Ship the feature' --body 'Acceptance criteria go here') ``` -It writes three profiles covering the built-in artifact types — `planner` (which -also carries the system types `space create` seeds: `architecture`, -`architecture-inventory`, `glossary`, `conventions`, and `adr`), `implementer`, -and `reviewer` — all on one identity. -Edit it, then register them (below); [`docs/radial-json.md`](docs/radial-json.md) -documents every field. +Full walkthrough: [`packages/sidecar/README.md`](packages/sidecar/README.md). -Declare your agent profiles in a `radial.json`. Agent identities belong to the -operator, not to a checkout, so it normally lives at -`~/.config/radial/radial.json` (`$XDG_CONFIG_HOME` and `$RADIAL_CONFIG_DIR` -override the directory, alongside the `$RADIAL_DATA_DIR` holding sessions). A -`radial.json` in the working directory or any parent takes precedence, which is -useful for testing or a second set of identities; `--config ` or -`$RADIAL_CONFIG` overrides both. +### Put an agent behind it -Profiles share one agent identity by default and inherit the top-level `pds`, -`harness`, and `models`; each publishes its own agent record keyed by profile -name, so one identity can offer several distinct capability sets: - -```json -{ - "identifier": "agent.example", - "harness": "claude", - "models": ["claude-opus-4-8=high"], - "agents": { - "planner": { - "artifactTypes": [ - "plan", - "architecture", - "architecture-inventory", - "glossary", - "conventions", - "adr" - ] - }, - "impl": { - "harness": "claude", - "models": ["claude-sonnet-4-5=high"], - "artifactTypes": ["implementation", "review"] - } - }, - "run": { - "spaces": ["at://did:plc:operator/com.disnetdev.radial.space/example"], - "stateDir": ".radial-state", - "image": "radial-turn:latest", - "concurrency": 1, - "forge": { "kind": "github" } - } -} -``` - -Replace the placeholder in `run.spaces` with the space URI created below. The -defaults are a 60-minute turn timeout (15 minutes for a check), one concurrent -turn, a 4 GB container memory floor, and a state directory under the Radial data -directory. On macOS with Docker Desktop, add `"turnTransport": "tcp"` to `run`; -it is a local-development escape hatch and should not be exposed to a LAN. - -**`artifactTypes` names types from the space's registry**, which is data rather -than lexicon: an admin adds `security-review` by writing a record, and every -client grows a button for it. Nothing grows an agent to answer it, so a type no -profile publishes leaves its requests sitting open with nobody to assign. -`radiald run` says so at startup, once per space. [`docs/radial-json.md`](docs/radial-json.md) -is the field-by-field reference, including how to add a type to a profile that is -already published. - -The PDS is resolved from the identity itself — a handle through its `_atproto` -DNS TXT record (falling back to `https:///.well-known/atproto-did`), -then the DID document's `AtprotoPersonalDataServer` endpoint. Set `pds` -explicitly only to override that, such as against a local PDS in testing. - -Register the profiles. One app password covers all of them, read from stdin or -`$RADIAL_PASSWORD` and never stored in the config. `init` is idempotent, so -re-running it is safe, and it prints what each profile now publishes: +An operator registers existing agent atproto accounts, declares what each +profile can produce, and runs a daemon that answers requests addressed to them: ```sh +radiald config init # write ~/.config/radial/radial.json printf '%s\n' "$AGENT_APP_PASSWORD" | radiald init --password-stdin -✓ planner did:plc:… plan, architecture, architecture-inventory, glossary, - conventions, adr -✓ implementer did:plc:… implementation -✓ reviewer did:plc:… review - -radiald init planner # or just the named ones -``` - -A profile that fails is reported and the rest still run; the exit status is -non-zero if any failed. - -Editing a profile's `artifactTypes` later changes nothing on its own: the -published agent record is what an assignee menu and auto-review read. Republish -it with `radiald init planner --update`. Without `--update` a changed profile is -refused rather than rewritten — that guard is what stops a second config, or a -stale checkout, from quietly redefining what a running agent advertises. - -A profile may override `identifier` to use a second agent account, in which case -that identity's app password is read from `$RADIAL__PASSWORD` (e.g. -`RADIAL_OTHER_EXAMPLE_PASSWORD` for `other.example`). - -Without a config, a single profile can still be registered entirely from flags: - -```sh -printf '%s\n' "$AGENT_APP_PASSWORD" | radiald init planner \ - --pds https://pds.example \ - --identifier planner.example \ - --name planner \ - --harness codex \ - --model gpt-5=high \ - --artifact-type plan \ - --password-stdin -``` - -Create a space and a request as the human. First authenticate the human account -with the `radial` CLI; then create the space, admit the agent, and create a -project and goal. Save the resulting references because each later command -uses the value returned by the previous one: - -```sh -printf '%s\n' "$HUMAN_APP_PASSWORD" | radial auth login \ - --profile human --identifier human.example --password-stdin - -SPACE=$(radial space create --profile human --name radial --description 'Agent demo') -radial member add --profile human --space "$SPACE" \ - --actor agent.example --kind agent --role agent - -PROJECT=$(radial project create --profile human --space "$SPACE" \ - --name radial-ng --git-url https://github.com/OWNER/REPOSITORY.git) -GOAL=$(radial goal create --profile human --project "$PROJECT" \ - --title 'Plan the feature' --body 'Describe the requested feature and acceptance criteria.') -``` - -Put the `SPACE` value in `run.spaces` in `radial.json`, then start the daemon. -It polls the space every five seconds by default, starts one isolated container -per eligible assigned request, and keeps durable records and turn ledgers in -`run.stateDir`: - -```sh radiald run ``` -In another terminal, commission work by assigning the request to the agent -identity. A plan turn receives a read-only checkout and submits its artifact -through the sidecar. An implementation turn receives a writable checkout, -pushes a `radial/impl-*` branch, opens or updates a PR, and submits its branch, -commit, and PR link; it never merges. +Docker, a forge login, and a model credential are involved, and the config has +real decisions in it. [`docs/running-an-agent.md`](docs/running-an-agent.md) is +the end-to-end walkthrough — GitHub and [tangled](https://tangled.org) projects, +how a v2 continues its predecessor's PR, and how to inspect what a daemon saw. +[`docs/radial-json.md`](docs/radial-json.md) is the field-by-field configuration +reference. -**One PR per version chain.** Asking for another version of an implementation — -which is how a review's findings get resolved — continues the same branch and -updates the same pull request rather than opening a second one. The daemon -selects the branch, and it reuses the predecessor's only when every guard holds: -the request is based on exactly one implementation of this goal, that -implementation links a branch in the daemon's own `radial/impl-` namespace and a -PR in this project's repository, and the PR is still live work. With -`run.forge` configured that last part is confirmed against GitHub (open, with -head ref, head repository and base ref all matching). Without it, the daemon -falls back to the local index plus one `git ls-remote`: no trusted `merge` -annotation on the predecessor, and the branch still on the remote. Read that -fallback honestly — merge annotations are written by the merge poller, which -only runs when a forge is configured, so with no forge there is never one to -find and the `ls-remote` is the guard doing the work. It keeps the sharp case -out: a branch deleted after its PR merged would otherwise be cloned on every -retry until the request gave up. What is left is a merged-but-undeleted branch -reading like an open one, and a PR closed without merging being invisible — the -turn recovers from the latter by opening a PR when the branch has none, and both -are why `run.forge` is in the `radiald init` scaffold. Anything else gets a fresh -`radial/impl-` branch and a new PR. A continued branch is cloned with -history and with its base fetched alongside it, because a `--depth 1` checkout -cannot merge anything. -`sqlite3 .radial-state/ledger.db "SELECT branch FROM turns WHERE request_uri = …"` -says which branch a turn actually took. +## Status -**A tangled project.** Radial speaks more than one forge, and picks per project by -the host of its git remote — a space can hold a GitHub project and a -[tangled](https://tangled.org) one at once. Configure it in `run.forges`: +Phases 0 through 6 of [`docs/plan.md`](docs/plan.md) are implemented: the +lexicons and the permutation-tested materializer, atproto identities and +polling ingestion, the full human CLI, agent turns in containers, implementation +artifacts with forge integration and checks, review turns with auto-review and +custom/system artifact types, and the web app. -```json -"run": { - "spaces": ["at://…"], - "forges": [ - { "kind": "github" }, - { "kind": "tangled", "knownHosts": ["tangled.org ssh-ed25519 AAAAC3Nza..."] } - ] -} -``` +Phase 7 — multi-operator hardening — is what remains: open requests and claims, +Jetstream ingestion, operator routing config, and the failure drills from design +§12. A second forge adapter ([tangled](https://tangled.org), alongside GitHub) +landed early, behind a per-project forge registry. -Then `radiald init` generates an ed25519 push key for the agent, publishes it as -an `sh.tangled.publicKey` record under the agent's own DID, and prints the one -step it cannot take for you: **add that DID as a collaborator** on each tangled -repository the agent implements in. Everything else is the same walkthrough. Two -differences are visible in the records: a tangled turn pushes over ssh and does -*not* open its own pull request — Radial writes the `sh.tangled.repo.pull` record -itself, as the same identity that signs the artifact, because tangled ships no -CLI a container could use — and `links.pr` therefore carries that record's -`at://` URI rather than a page URL. A second version appends a *round* to the -same pull record instead of opening a second one. Merges are observed through -[Bobbin](https://docs.tangled.org/bobbin), tangled's public read-only API -appview, which needs no credential and no configuration — set `api` only to -point at your own instance, or to `false` to read PDSes directly instead. Radial -reads those PDSes anyway whenever the appview reports a pull `open`, since that -is also what its index says when it has not seen the merge yet. See design §10 -and `docs/adr-tangled-forge.md`. +## Documentation + +| | | +|---|---| +| [`docs/design.md`](docs/design.md) | The protocol design: trust, data model, the turn model, forges, security, and the alternatives that lost. | +| [`docs/plan.md`](docs/plan.md) | Phased implementation plan, exit criteria, and what is consciously out of scope. | +| [`docs/running-an-agent.md`](docs/running-an-agent.md) | Operator walkthrough: agent identities, spaces, `radiald run`, GitHub and tangled. | +| [`docs/radial-json.md`](docs/radial-json.md) | Every field of the operator's `radial.json`, and how `artifactTypes` relates to a space's registry. | +| [`docs/adr-tangled-forge.md`](docs/adr-tangled-forge.md) | Why tangled is shaped the way it is, and what stays unverified without a live knot. | +| [`docs/phase6-ui-plan.md`](docs/phase6-ui-plan.md) | How the web app was built, step by step, against the approved comp. | +| [`PRODUCT.md`](PRODUCT.md), [`DESIGN.md`](DESIGN.md) | Who this is for and how it should feel; the design system as shipped. | + +## Development ```sh -PLAN_REQUEST=$(radial request create --profile human --goal "$GOAL" \ - --type plan --assignee agent.example \ - --brief 'Inspect the repository and propose a safe implementation plan.') - -# After reviewing the submitted plan, request implementation against it. -IMPLEMENTATION_REQUEST=$(radial request create --profile human --goal "$GOAL" \ - --type implementation --assignee agent.example --based-on "$PLAN_ARTIFACT" \ - --brief 'Implement the approved plan, run relevant tests, and open a PR.') +pnpm lint # typecheck every package; check generated lexicon types are current +pnpm test # unit, golden-scenario, and convergence tests +pnpm build +pnpm images # build the agent turn container image ``` -After the plan is fulfilled, obtain its exact reference from the daemon ledger -and use it as `PLAN_ARTIFACT` for the implementation request: - -```sh -PLAN_ARTIFACT=$(sqlite3 .radial-state/ledger.db \ - "SELECT accepted_ref_uri || '#' || accepted_ref_cid FROM turns WHERE request_uri = '$PLAN_REQUEST';") -``` - -Stop the daemon with `Ctrl-C`; it kills active containers and records -interrupted attempts. If a request exhausts its retry limit or needs to be -retried immediately, run `radiald turn reset `. - -See [the CLI walkthrough](packages/sidecar/README.md) for the human-only cycle. - -For read-only debugging, run a durable ingestion/materializer pass, then -inspect the same local view without hitting the network: - -```sh -radiald sync "$SPACE_URI" --records radial.db --checkpoints radial-sync.db -radiald index "$SPACE_URI" --records radial.db -``` - -### Polling-v1 provenance and removal - -`listRecords` exposes a current URI/CID/value snapshot, not the commit revision -at which each record was originally written. Repository revisions therefore -serve ingestion checkpoints only and never carry trust decisions. Member -removal revokes an exact `addMember` grant and retains nothing: every record -authored by a DID with no active grant is ignored, identically, on every -materializer regardless of ingestion timing. Re-adding a member is a new grant -that restores trust in their records. The transport seam allows Phase 7 -CAR/Jetstream ingestion to replace snapshot-head provenance with commit-level -history. - -The debug CLI reads one JSON record envelope (or an array of envelopes) per -file in a directory: - -```sh -pnpm --filter @radial/core debug -- \ - index at://did:plc:example/com.disnetdev.radial.goal/example \ - --dir ./fixtures \ - --as-of 2026-07-18T00:00:00Z -``` - -See [design.md](docs/design.md) for the protocol design and -[plan.md](docs/plan.md) for the remaining phases. +The index is sacred: any new record type or index rule lands in `core` with +permutation-property coverage — every arrival order folds to the same view — +before the daemon or the UI consumes it. Lexicon changes are additive-only; +records already live in people's repos. diff --git a/docs/running-an-agent.md b/docs/running-an-agent.md new file mode 100644 --- /dev/null +++ b/docs/running-an-agent.md @@ -0,0 +1,300 @@ +# Running an agent + +The operator walkthrough: register agent identities, create a space, and let +`radiald` fulfill requests in containers. Start from the +[readme](../readme.md) for what Radial is; [`radial-json.md`](radial-json.md) +is the field-by-field configuration reference. + +## The walkthrough + +This walkthrough runs a long-lived `radiald` process that watches a space and +fulfills requests assigned to its agent profiles. It uses an existing atproto +account for the agent and a separate existing account for the human operator. +The daemon currently launches the Claude Code CLI inside Docker, so the agent +profile below uses `claude` and an Anthropic model. + +Before starting, install Docker and authenticate the GitHub CLI if implementation +turns will push branches and open PRs on GitHub. (For a project hosted on +[tangled](https://tangled.org) the setup is different — see "A tangled project" +below — and `gh` is not involved at all.) For Claude, provide either a dedicated, +spend-capped `ANTHROPIC_API_KEY` or a `CLAUDE_CODE_OAUTH_TOKEN`; the daemon +forwards either credential only to turn containers. Build the turn image after +building the workspace: + +```sh +pnpm build +pnpm images +gh auth login # needed for implementation turns +export ANTHROPIC_API_KEY='...' +# Or, if your Claude Code subscription is authenticated with OAuth: +export CLAUDE_CODE_OAUTH_TOKEN='...' +``` + +Create the agent configuration. This prompts for an existing agent handle (or +accepts one as an argument): + +```sh +radiald config init # or: radiald config init agent.example +``` + +It writes three profiles covering the built-in artifact types — `planner` (which +also carries the system types `space create` seeds: `architecture`, +`architecture-inventory`, `glossary`, `conventions`, and `adr`), `implementer`, +and `reviewer` — all on one identity. +Edit it, then register them (below); [`radial-json.md`](radial-json.md) +documents every field. + +Declare your agent profiles in a `radial.json`. Agent identities belong to the +operator, not to a checkout, so it normally lives at +`~/.config/radial/radial.json` (`$XDG_CONFIG_HOME` and `$RADIAL_CONFIG_DIR` +override the directory, alongside the `$RADIAL_DATA_DIR` holding sessions). A +`radial.json` in the working directory or any parent takes precedence, which is +useful for testing or a second set of identities; `--config ` or +`$RADIAL_CONFIG` overrides both. + +Profiles share one agent identity by default and inherit the top-level `pds`, +`harness`, and `models`; each publishes its own agent record keyed by profile +name, so one identity can offer several distinct capability sets: + +```json +{ + "identifier": "agent.example", + "harness": "claude", + "models": ["claude-opus-4-8=high"], + "agents": { + "planner": { + "artifactTypes": [ + "plan", + "architecture", + "architecture-inventory", + "glossary", + "conventions", + "adr" + ] + }, + "impl": { + "harness": "claude", + "models": ["claude-sonnet-4-5=high"], + "artifactTypes": ["implementation", "review"] + } + }, + "run": { + "spaces": ["at://did:plc:operator/com.disnetdev.radial.space/example"], + "stateDir": ".radial-state", + "image": "radial-turn:latest", + "concurrency": 1, + "forge": { "kind": "github" } + } +} +``` + +Replace the placeholder in `run.spaces` with the space URI created below. The +defaults are a 60-minute turn timeout (15 minutes for a check), one concurrent +turn, a 4 GB container memory floor, and a state directory under the Radial data +directory. On macOS with Docker Desktop, add `"turnTransport": "tcp"` to `run`; +it is a local-development escape hatch and should not be exposed to a LAN. + +**`artifactTypes` names types from the space's registry**, which is data rather +than lexicon: an admin adds `security-review` by writing a record, and every +client grows a button for it. Nothing grows an agent to answer it, so a type no +profile publishes leaves its requests sitting open with nobody to assign. +`radiald run` says so at startup, once per space. [`radial-json.md`](radial-json.md) +is the field-by-field reference, including how to add a type to a profile that is +already published. + +The PDS is resolved from the identity itself — a handle through its `_atproto` +DNS TXT record (falling back to `https:///.well-known/atproto-did`), +then the DID document's `AtprotoPersonalDataServer` endpoint. Set `pds` +explicitly only to override that, such as against a local PDS in testing. + +Register the profiles. One app password covers all of them, read from stdin or +`$RADIAL_PASSWORD` and never stored in the config. `init` is idempotent, so +re-running it is safe, and it prints what each profile now publishes: + +```sh +printf '%s\n' "$AGENT_APP_PASSWORD" | radiald init --password-stdin +✓ planner did:plc:… plan, architecture, architecture-inventory, glossary, + conventions, adr +✓ implementer did:plc:… implementation +✓ reviewer did:plc:… review + +radiald init planner # or just the named ones +``` + +A profile that fails is reported and the rest still run; the exit status is +non-zero if any failed. + +Editing a profile's `artifactTypes` later changes nothing on its own: the +published agent record is what an assignee menu and auto-review read. Republish +it with `radiald init planner --update`. Without `--update` a changed profile is +refused rather than rewritten — that guard is what stops a second config, or a +stale checkout, from quietly redefining what a running agent advertises. + +A profile may override `identifier` to use a second agent account, in which case +that identity's app password is read from `$RADIAL__PASSWORD` (e.g. +`RADIAL_OTHER_EXAMPLE_PASSWORD` for `other.example`). + +Without a config, a single profile can still be registered entirely from flags: + +```sh +printf '%s\n' "$AGENT_APP_PASSWORD" | radiald init planner \ + --pds https://pds.example \ + --identifier planner.example \ + --name planner \ + --harness codex \ + --model gpt-5=high \ + --artifact-type plan \ + --password-stdin +``` + +Create a space and a request as the human. First authenticate the human account +with the `radial` CLI; then create the space, admit the agent, and create a +project and goal. Save the resulting references because each later command +uses the value returned by the previous one: + +```sh +printf '%s\n' "$HUMAN_APP_PASSWORD" | radial auth login \ + --profile human --identifier human.example --password-stdin + +SPACE=$(radial space create --profile human --name radial --description 'Agent demo') +radial member add --profile human --space "$SPACE" \ + --actor agent.example --kind agent --role agent + +PROJECT=$(radial project create --profile human --space "$SPACE" \ + --name radial-ng --git-url https://github.com/OWNER/REPOSITORY.git) +GOAL=$(radial goal create --profile human --project "$PROJECT" \ + --title 'Plan the feature' --body 'Describe the requested feature and acceptance criteria.') +``` + +Put the `SPACE` value in `run.spaces` in `radial.json`, then start the daemon. +It polls the space every five seconds by default, starts one isolated container +per eligible assigned request, and keeps durable records and turn ledgers in +`run.stateDir`: + +```sh +radiald run +``` + +In another terminal, commission work by assigning the request to the agent +identity. A plan turn receives a read-only checkout and submits its artifact +through the sidecar. An implementation turn receives a writable checkout, +pushes a `radial/impl-*` branch, opens or updates a PR, and submits its branch, +commit, and PR link; it never merges. + +```sh +PLAN_REQUEST=$(radial request create --profile human --goal "$GOAL" \ + --type plan --assignee agent.example \ + --brief 'Inspect the repository and propose a safe implementation plan.') + +# After reviewing the submitted plan, request implementation against it. +IMPLEMENTATION_REQUEST=$(radial request create --profile human --goal "$GOAL" \ + --type implementation --assignee agent.example --based-on "$PLAN_ARTIFACT" \ + --brief 'Implement the approved plan, run relevant tests, and open a PR.') +``` + +After the plan is fulfilled, obtain its exact reference from the daemon ledger +and use it as `PLAN_ARTIFACT` for the implementation request: + +```sh +PLAN_ARTIFACT=$(sqlite3 .radial-state/ledger.db \ + "SELECT accepted_ref_uri || '#' || accepted_ref_cid FROM turns WHERE request_uri = '$PLAN_REQUEST';") +``` + +Stop the daemon with `Ctrl-C`; it kills active containers and records +interrupted attempts. If a request exhausts its retry limit or needs to be +retried immediately, run `radiald turn reset `. + +## One PR per version chain + +Asking for another version of an implementation — which is how a review's +findings get resolved — continues the same branch and updates the same pull +request rather than opening a second one. The daemon +selects the branch, and it reuses the predecessor's only when every guard holds: +the request is based on exactly one implementation of this goal, that +implementation links a branch in the daemon's own `radial/impl-` namespace and a +PR in this project's repository, and the PR is still live work. With +`run.forge` configured that last part is confirmed against GitHub (open, with +head ref, head repository and base ref all matching). Without it, the daemon +falls back to the local index plus one `git ls-remote`: no trusted `merge` +annotation on the predecessor, and the branch still on the remote. Read that +fallback honestly — merge annotations are written by the merge poller, which +only runs when a forge is configured, so with no forge there is never one to +find and the `ls-remote` is the guard doing the work. It keeps the sharp case +out: a branch deleted after its PR merged would otherwise be cloned on every +retry until the request gave up. What is left is a merged-but-undeleted branch +reading like an open one, and a PR closed without merging being invisible — the +turn recovers from the latter by opening a PR when the branch has none, and both +are why `run.forge` is in the `radiald init` scaffold. Anything else gets a fresh +`radial/impl-` branch and a new PR. A continued branch is cloned with +history and with its base fetched alongside it, because a `--depth 1` checkout +cannot merge anything. +`sqlite3 .radial-state/ledger.db "SELECT branch FROM turns WHERE request_uri = …"` +says which branch a turn actually took. + +## A tangled project + +Radial speaks more than one forge, and picks per project by +the host of its git remote — a space can hold a GitHub project and a +[tangled](https://tangled.org) one at once. Configure it in `run.forges`: + +```json +"run": { + "spaces": ["at://…"], + "forges": [ + { "kind": "github" }, + { "kind": "tangled", "knownHosts": ["tangled.org ssh-ed25519 AAAAC3Nza..."] } + ] +} +``` + +Then `radiald init` generates an ed25519 push key for the agent, publishes it as +an `sh.tangled.publicKey` record under the agent's own DID, and prints the one +step it cannot take for you: **add that DID as a collaborator** on each tangled +repository the agent implements in. Everything else is the same walkthrough. Two +differences are visible in the records: a tangled turn pushes over ssh and does +*not* open its own pull request — Radial writes the `sh.tangled.repo.pull` record +itself, as the same identity that signs the artifact, because tangled ships no +CLI a container could use — and `links.pr` therefore carries that record's +`at://` URI rather than a page URL. A second version appends a *round* to the +same pull record instead of opening a second one. Merges are observed through +[Bobbin](https://docs.tangled.org/bobbin), tangled's public read-only API +appview, which needs no credential and no configuration — set `api` only to +point at your own instance, or to `false` to read PDSes directly instead. Radial +reads those PDSes anyway whenever the appview reports a pull `open`, since that +is also what its index says when it has not seen the merge yet. See design §10 +and [`adr-tangled-forge.md`](adr-tangled-forge.md). + +## Inspecting what a daemon sees + +See [the CLI walkthrough](../packages/sidecar/README.md) for the human-only cycle. + +For read-only debugging, run a durable ingestion/materializer pass, then +inspect the same local view without hitting the network: + +```sh +radiald sync "$SPACE_URI" --records radial.db --checkpoints radial-sync.db +radiald index "$SPACE_URI" --records radial.db +``` + +## Polling-v1 provenance and removal + +`listRecords` exposes a current URI/CID/value snapshot, not the commit revision +at which each record was originally written. Repository revisions therefore +serve ingestion checkpoints only and never carry trust decisions. Member +removal revokes an exact `addMember` grant and retains nothing: every record +authored by a DID with no active grant is ignored, identically, on every +materializer regardless of ingestion timing. Re-adding a member is a new grant +that restores trust in their records. The transport seam allows Phase 7 +CAR/Jetstream ingestion to replace snapshot-head provenance with commit-level +history. + +The debug CLI reads one JSON record envelope (or an array of envelopes) per +file in a directory: + +```sh +pnpm --filter @radial/core debug -- \ + index at://did:plc:example/com.disnetdev.radial.goal/example \ + --dir ./fixtures \ + --as-of 2026-07-18T00:00:00Z +``` +