diff --git a/Makefile b/Makefile index df1776e..715bed4 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ MODELS ?= models.json EMBED_CONFIG := config.json EMBED_MODELS := models.json -.PHONY: all help build build-check build-summary config check run once dry-run no-mutate \ +.PHONY: all help build build-check build-summary config embed-ready check run once dry-run no-mutate \ install uninstall print-service \ test coverage vet fmt fmt-check lint staticcheck vulcheck ci tidy clean @@ -49,13 +49,25 @@ build-summary: cat "$(EMBED_MODELS)"; \ fi -build: build-check build-summary +build: embed-ready build-check build-summary go build -ldflags="-w -s" -o $(BINARY) ./cmd ## config: create config.json from config.example.json if it doesn't exist yet config: @test -f $(CONFIG) && echo "$(CONFIG) already exists" || cp config.example.json $(CONFIG) +# embed-ready: make sure the files go:embed compiles in actually exist. +# +# config.json is gitignored, so a fresh clone or a git worktree — which is +# exactly what the agent builds every run in — has no config.json for +# embedded.go to embed, and the whole module fails to compile with +# "pattern config.json: no matching files found". Every target below that +# compiles anything depends on this, so testing a clean checkout works. +.PHONY: embed-ready +embed-ready: + @test -f $(EMBED_CONFIG) || cp config.example.json $(EMBED_CONFIG) + @test -f $(EMBED_MODELS) || { echo "missing $(EMBED_MODELS), which has no example to copy from"; exit 1; } + ## check: run start-up checks (binaries, auth, config) and exit check: build $(BINARY) --config $(CONFIG) --check @@ -99,15 +111,15 @@ print-service: build $(BINARY) --print-service ## test: run the full test suite with the race detector -test: +test: embed-ready go test -race $(PKG) ## coverage: run tests and print per-function coverage -coverage: +coverage: embed-ready go test -race -coverprofile=coverage.out $(PKG) go tool cover -func=coverage.out -vet: +vet: embed-ready go vet $(PKG) fmt: @@ -124,11 +136,11 @@ fmt-check: lint: vet fmt-check ## staticcheck: run staticcheck (must be installed: go install honnef.co/go/tools/cmd/staticcheck@latest) -staticcheck: +staticcheck: embed-ready staticcheck $(PKG) ## vulcheck: run govulncheck (must be installed: go install golang.org/x/vuln/cmd/govulncheck@latest) -vulcheck: +vulcheck: embed-ready govulncheck $(PKG) ## ci: everything CI should run — lint, then the full test suite diff --git a/README.md b/README.md index 704ac50..e08e12b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ gh search issues --label agent-ready gh search prs --author - [Quick start](#quick-start) - [CLI flags](#cli-flags) - [How work is selected](#how-work-is-selected) +- [Verification](#verification) - [Lifecycle of one issue](#lifecycle-of-one-issue) - [Responding to PR comments](#responding-to-pr-comments) - [Configuration reference](#configuration-reference) @@ -193,6 +194,37 @@ a `labels_failed` run event, and reported to Discord. Discovery is parallel across repositories; within one repository, work is serial (one issue in flight at a time), controlled by `run.max_concurrent_repos`. +## Verification + +The test command is the repository's own, never a built-in assumption about it. `verify.commands` +takes an explicit per-repo command; otherwise `verify.auto_detect` reads the worktree: a `Makefile` +with a `test:` target wins (it is the repo's own opinion about how it is tested), then `go.mod`, +then a `package.json` with a `test` script, then `Cargo.toml`, then `pyproject.toml`/`pytest.ini`/ +`tox.ini`. Nothing recognisable means nothing is run. + +Three outcomes are distinguished, because they mean different things to a reviewer: + +| Outcome | Meaning | +| ------------- | ---------------------------------------------------------------------------------- | +| `passed` | the command ran and exited zero | +| `failed` | the command ran and exited non-zero — the change is suspect | +| `unavailable` | the command could not be run at all — **the environment is wrong, not the change** | +| `skipped` | no command was configured or detected | + +`unavailable` exists because the two failure modes are easy to confuse and expensive to confuse. +A daemon started by systemd inherits `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` +— which contains no language toolchain — so a repository that tests itself perfectly well from a +login shell fails with `make: go: No such file or directory`. Reporting that as "tests failed" +blames the agent's code for the operator's `PATH`. The installed systemd unit sets a `PATH` covering +the usual install locations, and `GOCACHE`/`GOMODCACHE` under `~/.agent-loop/cache` (the unit makes +`$HOME` read-only, and Go's caches default to `$HOME`). Anything beyond that goes in `verify.env`. + +**Your repository must build from a clean checkout.** The agent works in a fresh `git worktree`, so +anything gitignored is absent there. This repository learned that the hard way: `embedded.go` has +`//go:embed config.json` while `config.json` is gitignored, so a clean checkout failed to compile +with `pattern config.json: no matching files found` — every compiling `make` target now depends on +`embed-ready`, which creates it from `config.example.json`. + ## Lifecycle of one issue 1. **Discover** — `gh search issues --label