coddy-agent
AGENTS.md187 starsChanged 9 days agoMITView on GitHub
Short map for automation-friendly contributors.
- Installs packages
We split this AGENTS.md into the rules it gives an agent
12 rules. About 0 are common, 2 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 44
Run make deb / make rpm for the Linux packages (PKG_ARCHS, PKG_TAGS, DIST_DIR knobs.
- Distinctive: line 97
write the draft first, then run it through the skills.
- Very specific: line 130
Every reviewer applies this section to changed files.
- Very specific: line 134
Do not change routes, request or response shapes, or status codes in external/httpserver/server.go without updating …
- Very specific: line 156
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 …
- Very specific: line 168
Do not commit screenshots or videos that no page references, and do not put pull request evidence under docs/assets/.
- Very specific: line 169
Do not edit inside a <!-- docsgen:*:start --> block by hand.
- Very specific: line 173
Do not treat a UI change as complete when only external/ui/src/ moved.
- Distinctive: line 177
When changing behavior for the OpenAI-compatible HTTP gateway or bundled UI:
- Very specific: line 180
If the external HTTP surface changes, update external/httpserver/openapi.go so the served OpenAPI matches handlers in …
- Very specific: line 181
Keep docs/reference/http-api.md aligned with the live behavior.
- Very specific: line 184
Run make test (the express run over the shipped tag set), then make lint.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 exporter's too, with a warning when it would be bound off loopback with no token), 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, Auxiliary for a subsystem that serves beside the surfaces and is never the only one (KindMetrics, the exporter: it does not count as enabled and may always stop); 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, shutdown in reverse declaration order so intake stops before the API, and Supervisor.Status, the live table Runtime.Status reads), the status snapshot (status.go: the Snapshot a running process reads off itself - its build, the subsystem rows with their state, running, starting, stopped or retained, RestartRequired derived when read and never recorded, the turns running and the oldest one's start, the unfinished background tasks of the model by kind and status, the disk of the session store, the prompts that wait for a person, the provider quota windows and the swarm state - each part read on its own from Supervisor.Status, bgtask.Pool.CountUnfinished, session.Manager.OldestTurnStartedAt, platform.DiskFree, agent.PendingPrompts, session.Manager.CachedProviderUsage (the usage cache only, never a request) and swarm.Joins / swarm.RelayNodes, a part with no source left out; the exporter reads one per scrape), the status document (status_page.go: Runtime.StatusDocument / RenderStatus, what GET /coddy/status answers and Settings → System draws - the build, how the process started, the subsystems with their addresses, the disk, the activity and the attention list decided in Go, with the exporter's last hour through SetMetricsSource in a binary built with metrics), 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 keyed by the project (mcp.TrustWorkspace: a linked git worktree shares its main checkout's), 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). Client.CallToolResult returns what a result carries: its text (text blocks, the text of embedded resources, resource links), its pictures (ToolImage) and its files (ToolFile: audio, binary resources), which internal/agent keeps with the session's assets. 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 answering with JSON (NewHTTPServer) or with event streams (NewEventStreamServer) and SSE - each answering one tool with its token, and all but the npx one a screenshot tool too (ScreenshotTool: the token and Picture, LargePicture over 4 MiB as base64, or several, Pictures). 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, a project server that does not name ${CWD} one per project (a linked git worktree shares the one of its main checkout) and any server 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/providerlogin | Signing a provider in without a key: Run (the device flows of NeuralDeep, Codex and OpenCode, Devin's browser sign-in, every request through the row's proxy, then the catalog llm.Apply*LoginToConfig adds to config.yaml), the resolution and endpoint rules coddy providers login shares (Resolve, NeuralDeepAPIBase, OpenCodeAPIBase, SummarizeAdded), and Ready, whether a provider row can answer, decided without a request or a credential helper. The console's /login (external/cli/login.go) runs it and installs the config it wrote through the manager's InstallConfig; the header points a console with no ready provider at it. Guide: docs/surfaces/console.md (Signing in to a provider). | |||
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/canvas, internal/tools/canvas | The canvas of the web UI (tools.canvas): every board is a .canvas file of the workspace (JSON Canvas 1.0, Obsidian's format) whose file nodes naming HTML pages run as live frames, shown as a tab of the Files window. board.go: Ref (the chat's bundle, its workspace, the file), Load, Show (on the named file, the chat's current one or a new canvas/<page folder>.canvas), Open, UpdateNodes, Rotate, Remove, and the chat's bookkeeping in its bundle (canvas.json, State: the current file, the generations of frame addresses and the roots wider than a page's folder, keyed by NodeKey file and node, applying only in the workspace they were written in), a subagent's pages on the canvas of the top session of its spawn tree and refused from a worktree of its own; file.go: reading a file into frames, items and edges, writing back only what changed under a per-file lock that makes one file's read-modify-write one turn across chats, FrameOf for the one frame a file request needs, FileStamp; jsoncanvas.go and obsidian_json.go: an ordered codec that keeps unknown keys and writes byte for byte as Obsidian does. The stamp of a frame's files (stamp.go), the frame capability (capability.go: an HMAC in the path naming the session, the file, the frame, its root, workspace and generation, the key derived for the canvas from the secret behind media tokens), serving a frame (serve.go: os.Root under the workspace, no links, no dot-names, the sandbox CSP, the bridge script bridge.js injected), the browser's reports in memory (reports.go, by session and NodeKey). internal/tools/canvas holds canvas_show and canvas_inspect; the routes are external/httpserver/canvas_http.go (every route names its file with ?file=), the relay's exception external/swarm (isCanvasFramePath), the board external/ui/src/ui/canvas/. A request whose Origin is null is refused with 403 on every API route but a frame's address (external/httpserver/auth.go, the relay's gate). Guide: docs/features/canvas.md, design: docs/plans/canvas.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 the project (mcp.TrustWorkspace), 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**, UserPromptSubmitandStopinto the loop,PreCompact/PostCompact into **CompactSession**, SubagentStart/SubagentStopinto the spawn runtime andNotification 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: (also @rule:<folder>/<name>) / @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 budget of characters (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 to the project a rule at the same path inside the rule folder, extension included; the same chain under every subfolder on the way down to a file a tool or a mention reaches, read on demand (Factory.Nested, issue #557), each such rule anchored at its own subfolder (Root and ScopeDir) and matched there only (MatchScoped needs the scope and the globs both), and under the one subfolder @rule:<folder>/<name> names, its .coddy/rules only (Factory.Folder, session.SubfolderRules); a rule is named by its file's path inside its rule folder (frontend/style) and @rule: addresses one tree of those names, a subproject's with the subproject in front (internal/session/rule_refs.go: the catalog first, then the deepest subproject; its completion steps down one level per query through Factory.FoldersWithRules); links in a rule folder are followed everywhere; 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. status_http.go serves GET /coddy/status (registered only when Options.Status is set, so a server without it answers 404 like an older one) and merges its OpenAPI schemas. 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. Settings → System (src/ui/settings/SystemSection.tsx, a lazy chunk, with systemStatus.ts and Sparkline.tsx; the reads and the badge in systemStatusApi.ts) draws GET /coddy/status of the server the page talks to (DESIGN.md, System tab). It installs as an app and shows system notifications from the background: the manifest, icons and service worker in public/, the notifications in src/ui/pwa/ (DESIGN.md, Installable app and notifications). 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 its icon alone at every width (the name in the tooltip), 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 with nothing over it: source coloured as one text with the chat's grammars, pictures, sound and film, a PDF in the browser's own viewer, an SVG as a picture and an HTML page in a sandboxed frame through Preview in the window's menu (src/ui/files/FilesView.tsx; DESIGN.md, Views of a chat, Workspace files window); 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. While it runs, Serve publishes its registry (swarm.PublishRelay) for the status snapshot. Shared vocabulary and the join client live untagged in internal/swarm, so coddy serve can join a relay without the tag; every running join set registers itself (internal/swarm/status.go: swarm.Joins, the relay host, kind, transport and online state of each registration, never a name or credential), and an agent registration yields to the relay of its own process (Client.yieldsToRelay): a process that runs both its API and a relay starts two join sets from one swarm.join that share the lease of one name, so the relay registers it, once, and the agent stays quiet until that relay stops. Guide: docs/operate/swarm.md. | |||
external/metrics | The Prometheus exporter coddy serve runs under metrics.enable, built with tags=metrics (in FULL_TAGS, so release binaries, image and packages carry it) and an auxiliary subsystem (serve.Subsystem.Auxiliary: never the only one, always free to stop; serve.Resolve refuses a config that enables only it). The package has no stub and cmd/coddy/metrics.go is its only importer; metrics_stub.go (no tag) keeps the descriptor, so a binary without the tag refuses metrics.enable naming the tag. collector.go: one private metrics.Set per family of github.com/VictoriaMetrics/metrics (expose.go writes HELP and TYPE in front of each, escapes label values in the series names and adds the library's Go and process families under a generic HELP), built by metricsWiring.Start right after rt.Init whenever the binary has the tag (collection spans the process; metrics.enable only opens the listener), fed by llm.AddCallObserver (internal/llm/call_observer.go: one event per provider attempt, its outcome one of llm.CallOutcomes, whether it is a retry, and the time to its first chunk with content, which Stream notes only for a caller's callback while someone observes, since a nil callback decides whether a failed stream may be retried), agent.AddToolCallObserver (internal/agent/tool_observer.go: one event per tool call, named by the built-in tool, mcp or other and never by an MCP server or tool, its outcome ok, error, refused or denied), session.Manager.AddTurnObserver (the turn edges and the duration between them) and Runtime.AddConfigObserver (a reload that leaves an exporter off loopback without a token is warned about), with the label budget of the llm families (16 providers and 64 named provider and model pairs for the life of the process, everything else other, decided when the attempt is observed, so labels are closed enums and configured names only; every admitted pair is seeded at zero); expose.go also writes the gauges from one serve.Snapshot per scrape, so a scrape asks no provider or server anything; every 15 s the Collector samples its own instrument sets the way a scrape writes them into a one-hour ring (Metrics: the providers and seven series of the status document), with no counter of its own and no scrape slot; serve.go serves GET /metrics behind the live bearer token (404 elsewhere, at most two scrapes at once across every listener of the collector), and since the Collector outlives Serve and the fingerprint is the listen address (no RestartKey), a new address rebuilds the listener in place with the counters kept. The Metrics tab of Settings is hidden without the tag (config.UISchemaJSONHiding, fed by httpserver.Options.UnbuiltSections). The alert rules and their promtool cases are examples/prometheus/, the Grafana dashboard and a Prometheus and Grafana compose stand examples/grafana/ (TestTheDashboardAndTheRulesReadOnlyExportedSeries holds both to what a scrape holds). A relay process runs an exporter of its own and does not carry its nodes'. Guide: docs/operate/monitoring.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 (a photo, a document or an album a person sends it reaches the session as the web UI's attachments do, downloaded through the Bot API with the caption as the prompt: telegram/media.go, issue #509), 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, answers a message with no text (a file on its own) that it reads text only; 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 ("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; every release commits the rendered cask to the tap coddy-project/homebrew-coddy-agent with scripts/publish-homebrew-tap.sh, and the Homebrew tap workflow, .github/workflows/homebrew.yaml, installs it with a real brew on macOS through scripts/check-homebrew-cask.sh) 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; make brew-publish and make brew-cask-check for the tap); 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; metrics.enable, auxiliary: it serves beside the others and never keeps the process up on its own) 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 metrics"). 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(setscore.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 thetoolchainline ofgo.modincluded) is what.github/workflows/security.yamlruns on pull requests, pushes to main and a weekly cron;make sec-reportscans without failing on findings. Reports and the severity summary land indist/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.yamlskip-dirs,nosemgrep) are indocs/contributing/security-scanning.md. - On commit,
.githooks/pre-commitcallsscripts/checks.sh, which runsmake lintby default:make fmt-check(gofmt -s -lover every Go file git tracks, whatever its build tags or platform),golangci-lintover the untagged tree, over every tag butuiand over the shipped tag set, so every Go file of the host platform but thehttp && !<tag>stubs ofexternal/httpserveris compiled once (.golangci.ymlenables thegofmt -sformatter there too), thentsc --noEmitover the SPA sources (make ui-typecheck); then, when the commit stages SPA sources, Prettier over the whole SPA (make ui-format-check,prettier --check .inexternal/ui,.prettierignorekeeping the build output and the vendored grammars out), which CI's Lint job runs too: the tree is Prettier-clean, andnpm run fmtinexternal/uikeeps it so. Commits touching only non-code files (docs, etc.) skip the gate. - Scope knobs:
CODDY_HOOK_TESTS=fastalso runs a quickgo test ./...,CODDY_HOOK_TESTS=fullthe expressmake test,CODDY_HOOK_TESTS=matrixevery tag combination;CODDY_HOOK_LINT=0skips the linter;CODDY_HOOK_FORMAT=0skips the Prettier check;CODDY_HOOK_SKIP=1bypasses 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-androidrunsgo vetwithGOOS=androidand 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, needsmake check-windows(cross-build plusgo vetover every non-uitag combination, test files included) andmake lint-windows. CI runs both, and additionally runs the complete Go suite (go test ./...with the shipped tag set), the rule adapters and the SPA's vitest suite on a realwindows-latestrunner (job Test suite (Windows)), so every_windows_test.gofile (internal/platform,internal/textencand the rest) runs there and every test that writes files or compares paths meets the runner's CRLF checkout. The platform packages and the console suite run on amacos-latestrunner as well (jobtest-macos), which also builds the console and drives the binary through a real pty withexamples/cli/cli_e2e_startup.py(first frame, keys, exit); theclijob of the Linux test matrix runs that pty script too, and the Telegram stand (tgfake) job builds the gateway binary with the exporter (TAGS="gateway metrics") and runsexamples/gateway/tg_e2e_offline.shwithTG_E2E_METRICS=1, which scrapes the exporter after the chat: the realcoddy serveagainst the fake Bot API and the scripted model of the tgfake release binary of the versiongo.modpins, 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, Claude Code). 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, Claude Code): 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.
Prose skills
Prose this repository publishes - the pages under docs/ in both languages, DESIGN.md, the dictionaries under external/ui/src/ui/i18n/messages/, commit messages, pull request and issue text, review replies - is often drafted by a coding agent, and an agent's draft carries the patterns of machine-written text: stock openers, binary contrasts, em dashes, officialese, calques from English in Russian. Whoever works on this repository with an agent installs these four skills and runs a draft through them before it is final:
| Skill | What it checks | Applies to |
| stop-slop | the patterns of machine-written prose | any prose |
| humanizer-ru | the same for Russian: officialese, calques, filler, translationese | Russian text |
| ru-text | Russian info style, editing and interface copy | Russian text, the ru dictionary |
| logika | whether the claims of a text agree with each other and follow from what it says | any text that explains or argues |
The universal skills CLI installs each one where the agents on the machine read skills:
npx skills add hardikpandya/stop-slop
npx skills add ilyautov/humanizer-ru
npx skills add talkstream/ru-text
npx skills add EvilFreelancer/logikaWithout it, clone each repository into your agent's skill folder under the skill's name: ~/.claude/skills/<name> (Claude Code), ~/.codex/skills/<name> (Codex), ~/.cursor/skills/<name> (Cursor), ~/.agents/skills/<name> or ${CODDY_HOME:-~/.coddy}/skills/<name> (Coddy). Coddy also installs logika by itself with coddy skills sync, from the rpa-skills catalog it ships as a built-in source.
How they apply here:
- write the draft first, then run it through the skills; humanize only what the agent wrote, never a quotation, the operator's own wording or literal text (code, keys, commands, the labels a page takes from the dictionaries);
- documentation and interface copy are technical text for humanizer-ru: fix the critical patterns and the calques, keep the structure, the terms and the format;
- where a skill and this repository disagree, the repository wins: typography follows the paired documentation translation rule (a hyphen with spaces, never an em or en dash; straight quotes, never the guillemets ru-text would put in), the glossary there and the paired Russian wording rule hold, and a translation still says what its English page says.
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.jsonwires.codex/hooks/attach_rules.pytoSessionStartand toPreToolUseonapply_patch/Edit/Write;- session start attaches every
alwaysApply: truerule; - pre-tool use extracts patch headers and structured path fields, then attaches unsent scoped rules whose scalar or YAML-list
globsmatch; - the hook parses
.mdcfrontmatter 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.gowithout updatingexternal/httpserver/openapi.goin 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 reconciledocs/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 throughllm.HTTPClientForProviderProxy; a test-only explicit client may be accepted, butnilmust return an error rather than fall back tohttp.DefaultClient. When a Settings sign-in starts before Save, apply the form's current proxy after resolving the row. Safe path: extendfeatures/provider_proxy.feature, keepTestProviderProxyGuardgreen, 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, orgatewaytags. Plainmake buildmust 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 throughTrustGateininternal/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 throughTrustStore. - The same holds for project-scope subagent definitions (
.agents/agentsand.coddy/agentsunder the workspace, or anysubagents.dirsentry there): they load and spawn only assubagents.project_trustallows, a receipt insubagents.TrustStoreis bound to the file digest, and a definition must never widen what the parent could do -permission_mode,toolsanddisallowed_toolsonly narrow, and the mandatory exclusions stay. Safe path: resolve the definition once, decide trust on that value withsubagents.Decide, and derive the child's tool set withsubagents.EffectiveTools. - The same holds for project scheduler jobs (
<workspace>/.coddy/scheduler/*.md): one runs, on cron or by hand, only asscheduler.project_trustallows, and a receipt in<home>/scheduler-trust.jsonis bound to the project (mcp.TrustWorkspace), the job id and the file digest. Trust is decided on onestorage.JobSnapshotand 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.statesidecar) lands in the checkout. Safe path: read withstorage.ReadSnapshot, decide withschedservice.Decide, and route mutations through the session's workspace (resolveSessionCWD), never a rawcwd. Design:docs/plans/scheduler-project-jobs.md. - A linked git worktree shares what its project decided, never more. Every trust store keys its receipts, and
mcp-overrides.jsonits switches, bymcp.TrustWorkspace: the canonical workspace, or for a folder of a linked worktree the same folder of its main checkout, whichgitws.MainCheckoutFolderanswers only when the repository, read where its storage really is, names the worktree back, the repository is not bare and the answer stays inside the main checkout once links are resolved, outside any repository nested there (a.gitfile copied into another folder, a.gitthat is a link, an archive that brings storage of its own or a link out of the main checkout lends nothing). It reads only regular files, each up to a small limit, so a FIFO or a device named.gitnever blocks a trust decision. The digest still binds, so what a branch adds or changes asks again, and each checkout keeps its own receipt under the shared key (Checkout,mcp.ReceiptCheckout):Approvereplaces only its checkout's receipt, an implicit revoke (a deleted or renamed scheduler job, a dropped marketplace entry) takes only its own, and an explicit untrust takes the project's. A project MCP server that does not name${CWD}has one pool entry for the project's checkouts; the scheduler scans projects (ScanWorkspacesand the registry go throughmcp.TrustWorkspace), so a job never runs on cron from a worktree. Paths stay on the real workspace: where a file is read, what a hook file is relative to, where a job file and its state live. Safe path: key a new store bymcp.TrustWorkspaceinside the store, never by a raw or merely canonical path. Design:docs/plans/worktree-shared-project-config.md.
Packaging
- Do not add, rename or remove a CLI subcommand, a
serveverb or a flag without updatingpackaging/man/coddy.1andpackaging/completions/coddy.bash/coddy.zshin the same change (andtopLevelCommandsincmd/coddy/usage_test.gofor a command). Every install route ships those files as the documentation of the command set - the release archive throughinstall.sh, the Homebrew cask andcoddy update, the packages, the formula - and nothing generates them: the test holds the commands and theserveverbs toprintUsage, flags are kept in step by hand. Step 11 of the paired workflow rule. - Do not move
/usr/bin/coddyor add a second packaged copy of the binary.coddy updateasksdpkg-query -S/rpm -qfwho owns the executable it is about to replace, and a packaged file that no database claims would be silently overwritten. Safe path: keep the singlecontentsentry inpackaging/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 withcoddy serve install/coddy serve uninstall. The unit is rendered byserve.PackagedUnitFile(): change the renderer and regeneratepackaging/systemd/coddy.service, never the file alone. - Do not add a file to the release
.tar.gzlayout without checking the Homebrew cask:packaging/homebrew/coddy.rb.tmpllinks 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. Itstest doblock is coupled the same way: it asserts on the output ofcoddy -v,coddy sessions list(theSESSION_IDheader) andcoddy skills list(theSearch roots:line), and once the formula is in homebrew/core those strings are checked by Homebrew's CI, not ours. - Do not document, script or test a Homebrew install from a URL or a file path (
brew install --cask <URL>,brew install --cask ./coddy.rb): current Homebrew loads a cask only out of a tap and refuses both outsideHOMEBREW_DEVELOPERmode (issue #269). The route is the tap,brew install coddy-project/coddy-agent/coddy. ItsCasks/coddy.rbis generated: changepackaging/homebrew/coddy.rb.tmpl, never the tap, and let the release publish it. A template change is proven by the Homebrew tap workflow installing it on macOS with current Homebrew, not byruby -calone: Homebrew 7 warns on stanzas that still parse (verified:, Ruby flight blocks, completions linked as abinary), andfeatures/homebrew_tap.featureholds the rendered cask to that list. - Do not conflate the two Homebrew artefacts. The cask installs prebuilt macOS archives and is upgraded with
brew upgrade --cask coddyout of aCaskroom; the formula builds from source and is upgraded withbrew upgrade coddyout of aCellar.internal/updatereads 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,BUILDPLATFORMand the rest) a default value in theDockerfile. A default wins over the value BuildKit passes, and thelinux/arm64image shipped an x86-64 binary that way for months (issue #482): Docker still labels the variant arm64, and a host withqemu-user-staticeven runs it. Keep the build stages on--platform=$BUILDPLATFORMwithGOOS/GOARCHtaken fromTARGETOS/TARGETARCH, so the Go stage cross-compiles and no step runs under emulation. Safe path:features/docker_image_platforms.feature(part ofmake test, it reads the Dockerfile the way BuildKit resolves those arguments) andmake 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 underfeatures/,surfaces/oroperate/, its entry indocs/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-checkcatches 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, thenmake 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 linksmake docs-checkreports 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 runmake docs.
Embedded UI
- Do not treat a UI change as complete when only
external/ui/src/moved. The binary servesgo:embedassets, so unrebuilt sources ship the previous SPA. Safe path: runmake 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, notfeatures/. See the paired workflow rule. - If the external HTTP surface changes, update
external/httpserver/openapi.goso the served OpenAPI matches handlers inexternal/httpserver/server.go. - Keep
docs/reference/http-api.mdaligned with the live behavior. - For UI changes, update sources under
external/ui/src/and rebuild embedded assets viamake 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), thenmake 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; every row of GET /v1/models but the agent / plan / ask profiles, told apart by their id: a provider row may itself be named coddy). 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, Claude Code - 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- runsmake build TAGS="http scheduler memory"(overrideTAGSas needed) and printscoddy -v.examples/test_acp.sh- wrapper that runsexamples/acp/test_acp.sh(all ACPacp_*.pydemos in one pass).examples/test_httpserver.sh- wrapper that runsexamples/httpserver/test_httpserver.sh(tempcoddy serve, all HTTP harnesses includinghttp_e2e_scheduler_apiandhttp_e2e_scheduler_agent).
Example HTTP scripts that call completion endpoints expect a reachable provider and return non-zero on HTTP errors.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository31 other files it gives its agents.
- .cursor/rules/acp-protocol.mdcCursor rule
- .cursor/rules/agent-model-choice.mdcCursor rule
- .cursor/rules/api-layer.mdcCursor rule
- .cursor/rules/architecture.mdcCursor rule
- .cursor/rules/build-tags.mdcCursor rule
- .cursor/rules/code-style.mdcCursor rule
- .cursor/rules/config-roundtrip.mdcCursor rule
- .cursor/rules/console-tui.mdcCursor rule
- .cursor/rules/core-modules.mdcCursor rule
- .cursor/rules/docs-translation.mdcCursor rule
- .cursor/rules/gateway.mdcCursor rule
- .cursor/rules/implementation-order.mdcCursor rule
- .cursor/rules/llm-streaming.mdcCursor rule
- .cursor/rules/logger-components.mdcCursor rule
- .cursor/rules/merged-branch-cleanup.mdcCursor rule
- .cursor/rules/process-lifecycle.mdcCursor rule
- .cursor/rules/provider-auth.mdcCursor rule
- .cursor/rules/provider-devin.mdcCursor rule
- .cursor/rules/provider-proxy.mdcCursor rule
- .cursor/rules/russian-wording.mdcCursor rule
- .cursor/rules/session-storage.mdcCursor rule
- .cursor/rules/skills-delivery.mdcCursor rule
- .cursor/rules/testing.mdcCursor rule
- .cursor/rules/text-encoding.mdcCursor rule
- .cursor/rules/tool-images.mdcCursor rule
- .cursor/rules/turn-admission.mdcCursor rule
- .cursor/rules/ui-assets.mdcCursor rule
- .cursor/rules/ui-spa.mdcCursor rule
- .cursor/rules/ui-verification.mdcCursor rule
- .cursor/rules/workflow.mdcCursor rule
- .cursor/rules/workspace-identity.mdcCursor rule
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

