# 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=<token>` and the same value passed in
  via `VEANS_E2E_TESTING_TOKEN`. Alternative path:
  `VEANS_E2E_ADMIN_TOKEN=<jwt>` 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

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.
- **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 `<ul data-type="taskList">` +
  `<li data-type="taskItem" data-checked="false"><p>…</p></li>` for
  interactive checkboxes. We deliberately do **not** convert
  markdown↔HTML in the CLI; the agent writes HTML directly, which
  avoids lossy roundtrips on `--description-replace-old/new`. `veans
  show` displays the raw HTML; humans skim it fine.

## 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`) → file
  (`~/.config/veans/credentials.yml`, mode 0600, atomic-write + flock
  serialization). `XDG_CONFIG_HOME` is deliberately not honored —
  agent-only audience runs in a known environment, and the env var
  was a path-traversal seam for no real benefit.
- `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.
- File writes go through a tmp file + `Rename`, with `Chmod 0o600`
  re-asserted on the destination inode so a pre-existing wider mode
  is narrowed. Concurrent writers (e.g. two `veans login` runs) are
  serialized via `flock` on `<path>.lock` (Unix only; Windows is a
  no-op stub since the audience is Linux/macOS).
- E2e tests override `HOME` per test and `filterEnv(..., "VEANS_")`
  strips any inherited `VEANS_TOKEN` so the developer's keyring
  stays 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]`.

## Audience split

The CLI is agent-only at runtime; humans never use it for day-to-day
work (they use Vikunja's web UI). Two commands serve a human running
one-off setup:

- **`init`** — bootstrap a repo: pick project + view, create bot,
  share, mint token, write `.veans.yml`, install hooks.
- **`login`** — rotate the bot's token.

Everything else (`list`, `show`, `create`, `update`, `claim`, `api`,
`prime`, `version`) is **agent-only**:

- **Emits JSON on stdout unconditionally.** No `--json` flag, no
  human-formatted variant. `list` is a raw array; `show` / `create` /
  `update` / `claim` return a single task object.
- **Errors are JSON on stderr** with non-zero exit — same envelope
  everywhere (`{"code": "...", "error": "..."}`), regardless of which
  command ran. Stable codes in `internal/output/errors.go`:
  `NOT_FOUND`, `CONFLICT`, `VALIDATION_ERROR`, `AUTH_ERROR`,
  `RATE_LIMITED`, `BOT_USERS_UNAVAILABLE`, `NOT_CONFIGURED`,
  `UNKNOWN`. Don't add ad-hoc strings — wrap with `output.New` /
  `output.Wrap`.
- **No `globals.JSON`, no dual rendering paths.** If you find yourself
  reaching for "if interactive, do X" on an agent-facing command,
  stop — it's not interactive, an agent is on the other end.

## Cobra surface conventions

- `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.
- Agent-facing commands return the task via `json.NewEncoder(...).Encode(task)`.
  Adding new top-level keys to `client.Task` is an implicit API
  change — bump `prime`'s "useful fields" note alongside.

## 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 rename canonical bucket titles** without updating
  `BucketTitleAliases[s][0]` in `internal/status/status.go`, the
  prompt template (`internal/commands/prompt.tmpl`), and the e2e
  assertions in lockstep — agents and humans both treat them as
  fixed strings.
