209 lines
10 KiB
Markdown
209 lines
10 KiB
Markdown
# 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.
|