docs(veans): AGENTS.md cheat sheet for coding agents

Captures the non-obvious things an agent will hit working on this
submodule:

- Wire-format quirks (view_kind/bucket_configuration_mode are JSON
  strings; Task.BucketID is always 0 in GET — use ?expand=buckets and
  CurrentBucketID; POST /tasks doesn't move buckets, use the dedicated
  bucket-tasks endpoint; bot creation is at /user/bots; APIToken
  expires_at is required, use FarFuture for "no expiry").
- Permission discovery via /routes (group names are path-derived; use
  PermissionsForBot at runtime instead of hard-coding).
- OAuth shape (PKCE/S256 mandatory, no client registration, JSON-only
  token exchange, loopback redirect via 127.0.0.1:0, Shutdown uses
  context.WithoutCancel to drain on outer cancel).
- Credential chain order + per-test HOME/XDG override.
- Identifier validation (runelength only) + base-36 timestamp suffix
  trick for unique e2e identifiers.
- mage Aliases map (without it, `mage test` rejects the namespace).
- License-header enforcement via local .golangci.yml + code-header-
  template.txt copy.
- Things to actively avoid: bare exec.Command, committing the built
  binary, stdout from `prime` outside a configured workspace.

CLAUDE.md is a symlink to AGENTS.md so Claude Code picks it up via
either name.
This commit is contained in:
Tink bot 2026-05-07 22:51:09 +00:00 committed by kolaente
parent 632579b304
commit cd7cc113a1
2 changed files with 155 additions and 0 deletions

154
veans/AGENTS.md Normal file
View File

@ -0,0 +1,154 @@
# 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` or `go test ./...`.
- E2e tests: assume an externally-running Vikunja at `VEANS_E2E_API_URL`
and admin creds in env (`VEANS_E2E_ADMIN_TOKEN`, or
`VEANS_E2E_ADMIN_USER` + `VEANS_E2E_ADMIN_PASS`). The package
self-skips when `VEANS_E2E_API_URL` is empty, so plain `go test` is
safe locally.
- Local e2e loop: from the parent repo root, build the API
(`mage build:build`), run it with sqlite-memory + a known JWT secret,
register an admin user via `POST /register`, then
`go test ./e2e/...` from `veans/` with the env vars above.
- 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
independently and gives fast feedback.
## Vikunja wire-format gotchas
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:
- **`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.
- **`POST /tasks/{id}` does NOT move tasks between buckets.** The
task↔bucket relation is row-shaped; use `client.MoveTaskToBucket()`
which hits `POST /projects/{p}/views/{v}/buckets/{b}/tasks`. The
Update path on the server only auto-moves on `done` flips.
- **Bot user creation is `PUT /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.
## API token permissions
- Vikunja validates token `permissions` against `apiTokenRoutes`, a map
built dynamically from registered routes. Group names are derived
from the URL path (params stripped, joined by `_`). Examples:
- `/projects/:project/views/:view/buckets/:bucket/tasks`
group `projects`, action `views_buckets_tasks`
- `/tasks/:task/comments` → group `tasks_comments`, action `create`
- `client.PermissionsForBot()` calls `GET /routes` at runtime and
grants only the intersection of what we want and what the server
exposes. **Don't hard-code permission group names** — they drift
across Vikunja versions, and discovery keeps the bot's grant valid
across upgrades.
## Bot ownership and token minting
- Creating a bot via `PUT /user/bots` automatically sets the bot's
`bot_owner_id` to the calling user. Only the owner can mint tokens
for the bot via `PUT /tokens` with `owner_id=<bot_id>`. The init
flow does these as a single human-JWT-authenticated batch.
- Bots have no password and **cannot** authenticate via `POST /login`.
After init, `veans login` re-authenticates as the human (not the
bot) and mints a fresh bot token.
## OAuth flow
- Vikunja's authorization server requires PKCE/S256 and accepts either
`vikunja-…://` custom schemes or RFC 8252 loopback URIs
(`http://127.0.0.1:NNN/`, `http://localhost:NNN/`, `http://[::1]:NNN/`).
No client registration needed — `client_id` can be any consistent
string (we use `veans-cli`).
- `internal/auth/oauth.go` binds a free port on 127.0.0.1, opens the
browser, and captures the callback. The `Shutdown` defer uses
`context.WithoutCancel(ctx)` so cancellation at the outer scope
still drains the loopback server cleanly.
- Token exchange is **JSON only**. Form-encoded POSTs to `/oauth/token`
fail; the standard `golang.org/x/oauth2` client speaks form encoding,
which is why we have a hand-rolled `client.ExchangeOAuthCode`.
## Credential store
- Lookup chain: keychain → env (`VEANS_TOKEN`, optionally pinned by
`VEANS_SERVER`) → file (`~/.config/veans/credentials.yml`, mode 0600,
honors `XDG_CONFIG_HOME`).
- `Chain.Set` falls through to the next backend on error so a missing
dbus on a CI runner doesn't block writes — the file backend is the
reliable last-resort.
- E2e tests override `HOME` and `XDG_CONFIG_HOME` per test to keep the
developer's keyring untouched. Don't bypass the credentials package
in tests — leaks between tests will surface as the wrong bot token.
## Project identifiers and bot usernames
- Project `Identifier` is `runelength(0|10)`, can be empty. When empty,
`Config.FormatTaskID` renders `#NN`; otherwise `PROJ-NN`. Both are
accepted by `runtime.resolveTaskID` along with bare integers.
- Bot username must start with `bot-`; the server enforces it. Hyphens,
digits, lowercase letters allowed; no spaces, no commas, no
`link-share-N` pattern. `config.SuggestedBotUsername` does the
folding for repo names.
- E2e tests deriving identifiers from a unique suffix should use the
trailing chars of `strconv.FormatInt(time.Now().UnixNano(), 36)`.
The leading chars barely change between consecutive runs and will
collide if you take `[:N]`.
## Cobra surface conventions
- Global `--json` flag flips both successful output (raw object for
`show`, raw array for `list`) and the error envelope.
- Stable error codes in `internal/output/errors.go`. Don't add new
ad-hoc strings — wrap with `output.New` / `output.Wrap`.
- `RunE` handlers that don't use `args []string` should rename it to
`_` to satisfy revive's `unused-parameter` rule.
- The bucket-move dance (`MoveTaskToBucket`) runs **after** the field
update on `update`, so a status transition can't clobber freshly
attached labels. Comments for `--status scrapped` post **before**
the bucket move so the audit trail reads in chronological order.
## Things to *not* do
- **Don't add an `os/exec.Command`** without ctx — `noctx` is enabled.
Use `exec.CommandContext(ctx, …)` and thread the context through.
- **Don't commit the built binary.** `veans/.gitignore` covers
`./veans` and `./veans.exe`.
- **Don't write to stdout from `prime` when no `.veans.yml` is found.**
The hook contract is silent + exit 0 so the snippet is safe to install
globally in `~/.claude/settings.json`.
- **Don't change canonical bucket titles** without updating
`internal/status/CanonicalBucketTitles`, the prompt template, and
the e2e assertions in lockstep — agents and humans both treat them
as fixed strings.

1
veans/CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md