diff --git a/veans/README.md b/veans/README.md new file mode 100644 index 000000000..54839c07e --- /dev/null +++ b/veans/README.md @@ -0,0 +1,171 @@ +# veans + +A beans-shaped CLI for Vikunja. Drop it into a repo, run `veans init`, paste a +hook snippet into your coding agent's settings, and the agent immediately +knows to track its work in Vikunja instead of in `TodoWrite` or `.beans/`. + +veans is a thin Go binary that wraps Vikunja's REST API with an opinionated +agent-friendly surface and emits a system prompt teaching agents the workflow +(claim → work → in-review → human closes). The agent prompt is re-emitted on +every `SessionStart` and `PreCompact`, so context never goes stale. + +## Quick start + +```sh +# 1. Build (or download) the binary +cd veans && mage build && sudo install ./veans /usr/local/bin/ + +# 2. In a repo with a Vikunja instance reachable +veans init --server https://vikunja.example.com + +# 3. Wire it into Claude Code (.claude/settings.json): +{ + "hooks": { + "SessionStart": [{ "hooks": [{ "type": "command", "command": "veans prime" }] }], + "PreCompact": [{ "hooks": [{ "type": "command", "command": "veans prime" }] }] + } +} + +# OpenCode (.opencode/plugin/veans-prime.ts): +export const VeansPrime = { + event: ["session.start", "compact.before"], + handler: async ({ exec }) => exec("veans prime"), +} +``` + +`veans prime` exits silently with status 0 when no `.veans.yml` is reachable +upward from cwd, so the hook is safe to install in a global `~/.claude/` +without breaking sessions in unrelated repos. + +## What `veans init` does + +1. Authenticates as you (`--token`, or interactive username/password — local + accounts only; SSO/OIDC users should paste a personal API token via + `--token`). +2. Asks you to pick a project and a Kanban view. +3. Bootstraps the canonical buckets if missing: `Todo`, `In Progress`, + `In Review`, `Done`, `Scrapped`. +4. Creates a `bot-` user (Vikunja bot user — no password, no + email, can't log in interactively). +5. Shares the project with the bot at read+write. +6. Mints a long-lived API token for the bot via `PUT /tokens` with + `owner_id`, scoped to the discovered route groups (tasks, comments, + labels, relations, assignees, etc.) the server actually exposes. +7. Stores the token in your OS keychain (or + `~/.config/veans/credentials.yml` if no keychain is available). +8. Writes `.veans.yml` to the repo root. + +The token stored is the bot's, not yours. The human's transient session is +discarded as soon as init finishes — rotate or revoke the bot independently +without affecting your own session. + +## Commands + +``` +veans init OAuth/login → create bot → mint token → write .veans.yml +veans prime emit system prompt for agents (silent if no .veans.yml) +veans list filtered list (--ready, --mine, --branch, --filter, --status, --json) +veans show view a task (--json for raw object) +veans create "title" --description, --label, --status, --priority, --parent, --blocked-by +veans update --status, --title, --priority, --label-add/remove, + --description, --description-replace-old/new, --description-append, + --comment, --reason, --if-unchanged-since +veans claim assign the bot, move to In Progress, tag with current branch label +veans api METHOD PATH raw REST passthrough — escape hatch for endpoints not wrapped here +veans login re-mint the bot's token (rotation) +veans version +``` + +Task IDs accept `PROJ-NN` (when the project has an identifier), `#NN` +(when it doesn't), or a bare integer. + +## `.veans.yml` + +Committed to the repo root. The numeric IDs are the source of truth; cached +identifiers and bot username are for human-readable output. + +```yaml +server: https://vikunja.example.com +project_id: 42 +project_identifier: PROJ # may be "" — task IDs render as #NN then +view_id: 7 +buckets: + todo: 11 + in_progress: 12 + in_review: 13 + done: 14 + scrapped: 15 +bot: + username: bot-myrepo + user_id: 99 +``` + +## Credentials + +Resolved in order on every command: + +1. **OS keychain** (macOS Keychain, Windows Credential Manager, + libsecret/gnome-keyring on Linux), via `github.com/zalando/go-keyring`. +2. **`VEANS_TOKEN`** env var (read-only). Optionally pin to a server with + `VEANS_SERVER`. Intended for CI / containers. +3. **`~/.config/veans/credentials.yml`** (mode 0600) — automatic fallback + when the keychain is unavailable. Honors `XDG_CONFIG_HOME`. + +## Mage targets + +``` +mage build # go build -o ./veans ./cmd/veans +mage test # unit tests across the module +mage test:filter EXPR # go test -run EXPR ./... +mage test:e2e # e2e suite (needs VEANS_E2E_API_URL) +mage lint / lint:fix # golangci-lint +mage fmt # go fmt ./... +mage clean # remove built binary +``` + +## End-to-end tests + +The suite in `e2e/` assumes a running Vikunja API. Locally, point it at any +dev instance: + +```sh +export VEANS_E2E_API_URL=http://localhost:3456 +export VEANS_E2E_ADMIN_USER=user1 +export VEANS_E2E_ADMIN_PASS=12345678 # canonical fixture password +mage test:e2e +``` + +CI spins Vikunja up the same way the frontend Playwright suite does — see +`.github/workflows/veans-e2e.yml`. The workflow builds the parent API +binary, starts it with `VIKUNJA_DATABASE_TYPE=sqlite`, +`VIKUNJA_DATABASE_PATH=memory`, fixtures from `pkg/db/fixtures/`, and runs +`mage test:e2e` from this directory. + +E2E tests never touch the developer's keychain — they override `HOME` and +`XDG_CONFIG_HOME` per test, which forces the credential store to fall +through to its file backend. + +## Status model + +| Status | Bucket name | Done flag | Who moves there? | +| ------------- | -------------- | --------- | ---------------------------------------- | +| `todo` | Todo | false | created here by default | +| `in-progress` | In Progress | false | `veans claim` / `update -s in-progress` | +| `in-review` | In Review | false | the agent, when work is finished | +| `completed` | Done | true | humans / merge hook only | +| `scrapped` | Scrapped | true | the agent, with `--reason` | + +The agent never moves tasks to `completed` itself — it parks them in +`In Review` and a human (or the future merge hook) closes them once the +PR lands. + +## Out of scope (for now) + +- OAuth 2.0 device flow (RFC 8628) — Vikunja's OAuth provider needs a + registered client + an existing JWT to authorize, which adds friction + v0 doesn't need; we use `POST /login` (or paste-in `--token`). +- Project-scoped API tokens — Vikunja doesn't ship them yet. The + credential schema's `scope` field is forward-compatible for when it does. +- Auto-installing hook snippets. We print them; you paste them. +- Merge-hook GitHub Action that auto-closes tasks on PR merge — separate + repo, future work.