AGENTS.md · git:20261008.a83033c · 2026-10-08 · sha256 79d225d48980127b
AGENTS.md git:20261008.a83033cA
Immutable. This exact content never changes and is served at /api/v1/blob/79d225d48980127b.
# 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 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, and the Russian translation under **`ru/`** (its **`nav.yaml`** and the same pages), never **`plans/`** or **`assets/`**; **`internal/docs`** reads them into a **`Library`** per language (**`library.go`**: **`Default`** (English), **`LoadLang`**, **`Resolve`** for every spelling of a page, **`Prev`** / **`Next`**; **`lang.go`**: **`For(lang)`**, **`Lang`** for any spelling of a locale, **`LangFromEnv`** (`CODDY_LANG`, `LC_ALL`, `LC_MESSAGES`, `LANG`), **`LangOfText`**; a translated heading answers to the anchor of the English heading at the same position, so an address names a section in both languages), 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 per language; Russian words stemmed by Snowball, **`stem_ru.go`**) 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 **`AGENTS.md`** and **`DESIGN.md`** documents come in three layers, always and in this order, whatever `rules.auto_discover`, `rules.systems` or the prompt template say (issue #425): the agent home's pair, then the workspace's (**`project_docs.go`**, **`LoadStanding`**), both in the system prompt, then the pair of every folder on the way down to a file a tool or a mention reaches (**`AgentsForPaths`**), attached to that result or message; no config key names them - the file existing is the switch. **`instructions.files`** (empty by default) only adds files after them, each under a `### <path>` heading like the documents, its entries resolved by **`internal/session/instructions_load.go`**, which also reports the entries whose file a session cannot read (**`UnreadInstructionFiles`**: a log warning per rules generation, the console header, **`--dry-run`**) - one naming the same file in every workspace (absolute, `~`, `${CODDY_HOME}`) whenever it is unreadable, a relative or `${CWD}` one only when its file is there and unreadable - since **`LoadStanding`** skips such a file without a word. Every file reaches the model once per rules generation, known by its real path (**`dockey.go`**, **`DocKey`**: links resolved, case folded on Windows and macOS), the first occurrence winning, so a nested document the system prompt already carries is not attached again. The standing parts are rendered once per rules generation of the session (**`session.RulesPrompt`**, **`internal/agent/rules_prompt.go`**) and placed in whatever template a turn runs on by **`internal/agent/system_prompt.go`** (**`renderWithDocuments`**: documents a template did not print are appended), 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`**. Everything Telegram-specific is **`src/ui/telegram/`** (the Mini App: its own bridge to the client, no script from telegram.org; **`DESIGN.md`**, *Telegram Mini App*). 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. The views of a chat are buttons in the chat header: Files as an icon with a short name on a desktop and a tablet, Background tasks at the right edge with its dot and counts; the dock shows the background tasks without a tab strip, the edits open in a window of their own (git's report of the folder, opened from git's count at the right edge of the plate joined to the top of the composer card, **`chat/WorkspaceBar.tsx`**), and so do the files, each shown as its source with nothing over it (**`src/ui/files/FilesView.tsx`**; **`DESIGN.md`**, *Views of a chat*); **`npm run check:files`** drives them through an authenticated swarm relay. |
| `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`**, **`-tags gateway.pachca`** or **`-tags gateway`** for both), each bot a subsystem of **`coddy serve`** of its own: the Telegram bot under **`gateways.telegram.enable`**, and the **Pachca** integration bot under **`gateways.pachca.enable`** (**`external/gateway/pachca`**: reads the bot's events history - newest first down to a `(created_at, id)` watermark in `gateway_pachca_state.json`, dispatched oldest first, deleted after handling - answers every message of a direct chat and, in a group, only a mention or a reply to the bot (Telegram follows the same group rule), streams into one edited message; a reply reaches the session with the replied-to message quoted in front (**`external/gateway/replyquote`**, both adapters); its offline fake is the untagged **`internal/pachcafake`**, guide **`docs/surfaces/pachca.md`**); session store, proxy support, the ACL over **`access.Policy`** shared by both, and the web UI as the Telegram bot's Mini App (**`telegram/miniapp.go`**: the menu button and **`/app`** under **`gateways.telegram.mini_app`**, advertised only behind a credential through **`gateway.Options.WebUI`**, which **`cmd/coddy/serve.go`** decides, and opened only by the bot's admins, whom the launch data signs in (**`POST /coddy/auth/telegram`**, **`external/httpserver/auth_telegram.go`**); a sender who is not in a bot's **`admins`** only chats: the turn carries **`access.NonAdminTurn()`** (**`session.TurnRestriction`**: config, model, subagent, worktree and scheduler tools refused, approvals always asked and refused by the bot); **`gateway.Fingerprint`** and **`gateway.PachcaFingerprint`** in the untagged **`serve.go`** hash each bot's whole block). 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`**. |
| tgfake (external module) | The offline stand for the Telegram gateway lives in its own repository, **[github.com/EvilFreelancer/tgfake](https://github.com/EvilFreelancer/tgfake)** ("tgfake server"): a fake Bot API (every method the adapter calls, long-polled `getUpdates`, the `/sim/*` simulation API, a chat page that also plays Telegram for a Mini App) plus a scripted OpenAI-compatible model behind **`--llm`**. **`go.mod`** pins one version and everything here uses that one: the gateway's Go tests import **`pkg/server`** (as `tgfake`, through **`external/gateway/telegram/fakeapi_test.go`**) and **`pkg/botapi`**, several suites import **`pkg/llmstub`** (with **`StripTags: ["turn_context"]`**), the scripts build **`github.com/EvilFreelancer/tgfake/cmd/tgfake`** (a **`tool`** of **`go.mod`**) unless **`CODDY_TGFAKE_BIN`** names a prebuilt binary, and CI's **Telegram stand (tgfake)** job installs the release of that version with tgfake's own action. **`coddy serve`** is pointed at it with **`CODDY_TELEGRAM_API_BASE`** (**`config.TelegramAPIBaseEnv`**, honoured by the bot and the **`--dry-run`** probe alike). A change Telegram's behaviour needs is made in tgfake, released, and bumped here. Runner: **`examples/gateway/tg_e2e_offline.sh`**. Guides: **`docs/contributing/tgfake.md`** (running and bumping it), **`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`** / **`make docs-stamp`**: the hub and the llms files from **`docs/nav.yaml`** (and the Russian ones from **`docs/ru/nav.yaml`**), the translation checks and the stamp of **`docs/ru/`** (**`translate.go`**), the link localization of a translated page (**`localize.go`**), 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`**; **`gateways.pachca.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.
Run **`make check-image`** to build the runtime image for **`linux/amd64`** and **`linux/arm64`** and check that each variant carries a **`/bin/coddy`** of its own platform (**`scripts/check-image.sh`** reads the ELF header; Docker with buildx is all it needs, the build stages cross-compile and nothing runs under emulation). The **Docker image** job runs it on every pull request and the release workflow runs it before it pushes. Guide: **`docs/getting-started/docker.md`** (Checking the platforms of the image).
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 + govulncheck over the checkout, docker fallback when the binaries are absent; govulncheck fails on a vulnerable function the code calls, the standard library of the `toolchain` line of **`go.mod`** included) 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_FAIL_GOVULNCHECK`, `GOVULNCHECK_TAGS`, `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`**); then, when the commit stages SPA sources, Prettier over the whole SPA (**`make ui-format-check`**, `prettier --check .` in **`external/ui`**, **`.prettierignore`** keeping the build output and the vendored grammars out), which CI's Lint job runs too: the tree is Prettier-clean, and **`npm run fmt`** in **`external/ui`** keeps it so. 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_FORMAT=0`** skips the Prettier check; **`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 the **Telegram stand (tgfake)** 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 the **tgfake** release binary of the version **`go.mod`** pins, installed by tgfake's own action, 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 documentation is also in Russian: **`docs/ru/<group>/<page>.md`** translates **`docs/<group>/<page>.md`** under the same path, **`docs/ru/nav.yaml`** and **`docs/ru/config-descriptions.yaml`** translate the map and the schema descriptions, and the binary carries both languages (every surface shows the one it knows the reader uses). A change to an English page carries into its translation in the same pull request and is restamped with **`make docs-stamp PAGES=...`**; **`make docs-check`** fails on a missing or stale translation and on headings that differ from the source. The conventions and the writing rules for both languages are the paired **documentation translation rule** ([Cursor](.cursor/rules/docs-translation.mdc), [Claude Code](.claude/rules/docs-translation.md)). The full guide, page types included, is **`docs/contributing/documentation.md`**.
A change to Coddy carries its documentation in the same pull request (step 10 of the **paired workflow rule** - [Cursor](.cursor/rules/workflow.mdc), [Claude Code](.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 `screenshots` branch of this repository (`coddy-project/coddy-agent`, never a personal fork) 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: the **paired workflow rule**.
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.
## Agent host integrations
**`AGENTS.md`** is the repository-wide baseline, and **`CLAUDE.md`** remains its compatibility symlink. Cursor and Claude Code use paired native rule trees under **`.cursor/rules/`** and **`.claude/rules/`**. Their bodies and activation intent stay equivalent, but neither vendor tree is a universal policy format.
Codex, OpenCode and ZCode use repository adapters that read **`.cursor/rules/*.mdc`**. The Cursor tree is the adapters' input as well as Cursor's native representation; it is not the conceptual owner of project policy. Run **`make test-agent-rules`** after changing an adapter or rule frontmatter.
A scoped model rule is context, not an enforcement boundary. Hooks and plugins can be untrusted, disabled, fail open or miss an opaque write, so critical trust, security and destructive-operation constraints stay in root policy, code, permissions, tests or CI.
### Codex
Codex resolves its `AGENTS.md` chain once per session, walking from the repository root down to the launch directory, so a session started at the root never discovers nested instructions later. The project hook closes the scoped-rule gap for the events and payloads it understands:
- **`.codex/hooks.json`** wires **`.codex/hooks/attach_rules.py`** to `SessionStart` and to `PreToolUse` on `apply_patch` / `Edit` / `Write`;
- session start attaches every `alwaysApply: true` rule;
- pre-tool use extracts patch headers and structured path fields, then attaches unsent scoped rules whose scalar or YAML-list `globs` match;
- the hook parses `.mdc` frontmatter directly, needs no manually maintained index and fails open.
Codex requires explicit approval for hooks and tracks it by content hash. Run **`/hooks`** once per clone and again after either hook file changes. Project-local hooks also require a trusted `.codex/` layer. The full guide and manual probes are in **`docs/contributing/codex-hooks.md`**.
### OpenCode
The project plugin **`.opencode/plugins/project-rules.js`** injects always-on and active scoped rules into model requests, accepts scalar and YAML-list `globs`, and preserves active rules through compaction. When a write tool first reveals a scoped rule, the plugin rejects that call once so OpenCode retries with the rule in context. See **`docs/contributing/opencode-hooks.md`**.
### ZCode
**`.zcode/config.json`** wires **`.zcode/hooks/attach_rules.py`** to `SessionStart` and `PreToolUse` on `Edit` / `Write` / `MultiEdit` / `ApplyPatch`. The behavior matches the Codex adapter, but ZCode supplies edited paths as structured JSON fields. Configuration-file hooks run because the committed config sets `hooks.enabled: true`; there is no per-hook trust prompt. The command uses `python` rather than the Microsoft Store `python3` stub on Windows. See **`docs/contributing/zcode-hooks.md`**.
## Code review invariants
Every reviewer applies this section to changed files. Keep entries behavioral 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`.
- The same holds for project scheduler jobs (`<workspace>/.coddy/scheduler/*.md`): one runs, on cron or by hand, only as `scheduler.project_trust` allows, and a receipt in `<home>/scheduler-trust.json` is bound to the workspace, the job id and the file digest. Trust is decided on one `storage.JobSnapshot` and that same snapshot is what runs; an edit through Coddy re-issues a receipt only for a job that was trusted before it; no agent tool approves a project job; links under the project folder are refused and nothing the scheduler writes (the `.state` sidecar) lands in the checkout. Safe path: read with `storage.ReadSnapshot`, decide with `schedservice.Decide`, and route mutations through the session's workspace (`resolveSessionCWD`), never a raw `cwd`. Design: `docs/plans/scheduler-project-jobs.md`.
### 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. Step 11 of the **paired workflow rule**.
- 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.
### Docker image
- Do not give an automatic platform argument (**`TARGETOS`**, **`TARGETARCH`**, **`TARGETPLATFORM`**, **`BUILDPLATFORM`** and the rest) a default value in the **`Dockerfile`**. A default wins over the value BuildKit passes, and the **`linux/arm64`** image shipped an x86-64 binary that way for months (issue #482): Docker still labels the variant arm64, and a host with **`qemu-user-static`** even runs it. Keep the build stages on **`--platform=$BUILDPLATFORM`** with **`GOOS`** / **`GOARCH`** taken from **`TARGETOS`** / **`TARGETARCH`**, so the Go stage cross-compiles and no step runs under emulation. Safe path: **`features/docker_image_platforms.feature`** (part of **`make test`**, it reads the Dockerfile the way BuildKit resolves those arguments) and **`make check-image`**, which builds the image and reads each binary.
### 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 the **paired workflow rule**.
- 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 the **paired workflow rule**. 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 **рабочее дерево** (the **paired Russian wording rule** - [Cursor](.cursor/rules/russian-wording.mdc), [Claude Code](.claude/rules/russian-wording.md) - 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.