Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Agent guidance

Shared guidance for coding agents working in this repository.

## What this is

Zoomies is a self-hosted GitHub Actions runner fleet controller: one Go binary
that watches for queued jobs, starts an ephemeral runner container per job, and
destroys it when the job finishes. SQLite for state, a Svelte 5 UI embedded with
`go:embed`, no Kubernetes and no database server.

`cmd/zoomies` is the only binary. It is the controller (`zoomies controller`),
the agent (`zoomies agent`), the installer (`zoomies init`) and the CLI, chosen
by subcommand. On a single VM the controller runs an agent inside itself, so the
whole system is one process and one SQLite file.

Read [docs/architecture.md](docs/architecture.md) before changing anything
structural; it explains the shape and the reasons for it.

## Commands

```sh
make build-nogui   # build without rebuilding the UI (the fast inner loop)
make build         # build the UI and embed it (needs npm)
make test          # go test -race -count=1 ./...
make bench         # the benchmarks, six runs each; compare two runs with benchstat (BENCH, BENCH_PKGS, BENCH_COUNT narrow it)
make lint          # go vet, gofmt check, staticcheck if present, UI lint
make fmt           # gofmt + prettier
make dev           # controller with auth off, on :8080, for UI work
make ui-dev        # Vite dev server proxying /api to that controller
make test-unit     # the UI's unit tests: no browser, a few seconds
make test-ui       # Playwright against a real built binary
make openapi       # regenerate both copies of api/openapi.yaml: the embedded Go source and the UI's TypeScript client
make screenshots   # recapture docs/screenshots from the real UI, both themes (needs Pillow)
```

`go build ./...` fails on a clean checkout: `internal/api` embeds
`internal/api/webdist`, which is a build product. Run `make build-nogui` once,
it writes a placeholder, before any plain Go command (`go build`, `go vet`,
`go test`) that compiles that package. The `make` targets that run Go (`test`,
`test-short`, `bench`, `cover`, `lint`, `generate`) write the placeholder
themselves, and never over a real UI build. CI does the same thing as its first
step.

Go 1.27.1 or later (`go.mod` sets the floor; CI builds with 1.27.2), Node 22 or
later. Node is a build-time dependency only; the shipped binary is
self-contained and static (`CGO_ENABLED=0`, pure-Go SQLite).

Run a single test with `go test -run TestName ./internal/pkg/`. `make test-e2e`
needs real GitHub credentials and skips itself without them.

A change made for speed is held to a number: run `make bench` before and after
(on an otherwise idle machine, once per package, `tee`d to files) and put the
`benchstat` comparison in the commit message. `CONTRIBUTING.md` has the
commands. A benchmark checks that its scenario is the one its name describes
before it times anything; keep that when adding one.

## Package boundaries

These are load-bearing. Code that crosses them is the main thing to catch in
review.

