AGENTS.md · diff

git:20261003.1d50911 to git:20261003.d6ed96b

1 added, 1 removed. Audit A to A.

# Agent Notes for Coddy
Short map for automation-friendly contributors.
## Repository navigation
| Area | Responsibility |
|------|------------------|
| `cmd/coddy` | CLI entry (bare `coddy` console, `cli`, `acp`, **`serve`**, `sessions`, `skills`, `plugin`, `mcp`, `providers login`, `rules list`, `update`), plus **`-t` / `--test-config`** on the console, `acp` and `serve`: the config check in **`internal/config/check.go`** (schema stage in **`schema.go`** over the embedded **`config.schema.json`**, then the loader's rules), reported as `file:line:col` with a fix line, exit 1 on errors, nothing written; and **`--dry-run`** on the same three, which runs that check and then **`internal/dryrun`**. |
| `internal/dryrun` | The **`--dry-run`** probes over a loaded config: **`Prepare`** (the static check, then **`config.LoadReadOnly`** and a **`config.Locator`** so every finding carries its line), **`Run`** (paths in **`paths.go`**, LLM providers through **`llm.ListModels`** with the configured models checked against the list in **`providers.go`**, listen addresses, the Telegram token, remote MCP servers, configured remotes and the `--remote` target, swarm joins and upstreams in **`network.go`**), and the checklist **`Report`** (`ok` / `warning` / `error` / `skipped` per config path, exit 1 on errors; alone the flag prints problems and the status line, with **`--test-config`** the config report and every probe). The servers of `<home>/mcp.json` only: project-local `.coddy/mcp.json` stays behind the trust gate. `coddy serve` hands it the subsystems **`serve.Resolve`** enabled and their addresses (**`cmd/coddy/dryrun.go`**). |
| `internal/serve` | What **`coddy serve`** is: the **`Runtime`** every subsystem shares (paths, logger, one session store and one **`session.Manager`**, the turn-mirror slot, and the surfaces that offered to show a detached subagent's permission prompt - the HTTP server, a Telegram bot - which are all asked at once, the first answer winning, and the process **background waker** (**`wake.go`**: a finished **`notify_on_finish`** task's turn goes to the surface that offered it first by rank - the Telegram chat bound to the session, then the HTTP server - or runs through the manager when neither is up), the **`Supervisor`** that runs them (**`subsystem.go`**: a **`Subsystem`** descriptor per surface with its **`enable`** key, its build tag and its **`Available`** flag, so an enabled surface the binary cannot run is refused by name, plus **`Fingerprint`** for what can be rebuilt in place and **`RestartKey`** for what only a fresh process can adopt; **`supervisor.go`**: one goroutine per surface with panic recovery, a diff applied on every config reload - start, stop or rebuild only what moved - **`ErrRestartRequested`** when a **`RestartKey`** moves under a **`Restartable`** process, and shutdown in reverse declaration order so intake stops before the API), and the **background dispatcher** (**`dispatcher.go`**: the restart policy and the **`Record`** at **`<home>/serve.json`**; **`daemon.go`**: **`StartDetached`**, **`RunDispatcher`**, the worker process and the **`CODDY_SERVE_ROLE`** split, exit status **`ExitRestart`** = 75), and the **systemd user service** (**`systemd.go`**: **`UserService`** behind **`coddy serve install`** / **`uninstall`**, the unit renderer shared with the package, the **`PATH`** drop-in, the role **`service`** the unit sets so a listen-address change exits 75 under systemd too). Untagged: it names no tagged package, and the surfaces reach it through **`Available`** consts and stubs. Guide: **`docs/operate/serve.md`**. |
| `internal/agent` | ReAct orchestration, MCP/tool wiring, and the opt-in wait for a hit usage limit (**`limit_wait.go`**: a **`llm.QuotaResetError`** from the resilient wrapper makes a top-level turn wait for the reset under **`agent.wait_for_limit_reset`**, sending **`provider_usage`** updates with **`resuming`** every 20 s, then re-issue the same call). |
| `internal/mcp` | MCP transports, merged server list (**`<home>/mcp.json`** and the project's **`.coddy/mcp.json`** only: **`config.yaml`** declares no server, and **`internal/config/legacy_keys.go`** moves an old **`mcp_servers`** key into the home file on load), and the **workspace trust gate** for project-local **`.coddy/mcp.json`** (**`trust.go`**, **`gate.go`**; policy **`mcp.project_trust`**, approvals in **`<home>/mcp-trust.json`**, **`ReadsEnvironment`** naming on every approval surface the variables a declaration's **`${NAME}`** values read). A server's `command` and `args` run exactly as written, their **`${CWD}`** and **`${NAME}`** resolved by **`config.ExpandMCPValue`** the same way in every file: nothing here knows `npx` or any other package runner. The session manager watches **`<home>/mcp.json`** (**`StartGlobalMCPServers`** starts the watcher, **`ReloadMCPDeclarations`** reconciles only the servers whose declaration or switch moved). **`mcptest`** serves test MCP servers of every kind a user configures - the test binary re-executed on stdio, a local npm package started by the real `npx`, streamable HTTP and SSE - each answering one tool with its token. The **server pool** (**`pool.go`**: **`Pool`**, **`PoolKey`**, **`WorkspaceBound`**) shares the configured servers between sessions: a session holds a lease (**`Client`** is a handle on a shared **`conn`**), a global server is one process for the whole process and a project server (or a global one naming **`${CWD}`**) one per workspace, a connection nothing holds stops after **`DefaultStopDelay`** unless the pool keeps it (**`Keep`**: the global servers **`session.Manager.StartGlobalMCPServers`** starts with `coddy serve`, the console and `coddy acp`, ended by **`CloseMCP`**), and a stdio server runs in its own process group, stopped by stdin EOF and then the group (**`client.go`**). The dial itself lives in **`internal/session/mcp_dial.go`** (every configured server through **`TrustGate.Acquire`** under its own 20 s bound, one more try at the next turn for one that did not answer), the manager's side of the pool in **`internal/session/mcp_pool.go`** (a server whose connection ended is parked and started again at the next turn) and the console's background connect in **`internal/session/mcp_background.go`**. Guide: **`docs/features/mcp.md`** (Shared servers). |
| `internal/remote` | Go client for a remote `coddy serve` server: SSE frames back into ACP updates, `/coddy` REST, permission/question answers, and the permission prompts of background subagents the server announces on its events stream (**`detached_prompts.go`**). Powers `--remote` on the console and `coddy acp`. Guide: **`docs/surfaces/console.md`** (Remote mode), **`docs/plans/remote-control.md`**. |
| `internal/bgtask` | Background task pool for detached shell commands (**`run_command`** **`background: true`** plus the **`background_*`** tools, the Tasks panel, and **`/coddy/sessions/{id}/background-tasks`**), for subagent runs started by **`spawn_agent`** (**`Pool.Launch`**, kind **`agent`**, **`Agent {name, session_id}`** on the row), and for in-process servers the **`preview_server`** tool starts (**`Pool.Launch`**, kind **`server`**, **`Spec.URL`** / **`Snapshot.URL`**, **`Spec.NoTimeout`** - no hard limit unless the call names one). **`Pool.Adopt`** takes over a foreground command that outlived its timeout instead of killing it. **`Pool.CanWake`** says whether this process subscribed a waker (**`WakeWatcherKey`**), so **`run_command`** and **`spawn_agent`** promise a **`notify_on_finish`** wake only where one will happen, and **`Pool.MarkWokeAgent`** records on the row (**`woke_agent`**, persisted) that a task's end did start a turn, which is what keeps the bell on a finished card in the Tasks panel and **`woke the agent`** in **`/tasks`**. Guide: **`docs/features/background-tasks.md`**. |
- | `internal/subagents` | Subagent definitions (markdown + YAML frontmatter from four default folders - **`~/.agents/agents`**, **`.agents/agents`**, **`${CODDY_HOME}/agents`**, **`.coddy/agents`**, always read, the later winning a name - and then the extra **`subagents.dirs`**, through **`config.Subagents.SearchDirs`**; built-ins **`general`**, **`explore`** and the hidden orchestrator **`crossreview`**), canonical **scopes**, the **trust receipts** for project-scope files (**`trust.go`**; policy **`subagents.project_trust`**, receipts in **`<home>/subagents-trust.json`**), the process-wide **`Limiter`**, and the catalog (**`coddy agents list`**, **`GET /coddy/subagents`** with the bounds each definition declares, the prompt block; the web UI lists it read-only under **Settings → Subagents**). A definition's **`spawns`** allowlist (builtin/user scope only) admits exactly the named children exactly one level past **`subagents.max_depth`** - the mechanism a coordinator uses to fan out. The runtime that spawns a child is **`internal/agent/subagent.go`**; it also continues a finished run in its own child session when the parent passes **`spawn_agent`** **`resume`**, instead of a second child starting over (issue #389). Child sessions are owned by **`internal/session`**, which stores each one inside the bundle of the session that spawned it (**`<parent>/subagents/<child>/`**) under an ordinary **`sess_`** id and reopens it from that bundle for a resumed run (**`SubagentSpec.Resume`**). Guide: **`docs/features/subagents.md`**. |
+ | `internal/subagents` | Subagent definitions (markdown + YAML frontmatter from four default folders - **`~/.agents/agents`**, **`.agents/agents`**, **`${CODDY_HOME}/agents`**, **`.coddy/agents`**, always read, the later winning a name - and then the extra **`subagents.dirs`**, through **`config.Subagents.SearchDirs`**; built-ins **`general`**, **`explore`** and the hidden orchestrator **`crossreview`**), canonical **scopes**, the **trust receipts** for project-scope files (**`trust.go`**; policy **`subagents.project_trust`**, receipts in **`<home>/subagents-trust.json`**), the process-wide **`Limiter`**, and the catalog (**`coddy agents list`**, **`GET /coddy/subagents`** with the bounds each definition declares, the prompt block; the web UI lists it under **Settings → Subagents**, where a project definition under **`ask`** carries the MCP shield that approves it, bound to the digest it showed). A definition's **`spawns`** allowlist (builtin/user scope only) admits exactly the named children exactly one level past **`subagents.max_depth`** - the mechanism a coordinator uses to fan out. The runtime that spawns a child is **`internal/agent/subagent.go`**; it also continues a finished run in its own child session when the parent passes **`spawn_agent`** **`resume`**, instead of a second child starting over (issue #389). Child sessions are owned by **`internal/session`**, which stores each one inside the bundle of the session that spawned it (**`<parent>/subagents/<child>/`**) under an ordinary **`sess_`** id and reopens it from that bundle for a resumed run (**`SubagentSpec.Resume`**). Guide: **`docs/features/subagents.md`**. |
| `internal/hooks` | Operator lifecycle hooks: definition files in Claude Code's shape (**`definition.go`**, **`matcher.go`** with tool-name aliases), the loader with canonical **scopes** (**`loader.go`**; policy **`hooks.project_trust`**), the **trust receipts** for project-scope files (**`trust.go`**; **`<home>/hooks-trust.json`**, keyed by canonical workspace, file and digest; surfaces **`coddy hooks list|trust|untrust`**, **`GET /coddy/hooks`**, **`POST /coddy/hooks/trust|untrust`**), the **catalog** (**`catalog.go`**), and the **runner** (**`runner.go`**: stdin JSON payload, exit-code and JSON answer, timeouts and process groups through **`internal/platform`**). **`internal/agent/hooks.go`** wires `PreToolUse` / `PostToolUse` / `PostToolUseFailure` into **`executeToolCall`**, `UserPromptSubmit` and `Stop` into the loop, `PreCompact` / `PostCompact` into **`CompactSession`**, `SubagentStart` / `SubagentStop` into the spawn runtime and `Notification` into the permission gate; **`internal/session/hooks.go`** fires `SessionStart` from the manager and keeps its context on the session. **`hooktest`** re-executes a test binary as the hook process. Guide: **`docs/features/hooks.md`**, design: **`docs/plans/hooks.md`**. |
| `internal/session` | Session manager, Filesystem persistence, Acp hooks, rules catalog, the **paged reads** of a history (**`message_window.go`**: **`PageMessages`** step-aligned pages and the counts a client numbers them from, **`MessagePage.UILog`**; behind **`GET /coddy/sessions/{id}/messages?limit=&before=&from=`**, issue #338), and the **provider usage** cache and schedule (**`provider_usage.go`**: the account quota behind a session's model, published as **`provider_usage`** at session ready and from every turn's release, served over **`GET /coddy/providers/{name}/usage`** and the events stream; **`provider_usage_sources.go`** picks the reader per provider type - **`internal/llm/neuraldeep_usage.go`** for the hub's `GET /v1/limits`, **`codex_usage.go`** for the Codex backend's usage endpoint, **`devin_usage.go`** for the seat-management status RPC), and the **tool-call pairing** guard (**`tool_pairing.go`**: **`ValidateToolPairing`** checks assistant batches against their contiguous tool results, **`RepairMissingToolResults`** inserts stable synthetic results for unambiguous missing calls on the outbound projection only - the stored transcript is never rewritten; the agent refuses a still-malformed history at the send boundary). Design: **`docs/plans/neuraldeep-usage.md`**. |
| `internal/mention` | The one grammar of **`@`** mentions (**`grammar.go`**: relative, absolute, **`~`** and **`../`** paths, quoted paths, **`:N-M`** / **`#L`** ranges, folders, **`@session:`** / **`@rule:`** / **`@agent:`** / **`@coddy:`**, web pages; every reading of an ambiguous token, the one that exists winning), path resolution (**`paths.go`**), the workspace index for completion (**`index.go`**: **`git ls-files`** or a walk, cached per root, rebuilt when a mention starts), fuzzy ranking (**`fuzzy.go`**) and the **`<coddy_attachment>`** element plus its display twin **`ForDisplay`** (**`attachment.go`**). **`internal/session/mentions.go`** resolves a prompt's mentions once, into that user message; **`mention_search.go`** answers completion (**`GET /coddy/mentions`**, the console in-process); **`mention_check.go`** runs the resolver dry for the web composer's highlight (**`POST /coddy/mentions/check`**), so only a mention sending would attach is marked; **`internal/agent/mentions.go`** adds the rules a mentioned path activates to the same message, so a mention never moves the system prompt. The SPA twin is **`external/ui/src/ui/skills/draftAt.ts`**, held to **`internal/mention/testdata/grammar_cases.json`** like the Go side. Guide: **`docs/features/mentions.md`**. |
| `internal/docs`, `docs/embed.go` | The documentation built into the binary: **`docs/embed.go`** (package **`docs`**, the only place the embed pattern reaches the tree from) carries **`nav.yaml`** and the pages of its groups, never **`plans/`** or **`assets/`**; **`internal/docs`** reads them into a **`Library`** (**`library.go`**: **`Default`**, **`Resolve`** for every spelling of a page, **`Prev`** / **`Next`**), splits pages at headings with GitHub's anchors (**`markdown.go`**: **`Anchor`** and **`HeadingAnchors`**, which **`internal/docsgen`**'s link check uses too, and the link rewriting to **`coddy:<slug>#<anchor>`** and to GitHub at the release tag), ranks sections with BM25F (**`search.go`**, index built once on first use) and reads a page or a section under a byte budget (**`read.go`**). Consumers: **`coddy_docs_search`** / **`coddy_docs_read`** (**`internal/tools`**), **`@coddy:`** (**`internal/session/mentions.go`**, **`mention_search.go`**), **`GET /coddy/docs*`** (**`external/httpserver/docs_http.go`**), the web reader (**`external/ui/src/ui/docs/`**), the console's F1 help (**`external/cli/docs_modal.go`**), **`coddy docs`** (**`cmd/coddy/docs.go`**). A new group folder under **`docs/`** goes into the embed pattern; **`TestDefaultCarriesEveryPageOfTheMap`** fails otherwise. Guide: **`docs/features/built-in-docs.md`**. |
| `internal/rules` | Rules: discovery (**`factory.go`**) reads **one** project folder, the first of `.coddy/rules`, the shared `.agents/rules` as system **`agents-dir`**, `.cursor/rules`, `.claude/rules` and `.codex/rules` that holds a rule file - so another agent's mirror of the same rules is never loaded next to it - plus the operator's own **`${CODDY_HOME}/rules`** as system **`user`**, which applies in every workspace and loses a name clash to the project; nested `AGENTS.md` and `DESIGN.md` files read on demand for the folders a tool enters and never walked for (**`agents.go`**, **`AgentsForPaths`**), the dialect chosen by extension in **`markdown.go`** (**`.mdc`** Cursor: `description` / `globs` / `alwaysApply`, default manual; **`.md`** Claude Code: `paths`, unconditional without them) with a lenient reader for Cursor's YAML-invalid `globs: **/*.go` headers, doublestar glob matching anchored at the session cwd (**`MatchGlob`**), activation from mentioned paths (`@path`, an editor's `file://` attachment) and filesystem tool paths (**`select.go`**, **`scope.go`**): only the always-on rules (**`AlwaysOnRules`**) are in **`{{.Rules}}`**, a rule a tool call activates rides in that call's result (**`llm.Message.Rules`**, **`internal/agent/rules_activation.go`**) and one a mention brings in that user message, each once until a compaction folds it away, and the catalog behind **`coddy rules list`**, which names the folder read and the ones skipped. The project docs preamble (**`project_docs.go`**) puts the agent home's **`AGENTS.md`** and **`DESIGN.md`** first, above the workspace's own pair, and no config key names them - the file existing is the switch; **`AgentsForPaths`** reads that same pair for a nested folder a tool enters. The instruction files of **`instructions.files`** are a separate block, read by **`internal/session/instructions_load.go`** into **`{{.Instructions}}`**; a file the preamble already carries is not sent twice. Both blocks are rendered once per rules generation of the session (**`session.RulesPrompt`**, **`internal/agent/rules_prompt.go`**), so an `AGENTS.md` edited mid-session does not move the cached system prompt; a compaction, a config reload or a workspace switch starts the next generation. Guide: **`docs/features/rules.md`**. |
| `external/httpserver` | The HTTP surface **`coddy serve`** runs under **`httpserver.enable`**, built with **`tags=http`** (SSE bridge, Swagger statics, `/coddy` REST, ServeMux wiring). **`Serve(ctx, Options)`** takes an already-built manager; **`turn_mirror.go`** implements **`session.TurnMirror`**, publishing a turn another surface started into that session's composer relay so a browser can watch it - session updates fan out, permission and question requests stay with the surface that has a human. **`subagent_permission_hub.go`** is this server's offer to **`serve.Runtime`** for detached permission prompts: a subagent whose spawning turn has ended publishes its prompt on the agent task row (**`pending_permission`**, shown in the parent chat) and on **`GET /coddy/events`** (**`subagent_permission`**), answered through the ordinary **`POST /coddy/sessions/{child}/permission`**. |
| `external/ui` | Embedded SPA (`go:embed`) when built with **`tags=http,ui`**. A long conversation is a sliding window: the newest page first, older pages as the reader scrolls up, a bounded slice of rows in the DOM (**`chat/TranscriptList.tsx`**, **`useTranscriptWindow`**; **`DESIGN.md`**, *Transcript window*); **`npm run check:transcript`** measures it in a real browser against the binary. |
| `external/memory` | Long-term memory (**`-tags memory`**): the memory subagent's prompt template, task and tool set (**`agent.go`**), the note storage and the six **`coddy_memory_*`** tools. The run itself is launched by **`internal/agent/memory_hooks.go`** as a system child in the task pool (bookkeeping in **`memory_run.go`**, untagged): one run per user turn, a bounded wait for its report, delivery in the turn context block after the history (never the system message, so the provider's prompt cache holds), retention of finished runs. See README there and **`docs/features/memory.md`**. |
| `external/cli` | Interactive console TUI (**`-tags cli`**): bare **`coddy`** on a terminal, pi-style rendering in **`external/cli/tui`**. Guide: **`docs/surfaces/console.md`**. |
| `external/swarm` | The relay **`coddy serve`** runs under **`swarm.enable`**, built with **`tags=swarm`** (part of **`FULL_TAGS`**, so the release binaries, image and packages carry it): the stateless relay - lease registry, per-node mount, aggregated sessions, ring-aware topology, and the reverse HTTP/2 tunnel for nodes that can only dial out - plus the relay's own settings (**`settings.go`**: **`/coddy/config*`** over **`internal/configapi`** with the relay's form, **`config.RelayUISchemaJSON`**, the document scoped to **`swarm`** and **`logger`** so a relay client neither reads nor writes the rest of the host's configuration, and a write through a mount refused when the node behind it is itself a relay). It serves nothing else of an agent: no **`/v1`**, no sessions, no documentation. **`Server.Close`** ends the tunnels, so a relay rebuilt on a config reload is dialled again. Shared vocabulary and the join client live untagged in **`internal/swarm`**, so **`coddy serve`** can join a relay without the tag. Guide: **`docs/operate/swarm.md`**. |
| `internal/configapi` | The settings editor's four routes (**`GET /coddy/config/schema`**, **`GET`** / **`PUT /coddy/config`**, **`POST /coddy/config/validate`**) over a **`Backend`** - the live configuration, how a new one is installed, which schema and document decoration go with it - so the HTTP server and the swarm relay share one PUT transaction under **`config.WithConfigFileLock`**, **`Sections`** to serve and take only some top-level sections (the relay's), and **`Revisions`**, the last 32 configurations a process served, which a **`PUT`** is measured against. Untagged. |
| `external/gateway` | Messenger gateway (**`-tags gateway.telegram`** or **`-tags gateway`**), started by **`coddy serve`** under **`gateways.telegram.enable`**: Telegram bot adapter, session store, proxy support. Chat turns run through the shared manager and are mirrored into the HTTP relay, so a conversation is live in the web UI; what the messenger needs is said twice and kept out of the transcript both times - a system prompt block for the turn (**`prompt.go`**, carried by **`session.PromptRunOpts.SurfaceSystemPrompt`**) and the outbound rendering of the answer (**`markdown.go`**); the poller stops intake before generation on shutdown, so a restart from a settings change does not truncate an answer. Full guide: **`docs/surfaces/gateway.md`**, rules: **`.cursor/rules/gateway.mdc`**. |
| `internal/tgfake`, `cmd/tgfake` | The offline stand for the Telegram gateway: a fake Bot API server (every method the adapter calls, long-polled `getUpdates`, the `/sim/*` injection API, a chat page) plus **`llmstub`**, a scripted OpenAI-compatible model behind **`--llm`**. Untagged and free of Telegram library types, so the gateway's tests run it on httptest (**`features/gateway_telegram_polling.feature`**) and **`go run ./cmd/tgfake --llm`** serves it on a port; **`coddy serve`** is pointed at it with **`CODDY_TELEGRAM_API_BASE`** (**`config.TelegramAPIBaseEnv`**, honoured by the bot and the **`--dry-run`** probe alike). Runner: **`examples/gateway/tg_e2e_offline.sh`**. Guide: **`docs/surfaces/gateway.md`** (Debugging against a fake Bot API). |
| `packaging` | Distribution package sources: the **nfpm** recipe (**`nfpm.yaml`**) for the Linux **`.deb`** and **`.rpm`**, the **systemd user unit** (**`systemd/coddy.service`**, installed and not enabled, rendered by **`serve.PackagedUnitFile()`** and held to it by a test), the Homebrew **cask** template (**`homebrew/coddy.rb.tmpl`**, prebuilt macOS archives) and **formula** template (**`homebrew/coddy-formula.rb.tmpl`**, built from source - the artefact **homebrew/core** takes), the man page (**`man/coddy.1`**), bash and zsh completions, and the maintainer scripts (**`scripts/postinstall.sh`**, **`scripts/preremove.sh`**: what the user has to do next, the unit not enabled included). The package enables nothing and creates **no system service, system account or `/etc` files**; Coddy's state lives in the invoking user's **`~/.coddy`**, and **`coddy serve install`** (**`internal/serve/systemd.go`**) enables the unit for one account, or writes one under **`~/.config/systemd/user`** for a binary the install script put in place. Built by **`scripts/build-packages.sh`**, **`scripts/build-homebrew-cask.sh`** and **`scripts/build-homebrew-formula.sh`** (**`make deb`**, **`make rpm`**, **`make brew`**, **`make brew-formula`**); **`scripts/check-homebrew-submission.sh`** (**`make brew-check`**) is the preflight for the homebrew/core submission. Guides: **`docs/getting-started/install.md`**, **`docs/getting-started/homebrew.md`** (which Homebrew repository takes what, and the submission path), **`docs/contributing/build.md`** (Distribution packages), **`docs/getting-started/update.md`** (installations owned by a package manager). |
| `internal/docsgen`, `cmd/docsgen` | The documentation generator and checker behind **`make docs`** / **`make docs-check`**: the hub and the llms files from **`docs/nav.yaml`**, the config field tables from the schema and **`config.DocDefaults`**, the CLI reference from the `--help` screens, the assets inventory, the relative-link and anchor check. Every tree walk asks git what it excludes and skips it (**`gitignore.go`**), so local scratch under **`docs/`** is never reported as a page missing from the map; without git nothing is filtered. |
## Builds
**`coddy serve`** is the only long-running command: it starts every subsystem **`config.yaml`** enables (**`httpserver.enable`**, default true; **`gateways.telegram.enable`**; **`swarm.enable`**; **`scheduler.enable`**) over one session manager. A subsystem enabled but not compiled in is a startup error naming the tag. Run **`make build TAGS=http`** for the HTTP surface only (REST and **`/docs`**, no **npm**). Run **`make build TAGS=cli`** for the interactive console (**bare `coddy`** on a terminal; see **`docs/surfaces/console.md`**). Run **`make build TAGS="http ui"`** to link the embedded SPA (**Makefile** runs **ui-build** before **go build**). Recommended full image matches **`Dockerfile`** (**`make build TAGS="http ui scheduler memory cli gateway swarm"`**). Default **`make build`** omits HTTPServer, scheduler, and memory to keep dependency surface lean.
Primary conversational surface for bundled UI lives at **`POST /v1/responses`** with **`stream:true`**. Prefer it over **`POST /v1/chat/completions`** when shipping Coddy-hosted experiences.
Run **`make android`** for the Android (Termux) binaries, **`build/coddy-android-arm64`** and **`build/coddy-android-amd64`**: **`GOOS=android`** with cgo, linked by the Android NDK against Bionic (**`scripts/android-cc.sh`** finds the NDK), a position-independent executable naming **`/system/bin/linker64`**, which Termux can start where it runs programs through the system linker (the static **`GOOS=linux`** build is refused there with `has unexpected e_type: 2`). The release publishes them as **`coddy_X.Y.Z_android_arm64.tar.gz`** and **`coddy_X.Y.Z_android_amd64.tar.gz`**; a build without cgo does not compile (**`internal/platform/android_nocgo.go`**). What Termux patches into its own Go is done at run time by **`internal/platform/android*.go`** (the binary's own path under the linker, certificates, **`TMPDIR`**), and every **`exec.Cmd`** goes through **`platform.AdaptCommand`** before **`Start`** (held by **`TestEverySpawnSiteAdaptsTheCommand`**). Guide: **`docs/getting-started/android.md`**.
Run **`make deb`** / **`make rpm`** for the Linux packages (**`PKG_ARCHS`**, **`PKG_TAGS`**, **`DIST_DIR`** knobs; nfpm is fetched on demand, nothing to install) and **`make brew VERSION=X.Y.Z`** to render the Homebrew cask for a release. CI builds the Linux packages on every pull request.
Swagger lives at **`/docs/`**, OpenAPI YAML at **`/openapi.yaml`**.
## Pre-commit gate
A git **`pre-commit`** hook runs the linter before every commit, so nothing lands with lint errors. It is the single enforcement point for humans and coding agents alike. Tests are **opt-in** on commit: **`make test`** (the express run: the SPA's vitest suite, then every optional module compiled in once) belongs before the push, and the per-combination tag matrix (**`make test-matrix`**) runs on GitHub Actions for every pull request, as does **`make test-race`**: the race detector over the whole tree with every tag but `ui`, clean in every package. Run it before the push when a change touches goroutines, locks or a test harness that drives a live loop. Two groups run on their own: **`make test-cache`** (the **`features/prompt_cache_*.feature`** specs and the **`TestPromptCache*`** tests, part of `make test`: what each request sends the provider, byte for byte across a session - run it when a change touches the system prompt, the rules, the history or the send boundary) and **`make test-perf`** (the benchmarks of the **`perf_test.go`** files, never part of `make test`: timings and the context a session costs, as token metrics).
- Enable once per clone: **`make hooks`** (sets **`core.hooksPath=.githooks`**; this is local config and is not committed, so every clone runs it once).
- AppSec scanning is **opt-in** the same way: **`make security`** (trivy + semgrep over the checkout, docker fallback when the binaries are absent) is what `.github/workflows/security.yaml` runs on pull requests, pushes to main and a weekly cron; **`make sec-report`** scans without failing on findings. Reports and the severity summary land in **`dist/security/`**; the gate knobs (`SEC_FAIL_TRIVY`, `SEC_FAIL_SEMGREP`, `SEC_DOCKER`, `SEMGREP_CONFIGS`) and the triage workflow (`.trivyignore.yaml`, `trivy.yaml` skip-dirs, `nosemgrep`) are in **`docs/contributing/security-scanning.md`**.
- On commit, **`.githooks/pre-commit`** calls **`scripts/checks.sh`**, which runs **`make lint`** by default: `golangci-lint` over the untagged tree, over every tag but `ui` and over the shipped tag set, so every Go file of the host platform is compiled once, then `tsc --noEmit` over the SPA sources (**`make ui-typecheck`**). Commits touching only non-code files (docs, etc.) skip the gate.
- Scope knobs: **`CODDY_HOOK_TESTS=fast`** also runs a quick **`go test ./...`**, **`CODDY_HOOK_TESTS=full`** the express **`make test`**, **`CODDY_HOOK_TESTS=matrix`** every tag combination; **`CODDY_HOOK_LINT=0`** skips the linter; **`CODDY_HOOK_SKIP=1`** bypasses everything.
- Emergency bypass for a single commit: **`git commit --no-verify`**.
- The Android half of **`internal/platform`** (**`//go:build android`**) is compiled by neither gate either: **`make check-android`** runs **`go vet`** with **`GOOS=android`** and cgo through the Android NDK, test files included, and CI runs it in the **Android cross-build** job, which also uploads both release-shaped binaries as artifacts.
- Both gates compile the **host** platform only. Changing a file behind **`//go:build windows`**, or a signature it shares with the rest of the tree, needs **`make check-windows`** (cross-build plus **`go vet`** over every non-**`ui`** tag combination, test files included) and **`make lint-windows`**. CI runs both, and additionally runs **`go test`** on a real **`windows-latest`** runner for **`internal/platform`**, **`internal/bgtask`**, **`internal/tools/shell`**, **`internal/update`**, **`internal/hooks`**, **`internal/rules`** and the console TUI (**`external/cli`**, `-tags=cli`), plus **`cmd/coddy`**, **`internal/config`**, **`internal/dryrun`** and **`internal/swarm`**, whose tests write files and compare paths on the runner's CRLF checkout. The platform packages and the console suite run on a **`macos-latest`** runner as well (job **`test-macos`**), which also builds the console and drives the binary through a real pty with **`examples/cli/cli_e2e_startup.py`** (first frame, keys, exit); the `cli` job of the Linux test matrix runs that pty script too, and its `gateway` job builds the gateway binary and runs **`examples/gateway/tg_e2e_offline.sh`**: the real **`coddy serve`** against the fake Bot API and the scripted model of **`cmd/tgfake`**, no Telegram and no LLM.
## Documentation contract
The documentation is the tree under **`docs/`**, mapped by **`docs/nav.yaml`**: `getting-started/`, `surfaces/`, `operate/`, `features/`, `reference/`, `contributing/`, `tutorials/`, and `docs/assets/` for everything a page embeds. **`docs/plans/`** (design records, never rewritten to match a later rename) is internal and stays out of the map, so the hub and the llms files do not carry it. Anything git ignores under **`docs/`** is not documentation at all and is not walked. **`docs/README.md`** (the hub), the field tables of **`docs/reference/config.md`**, the help screens of **`docs/reference/cli.md`** and the inventory of **`docs/assets/INDEX.md`** are generated by **`internal/docsgen`** (`make docs`), which also builds **`llms.txt`** and **`llms-full.txt`** straight into the site layer - they are a concatenation of every page, so a copy in this index conflicted in every branch that touched any page, and `.gitignore` keeps them out; **`make docs-check`** (the CI job **Documentation**, and the pre-commit hook for commits that touch the docs) fails on a stale generated block, a page missing from the map, a broken relative link or anchor, and an asset no page uses. **`make docs-changelog`** refreshes **`docs/getting-started/changelog.md`** from the GitHub Releases. The full guide, page types included, is **`docs/contributing/documentation.md`**.
A change to Coddy carries its documentation in the same pull request (workflow step 10 in **`.claude/rules/workflow.md`**): a new capability gets its page, its `nav.yaml` entry and its row in the reference of its kind (`tools.md`, `slash-commands.md`, `environment-variables.md`, `keyboard.md`); a changed behaviour updates the page that describes it (`git grep -n '<key or command>' docs/`); a web UI or console feature is shown with a screenshot of the real surface embedded on its page, Dark 1280 wide, named `<feature>-<state>-<theme>-<width>.png` under `docs/assets/<page>/`; a demo video goes to `docs/assets/video/` and is embedded on the page as a GitHub attachment URL with the repository copy linked under it (Videos in `docs/contributing/documentation.md`). Add screenshots under `docs/assets/` only when they document a new UI/UX feature or a changed existing UI/UX feature, and embed every such asset on its documentation page. Keep pull-request-only evidence out of `docs/assets`: publish it on the fork's `screenshots` branch under a pull-request-specific directory, then embed its raw GitHub URLs in the pull request description. `make docs-check` governs every asset committed under `docs/assets/`. A page that moves takes its address with it: there are no redirect stubs. Every page of the map also has a stable address on **coddy.dev**: `coddy.dev/docs/<slug>` sends a person to the page on GitHub and `coddy.dev/docs/<slug>.md` to its raw Markdown (one generated script behind the site's `404.html`, nothing duplicated), while `llms.txt` and `llms-full.txt` at the site root index the raw Markdown on `main`; the binary, the schema and the bundled skill print those addresses and nothing else. **`make site-docs`** renders the layer into the site checkout and **`make site-docs-check`** reports drift - a change to a page, to `nav.yaml` or to the schema is published to the site with the same pull request (workflow step 8).
Human prose for HTTP lives in **`docs/reference/http-api.md`**. Visual spec for SPA lives in **`DESIGN.md`** (this repo root). Architectural narrative remains under **`docs/contributing/architecture.md`**.
**`internal/config/config.schema.json`** is embedded into the binary (`go:embed` in **`internal/config/schema.go`**: it is what **`-t` / `--test-config`** validates a file against, its `description` strings are the `doc:` lines of that report and the Description column of the generated **`docs/reference/config.md`** tables) and leaves this repository: it is served at **`https://coddy.dev/config.schema.json`**, the address Coddy writes as a modeline into every config it saves, out of the site repository **`coddy-project.github.io`** as a verbatim copy. Touching the YAML config surface means **`make docs`** and **`make site-schema`** (and **`make site-schema-check`** to see whether the published copy is stale) - a schema that stops here leaves every editor validating against one the binary no longer matches. Hold the site push until the release when a key was renamed or removed: the schema sets **`additionalProperties: false`**, so the new file marks the old key as an error in configs already on disk. Full step: **`.claude/rules/workflow.md`**.
All **code comments** plus **technical markdown authored for this repo** (including `docs/`, `DESIGN.md`, `AGENTS.md`) stay **English** unless an operator explicitly asks for another natural language.
## Codex, OpenCode, Cursor and ZCode rules
Codex uses this **`AGENTS.md`** file as its repo instruction entrypoint, and ZCode resolves its `AGENTS.md` chain the same way. The detailed project rules live in **`.cursor/rules/*.mdc`**, which is their single source of truth. Do not copy rules into `.codex/`, `.claude/`, or `.zcode/` by hand.
Codex resolves its `AGENTS.md` chain once per session, walking from the repository root down to the launch directory, so nested instruction files never load for a session started at the root. A lifecycle hook covers that gap and delivers the Cursor rules deterministically instead of asking the model to fetch them:
- **`.codex/hooks.json`** wires **`.codex/hooks/attach_rules.py`** to `SessionStart` and to `PreToolUse` on `apply_patch` / `Edit` / `Write`.
- On session start the hook injects every rule with `alwaysApply: true`.
- Before each patch it injects the rules whose `globs` cover the files being touched, at most once per rule per session.
- The hook parses `.mdc` frontmatter directly, so adding, renaming, or removing a rule file needs no change here. It fails open, and a malformed rule never blocks an edit.
Codex requires explicit approval for hooks and tracks them by content hash. Run **`/hooks`** once per clone, and again after editing `attach_rules.py`, otherwise the hook is skipped silently. Project-local hooks also load only when the `.codex/` layer is trusted. **`.codex/rules.md`** keeps the human-readable index of the rule files; the full guide, including how to probe the hook by hand, is **`docs/contributing/codex-hooks.md`**.
When working in OpenCode, the project plugin **`.opencode/plugins/project-rules.js`** delivers the same rule set without copying it. It injects `alwaysApply: true` rules into every model request, activates scoped rules when a tool touches a path covered by their `globs`, and preserves the active set through compaction. If a write tool is the first operation to reveal a new scoped rule, the plugin rejects that one call so OpenCode can retry with the rule in its model context. See **`docs/contributing/opencode-hooks.md`** and run **`make test-opencode-rules`** after changing the plugin.
ZCode loads its `AGENTS.md` chain the same way and has the same gap, so a parallel hook delivers the Cursor rules there too:
- **`.zcode/config.json`** wires **`.zcode/hooks/attach_rules.py`** to `SessionStart` and to `PreToolUse` on `Edit` / `Write` / `MultiEdit` / `ApplyPatch`, under `hooks.events` with `hooks.enabled: true`.
- The behaviour matches the Codex hook (always-on rules at session start, scoped rules per edit, once per rule per session, fail open). The single difference is how edited paths are recovered: ZCode carries them as JSON fields of `tool_input` (`file_path`, `path`, ...), so this variant walks those fields instead of parsing an `apply_patch` blob.
- Configuration-file hooks need no per-hook approval: a workspace config with `enabled: true` runs unconditionally. The command invokes `python` (not `python3`) because the `python3` name is a no-op Microsoft Store stub on Windows. The full guide, including how to probe the hook by hand and the Windows interpreter note, is **`docs/contributing/zcode-hooks.md`**.
## Code Review Rules
Codex code review reads this section and applies it to changed files. Keep entries behavioural and repository-specific; formatting and lint stay in CI, where the pre-commit gate already runs **`make lint`**.
### HTTP surface
- Do not change routes, request or response shapes, or status codes in `external/httpserver/server.go` without updating `external/httpserver/openapi.go` in the same change. The served spec is what `/docs/` and generated clients consume, so drift is a silent API break. Safe path: update both, then reconcile `docs/reference/http-api.md`.
### Provider transport
- Do not add a provider request path that can bypass `providers[].proxy`. Completions, streams, model discovery, usage, OAuth/device sign-in, refresh and revoke all build through `llm.HTTPClientForProviderProxy`; a test-only explicit client may be accepted, but `nil` must return an error rather than fall back to `http.DefaultClient`. When a Settings sign-in starts before Save, apply the form's current proxy after resolving the row. Safe path: extend `features/provider_proxy.feature`, keep `TestProviderProxyGuard` green, and cover the request class through the proxy harness.
### Build tags
- Do not let a package that builds by default import one that lives behind the `http`, `ui`, `scheduler`, `memory`, or `gateway` tags. Plain `make build` must keep compiling with the lean dependency set. Safe path: put the new code behind the same tag, or invert the dependency into an interface owned by the core package.
### Project-local configuration
- Do not read, merge, or execute project-local configuration (`.coddy/mcp.json`, MCP server definitions, hook scripts) without routing it through `TrustGate` in `internal/mcp`. Opening an untrusted checkout must not by itself grant code execution. Safe path: gate the read on the workspace trust decision and persist the approval through `TrustStore`.
- The same holds for project-scope subagent definitions (`.agents/agents` and `.coddy/agents` under the workspace, or any `subagents.dirs` entry there): they load and spawn only as `subagents.project_trust` allows, a receipt in `subagents.TrustStore` is bound to the file digest, and a definition must never widen what the parent could do - `permission_mode`, `tools` and `disallowed_tools` only narrow, and the mandatory exclusions stay. Safe path: resolve the definition once, decide trust on that value with `subagents.Decide`, and derive the child's tool set with `subagents.EffectiveTools`.
### Packaging
- Do not add, rename or remove a CLI subcommand, a **`serve`** verb or a flag without updating **`packaging/man/coddy.1`** and **`packaging/completions/coddy.bash`** / **`coddy.zsh`** in the same change (and **`topLevelCommands`** in **`cmd/coddy/usage_test.go`** for a command). Every install route ships those files as the documentation of the command set - the release archive through **`install.sh`**, the Homebrew cask and **`coddy update`**, the packages, the formula - and nothing generates them: the test holds the commands and the `serve` verbs to **`printUsage`**, flags are kept in step by hand. Workflow step 11 in **`.claude/rules/workflow.md`**.
- Do not move **`/usr/bin/coddy`** or add a second packaged copy of the binary. **`coddy update`** asks **`dpkg-query -S`** / **`rpm -qf`** who owns the executable it is about to replace, and a packaged file that no database claims would be silently overwritten. Safe path: keep the single **`contents`** entry in **`packaging/nfpm.yaml`**.
- Keep the packaged service an opt-in **systemd user unit** that the package installs and never enables. Do not add a system service, a system account or files under **`/etc`**, and do not enable, start or disable the unit from a maintainer script. Coddy's state is per-user under **`~/.coddy`**, and each user decides with **`coddy serve install`** / **`coddy serve uninstall`**. The unit is rendered by **`serve.PackagedUnitFile()`**: change the renderer and regenerate **`packaging/systemd/coddy.service`**, never the file alone.
- Do not add a file to the release **`.tar.gz`** layout without checking the Homebrew cask: **`packaging/homebrew/coddy.rb.tmpl`** links artefacts out of that archive by name.
- Do not move the man page or the completions inside the repository without updating **`packaging/homebrew/coddy-formula.rb.tmpl`**. The formula installs them from their source paths, and a from-source build is the only packaging route that reads the tree rather than a staged archive. Its **`test do`** block is coupled the same way: it asserts on the output of **`coddy -v`**, **`coddy sessions list`** (the **`SESSION_ID`** header) and **`coddy skills list`** (the **`Search roots:`** line), and once the formula is in homebrew/core those strings are checked by Homebrew's CI, not ours.
- Do not conflate the two Homebrew artefacts. The **cask** installs prebuilt macOS archives and is upgraded with **`brew upgrade --cask coddy`** out of a **`Caskroom`**; the **formula** builds from source and is upgraded with **`brew upgrade coddy`** out of a **`Cellar`**. **`internal/update`** reads which of the two owns the running executable off its path, so a change to that detection has to keep both hints correct - the wrong flag fails on the user's machine.
### Documentation
- Do not add a user-visible capability, change what a documentation page describes, or add a tool, a slash command, an environment variable or a key without carrying the change into **`docs/`** in the same pull request: the page under `features/`, `surfaces/` or `operate/`, its entry in **`docs/nav.yaml`**, the reference of its kind, a screenshot on the page when the change is visible in the web UI or the console. **`make docs-check`** catches a missing map entry, a broken link and an unused asset, but it cannot tell that a paragraph went stale - that is what the review is for. Safe path: `git grep -n '<key or command>' docs/` before the change, update every hit, then `make docs`.
- Do not commit screenshots or videos that no page references, and do not put pull request evidence under **`docs/assets/`**. Do not delete or move a page without repairing the links `make docs-check` reports and republishing the site layer (`make site-docs`).
- Do not edit inside a `<!-- docsgen:*:start -->` block by hand; change the source (`nav.yaml`, the schema description, `printUsage`) and run **`make docs`**.
### Embedded UI
- Do not treat a UI change as complete when only `external/ui/src/` moved. The binary serves `go:embed` assets, so unrebuilt sources ship the previous SPA. Safe path: run `make build TAGS="http ui"` and include the regenerated assets in the change.
## HTTP API development flow
When changing behavior for the OpenAI-compatible HTTP gateway or bundled UI:
- Add or update tests first (red), then implement (green). The **happy path** of a feature (and a bug's reproduction) is a Gherkin spec in the repo-root **`features/`** directory, run by a godog harness (e.g. `external/httpserver/bdd_*_test.go`); **edge/error cases go in ordinary unit tests**, not `features/`. See **`.cursor/rules/workflow.mdc`** / **`.claude/rules/workflow.md`**.
- If the external HTTP surface changes, update `external/httpserver/openapi.go` so the served OpenAPI matches handlers in `external/httpserver/server.go`.
- Keep `docs/reference/http-api.md` aligned with the live behavior.
- For UI changes, update sources under **`external/ui/src/`** and rebuild embedded assets via **`make build TAGS="http ui"`** (runs **npm** via **make ui-build**).
- **Every** UI edit in a PR ships with a screenshot of the surface it changed (before/after when the surface already existed) — see step 5 of **`.claude/rules/workflow.md`**. If a surface cannot be captured, say so in the PR instead of omitting it.
- Run `make test` (the express run over the shipped tag set), then `make lint`. The tag matrix runs in CI on the pull request; do not walk it locally.
## UI sources (`external/ui/`)
**`DESIGN.md`** is the contract for layout, tokens, and SPA component behavior. After changing **`external/ui/src/`**, rebuild embedded assets with **`make build TAGS="http ui"`** before relying on **`go:embed`**.
The composer exposes **`Mode`** (**`agent`** / **`plan`** / **`ask`**) and a separate **`Model`** YAML backend selector (**`metadata.model`**; list rows with **`owned_by`** other than **`coddy`** from **`GET /v1/models`**). A new chat takes the model the cookie **`coddy_llm_model`** remembers when it is still listed, else the alphabetically first YAML row; the row that response marks **`default: true`** (what a session with no selection runs on: **`agent.model`**, or the first **`models[]`** row when it is not listed) is for API clients and the remote console and does not drive the pick. Parallel **`POST /v1/responses`** per session, **Stop** (**cancel** + partial assistant persistence), and transcript merge after **`GET .../messages`** are specified in **`DESIGN.md`** (**Multi-session streaming and Stop**) and **`docs/surfaces/web-ui.md`**.
**`MarkdownLineEditor`** (`external/ui/src/ui/markdown/`) is the shared markdown body editor (line gutter, wrap-aware numbering, active-line highlight, content-driven height). Used in the plan document card and scheduler job body. Visual and behaviour contract: **`DESIGN.md`** (**Markdown line editor**, **Plan mode plan document card**); functional checklist: **`docs/surfaces/web-ui.md`**.
**UI localization** lives in **`external/ui/src/ui/i18n/`** (**`translate`/`t`**, **`I18nProvider`** + **`useT()`**). **`locales.ts`** is the single registry that connects each supported locale to its picker label and dictionary; English and Russian ship today. Add a locale by adding its dictionary and one registry entry — picker options, locale validation, bootstrap, and parity coverage derive from that registry. In Russian copy the adjective for agent/agentic is **агентный**, never **агентский**; a spawned agent is a **субагент**, never **сабагент**; a git worktree stays **worktree**, never **рабочее дерево** (rule **`.cursor/rules/russian-wording.mdc`**, held by **`ruWording.test.ts`**; `git grep -n -i 'агентск'` and `git grep -n -i 'сабагент'` find nothing but the rules that forbid them). When user-facing copy is added or changed on a localized surface, add the key to **every registered dictionary in the same change**; **`messagesParity.test.ts`** enforces key and interpolation-token parity. Counted copy ("N tasks") goes through **`translatePlural`** / **`tp(key, count)`** with one entry per CLDR category (**`key.one`** / **`key.few`** / **`key.many`** / **`key.other`**) so each language declines by number; a locale must carry exactly the categories its own **`Intl.PluralRules`** produces. **`main.tsx`** bootstraps locale (**`?lang=` > cookie > `navigator.language`**) and wraps **`<App/>`** plus the shared confirmation provider in **`<I18nProvider>`**. The language picker is in **Settings → Appearance**, under the theme grid (**`AppearanceLanguagePicker`**); cookie **`coddy_ui_lang`** mirrors **`coddy_ui_theme`** (client-side only, no config save). Appearance, Settings, shared destructive confirmation dialogs, and the conversation surfaces (nav, hero, composer, messages, prompts, plan card, History, scheduler, background tasks, env banner) and the swarm screen (including the topology graph) are translated. Visual contract: **`DESIGN.md`** (**Localization and language**); functional checklist: **`docs/surfaces/web-ui.md`**.
## Python samples (`examples/`)
See **`examples/README.md`** for layout (**`examples/httpserver/`**, **`examples/acp/`**, **`examples/shared/`**). Scripts may use a project-local interpreter (`.venv` recommended); follow each script header for prerequisites.
- **`examples/build_coddy.sh`** - runs **`make build TAGS="http scheduler memory"`** (override **`TAGS`** as needed) and prints **`coddy -v`**.
- **`examples/test_acp.sh`** - wrapper that runs **`examples/acp/test_acp.sh`** (all ACP **`acp_*.py`** demos in one pass).
- **`examples/test_httpserver.sh`** - wrapper that runs **`examples/httpserver/test_httpserver.sh`** (temp **`coddy serve`**, all HTTP harnesses including **`http_e2e_scheduler_api`** and **`http_e2e_scheduler_agent`**).
Example HTTP scripts that call completion endpoints expect a reachable provider and return non-zero on HTTP errors.