# AGENT Instructions for veans Things to know before touching this submodule that aren't obvious from reading the code. The parent repo's `CLAUDE.md` covers the rest of Vikunja; this file is veans-specific. ## Module layout - `veans/` is its own Go module (`code.vikunja.io/veans`), separate from the parent. Don't try to import `code.vikunja.io/api/...` — that pulls XORM into the CLI binary. Wire types live in `internal/client/types.go` as plain JSON-tagged structs that mirror the parent models. - License headers are enforced by `goheader` in `veans/.golangci.yml`. Every new `.go` file needs the AGPLv3 banner from `veans/code-header-template.txt` (a copy of the parent's, kept local so the linter resolves the path relative to this module). ## Building and testing - `mage build` → `./veans` binary. The `Aliases` map in `magefile.go` routes bare names like `mage test` to `Test.All` — without aliases, mage rejects namespace invocations ("Unknown target specified"). - Unit tests: `mage test` (passes `-short`) or `go test -short ./...`. The e2e package's `TestMain` gates the suite on `-short`, mirroring the parent monorepo's `pkg/webtests` convention. Without `-short` and without `VEANS_E2E_API_URL` set, the e2e tests fail loudly with a "configure or pass -short" hint. - E2e tests: `mage test:e2e` (no `-short`). Assumes an externally- running Vikunja at `VEANS_E2E_API_URL`. The harness seeds its own admin user via `PATCH /api/v1/test/users` — same mechanism the playwright suite uses — so the API must be booted with `VIKUNJA_SERVICE_TESTINGTOKEN=` and the same value passed in via `VEANS_E2E_TESTING_TOKEN`. Alternative path: `VEANS_E2E_ADMIN_TOKEN=` skips the seed and uses the given token as-is, for driving a long-lived Vikunja the suite shouldn't mutate user rows on. - Local e2e loop: from the parent repo root, build the API (`mage build:build`), run it with sqlite-memory + a known JWT secret + `VIKUNJA_SERVICE_TESTINGTOKEN`, then `mage test:e2e` from `veans/` with `VEANS_E2E_API_URL` + `VEANS_E2E_TESTING_TOKEN`. No manual seeding step — the test harness handles it. - CI: the `test-veans-e2e` job in `.github/workflows/test.yml` consumes the existing `vikunja_bin` artifact from `api-build`; don't recompile the API in a parallel workflow. The `veans-test` job runs unit tests with `-short` for fast feedback, independent of `api-build`. ## Vikunja wire-format gotchas veans targets the Huma-backed **`/api/v2`** exclusively (`apiBasePath` in `internal/client/client.go`). v1 is frozen, and the kanban-bucket CRUD veans relies on only exists on v2. Most failures surface when crossing the JSON boundary. The list below is what's bitten me; if a new endpoint behaves oddly, suspect one of these: - **Lists come wrapped in the standard envelope.** Every v2 list returns `{"items":[...],"total":N,"page":N,"per_page":N,"total_pages":N}`, not a bare array, and there is no `x-pagination-total-pages` header anymore. Decode with the generic `Paginated[T]` helper. **Most lists are server-paginated** — their model's `ReadAll` applies a 50-item page limit: tasks, projects, labels, comments and bots. Page through those with `doListAll` until `page >= total_pages`; returning only page 1 silently truncates (>50 comments on a task is realistic). **Buckets and project views are the exception**: their `ReadAll` takes `_ int, _ int` and returns every row in one page, so fetch them with a single `doList` and unwrap `.items` — paging those would re-fetch the full set and duplicate it. Single-object responses (create/update/read of one entity) stay UNWRAPPED. - **v2 flips the create/update verbs.** Creates are **POST** (v1 used PUT): projects, labels, tokens, bot users, project shares, task create, comments, relations, assignees, label-attach, bucket create. Task update is **PATCH** (see below). The bucket-task move is **PUT**. - **Task update is `PATCH /tasks/{id}` with `application/merge-patch+json`** (`client.DoMerge` → `UpdateTask(*TaskPatch)`). Only the fields present in the body are written; absent fields are left intact. Build the body from `TaskPatch` (pointer fields, omitempty) — never a whole `client.Task`, whose no-omitempty `done`/`title` would clobber those columns on every call (this was issue #2962). - **List search is `q`**, not v1's `s` (`ListParams.Q`). Task-list `filter`/`expand`/`page`/`per_page` keep their names. - **`ProjectView.view_kind` and `bucket_configuration_mode` are strings**, not ints. The parent enums (`ProjectViewKind`, `BucketConfigurationModeKind`) have custom `MarshalJSON` that emits `"kanban"` / `"manual"` etc. Use the string constants in `internal/client/types.go`. - **`Task.BucketID` is always 0** in `GET /tasks/:id`. The model has `xorm:"-"` on it — the actual bucket lives in a separate `task_buckets` table. Fetch with `?expand=buckets` and use `task.CurrentBucketID(viewID)` to read it. - **Task updates do NOT move tasks between buckets.** The task↔bucket relation is row-shaped; use `client.MoveTaskToBucket()` which hits **`PUT /projects/{p}/views/{v}/buckets/{b}/tasks`** with a `{"task_id":N}` body (project/view/bucket all come from the URL). The Update path on the server only auto-moves on `done` flips. - **Bot user creation is `POST /user/bots`**, not `/bots` — the routes are registered under the `/user` subgroup. Same prefix for `GET /user/bots`. - **`APIToken.expires_at` is required.** The struct field has `valid:"required"` upstream; sending it omitted or zero fails validation. Use `client.FarFuture` (year 9999) when you mean "no expiry" — the frontend does the same. - **Task descriptions and comments are HTML, not markdown.** The Vikunja web UI uses TipTap, which calls `getHTML()` on save. The stored field is therefore HTML. The agent prompt template (`internal/commands/prompt.tmpl`) teaches agents the canonical TipTap shapes — most importantly `