| Package | Rule |
| --- | --- |
| `internal/store` | The **only** place SQL is written. Domain types, embedded migrations, every query. No other package imports `database/sql`. |
| `internal/scheduler` | **Pure.** `Decide` takes a snapshot and returns a `Plan`. No clock reads, no database, no network; that is what makes scaling behaviour testable, and it is where every decision's operator-facing *reason string* comes from. |
| `internal/updates` | **Pure.** `Choose` takes a list of releases, the mode, the soak and the instant, and picks the release the mode would take and says why. It imports the standard library and `internal/version` and reads no clock; `purity_test.go` enforces both. `Decide`, the planner for automatic updates, has joined it: it takes a snapshot and returns the actions and the sentence the status shows, under the same rules. |
| `internal/updates/channel` | The impure half of `internal/updates`: finds the update folder, writes a request into it and reads what the helper left. It is the only place the service touches that folder, and it never creates it (the installer does, with the service account's ownership). |
| `internal/api` | Transport only. A handler reads a request, asks the controller / auth / store, and renders the shape `api/openapi.yaml` promises. It has no opinions about the fleet. The resource views themselves (`HostView`, `PoolView`, …) are `internal/controller/views.go` types the handlers alias, because the event stream renders the same JSON and is fed from the controller. |
| `internal/provider` | The infrastructure-provider contract, a fake that obeys it, and `RunContractTests`, the conformance suite every provider passes. It may import `internal/store`'s domain types and `config.Finding` and **nothing else**; two tests enforce that, and that it names nothing provider-specific. Renting a machine is not placing a runner: this is separate from `internal/backend` on purpose. |
| `internal/cacheservice` | **Stdlib only.** The host-local Actions cache protocol (`actions/cache`) and nothing about the fleet: no store, no config, no controller, no clock of its own. The agent decides where and when it runs. `purity_test.go` enforces the imports. Every failure it cannot answer is a miss, never a failed build. |
| `internal/provider/proxmox` | The first provider. The Proxmox VE API is hand-rolled `net/http` for the same reason the Docker one is. |
| `internal/controller` | Wiring: the reconcile loop, the machine loop, webhook ingest, the agent task queue, the log relay, and every payload the event stream carries (`views.go`, `derived.go`). The machine loop is **not** part of `Reconcile`: a clone takes minutes and `reconcileMu` is held for a whole scheduling pass. |
| `internal/config` | `zoomies.yaml` + `ZOOMIES_*` overrides, and the validator. |
| `internal/github` | App auth, JIT configs, webhook validation, the fallback poller, and `fake.go`, a fake GitHub used by tests. |
| `internal/backend` | Docker, Podman, bare process. The Docker API is hand-rolled `net/http` against the Engine API on purpose (see below). |
| `internal/agent` | The runner-executing half and its outbound transport. |
| `internal/mcp` | The MCP tools and both transports (stdio for `zoomies mcp`, Streamable HTTP for `/mcp`). A tool calls the REST API through the `mcp.API` interface and nothing else (no store, no controller), so it can never do what the caller's token could not. |

The rules that can be stated as an import are held by a test, not by review
alone: `internal/scheduler`, `internal/updates` and `internal/provider` each have
a purity or boundary test, `internal/mcp` may import only what its test lists,
and a test in `internal/store` fails if any other package's shipped code imports
`database/sql` or the SQLite driver. A test that fails on one of these names the
rule; when the import is a pure helper that belongs, add it to that test's
allowlist with a reason rather than loosening the rule.

Other invariants worth knowing before you edit:

* **The controller never dials an agent.** Agents connect outbound only (long-poll
  for tasks, POST results), so a host behind NAT needs no inbound rule. That is
  why log streaming is inverted: the controller queues a `stream_logs` task and
  the agent opens a chunked POST that gets relayed to the browser's SSE stream.
* **Webhook deliveries are at-least-once and can arrive out of order.** The jobs
  upsert refuses to move a job backwards through its lifecycle. Keep it that way.
* **Every `*.updated` event is the resource's `GET` shape.** The UI drops a frame
  straight into its cache, so a `host.updated` carrying a bare store row (no
  `healthy`, no `free`) repaints the host wrong. Publish through the
  controller's `publish*`/`Publish*` helpers, which render the view; never put a
  store row on the bus. `stats`, `problems.updated` and each host's view are
  computed by the pass and the housekeeping tick and sent only when they change,
  so a new kind of problem (or a host whose slots or heartbeat moved with no row
  written) needs no publish call of its own. They are paced (`publishDerivedPaced`
  leaves them alone for five seconds at least, longer when they are dear, and a
  pass it turns away sets a timer that nudges the scheduler when the gap is over)
  and each is worked out only if some subscriber asked for its kind
  (`Bus.Wants`); what an operator just did calls `publishDerived`, which is
  neither, or nudges with `NudgeOperator` (as every handler that edits something
  does, and `UpdateConfig`), which skips the pacing once. A handler that edits
  something and calls plain `Nudge` leaves the problems bell and the tiles
  standing still for the rest of the gap.
Read the scoped guidance when working in these directories:

* [`internal/store/AGENTS.md`](internal/store/AGENTS.md): one writer, the runner state machine, sentinel errors, IDs.
* [`internal/config/AGENTS.md`](internal/config/AGENTS.md): configuration: settings live in the database.
* [`web/AGENTS.md`](web/AGENTS.md): the UI: tokens, status colours, no new libraries.
* [`internal/hosttune/AGENTS.md`](internal/hosttune/AGENTS.md): host health and tuning: consent, never tune from an upgrade, never restart Docker under work.
* [`.github/AGENTS.md`](.github/AGENTS.md): "Things CI will fail you on": the files CI diffs against their sources.

## Dependencies

Every dependency carries a one-line justification in
[docs/dependencies.md](docs/dependencies.md), and that is enforced by review.
Before adding one: could the standard library or fifty lines of our own do it?
Is it maintained? What does it pull in transitively? What does it cost against
the shell budget if it ships to the browser? Then add the row, in the same
voice, *why*, not *what*.

Some omissions are deliberate and documented there, notably
`github.com/docker/docker` (the hand-rolled Engine API client in
`internal/backend/dockerapi.go` is a few hundred lines and gets Podman almost
free, because `podman.sock` speaks the same protocol).

## Testing

Table-driven and standard-library `testing` throughout; no assertion framework.
Tests use `:memory:` SQLite stores, injected clocks (`store.Options.Now`), and
the fake GitHub in `internal/github/fake.go` rather than network calls. Helper
constructors take `t` and call `t.Helper()` / `t.Cleanup`. Coverage is broad
(roughly one test file for every source file) so a new behaviour is expected to
arrive with one.

Test names are sentences about the behaviour, not the method
(`TestOpenCreatesTheDatabaseDirectory`), and a test often carries a comment
explaining *why the behaviour matters*, not what the code does.

## Voice

The prose in this repository (comments, docs, error messages, commit messages)
has a consistent voice, and matching it is part of a change looking finished.

* **British spelling** in prose: *behaviour*, *authorisation*, *organisation*,
  *utilisation*, *licence*. (JSON field names mirroring GitHub's API stay
  American: `organization` in a webhook payload is correct as-is.)
* **Comments explain why, not what.** The interesting ones name the failure mode
  being designed out, or the trade-off taken. Follow that; do not narrate code.
* **Error and warning messages are written for a person to act on.** An operator
  who gets a 403 should be told which role they are missing. Findings say what
  to change; API errors carry a human message and a stable code.
* **No em dashes.** Do not write `—` anywhere in this repository: prose,
  docs, comments, error messages, UI strings, commit messages or pull request
  text. Use a comma, a colon, a semicolon or parentheses, or split the sentence.
  Do not use `--` as a stand-in either. `internal/docs/no_em_dash_test.go`
  fails the build on a spaced em dash, and `internal/docs/double_hyphen_test.go`
  on a file that gains a spaced `--`. Older code comments still carry about six
  thousand of the latter, and they go as their lines are rewritten, not in one
  sweep that would bury every file's history. The one exception is the UI's
  placeholder for an absent value, which is two hyphens: write it as `NO_VALUE`
  from `web/src/lib/format.ts`, never as a dash of your own;
  `web/unit/absent-value.test.ts` guards it.
* **Diagrams are Mermaid**, in a `mermaid` fenced block beside the prose they
  explain, never ASCII art. The site renders them (`mkdocs.yml` registers the
  fence) and so does GitHub, so a diagram lives in the Markdown it belongs to and
  there is no exported image to go stale. Do not colour one by hand: Material
  themes it for light and dark, and a hard-coded fill is wrong in one of them.
* **Commit messages are imperative sentences in plain prose**, sentence case, no
  Conventional Commits prefix: *"Say why a pool has no host, and let a host say
  it can run again"*, *"Make both halves of 'no capacity' something an operator
  can do"*.

## Layout

```text
cmd/zoomies         the binary: controller, agent, gateway, init, CLI
internal/store      domain model, SQLite schema, every query
internal/config     zoomies.yaml + env, and the validator that warns
internal/scheduler  pure scaling decisions and label matching
internal/updates    which release an update mode would take, and why; pure
internal/updates/channel  the update folder: the service's side of the helper's files
internal/github     App auth, JIT configs, webhooks, the fallback poller
internal/backend    Docker, Podman and bare-process runner backends
internal/auth       identity, RBAC, tokens, audit, OIDC
internal/api        REST, SSE, metrics, and the embedded UI
internal/provider   the infrastructure-provider contract, its fake, and Proxmox
internal/cacheservice the Actions cache protocol, served from a host's own disk
internal/registry   what a container registry says a tag points at, for the digest-pinning generators
internal/controller the reconcile loop, the machine loop and the agent task queue
internal/agent      the runner-executing half
internal/gateway    zoomies gateway: a private provider's end of the tunnel
internal/installer  zoomies init / uninstall / agent join, and the unit,
                    compose and env templates they write
internal/backup     one copy of the database: taking, listing, verifying, archiving,
                    encrypting, shipping it to an S3-compatible remote and restoring it,
                    shared by the CLI, the scheduler and the API
internal/cryptox    AES-256-GCM at rest, argon2id, token hashing
internal/events     in-process pub/sub that the SSE endpoint fans out
internal/migrate    rewriting workflows' runs-on lines
internal/mcp        the fleet over MCP: tools, stdio and Streamable HTTP
internal/aicontext  commit-pinned repository context, independent of the fleet
internal/agentguidance  inspects a repository's agent instruction files without running them
internal/analysis   evidence-backed findings computed from explicit snapshots
internal/assistant  the model-provider contract behind the assistant, and its adapters
internal/catalog    the one machine-readable list of problem codes and Kennel Club checks
internal/cliconfig  ~/.config/zoomies/cli.yaml: where the CLI finds its controller
internal/conflictresolve  asks a model to resolve the text conflicts a merge worker found; edits only those files
internal/docs       tests that hold the documentation to the code it describes; no source of its own
internal/hosttune   host health and tuning: detection that never writes, and approved changes
internal/kennel     pure evaluation of what is wrong with a repository's workflows
internal/logging    the process-wide structured logger
internal/machine    which host this process runs on: distribution, release, capacity
internal/naming     the grammar for pool, host and runner names
internal/optimisation  reviewable proposals computed from evidence handed in; reads no source and calls no model
internal/provenance keeps third-party names out of the tree
internal/proxmoxsetup  connecting a Proxmox host: its own principal, binary, service and state
internal/prrepair   bounds what the model may edit when it proposes a pull request
internal/gitmerge   the bounded, isolated three-way merge for Eli conflict repair
internal/gitbare    a bare repository built from a commit's archive, so a controller without Git can hand the worker one
internal/gitfetch   the three pinned commits fetched into that repository, with the tree check and the cleanup
internal/qr         the smallest QR encoder that serves the two-step verification picture
internal/redact     strips credentials and commit emails from text bound for a model
internal/releasekey the release signing key: Ed25519 verification of a release's checksums
internal/toolscan   reads workflows for the toolchains their jobs install
internal/terminalui terminal presentation: the CLI home, paw indicator and command progress
internal/version    build metadata stamped in at link time
internal/wsl        keeps an agent alive inside a WSL2 distro
web/                the Svelte 5 UI
test/e2e            the Docker end-to-end test, behind the `e2e` build tag
api/openapi.yaml    the API contract both clients are generated from
deploy/             the controller and runner images, and the runner entrypoint
design/             the Mission Control style review: concept images and their validation
docker-compose.yml  the compose deployment, at the root so it is the one people find
docs/               the zoomies.sh site, built by mkdocs.yml
overrides/          the site's theme overrides: sharing tags, structured data, sitemap,
                    the header's repository facts
hooks/              the site's build-time metadata: git dates, llms.txt, and the
                    latest release and star count the header shows
site-worker/        the edge that serves zoomies.sh: the built site as the
                    worker's static assets, content negotiation for agents,
                    the discovery mirrors and every response header the
                    assets pipeline cannot set
weekly-seo/         the weekly search check for zoomies.sh: its config, notes and
                    the report and memory files the .claude/agents/weekly-seo.md
                    agent writes
ROADMAP.md          the follow-on roadmap, and the decisions it asks the owner to take
roadmap/            what supports it: the work-package record, decision records,
                    gate evidence, dated plans, the model guidance, the source
                    document and an archive of finished ones; its README says which
                    document answers which question
skills/             what a coding agent installs: the zoomies skill, with a command
                    reference generated from the binary
plans/              the improve advisor's output: prioritised, self-contained
                    implementation plans and their index, each stamped with the
                    commit it was written against; executors update the status
                    table in plans/README.md
install.sh          the one-line installer, served from the site root
nixpacks.toml       the build a Nixpacks-based PaaS runs; see docs/paas.md
```

## Instruction hierarchy

`AGENTS.md` contains shared engineering guidance. Claude imports it through
`CLAUDE.md`. Scoped `AGENTS.md` files add detail beside the code; their Claude
wrappers import the same rules. Fix contradictions rather than relying on load
order. Keep entry points concise and put retrieval detail in the context guide.

## Host health implementation

Follow the consent rules in `internal/hosttune/AGENTS.md` and
`docs/host-health.md`. Upgrades are doctor-only. Container health comes from
the native host binary; no privileged container mounts. All host-tuning tests
use injected filesystems and commands. Preserve reversal records on uninstall.

<!-- zoomies-ai-context:start -->

## Zoomies AI Context

Before using prepared repository context, read `.zoomies/AI_CONTEXT.md`. It explains available access routes, revision checks and selective retrieval.

<!-- zoomies-ai-context:end -->

## Agent skills

### Eli Repair implementation

Before changing Eli PR repairs, read
[`roadmap/eli-repair-implementation-handoff.md`](roadmap/eli-repair-implementation-handoff.md).
Every PR for this work must update that file in the same PR: record the actual
scope, implementation status, validation results, PR link and next unfinished
step. Keep planned capabilities distinct from shipped ones. A new session must
be able to continue from the file without the chat history.

### Issue tracker

Issues live in GitHub Issues for eyupio/zoomies, driven by the `gh` CLI. See `docs/agents/issue-tracker.md`.

### Domain docs

Single-context: one `GLOSSARY.md` at the repo root, and the numbered decision records in `roadmap/decisions/`. See `docs/agents/domain.md`.