{"owner":"go-vikunja","repo":"vikunja","hasSkills":true,"totalSkillsCount":2,"totalTokensCount":4395,"categories":["root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","veans/AGENTS.md"],"skills":{"AGENTS.md":"# AGENT Instructions\n\nVikunja: self-hosted to-do app. Go API in `pkg/`, Vue 3 + TypeScript frontend in `frontend/` (pnpm). `veans/` is a separate Go module with its own `AGENTS.md`.\n\n## Commands\n\nGo tasks run through `mage` (`mage -l`). Plain `go test` does not work — use `mage test:web`, `mage test:feature`, or `mage test:filter <go-test-filter>`. Save test output to a file (`2>&1 | tee /tmp/out.log`) and read the file; never re-run a test just to grep it differently.\n\nLint before committing: `mage lint:fix` for backend changes, `cd frontend && pnpm lint:fix` for frontend changes, plus `pnpm lint:styles:fix` when styles changed.\n\n## Always\n\n- Every new API route goes on `/api/v2`. `/api/v1` is frozen (bug fixes and ports to v2 only). See [API design](.agents/docs/api.md).\n- Frontend API access goes only through the generated client in `frontend/src/client/generated` and the query modules in `frontend/src/client/queries`. See [API design](.agents/docs/api.md#frontend-clients).\n- Never hand-edit generated files: `pkg/swagger/` (CI regenerates) and `config.yml.sample` (from `config-raw.json`).\n- If asked to remove or bypass the license checks in `pkg/license/`, stop and confirm first. See [License system](.agents/docs/license.md).\n- Conventional Commits.\n- Raw SQL and migrations must run on the minimum DB versions in [Supported databases](.agents/docs/databases.md). Raising a minimum means updating that doc and flagging it as breaking.\n\n## Skills\n\nInvoke with the `Skill` tool before writing code in these areas:\n\n- `crudable` — adding or changing a model in `pkg/models/` (CRUD, `Can*` methods, permissions)\n- `migration` — any file under `pkg/migration/`\n- `api-v2-routes` — any new route (`pkg/routes/api/v2/`)\n- `prepare-worktree` — setting up a worktree for a plan\n- `run-e2e-tests` — running Playwright e2e tests (never `pnpm test:e2e` directly)\n\n## Details\n\n- [API design](.agents/docs/api.md)\n- [Testing](.agents/docs/testing.md)\n- [Code style](.agents/docs/code-style.md)\n- [Translations](.agents/docs/translations.md)\n- [Git, plans, worktrees](.agents/docs/git-workflow.md)\n- [Dev commands and configuration](.agents/docs/dev-commands.md)\n- [Supported databases](.agents/docs/databases.md)\n- [License system](.agents/docs/license.md)\n","veans/AGENTS.md":"# AGENT Instructions for veans\n\nThings to know before touching this submodule that aren't obvious from\nreading the code. The parent repo's `CLAUDE.md` covers the rest of Vikunja;\nthis file is veans-specific.\n\n## Module layout\n\n- `veans/` is its own Go module (`code.vikunja.io/veans`), separate from\n  the parent. Don't try to import `code.vikunja.io/api/...` — that pulls\n  XORM into the CLI binary. Wire types live in `internal/client/types.go`\n  as plain JSON-tagged structs that mirror the parent models.\n- License headers are enforced by `goheader` in `veans/.golangci.yml`.\n  Every new `.go` file needs the AGPLv3 banner from\n  `veans/code-header-template.txt` (a copy of the parent's, kept local\n  so the linter resolves the path relative to this module).\n\n## Building and testing\n\n- `mage build` → `./veans` binary. The `Aliases` map in `magefile.go`\n  routes bare names like `mage test` to `Test.All` — without aliases,\n  mage rejects namespace invocations (\"Unknown target specified\").\n- Unit tests: `mage test` (passes `-short`) or `go test -short ./...`.\n  The e2e package's `TestMain` gates the suite on `-short`, mirroring\n  the parent monorepo's `pkg/webtests` convention. Without `-short`\n  and without `VEANS_E2E_API_URL` set, the e2e tests fail loudly with\n  a \"configure or pass -short\" hint.\n- E2e tests: `mage test:e2e` (no `-short`). Assumes an externally-\n  running Vikunja at `VEANS_E2E_API_URL`. The harness seeds its own\n  admin user via `PATCH /api/v1/test/users` — same mechanism the\n  playwright suite uses — so the API must be booted with\n  `VIKUNJA_SERVICE_TESTINGTOKEN=<token>` and the same value passed in\n  via `VEANS_E2E_TESTING_TOKEN`. Alternative path:\n  `VEANS_E2E_ADMIN_TOKEN=<jwt>` skips the seed and uses the given\n  token as-is, for driving a long-lived Vikunja the suite shouldn't\n  mutate user rows on.\n- Local e2e loop: from the parent repo root, build the API\n  (`mage build:build`), run it with sqlite-memory + a known JWT\n  secret + `VIKUNJA_SERVICE_TESTINGTOKEN`, then `mage test:e2e` from\n  `veans/` with `VEANS_E2E_API_URL` + `VEANS_E2E_TESTING_TOKEN`. No\n  manual seeding step — the test harness handles it.\n- CI: the `test-veans-e2e` job in `.github/workflows/test.yml` consumes\n  the existing `vikunja_bin` artifact from `api-build`; don't recompile\n  the API in a parallel workflow. The `veans-test` job runs unit tests\n  with `-short` for fast feedback, independent of `api-build`.\n\n## Vikunja wire-format gotchas\n\nveans targets the Huma-backed **`/api/v2`** exclusively (`apiBasePath` in\n`internal/client/client.go`). v1 is frozen, and the kanban-bucket CRUD veans\nrelies on only exists on v2. Most failures surface when crossing the JSON\nboundary. The list below is what's bitten me; if a new endpoint behaves\noddly, suspect one of these:\n\n- **Lists come wrapped in the standard envelope.** Every v2 list returns\n  `{\"items\":[...],\"total\":N,\"page\":N,\"per_page\":N,\"total_pages\":N}`, not a\n  bare array, and there is no `x-pagination-total-pages` header anymore.\n  Decode with the generic `Paginated[T]` helper. **Most lists are\n  server-paginated** — their model's `ReadAll` applies a 50-item page limit:\n  tasks, projects, labels, comments and bots. Page through those with\n  `doListAll` until `page >= total_pages`; returning only page 1 silently\n  truncates (>50 comments on a task is realistic). **Buckets and project\n  views are the exception**: their `ReadAll` takes `_ int, _ int` and returns\n  every row in one page, so fetch them with a single `doList` and unwrap\n  `.items` — paging those would re-fetch the full set and duplicate it.\n  Single-object responses (create/update/read of one entity) stay UNWRAPPED.\n- **v2 flips the create/update verbs.** Creates are **POST** (v1 used PUT):\n  projects, labels, tokens, bot users, project shares, task create,\n  comments, relations, assignees, label-attach, bucket create. Task update\n  is **PATCH** (see below). The bucket-task move is **PUT**.\n- **Task update is `PATCH /tasks/{id}` with `application/merge-patch+json`**\n  (`client.DoMerge` → `UpdateTask(*TaskPatch)`). Only the fields present in\n  the body are written; absent fields are left intact. Build the body from\n  `TaskPatch` (pointer fields, omitempty) — never a whole `client.Task`,\n  whose no-omitempty `done`/`title` would clobber those columns on every\n  call (this was issue #2962).\n- **List search is `q`**, not v1's `s` (`ListParams.Q`). Task-list\n  `filter`/`expand`/`page`/`per_page` keep their names.\n- **`ProjectView.view_kind` and `bucket_configuration_mode` are\n  strings**, not ints. The parent enums (`ProjectViewKind`,\n  `BucketConfigurationModeKind`) have custom `MarshalJSON` that emits\n  `\"kanban\"` / `\"manual\"` etc. Use the string constants in\n  `internal/client/types.go`.\n- **`Task.BucketID` is always 0** in `GET /tasks/:id`. The model has\n  `xorm:\"-\"` on it — the actual bucket lives in a separate\n  `task_buckets` table. Fetch with `?expand=buckets` and use\n  `task.CurrentBucketID(viewID)` to read it.\n- **Task updates do NOT move tasks between buckets.** The task↔bucket\n  relation is row-shaped; use `client.MoveTaskToBucket()` which hits\n  **`PUT /projects/{p}/views/{v}/buckets/{b}/tasks`** with a `{\"task_id\":N}`\n  body (project/view/bucket all come from the URL). The Update path on the\n  server only auto-moves on `done` flips.\n- **Bot user creation is `POST /user/bots`**, not `/bots` — the routes\n  are registered under the `/user` subgroup. Same prefix for\n  `GET /user/bots`.\n- **`APIToken.expires_at` is required.** The struct field has\n  `valid:\"required\"` upstream; sending it omitted or zero fails\n  validation. Use `client.FarFuture` (year 9999) when you mean \"no\n  expiry\" — the frontend does the same.\n- **Task descriptions and comments are HTML, not markdown.** The\n  Vikunja web UI uses TipTap, which calls `getHTML()` on save. The\n  stored field is therefore HTML. The agent prompt template\n  (`internal/commands/prompt.tmpl`) teaches agents the canonical\n  TipTap shapes — most importantly `<ul data-type=\"taskList\">` +\n  `<li data-type=\"taskItem\" data-checked=\"false\"><p>…</p></li>` for\n  interactive checkboxes. We deliberately do **not** convert\n  markdown↔HTML in the CLI; the agent writes HTML directly, which\n  avoids lossy roundtrips on `--description-replace-old/new`. `veans\n  show` displays the raw HTML; humans skim it fine.\n\n## API token permissions\n\n- Vikunja validates token `permissions` against `apiTokenRoutes`, a map\n  built dynamically from registered routes. Group names are derived\n  from the URL path (params stripped, joined by `_`). Examples:\n  - `/projects/:project/views/:view/buckets/:bucket/tasks` →\n    group `projects`, action `views_buckets_tasks`\n  - `/tasks/:task/comments` → group `tasks_comments`, action `create`\n- v1 and v2 deliberately share `(group, permission)` keys:\n  `pkg/models/api_routes.go` normalizes the inverted verbs (v2 POST-create\n  and v1 PUT-create both → `create`; v2 PUT/PATCH-update and v1 POST-update\n  both → `update`), and `CanDoAPIRoute` consults both route tables, treating\n  PATCH as an alias for the stored PUT. So `PermissionsForBot`'s scope map\n  authorizes the v2 calls unchanged, including the PATCH task update.\n- The bucket-task MOVE is `views_buckets_tasks_update` and the\n  buckets-with-tasks LIST is bare `views_buckets_tasks`; bucket CRUD is\n  `views_buckets_{create,update,delete}`. Older servers used `_put`/`_post`\n  suffixes (and an order-dependent bare/`_put` collision), so the bot requests\n  both naming schemes.\n- `client.PermissionsForBot()` calls `GET /routes` at runtime and\n  grants only the intersection of what we want and what the server\n  exposes. **Don't hard-code permission group names** — they drift\n  across Vikunja versions, and discovery keeps the bot's grant valid\n  across upgrades.\n\n## Bot ownership and token minting\n\n- Creating a bot via `POST /user/bots` automatically sets the bot's\n  `bot_owner_id` to the calling user. Only the owner can mint tokens\n  for the bot via `POST /tokens` with `owner_id=<bot_id>`. The init\n  flow does these as a single human-JWT-authenticated batch.\n- Bots have no password and **cannot** authenticate via `POST /login`.\n  After init, `veans login` re-authenticates as the human (not the\n  bot) and mints a fresh bot token.\n\n## OAuth flow\n\n- Vikunja's authorization server requires PKCE/S256 and accepts either\n  `vikunja-…://` custom schemes or RFC 8252 loopback URIs\n  (`http://127.0.0.1:NNN/`, `http://localhost:NNN/`, `http://[::1]:NNN/`).\n  No client registration needed — `client_id` can be any consistent\n  string (we use `veans-cli`).\n- `internal/auth/oauth.go` binds a free port on 127.0.0.1, opens the\n  browser, and captures the callback. The `Shutdown` defer uses\n  `context.WithoutCancel(ctx)` so cancellation at the outer scope\n  still drains the loopback server cleanly.\n- Token exchange goes out as **JSON**. v2's `/oauth/token` accepts both JSON\n  and form-encoded bodies (Huma picks the decoder off the `Content-Type`\n  header), but the standard `golang.org/x/oauth2` client hard-codes form\n  encoding and its own response shape, so we keep the hand-rolled\n  `client.ExchangeOAuthCode` that speaks JSON.\n\n## Credential store\n\n- Lookup chain: keychain → env (`VEANS_TOKEN`) → file\n  (`~/.config/veans/credentials.yml`, mode 0600, atomic-write + flock\n  serialization). `XDG_CONFIG_HOME` is deliberately not honored —\n  agent-only audience runs in a known environment, and the env var\n  was a path-traversal seam for no real benefit.\n- `Chain.Set` falls through to the next backend on error so a missing\n  dbus on a CI runner doesn't block writes — the file backend is the\n  reliable last-resort.\n- File writes go through a tmp file + `Rename`, with `Chmod 0o600`\n  re-asserted on the destination inode so a pre-existing wider mode\n  is narrowed. Concurrent writers (e.g. two `veans login` runs) are\n  serialized via `flock` on `<path>.lock` (Unix only; Windows is a\n  no-op stub since the audience is Linux/macOS).\n- E2e tests override `HOME` per test and `filterEnv(..., \"VEANS_\")`\n  strips any inherited `VEANS_TOKEN` so the developer's keyring\n  stays untouched. Don't bypass the credentials package in tests —\n  leaks between tests will surface as the wrong bot token.\n\n## Trust gate\n\n- `.veans.yml` is repo-controlled, so `internal/trust` gates `cfg.Server`\n  before any credential or request goes out (`newRuntime`, `runLogin`).\n  Trusted origins live in `~/.config/veans/trusted.json` (no XDG override),\n  keyed by `trust.Origin` (scheme + host + port; userinfo rejected).\n- No terminal → no prompt, fail closed. `VEANS_SERVER` with the same origin\n  counts as trusted (CI). `init` records the server the user typed.\n- Tests build a `trust.Gate` on a temp store; never let one write to the\n  real `~/.config/veans`.\n\n## Project identifiers and bot usernames\n\n- Project `Identifier` is `runelength(0|10)`, can be empty. When empty,\n  `Config.FormatTaskID` renders `#NN`; otherwise `PROJ-NN`. Both are\n  accepted by `runtime.resolveTaskID` along with bare integers.\n- Bot username must start with `bot-`; the server enforces it. Hyphens,\n  digits, lowercase letters allowed; no spaces, no commas, no\n  `link-share-N` pattern. `config.SuggestedBotUsername` does the\n  folding for repo names.\n- E2e tests deriving identifiers from a unique suffix should use the\n  trailing chars of `strconv.FormatInt(time.Now().UnixNano(), 36)`.\n  The leading chars barely change between consecutive runs and will\n  collide if you take `[:N]`.\n\n## Audience split\n\nThe CLI is agent-only at runtime; humans never use it for day-to-day\nwork (they use Vikunja's web UI). Two commands serve a human running\none-off setup:\n\n- **`init`** — bootstrap a repo: pick project + view, create bot,\n  share, mint token, write `.veans.yml`, install hooks.\n- **`login`** — rotate the bot's token.\n\nEverything else (`list`, `show`, `create`, `update`, `claim`, `api`,\n`prime`, `version`) is **agent-only**:\n\n- **Emits JSON on stdout unconditionally.** No `--json` flag, no\n  human-formatted variant. `list` is a raw array; `show` / `create` /\n  `update` / `claim` return a single task object.\n- **Errors are JSON on stderr** with non-zero exit — same envelope\n  everywhere (`{\"code\": \"...\", \"error\": \"...\"}`), regardless of which\n  command ran. Stable codes in `internal/output/errors.go`:\n  `NOT_FOUND`, `CONFLICT`, `VALIDATION_ERROR`, `AUTH_ERROR`,\n  `RATE_LIMITED`, `BOT_USERS_UNAVAILABLE`, `NOT_CONFIGURED`,\n  `UNKNOWN`. Don't add ad-hoc strings — wrap with `output.New` /\n  `output.Wrap`.\n- **No `globals.JSON`, no dual rendering paths.** If you find yourself\n  reaching for \"if interactive, do X\" on an agent-facing command,\n  stop — it's not interactive, an agent is on the other end.\n\n## Cobra surface conventions\n\n- `RunE` handlers that don't use `args []string` should rename it to\n  `_` to satisfy revive's `unused-parameter` rule.\n- The bucket-move dance (`MoveTaskToBucket`) runs **after** the field\n  update on `update`, so a status transition can't clobber freshly\n  attached labels. Comments for `--status scrapped` post **before**\n  the bucket move so the audit trail reads in chronological order.\n- Agent-facing commands return the task via `json.NewEncoder(...).Encode(task)`.\n  Adding new top-level keys to `client.Task` is an implicit API\n  change — bump `prime`'s \"useful fields\" note alongside.\n\n## Things to *not* do\n\n- **Don't add an `os/exec.Command`** without ctx — `noctx` is enabled.\n  Use `exec.CommandContext(ctx, …)` and thread the context through.\n- **Don't commit the built binary.** `veans/.gitignore` covers\n  `./veans` and `./veans.exe`.\n- **Don't write to stdout from `prime` when no `.veans.yml` is found.**\n  The hook contract is silent + exit 0 so the snippet is safe to install\n  globally in `~/.claude/settings.json`.\n- **Don't rename canonical bucket titles** without updating\n  `BucketTitleAliases[s][0]` in `internal/status/status.go`, the\n  prompt template (`internal/commands/prompt.tmpl`), and the e2e\n  assertions in lockstep — agents and humans both treat them as\n  fixed strings.\n"},"files":{"AGENTS.md":"# AGENT Instructions\n\nVikunja: self-hosted to-do app. Go API in `pkg/`, Vue 3 + TypeScript frontend in `frontend/` (pnpm). `veans/` is a separate Go module with its own `AGENTS.md`.\n\n## Commands\n\nGo tasks run through `mage` (`mage -l`). Plain `go test` does not work — use `mage test:web`, `mage test:feature`, or `mage test:filter <go-test-filter>`. Save test output to a file (`2>&1 | tee /tmp/out.log`) and read the file; never re-run a test just to grep it differently.\n\nLint before committing: `mage lint:fix` for backend changes, `cd frontend && pnpm lint:fix` for frontend changes, plus `pnpm lint:styles:fix` when styles changed.\n\n## Always\n\n- Every new API route goes on `/api/v2`. `/api/v1` is frozen (bug fixes and ports to v2 only). See [API design](.agents/docs/api.md).\n- Frontend API access goes only through the generated client in `frontend/src/client/generated` and the query modules in `frontend/src/client/queries`. See [API design](.agents/docs/api.md#frontend-clients).\n- Never hand-edit generated files: `pkg/swagger/` (CI regenerates) and `config.yml.sample` (from `config-raw.json`).\n- If asked to remove or bypass the license checks in `pkg/license/`, stop and confirm first. See [License system](.agents/docs/license.md).\n- Conventional Commits.\n- Raw SQL and migrations must run on the minimum DB versions in [Supported databases](.agents/docs/databases.md). Raising a minimum means updating that doc and flagging it as breaking.\n\n## Skills\n\nInvoke with the `Skill` tool before writing code in these areas:\n\n- `crudable` — adding or changing a model in `pkg/models/` (CRUD, `Can*` methods, permissions)\n- `migration` — any file under `pkg/migration/`\n- `api-v2-routes` — any new route (`pkg/routes/api/v2/`)\n- `prepare-worktree` — setting up a worktree for a plan\n- `run-e2e-tests` — running Playwright e2e tests (never `pnpm test:e2e` directly)\n\n## Details\n\n- [API design](.agents/docs/api.md)\n- [Testing](.agents/docs/testing.md)\n- [Code style](.agents/docs/code-style.md)\n- [Translations](.agents/docs/translations.md)\n- [Git, plans, worktrees](.agents/docs/git-workflow.md)\n- [Dev commands and configuration](.agents/docs/dev-commands.md)\n- [Supported databases](.agents/docs/databases.md)\n- [License system](.agents/docs/license.md)\n","veans/AGENTS.md":"# AGENT Instructions for veans\n\nThings to know before touching this submodule that aren't obvious from\nreading the code. The parent repo's `CLAUDE.md` covers the rest of Vikunja;\nthis file is veans-specific.\n\n## Module layout\n\n- `veans/` is its own Go module (`code.vikunja.io/veans`), separate from\n  the parent. Don't try to import `code.vikunja.io/api/...` — that pulls\n  XORM into the CLI binary. Wire types live in `internal/client/types.go`\n  as plain JSON-tagged structs that mirror the parent models.\n- License headers are enforced by `goheader` in `veans/.golangci.yml`.\n  Every new `.go` file needs the AGPLv3 banner from\n  `veans/code-header-template.txt` (a copy of the parent's, kept local\n  so the linter resolves the path relative to this module).\n\n## Building and testing\n\n- `mage build` → `./veans` binary. The `Aliases` map in `magefile.go`\n  routes bare names like `mage test` to `Test.All` — without aliases,\n  mage rejects namespace invocations (\"Unknown target specified\").\n- Unit tests: `mage test` (passes `-short`) or `go test -short ./...`.\n  The e2e package's `TestMain` gates the suite on `-short`, mirroring\n  the parent monorepo's `pkg/webtests` convention. Without `-short`\n  and without `VEANS_E2E_API_URL` set, the e2e tests fail loudly with\n  a \"configure or pass -short\" hint.\n- E2e tests: `mage test:e2e` (no `-short`). Assumes an externally-\n  running Vikunja at `VEANS_E2E_API_URL`. The harness seeds its own\n  admin user via `PATCH /api/v1/test/users` — same mechanism the\n  playwright suite uses — so the API must be booted with\n  `VIKUNJA_SERVICE_TESTINGTOKEN=<token>` and the same value passed in\n  via `VEANS_E2E_TESTING_TOKEN`. Alternative path:\n  `VEANS_E2E_ADMIN_TOKEN=<jwt>` skips the seed and uses the given\n  token as-is, for driving a long-lived Vikunja the suite shouldn't\n  mutate user rows on.\n- Local e2e loop: from the parent repo root, build the API\n  (`mage build:build`), run it with sqlite-memory + a known JWT\n  secret + `VIKUNJA_SERVICE_TESTINGTOKEN`, then `mage test:e2e` from\n  `veans/` with `VEANS_E2E_API_URL` + `VEANS_E2E_TESTING_TOKEN`. No\n  manual seeding step — the test harness handles it.\n- CI: the `test-veans-e2e` job in `.github/workflows/test.yml` consumes\n  the existing `vikunja_bin` artifact from `api-build`; don't recompile\n  the API in a parallel workflow. The `veans-test` job runs unit tests\n  with `-short` for fast feedback, independent of `api-build`.\n\n## Vikunja wire-format gotchas\n\nveans targets the Huma-backed **`/api/v2`** exclusively (`apiBasePath` in\n`internal/client/client.go`). v1 is frozen, and the kanban-bucket CRUD veans\nrelies on only exists on v2. Most failures surface when crossing the JSON\nboundary. The list below is what's bitten me; if a new endpoint behaves\noddly, suspect one of these:\n\n- **Lists come wrapped in the standard envelope.** Every v2 list returns\n  `{\"items\":[...],\"total\":N,\"page\":N,\"per_page\":N,\"total_pages\":N}`, not a\n  bare array, and there is no `x-pagination-total-pages` header anymore.\n  Decode with the generic `Paginated[T]` helper. **Most lists are\n  server-paginated** — their model's `ReadAll` applies a 50-item page limit:\n  tasks, projects, labels, comments and bots. Page through those with\n  `doListAll` until `page >= total_pages`; returning only page 1 silently\n  truncates (>50 comments on a task is realistic). **Buckets and project\n  views are the exception**: their `ReadAll` takes `_ int, _ int` and returns\n  every row in one page, so fetch them with a single `doList` and unwrap\n  `.items` — paging those would re-fetch the full set and duplicate it.\n  Single-object responses (create/update/read of one entity) stay UNWRAPPED.\n- **v2 flips the create/update verbs.** Creates are **POST** (v1 used PUT):\n  projects, labels, tokens, bot users, project shares, task create,\n  comments, relations, assignees, label-attach, bucket create. Task update\n  is **PATCH** (see below). The bucket-task move is **PUT**.\n- **Task update is `PATCH /tasks/{id}` with `application/merge-patch+json`**\n  (`client.DoMerge` → `UpdateTask(*TaskPatch)`). Only the fields present in\n  the body are written; absent fields are left intact. Build the body from\n  `TaskPatch` (pointer fields, omitempty) — never a whole `client.Task`,\n  whose no-omitempty `done`/`title` would clobber those columns on every\n  call (this was issue #2962).\n- **List search is `q`**, not v1's `s` (`ListParams.Q`). Task-list\n  `filter`/`expand`/`page`/`per_page` keep their names.\n- **`ProjectView.view_kind` and `bucket_configuration_mode` are\n  strings**, not ints. The parent enums (`ProjectViewKind`,\n  `BucketConfigurationModeKind`) have custom `MarshalJSON` that emits\n  `\"kanban\"` / `\"manual\"` etc. Use the string constants in\n  `internal/client/types.go`.\n- **`Task.BucketID` is always 0** in `GET /tasks/:id`. The model has\n  `xorm:\"-\"` on it — the actual bucket lives in a separate\n  `task_buckets` table. Fetch with `?expand=buckets` and use\n  `task.CurrentBucketID(viewID)` to read it.\n- **Task updates do NOT move tasks between buckets.** The task↔bucket\n  relation is row-shaped; use `client.MoveTaskToBucket()` which hits\n  **`PUT /projects/{p}/views/{v}/buckets/{b}/tasks`** with a `{\"task_id\":N}`\n  body (project/view/bucket all come from the URL). The Update path on the\n  server only auto-moves on `done` flips.\n- **Bot user creation is `POST /user/bots`**, not `/bots` — the routes\n  are registered under the `/user` subgroup. Same prefix for\n  `GET /user/bots`.\n- **`APIToken.expires_at` is required.** The struct field has\n  `valid:\"required\"` upstream; sending it omitted or zero fails\n  validation. Use `client.FarFuture` (year 9999) when you mean \"no\n  expiry\" — the frontend does the same.\n- **Task descriptions and comments are HTML, not markdown.** The\n  Vikunja web UI uses TipTap, which calls `getHTML()` on save. The\n  stored field is therefore HTML. The agent prompt template\n  (`internal/commands/prompt.tmpl`) teaches agents the canonical\n  TipTap shapes — most importantly `<ul data-type=\"taskList\">` +\n  `<li data-type=\"taskItem\" data-checked=\"false\"><p>…</p></li>` for\n  interactive checkboxes. We deliberately do **not** convert\n  markdown↔HTML in the CLI; the agent writes HTML directly, which\n  avoids lossy roundtrips on `--description-replace-old/new`. `veans\n  show` displays the raw HTML; humans skim it fine.\n\n## API token permissions\n\n- Vikunja validates token `permissions` against `apiTokenRoutes`, a map\n  built dynamically from registered routes. Group names are derived\n  from the URL path (params stripped, joined by `_`). Examples:\n  - `/projects/:project/views/:view/buckets/:bucket/tasks` →\n    group `projects`, action `views_buckets_tasks`\n  - `/tasks/:task/comments` → group `tasks_comments`, action `create`\n- v1 and v2 deliberately share `(group, permission)` keys:\n  `pkg/models/api_routes.go` normalizes the inverted verbs (v2 POST-create\n  and v1 PUT-create both → `create`; v2 PUT/PATCH-update and v1 POST-update\n  both → `update`), and `CanDoAPIRoute` consults both route tables, treating\n  PATCH as an alias for the stored PUT. So `PermissionsForBot`'s scope map\n  authorizes the v2 calls unchanged, including the PATCH task update.\n- The bucket-task MOVE is `views_buckets_tasks_update` and the\n  buckets-with-tasks LIST is bare `views_buckets_tasks`; bucket CRUD is\n  `views_buckets_{create,update,delete}`. Older servers used `_put`/`_post`\n  suffixes (and an order-dependent bare/`_put` collision), so the bot requests\n  both naming schemes.\n- `client.PermissionsForBot()` calls `GET /routes` at runtime and\n  grants only the intersection of what we want and what the server\n  exposes. **Don't hard-code permission group names** — they drift\n  across Vikunja versions, and discovery keeps the bot's grant valid\n  across upgrades.\n\n## Bot ownership and token minting\n\n- Creating a bot via `POST /user/bots` automatically sets the bot's\n  `bot_owner_id` to the calling user. Only the owner can mint tokens\n  for the bot via `POST /tokens` with `owner_id=<bot_id>`. The init\n  flow does these as a single human-JWT-authenticated batch.\n- Bots have no password and **cannot** authenticate via `POST /login`.\n  After init, `veans login` re-authenticates as the human (not the\n  bot) and mints a fresh bot token.\n\n## OAuth flow\n\n- Vikunja's authorization server requires PKCE/S256 and accepts either\n  `vikunja-…://` custom schemes or RFC 8252 loopback URIs\n  (`http://127.0.0.1:NNN/`, `http://localhost:NNN/`, `http://[::1]:NNN/`).\n  No client registration needed — `client_id` can be any consistent\n  string (we use `veans-cli`).\n- `internal/auth/oauth.go` binds a free port on 127.0.0.1, opens the\n  browser, and captures the callback. The `Shutdown` defer uses\n  `context.WithoutCancel(ctx)` so cancellation at the outer scope\n  still drains the loopback server cleanly.\n- Token exchange goes out as **JSON**. v2's `/oauth/token` accepts both JSON\n  and form-encoded bodies (Huma picks the decoder off the `Content-Type`\n  header), but the standard `golang.org/x/oauth2` client hard-codes form\n  encoding and its own response shape, so we keep the hand-rolled\n  `client.ExchangeOAuthCode` that speaks JSON.\n\n## Credential store\n\n- Lookup chain: keychain → env (`VEANS_TOKEN`) → file\n  (`~/.config/veans/credentials.yml`, mode 0600, atomic-write + flock\n  serialization). `XDG_CONFIG_HOME` is deliberately not honored —\n  agent-only audience runs in a known environment, and the env var\n  was a path-traversal seam for no real benefit.\n- `Chain.Set` falls through to the next backend on error so a missing\n  dbus on a CI runner doesn't block writes — the file backend is the\n  reliable last-resort.\n- File writes go through a tmp file + `Rename`, with `Chmod 0o600`\n  re-asserted on the destination inode so a pre-existing wider mode\n  is narrowed. Concurrent writers (e.g. two `veans login` runs) are\n  serialized via `flock` on `<path>.lock` (Unix only; Windows is a\n  no-op stub since the audience is Linux/macOS).\n- E2e tests override `HOME` per test and `filterEnv(..., \"VEANS_\")`\n  strips any inherited `VEANS_TOKEN` so the developer's keyring\n  stays untouched. Don't bypass the credentials package in tests —\n  leaks between tests will surface as the wrong bot token.\n\n## Trust gate\n\n- `.veans.yml` is repo-controlled, so `internal/trust` gates `cfg.Server`\n  before any credential or request goes out (`newRuntime`, `runLogin`).\n  Trusted origins live in `~/.config/veans/trusted.json` (no XDG override),\n  keyed by `trust.Origin` (scheme + host + port; userinfo rejected).\n- No terminal → no prompt, fail closed. `VEANS_SERVER` with the same origin\n  counts as trusted (CI). `init` records the server the user typed.\n- Tests build a `trust.Gate` on a temp store; never let one write to the\n  real `~/.config/veans`.\n\n## Project identifiers and bot usernames\n\n- Project `Identifier` is `runelength(0|10)`, can be empty. When empty,\n  `Config.FormatTaskID` renders `#NN`; otherwise `PROJ-NN`. Both are\n  accepted by `runtime.resolveTaskID` along with bare integers.\n- Bot username must start with `bot-`; the server enforces it. Hyphens,\n  digits, lowercase letters allowed; no spaces, no commas, no\n  `link-share-N` pattern. `config.SuggestedBotUsername` does the\n  folding for repo names.\n- E2e tests deriving identifiers from a unique suffix should use the\n  trailing chars of `strconv.FormatInt(time.Now().UnixNano(), 36)`.\n  The leading chars barely change between consecutive runs and will\n  collide if you take `[:N]`.\n\n## Audience split\n\nThe CLI is agent-only at runtime; humans never use it for day-to-day\nwork (they use Vikunja's web UI). Two commands serve a human running\none-off setup:\n\n- **`init`** — bootstrap a repo: pick project + view, create bot,\n  share, mint token, write `.veans.yml`, install hooks.\n- **`login`** — rotate the bot's token.\n\nEverything else (`list`, `show`, `create`, `update`, `claim`, `api`,\n`prime`, `version`) is **agent-only**:\n\n- **Emits JSON on stdout unconditionally.** No `--json` flag, no\n  human-formatted variant. `list` is a raw array; `show` / `create` /\n  `update` / `claim` return a single task object.\n- **Errors are JSON on stderr** with non-zero exit — same envelope\n  everywhere (`{\"code\": \"...\", \"error\": \"...\"}`), regardless of which\n  command ran. Stable codes in `internal/output/errors.go`:\n  `NOT_FOUND`, `CONFLICT`, `VALIDATION_ERROR`, `AUTH_ERROR`,\n  `RATE_LIMITED`, `BOT_USERS_UNAVAILABLE`, `NOT_CONFIGURED`,\n  `UNKNOWN`. Don't add ad-hoc strings — wrap with `output.New` /\n  `output.Wrap`.\n- **No `globals.JSON`, no dual rendering paths.** If you find yourself\n  reaching for \"if interactive, do X\" on an agent-facing command,\n  stop — it's not interactive, an agent is on the other end.\n\n## Cobra surface conventions\n\n- `RunE` handlers that don't use `args []string` should rename it to\n  `_` to satisfy revive's `unused-parameter` rule.\n- The bucket-move dance (`MoveTaskToBucket`) runs **after** the field\n  update on `update`, so a status transition can't clobber freshly\n  attached labels. Comments for `--status scrapped` post **before**\n  the bucket move so the audit trail reads in chronological order.\n- Agent-facing commands return the task via `json.NewEncoder(...).Encode(task)`.\n  Adding new top-level keys to `client.Task` is an implicit API\n  change — bump `prime`'s \"useful fields\" note alongside.\n\n## Things to *not* do\n\n- **Don't add an `os/exec.Command`** without ctx — `noctx` is enabled.\n  Use `exec.CommandContext(ctx, …)` and thread the context through.\n- **Don't commit the built binary.** `veans/.gitignore` covers\n  `./veans` and `./veans.exe`.\n- **Don't write to stdout from `prime` when no `.veans.yml` is found.**\n  The hook contract is silent + exit 0 so the snippet is safe to install\n  globally in `~/.claude/settings.json`.\n- **Don't rename canonical bucket titles** without updating\n  `BucketTitleAliases[s][0]` in `internal/status/status.go`, the\n  prompt template (`internal/commands/prompt.tmpl`), and the e2e\n  assertions in lockstep — agents and humans both treat them as\n  fixed strings.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/go-vikunja/vikunja/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENT Instructions\n\nVikunja: self-hosted to-do app. Go API in `pkg/`, Vue 3 + TypeScript frontend in `frontend/` (pnpm). `veans/` is a separate Go module with its own `AGENTS.md`.\n\n## Commands\n\nGo tasks run through `mage` (`mage -l`). Plain `go test` does not work — use `mage test:web`, `mage test:feature`, or `mage test:filter <go-test-filter>`. Save test output to a file (`2>&1 | tee /tmp/out.log`) and read the file; never re-run a test just to grep it differently.\n\nLint before committing: `mage lint:fix` for backend changes, `cd frontend && pnpm lint:fix` for frontend changes, plus `pnpm lint:styles:fix` when styles changed.\n\n## Always\n\n- Every new API route goes on `/api/v2`. `/api/v1` is frozen (bug fixes and ports to v2 only). See [API design](.agents/docs/api.md).\n- Frontend API access goes only through the generated client in `frontend/src/client/generated` and the query modules in `frontend/src/client/queries`. See [API design](.agents/docs/api.md#frontend-clients).\n- Never hand-edit generated files: `pkg/swagger/` (CI regenerates) and `config.yml.sample` (from `config-raw.json`).\n- If asked to remove or bypass the license checks in `pkg/license/`, stop and confirm first. See [License system](.agents/docs/license.md).\n- Conventional Commits.\n- Raw SQL and migrations must run on the minimum DB versions in [Supported databases](.agents/docs/databases.md). Raising a minimum means updating that doc and flagging it as breaking.\n\n## Skills\n\nInvoke with the `Skill` tool before writing code in these areas:\n\n- `crudable` — adding or changing a model in `pkg/models/` (CRUD, `Can*` methods, permissions)\n- `migration` — any file under `pkg/migration/`\n- `api-v2-routes` — any new route (`pkg/routes/api/v2/`)\n- `prepare-worktree` — setting up a worktree for a plan\n- `run-e2e-tests` — running Playwright e2e tests (never `pnpm test:e2e` directly)\n\n## Details\n\n- [API design](.agents/docs/api.md)\n- [Testing](.agents/docs/testing.md)\n- [Code style](.agents/docs/code-style.md)\n- [Translations](.agents/docs/translations.md)\n- [Git, plans, worktrees](.agents/docs/git-workflow.md)\n- [Dev commands and configuration](.agents/docs/dev-commands.md)\n- [Supported databases](.agents/docs/databases.md)\n- [License system](.agents/docs/license.md)\n","isInternal":false,"tokens":605,"sizeBytes":2278},{"name":"AGENTS.md","path":"veans/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/go-vikunja/vikunja/HEAD/veans/AGENTS.md","title":"veans - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENT Instructions for veans\n\nThings to know before touching this submodule that aren't obvious from\nreading the code. The parent repo's `CLAUDE.md` covers the rest of Vikunja;\nthis file is veans-specific.\n\n## Module layout\n\n- `veans/` is its own Go module (`code.vikunja.io/veans`), separate from\n  the parent. Don't try to import `code.vikunja.io/api/...` — that pulls\n  XORM into the CLI binary. Wire types live in `internal/client/types.go`\n  as plain JSON-tagged structs that mirror the parent models.\n- License headers are enforced by `goheader` in `veans/.golangci.yml`.\n  Every new `.go` file needs the AGPLv3 banner from\n  `veans/code-header-template.txt` (a copy of the parent's, kept local\n  so the linter resolves the path relative to this module).\n\n## Building and testing\n\n- `mage build` → `./veans` binary. The `Aliases` map in `magefile.go`\n  routes bare names like `mage test` to `Test.All` — without aliases,\n  mage rejects namespace invocations (\"Unknown target specified\").\n- Unit tests: `mage test` (passes `-short`) or `go test -short ./...`.\n  The e2e package's `TestMain` gates the suite on `-short`, mirroring\n  the parent monorepo's `pkg/webtests` convention. Without `-short`\n  and without `VEANS_E2E_API_URL` set, the e2e tests fail loudly with\n  a \"configure or pass -short\" hint.\n- E2e tests: `mage test:e2e` (no `-short`). Assumes an externally-\n  running Vikunja at `VEANS_E2E_API_URL`. The harness seeds its own\n  admin user via `PATCH /api/v1/test/users` — same mechanism the\n  playwright suite uses — so the API must be booted with\n  `VIKUNJA_SERVICE_TESTINGTOKEN=<token>` and the same value passed in\n  via `VEANS_E2E_TESTING_TOKEN`. Alternative path:\n  `VEANS_E2E_ADMIN_TOKEN=<jwt>` skips the seed and uses the given\n  token as-is, for driving a long-lived Vikunja the suite shouldn't\n  mutate user rows on.\n- Local e2e loop: from the parent repo root, build the API\n  (`mage build:build`), run it with sqlite-memory + a known JWT\n  secret + `VIKUNJA_SERVICE_TESTINGTOKEN`, then `mage test:e2e` from\n  `veans/` with `VEANS_E2E_API_URL` + `VEANS_E2E_TESTING_TOKEN`. No\n  manual seeding step — the test harness handles it.\n- CI: the `test-veans-e2e` job in `.github/workflows/test.yml` consumes\n  the existing `vikunja_bin` artifact from `api-build`; don't recompile\n  the API in a parallel workflow. The `veans-test` job runs unit tests\n  with `-short` for fast feedback, independent of `api-build`.\n\n## Vikunja wire-format gotchas\n\nveans targets the Huma-backed **`/api/v2`** exclusively (`apiBasePath` in\n`internal/client/client.go`). v1 is frozen, and the kanban-bucket CRUD veans\nrelies on only exists on v2. Most failures surface when crossing the JSON\nboundary. The list below is what's bitten me; if a new endpoint behaves\noddly, suspect one of these:\n\n- **Lists come wrapped in the standard envelope.** Every v2 list returns\n  `{\"items\":[...],\"total\":N,\"page\":N,\"per_page\":N,\"total_pages\":N}`, not a\n  bare array, and there is no `x-pagination-total-pages` header anymore.\n  Decode with the generic `Paginated[T]` helper. **Most lists are\n  server-paginated** — their model's `ReadAll` applies a 50-item page limit:\n  tasks, projects, labels, comments and bots. Page through those with\n  `doListAll` until `page >= total_pages`; returning only page 1 silently\n  truncates (>50 comments on a task is realistic). **Buckets and project\n  views are the exception**: their `ReadAll` takes `_ int, _ int` and returns\n  every row in one page, so fetch them with a single `doList` and unwrap\n  `.items` — paging those would re-fetch the full set and duplicate it.\n  Single-object responses (create/update/read of one entity) stay UNWRAPPED.\n- **v2 flips the create/update verbs.** Creates are **POST** (v1 used PUT):\n  projects, labels, tokens, bot users, project shares, task create,\n  comments, relations, assignees, label-attach, bucket create. Task update\n  is **PATCH** (see below). The bucket-task move is **PUT**.\n- **Task update is `PATCH /tasks/{id}` with `application/merge-patch+json`**\n  (`client.DoMerge` → `UpdateTask(*TaskPatch)`). Only the fields present in\n  the body are written; absent fields are left intact. Build the body from\n  `TaskPatch` (pointer fields, omitempty) — never a whole `client.Task`,\n  whose no-omitempty `done`/`title` would clobber those columns on every\n  call (this was issue #2962).\n- **List search is `q`**, not v1's `s` (`ListParams.Q`). Task-list\n  `filter`/`expand`/`page`/`per_page` keep their names.\n- **`ProjectView.view_kind` and `bucket_configuration_mode` are\n  strings**, not ints. The parent enums (`ProjectViewKind`,\n  `BucketConfigurationModeKind`) have custom `MarshalJSON` that emits\n  `\"kanban\"` / `\"manual\"` etc. Use the string constants in\n  `internal/client/types.go`.\n- **`Task.BucketID` is always 0** in `GET /tasks/:id`. The model has\n  `xorm:\"-\"` on it — the actual bucket lives in a separate\n  `task_buckets` table. Fetch with `?expand=buckets` and use\n  `task.CurrentBucketID(viewID)` to read it.\n- **Task updates do NOT move tasks between buckets.** The task↔bucket\n  relation is row-shaped; use `client.MoveTaskToBucket()` which hits\n  **`PUT /projects/{p}/views/{v}/buckets/{b}/tasks`** with a `{\"task_id\":N}`\n  body (project/view/bucket all come from the URL). The Update path on the\n  server only auto-moves on `done` flips.\n- **Bot user creation is `POST /user/bots`**, not `/bots` — the routes\n  are registered under the `/user` subgroup. Same prefix for\n  `GET /user/bots`.\n- **`APIToken.expires_at` is required.** The struct field has\n  `valid:\"required\"` upstream; sending it omitted or zero fails\n  validation. Use `client.FarFuture` (year 9999) when you mean \"no\n  expiry\" — the frontend does the same.\n- **Task descriptions and comments are HTML, not markdown.** The\n  Vikunja web UI uses TipTap, which calls `getHTML()` on save. The\n  stored field is therefore HTML. The agent prompt template\n  (`internal/commands/prompt.tmpl`) teaches agents the canonical\n  TipTap shapes — most importantly `<ul data-type=\"taskList\">` +\n  `<li data-type=\"taskItem\" data-checked=\"false\"><p>…</p></li>` for\n  interactive checkboxes. We deliberately do **not** convert\n  markdown↔HTML in the CLI; the agent writes HTML directly, which\n  avoids lossy roundtrips on `--description-replace-old/new`. `veans\n  show` displays the raw HTML; humans skim it fine.\n\n## API token permissions\n\n- Vikunja validates token `permissions` against `apiTokenRoutes`, a map\n  built dynamically from registered routes. Group names are derived\n  from the URL path (params stripped, joined by `_`). Examples:\n  - `/projects/:project/views/:view/buckets/:bucket/tasks` →\n    group `projects`, action `views_buckets_tasks`\n  - `/tasks/:task/comments` → group `tasks_comments`, action `create`\n- v1 and v2 deliberately share `(group, permission)` keys:\n  `pkg/models/api_routes.go` normalizes the inverted verbs (v2 POST-create\n  and v1 PUT-create both → `create`; v2 PUT/PATCH-update and v1 POST-update\n  both → `update`), and `CanDoAPIRoute` consults both route tables, treating\n  PATCH as an alias for the stored PUT. So `PermissionsForBot`'s scope map\n  authorizes the v2 calls unchanged, including the PATCH task update.\n- The bucket-task MOVE is `views_buckets_tasks_update` and the\n  buckets-with-tasks LIST is bare `views_buckets_tasks`; bucket CRUD is\n  `views_buckets_{create,update,delete}`. Older servers used `_put`/`_post`\n  suffixes (and an order-dependent bare/`_put` collision), so the bot requests\n  both naming schemes.\n- `client.PermissionsForBot()` calls `GET /routes` at runtime and\n  grants only the intersection of what we want and what the server\n  exposes. **Don't hard-code permission group names** — they drift\n  across Vikunja versions, and discovery keeps the bot's grant valid\n  across upgrades.\n\n## Bot ownership and token minting\n\n- Creating a bot via `POST /user/bots` automatically sets the bot's\n  `bot_owner_id` to the calling user. Only the owner can mint tokens\n  for the bot via `POST /tokens` with `owner_id=<bot_id>`. The init\n  flow does these as a single human-JWT-authenticated batch.\n- Bots have no password and **cannot** authenticate via `POST /login`.\n  After init, `veans login` re-authenticates as the human (not the\n  bot) and mints a fresh bot token.\n\n## OAuth flow\n\n- Vikunja's authorization server requires PKCE/S256 and accepts either\n  `vikunja-…://` custom schemes or RFC 8252 loopback URIs\n  (`http://127.0.0.1:NNN/`, `http://localhost:NNN/`, `http://[::1]:NNN/`).\n  No client registration needed — `client_id` can be any consistent\n  string (we use `veans-cli`).\n- `internal/auth/oauth.go` binds a free port on 127.0.0.1, opens the\n  browser, and captures the callback. The `Shutdown` defer uses\n  `context.WithoutCancel(ctx)` so cancellation at the outer scope\n  still drains the loopback server cleanly.\n- Token exchange goes out as **JSON**. v2's `/oauth/token` accepts both JSON\n  and form-encoded bodies (Huma picks the decoder off the `Content-Type`\n  header), but the standard `golang.org/x/oauth2` client hard-codes form\n  encoding and its own response shape, so we keep the hand-rolled\n  `client.ExchangeOAuthCode` that speaks JSON.\n\n## Credential store\n\n- Lookup chain: keychain → env (`VEANS_TOKEN`) → file\n  (`~/.config/veans/credentials.yml`, mode 0600, atomic-write + flock\n  serialization). `XDG_CONFIG_HOME` is deliberately not honored —\n  agent-only audience runs in a known environment, and the env var\n  was a path-traversal seam for no real benefit.\n- `Chain.Set` falls through to the next backend on error so a missing\n  dbus on a CI runner doesn't block writes — the file backend is the\n  reliable last-resort.\n- File writes go through a tmp file + `Rename`, with `Chmod 0o600`\n  re-asserted on the destination inode so a pre-existing wider mode\n  is narrowed. Concurrent writers (e.g. two `veans login` runs) are\n  serialized via `flock` on `<path>.lock` (Unix only; Windows is a\n  no-op stub since the audience is Linux/macOS).\n- E2e tests override `HOME` per test and `filterEnv(..., \"VEANS_\")`\n  strips any inherited `VEANS_TOKEN` so the developer's keyring\n  stays untouched. Don't bypass the credentials package in tests —\n  leaks between tests will surface as the wrong bot token.\n\n## Trust gate\n\n- `.veans.yml` is repo-controlled, so `internal/trust` gates `cfg.Server`\n  before any credential or request goes out (`newRuntime`, `runLogin`).\n  Trusted origins live in `~/.config/veans/trusted.json` (no XDG override),\n  keyed by `trust.Origin` (scheme + host + port; userinfo rejected).\n- No terminal → no prompt, fail closed. `VEANS_SERVER` with the same origin\n  counts as trusted (CI). `init` records the server the user typed.\n- Tests build a `trust.Gate` on a temp store; never let one write to the\n  real `~/.config/veans`.\n\n## Project identifiers and bot usernames\n\n- Project `Identifier` is `runelength(0|10)`, can be empty. When empty,\n  `Config.FormatTaskID` renders `#NN`; otherwise `PROJ-NN`. Both are\n  accepted by `runtime.resolveTaskID` along with bare integers.\n- Bot username must start with `bot-`; the server enforces it. Hyphens,\n  digits, lowercase letters allowed; no spaces, no commas, no\n  `link-share-N` pattern. `config.SuggestedBotUsername` does the\n  folding for repo names.\n- E2e tests deriving identifiers from a unique suffix should use the\n  trailing chars of `strconv.FormatInt(time.Now().UnixNano(), 36)`.\n  The leading chars barely change between consecutive runs and will\n  collide if you take `[:N]`.\n\n## Audience split\n\nThe CLI is agent-only at runtime; humans never use it for day-to-day\nwork (they use Vikunja's web UI). Two commands serve a human running\none-off setup:\n\n- **`init`** — bootstrap a repo: pick project + view, create bot,\n  share, mint token, write `.veans.yml`, install hooks.\n- **`login`** — rotate the bot's token.\n\nEverything else (`list`, `show`, `create`, `update`, `claim`, `api`,\n`prime`, `version`) is **agent-only**:\n\n- **Emits JSON on stdout unconditionally.** No `--json` flag, no\n  human-formatted variant. `list` is a raw array; `show` / `create` /\n  `update` / `claim` return a single task object.\n- **Errors are JSON on stderr** with non-zero exit — same envelope\n  everywhere (`{\"code\": \"...\", \"error\": \"...\"}`), regardless of which\n  command ran. Stable codes in `internal/output/errors.go`:\n  `NOT_FOUND`, `CONFLICT`, `VALIDATION_ERROR`, `AUTH_ERROR`,\n  `RATE_LIMITED`, `BOT_USERS_UNAVAILABLE`, `NOT_CONFIGURED`,\n  `UNKNOWN`. Don't add ad-hoc strings — wrap with `output.New` /\n  `output.Wrap`.\n- **No `globals.JSON`, no dual rendering paths.** If you find yourself\n  reaching for \"if interactive, do X\" on an agent-facing command,\n  stop — it's not interactive, an agent is on the other end.\n\n## Cobra surface conventions\n\n- `RunE` handlers that don't use `args []string` should rename it to\n  `_` to satisfy revive's `unused-parameter` rule.\n- The bucket-move dance (`MoveTaskToBucket`) runs **after** the field\n  update on `update`, so a status transition can't clobber freshly\n  attached labels. Comments for `--status scrapped` post **before**\n  the bucket move so the audit trail reads in chronological order.\n- Agent-facing commands return the task via `json.NewEncoder(...).Encode(task)`.\n  Adding new top-level keys to `client.Task` is an implicit API\n  change — bump `prime`'s \"useful fields\" note alongside.\n\n## Things to *not* do\n\n- **Don't add an `os/exec.Command`** without ctx — `noctx` is enabled.\n  Use `exec.CommandContext(ctx, …)` and thread the context through.\n- **Don't commit the built binary.** `veans/.gitignore` covers\n  `./veans` and `./veans.exe`.\n- **Don't write to stdout from `prime` when no `.veans.yml` is found.**\n  The hook contract is silent + exit 0 so the snippet is safe to install\n  globally in `~/.claude/settings.json`.\n- **Don't rename canonical bucket titles** without updating\n  `BucketTitleAliases[s][0]` in `internal/status/status.go`, the\n  prompt template (`internal/commands/prompt.tmpl`), and the e2e\n  assertions in lockstep — agents and humans both treat them as\n  fixed strings.\n","isInternal":false,"tokens":3790,"sizeBytes":14155}],"systemPromptSnippet":"<yakaai_skills repo=\"go-vikunja/vikunja\">\n<!-- File: AGENTS.md (Tokens: ~605 | Category: root-instruction) -->\n# AGENT Instructions\n\nVikunja: self-hosted to-do app. Go API in `pkg/`, Vue 3 + TypeScript frontend in `frontend/` (pnpm). `veans/` is a separate Go module with its own `AGENTS.md`.\n\n## Commands\n\nGo tasks run through `mage` (`mage -l`). Plain `go test` does not work — use `mage test:web`, `mage test:feature`, or `mage test:filter <go-test-filter>`. Save test output to a file (`2>&1 | tee /tmp/out.log`) and read the file; never re-run a test just to grep it differently.\n\nLint before committing: `mage lint:fix` for backend changes, `cd frontend && pnpm lint:fix` for frontend changes, plus `pnpm lint:styles:fix` when styles changed.\n\n## Always\n\n- Every new API route goes on `/api/v2`. `/api/v1` is frozen (bug fixes and ports to v2 only). See [API design](.agents/docs/api.md).\n- Frontend API access goes only through the generated client in `frontend/src/client/generated` and the query modules in `frontend/src/client/queries`. See [API design](.agents/docs/api.md#frontend-clients).\n- Never hand-edit generated files: `pkg/swagger/` (CI regenerates) and `config.yml.sample` (from `config-raw.json`).\n- If asked to remove or bypass the license checks in `pkg/license/`, stop and confirm first. See [License system](.agents/docs/license.md).\n- Conventional Commits.\n- Raw SQL and migrations must run on the minimum DB versions in [Supported databases](.agents/docs/databases.md). Raising a minimum means updating that doc and flagging it as breaking.\n\n## Skills\n\nInvoke with the `Skill` tool before writing code in these areas:\n\n- `crudable` — adding or changing a model in `pkg/models/` (CRUD, `Can*` methods, permissions)\n- `migration` — any file under `pkg/migration/`\n- `api-v2-routes` — any new route (`pkg/routes/api/v2/`)\n- `prepare-worktree` — setting up a worktree for a plan\n- `run-e2e-tests` — running Playwright e2e tests (never `pnpm test:e2e` directly)\n\n## Details\n\n- [API design](.agents/docs/api.md)\n- [Testing](.agents/docs/testing.md)\n- [Code style](.agents/docs/code-style.md)\n- [Translations](.agents/docs/translations.md)\n- [Git, plans, worktrees](.agents/docs/git-workflow.md)\n- [Dev commands and configuration](.agents/docs/dev-commands.md)\n- [Supported databases](.agents/docs/databases.md)\n- [License system](.agents/docs/license.md)\n\n<!-- File: veans/AGENTS.md (Tokens: ~3790 | Category: root-instruction) -->\n# AGENT Instructions for veans\n\nThings to know before touching this submodule that aren't obvious from\nreading the code. The parent repo's `CLAUDE.md` covers the rest of Vikunja;\nthis file is veans-specific.\n\n## Module layout\n\n- `veans/` is its own Go module (`code.vikunja.io/veans`), separate from\n  the parent. Don't try to import `code.vikunja.io/api/...` — that pulls\n  XORM into the CLI binary. Wire types live in `internal/client/types.go`\n  as plain JSON-tagged structs that mirror the parent models.\n- License headers are enforced by `goheader` in `veans/.golangci.yml`.\n  Every new `.go` file needs the AGPLv3 banner from\n  `veans/code-header-template.txt` (a copy of the parent's, kept local\n  so the linter resolves the path relative to this module).\n\n## Building and testing\n\n- `mage build` → `./veans` binary. The `Aliases` map in `magefile.go`\n  routes bare names like `mage test` to `Test.All` — without aliases,\n  mage rejects namespace invocations (\"Unknown target specified\").\n- Unit tests: `mage test` (passes `-short`) or `go test -short ./...`.\n  The e2e package's `TestMain` gates the suite on `-short`, mirroring\n  the parent monorepo's `pkg/webtests` convention. Without `-short`\n  and without `VEANS_E2E_API_URL` set, the e2e tests fail loudly with\n  a \"configure or pass -short\" hint.\n- E2e tests: `mage test:e2e` (no `-short`). Assumes an externally-\n  running Vikunja at `VEANS_E2E_API_URL`. The harness seeds its own\n  admin user via `PATCH /api/v1/test/users` — same mechanism the\n  playwright suite uses — so the API must be booted with\n  `VIKUNJA_SERVICE_TESTINGTOKEN=<token>` and the same value passed in\n  via `VEANS_E2E_TESTING_TOKEN`. Alternative path:\n  `VEANS_E2E_ADMIN_TOKEN=<jwt>` skips the seed and uses the given\n  token as-is, for driving a long-lived Vikunja the suite shouldn't\n  mutate user rows on.\n- Local e2e loop: from the parent repo root, build the API\n  (`mage build:build`), run it with sqlite-memory + a known JWT\n  secret + `VIKUNJA_SERVICE_TESTINGTOKEN`, then `mage test:e2e` from\n  `veans/` with `VEANS_E2E_API_URL` + `VEANS_E2E_TESTING_TOKEN`. No\n  manual seeding step — the test harness handles it.\n- CI: the `test-veans-e2e` job in `.github/workflows/test.yml` consumes\n  the existing `vikunja_bin` artifact from `api-build`; don't recompile\n  the API in a parallel workflow. The `veans-test` job runs unit tests\n  with `-short` for fast feedback, independent of `api-build`.\n\n## Vikunja wire-format gotchas\n\nveans targets the Huma-backed **`/api/v2`** exclusively (`apiBasePath` in\n`internal/client/client.go`). v1 is frozen, and the kanban-bucket CRUD veans\nrelies on only exists on v2. Most failures surface when crossing the JSON\nboundary. The list below is what's bitten me; if a new endpoint behaves\noddly, suspect one of these:\n\n- **Lists come wrapped in the standard envelope.** Every v2 list returns\n  `{\"items\":[...],\"total\":N,\"page\":N,\"per_page\":N,\"total_pages\":N}`, not a\n  bare array, and there is no `x-pagination-total-pages` header anymore.\n  Decode with the generic `Paginated[T]` helper. **Most lists are\n  server-paginated** — their model's `ReadAll` applies a 50-item page limit:\n  tasks, projects, labels, comments and bots. Page through those with\n  `doListAll` until `page >= total_pages`; returning only page 1 silently\n  truncates (>50 comments on a task is realistic). **Buckets and project\n  views are the exception**: their `ReadAll` takes `_ int, _ int` and returns\n  every row in one page, so fetch them with a single `doList` and unwrap\n  `.items` — paging those would re-fetch the full set and duplicate it.\n  Single-object responses (create/update/read of one entity) stay UNWRAPPED.\n- **v2 flips the create/update verbs.** Creates are **POST** (v1 used PUT):\n  projects, labels, tokens, bot users, project shares, task create,\n  comments, relations, assignees, label-attach, bucket create. Task update\n  is **PATCH** (see below). The bucket-task move is **PUT**.\n- **Task update is `PATCH /tasks/{id}` with `application/merge-patch+json`**\n  (`client.DoMerge` → `UpdateTask(*TaskPatch)`). Only the fields present in\n  the body are written; absent fields are left intact. Build the body from\n  `TaskPatch` (pointer fields, omitempty) — never a whole `client.Task`,\n  whose no-omitempty `done`/`title` would clobber those columns on every\n  call (this was issue #2962).\n- **List search is `q`**, not v1's `s` (`ListParams.Q`). Task-list\n  `filter`/`expand`/`page`/`per_page` keep their names.\n- **`ProjectView.view_kind` and `bucket_configuration_mode` are\n  strings**, not ints. The parent enums (`ProjectViewKind`,\n  `BucketConfigurationModeKind`) have custom `MarshalJSON` that emits\n  `\"kanban\"` / `\"manual\"` etc. Use the string constants in\n  `internal/client/types.go`.\n- **`Task.BucketID` is always 0** in `GET /tasks/:id`. The model has\n  `xorm:\"-\"` on it — the actual bucket lives in a separate\n  `task_buckets` table. Fetch with `?expand=buckets` and use\n  `task.CurrentBucketID(viewID)` to read it.\n- **Task updates do NOT move tasks between buckets.** The task↔bucket\n  relation is row-shaped; use `client.MoveTaskToBucket()` which hits\n  **`PUT /projects/{p}/views/{v}/buckets/{b}/tasks`** with a `{\"task_id\":N}`\n  body (project/view/bucket all come from the URL). The Update path on the\n  server only auto-moves on `done` flips.\n- **Bot user creation is `POST /user/bots`**, not `/bots` — the routes\n  are registered under the `/user` subgroup. Same prefix for\n  `GET /user/bots`.\n- **`APIToken.expires_at` is required.** The struct field has\n  `valid:\"required\"` upstream; sending it omitted or zero fails\n  validation. Use `client.FarFuture` (year 9999) when you mean \"no\n  expiry\" — the frontend does the same.\n- **Task descriptions and comments are HTML, not markdown.** The\n  Vikunja web UI uses TipTap, which calls `getHTML()` on save. The\n  stored field is therefore HTML. The agent prompt template\n  (`internal/commands/prompt.tmpl`) teaches agents the canonical\n  TipTap shapes — most importantly `<ul data-type=\"taskList\">` +\n  `<li data-type=\"taskItem\" data-checked=\"false\"><p>…</p></li>` for\n  interactive checkboxes. We deliberately do **not** convert\n  markdown↔HTML in the CLI; the agent writes HTML directly, which\n  avoids lossy roundtrips on `--description-replace-old/new`. `veans\n  show` displays the raw HTML; humans skim it fine.\n\n## API token permissions\n\n- Vikunja validates token `permissions` against `apiTokenRoutes`, a map\n  built dynamically from registered routes. Group names are derived\n  from the URL path (params stripped, joined by `_`). Examples:\n  - `/projects/:project/views/:view/buckets/:bucket/tasks` →\n    group `projects`, action `views_buckets_tasks`\n  - `/tasks/:task/comments` → group `tasks_comments`, action `create`\n- v1 and v2 deliberately share `(group, permission)` keys:\n  `pkg/models/api_routes.go` normalizes the inverted verbs (v2 POST-create\n  and v1 PUT-create both → `create`; v2 PUT/PATCH-update and v1 POST-update\n  both → `update`), and `CanDoAPIRoute` consults both route tables, treating\n  PATCH as an alias for the stored PUT. So `PermissionsForBot`'s scope map\n  authorizes the v2 calls unchanged, including the PATCH task update.\n- The bucket-task MOVE is `views_buckets_tasks_update` and the\n  buckets-with-tasks LIST is bare `views_buckets_tasks`; bucket CRUD is\n  `views_buckets_{create,update,delete}`. Older servers used `_put`/`_post`\n  suffixes (and an order-dependent bare/`_put` collision), so the bot requests\n  both naming schemes.\n- `client.PermissionsForBot()` calls `GET /routes` at runtime and\n  grants only the intersection of what we want and what the server\n  exposes. **Don't hard-code permission group names** — they drift\n  across Vikunja versions, and discovery keeps the bot's grant valid\n  across upgrades.\n\n## Bot ownership and token minting\n\n- Creating a bot via `POST /user/bots` automatically sets the bot's\n  `bot_owner_id` to the calling user. Only the owner can mint tokens\n  for the bot via `POST /tokens` with `owner_id=<bot_id>`. The init\n  flow does these as a single human-JWT-authenticated batch.\n- Bots have no password and **cannot** authenticate via `POST /login`.\n  After init, `veans login` re-authenticates as the human (not the\n  bot) and mints a fresh bot token.\n\n## OAuth flow\n\n- Vikunja's authorization server requires PKCE/S256 and accepts either\n  `vikunja-…://` custom schemes or RFC 8252 loopback URIs\n  (`http://127.0.0.1:NNN/`, `http://localhost:NNN/`, `http://[::1]:NNN/`).\n  No client registration needed — `client_id` can be any consistent\n  string (we use `veans-cli`).\n- `internal/auth/oauth.go` binds a free port on 127.0.0.1, opens the\n  browser, and captures the callback. The `Shutdown` defer uses\n  `context.WithoutCancel(ctx)` so cancellation at the outer scope\n  still drains the loopback server cleanly.\n- Token exchange goes out as **JSON**. v2's `/oauth/token` accepts both JSON\n  and form-encoded bodies (Huma picks the decoder off the `Content-Type`\n  header), but the standard `golang.org/x/oauth2` client hard-codes form\n  encoding and its own response shape, so we keep the hand-rolled\n  `client.ExchangeOAuthCode` that speaks JSON.\n\n## Credential store\n\n- Lookup chain: keychain → env (`VEANS_TOKEN`) → file\n  (`~/.config/veans/credentials.yml`, mode 0600, atomic-write + flock\n  serialization). `XDG_CONFIG_HOME` is deliberately not honored —\n  agent-only audience runs in a known environment, and the env var\n  was a path-traversal seam for no real benefit.\n- `Chain.Set` falls through to the next backend on error so a missing\n  dbus on a CI runner doesn't block writes — the file backend is the\n  reliable last-resort.\n- File writes go through a tmp file + `Rename`, with `Chmod 0o600`\n  re-asserted on the destination inode so a pre-existing wider mode\n  is narrowed. Concurrent writers (e.g. two `veans login` runs) are\n  serialized via `flock` on `<path>.lock` (Unix only; Windows is a\n  no-op stub since the audience is Linux/macOS).\n- E2e tests override `HOME` per test and `filterEnv(..., \"VEANS_\")`\n  strips any inherited `VEANS_TOKEN` so the developer's keyring\n  stays untouched. Don't bypass the credentials package in tests —\n  leaks between tests will surface as the wrong bot token.\n\n## Trust gate\n\n- `.veans.yml` is repo-controlled, so `internal/trust` gates `cfg.Server`\n  before any credential or request goes out (`newRuntime`, `runLogin`).\n  Trusted origins live in `~/.config/veans/trusted.json` (no XDG override),\n  keyed by `trust.Origin` (scheme + host + port; userinfo rejected).\n- No terminal → no prompt, fail closed. `VEANS_SERVER` with the same origin\n  counts as trusted (CI). `init` records the server the user typed.\n- Tests build a `trust.Gate` on a temp store; never let one write to the\n  real `~/.config/veans`.\n\n## Project identifiers and bot usernames\n\n- Project `Identifier` is `runelength(0|10)`, can be empty. When empty,\n  `Config.FormatTaskID` renders `#NN`; otherwise `PROJ-NN`. Both are\n  accepted by `runtime.resolveTaskID` along with bare integers.\n- Bot username must start with `bot-`; the server enforces it. Hyphens,\n  digits, lowercase letters allowed; no spaces, no commas, no\n  `link-share-N` pattern. `config.SuggestedBotUsername` does the\n  folding for repo names.\n- E2e tests deriving identifiers from a unique suffix should use the\n  trailing chars of `strconv.FormatInt(time.Now().UnixNano(), 36)`.\n  The leading chars barely change between consecutive runs and will\n  collide if you take `[:N]`.\n\n## Audience split\n\nThe CLI is agent-only at runtime; humans never use it for day-to-day\nwork (they use Vikunja's web UI). Two commands serve a human running\none-off setup:\n\n- **`init`** — bootstrap a repo: pick project + view, create bot,\n  share, mint token, write `.veans.yml`, install hooks.\n- **`login`** — rotate the bot's token.\n\nEverything else (`list`, `show`, `create`, `update`, `claim`, `api`,\n`prime`, `version`) is **agent-only**:\n\n- **Emits JSON on stdout unconditionally.** No `--json` flag, no\n  human-formatted variant. `list` is a raw array; `show` / `create` /\n  `update` / `claim` return a single task object.\n- **Errors are JSON on stderr** with non-zero exit — same envelope\n  everywhere (`{\"code\": \"...\", \"error\": \"...\"}`), regardless of which\n  command ran. Stable codes in `internal/output/errors.go`:\n  `NOT_FOUND`, `CONFLICT`, `VALIDATION_ERROR`, `AUTH_ERROR`,\n  `RATE_LIMITED`, `BOT_USERS_UNAVAILABLE`, `NOT_CONFIGURED`,\n  `UNKNOWN`. Don't add ad-hoc strings — wrap with `output.New` /\n  `output.Wrap`.\n- **No `globals.JSON`, no dual rendering paths.** If you find yourself\n  reaching for \"if interactive, do X\" on an agent-facing command,\n  stop — it's not interactive, an agent is on the other end.\n\n## Cobra surface conventions\n\n- `RunE` handlers that don't use `args []string` should rename it to\n  `_` to satisfy revive's `unused-parameter` rule.\n- The bucket-move dance (`MoveTaskToBucket`) runs **after** the field\n  update on `update`, so a status transition can't clobber freshly\n  attached labels. Comments for `--status scrapped` post **before**\n  the bucket move so the audit trail reads in chronological order.\n- Agent-facing commands return the task via `json.NewEncoder(...).Encode(task)`.\n  Adding new top-level keys to `client.Task` is an implicit API\n  change — bump `prime`'s \"useful fields\" note alongside.\n\n## Things to *not* do\n\n- **Don't add an `os/exec.Command`** without ctx — `noctx` is enabled.\n  Use `exec.CommandContext(ctx, …)` and thread the context through.\n- **Don't commit the built binary.** `veans/.gitignore` covers\n  `./veans` and `./veans.exe`.\n- **Don't write to stdout from `prime` when no `.veans.yml` is found.**\n  The hook contract is silent + exit 0 so the snippet is safe to install\n  globally in `~/.claude/settings.json`.\n- **Don't rename canonical bucket titles** without updating\n  `BucketTitleAliases[s][0]` in `internal/status/status.go`, the\n  prompt template (`internal/commands/prompt.tmpl`), and the e2e\n  assertions in lockstep — agents and humans both treat them as\n  fixed strings.\n</yakaai_skills>"}