@maudecode/kanvil 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -2
- package/apps/server/.env.example +41 -0
- package/apps/server/dist/auth-BscXb1NQ.mjs +2666 -0
- package/apps/server/dist/config-Qxr_7oqZ.mjs +575 -0
- package/apps/server/dist/configure.mjs +23 -0
- package/apps/server/dist/index.mjs +1754 -0
- package/apps/server/dist/migrate-C9f4ozB4.mjs +17 -0
- package/apps/server/dist/migrate.mjs +12 -0
- package/apps/server/dist/recover.mjs +52 -0
- package/apps/server/drizzle/0000_foundation.sql +437 -0
- package/apps/server/drizzle/0001_claim_identity.sql +1 -0
- package/apps/server/drizzle/0002_transactional_attachments.sql +6 -0
- package/apps/server/drizzle/0003_durable_claim_fence.sql +4 -0
- package/apps/server/drizzle/0004_lumpy_archangel.sql +6 -0
- package/apps/server/drizzle/0005_unique_toxin.sql +28 -0
- package/apps/server/drizzle/0006_instance_services.sql +48 -0
- package/apps/server/drizzle/0007_optional_broker.sql +6 -0
- package/apps/server/drizzle/0008_workspaces.sql +37 -0
- package/apps/server/drizzle/0009_project_icons.sql +6 -0
- package/apps/server/drizzle/0010_project_labels.sql +14 -0
- package/apps/server/drizzle/0011_due_dates.sql +1 -0
- package/apps/server/drizzle/0012_label_colors.sql +7 -0
- package/apps/server/drizzle/meta/0000_snapshot.json +3210 -0
- package/apps/server/drizzle/meta/0001_snapshot.json +3216 -0
- package/apps/server/drizzle/meta/0002_snapshot.json +3255 -0
- package/apps/server/drizzle/meta/0003_snapshot.json +3262 -0
- package/apps/server/drizzle/meta/0004_snapshot.json +3302 -0
- package/apps/server/drizzle/meta/0005_snapshot.json +3518 -0
- package/apps/server/drizzle/meta/0006_snapshot.json +3842 -0
- package/apps/server/drizzle/meta/0007_snapshot.json +3874 -0
- package/apps/server/drizzle/meta/0008_snapshot.json +4051 -0
- package/apps/server/drizzle/meta/0009_snapshot.json +4105 -0
- package/apps/server/drizzle/meta/0010_snapshot.json +4174 -0
- package/apps/server/drizzle/meta/0011_snapshot.json +4180 -0
- package/apps/server/drizzle/meta/0012_snapshot.json +4226 -0
- package/apps/server/drizzle/meta/_journal.json +97 -0
- package/apps/web/dist/apple-touch-icon.png +0 -0
- package/apps/web/dist/assets/CompositeItem-D9KvUNiq.js +1 -0
- package/apps/web/dist/assets/admin-settings-B0u2-aAK.js +1 -0
- package/apps/web/dist/assets/camera-Cv5wKkKn.js +1 -0
- package/apps/web/dist/assets/date-picker-lhXxKIlN.js +1 -0
- package/apps/web/dist/assets/ellipsis-CN74O_EP.js +1 -0
- package/apps/web/dist/assets/forms-456UbfIC.js +92 -0
- package/apps/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/apps/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/apps/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/apps/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/apps/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/apps/web/dist/assets/index-CVzq9BMC.css +2 -0
- package/apps/web/dist/assets/index-Dt8wYIVA.js +51 -0
- package/apps/web/dist/assets/instance-BCNvOvYa.js +1 -0
- package/apps/web/dist/assets/list-order-1UoOT5nR.js +1 -0
- package/apps/web/dist/assets/lucide-U995NW8E.js +1 -0
- package/apps/web/dist/assets/member-directory-C81ugis6.js +1 -0
- package/apps/web/dist/assets/popover-BWk65ByY.js +1 -0
- package/apps/web/dist/assets/project-settings-DnpVfVf5.js +1 -0
- package/apps/web/dist/assets/rotate-ccw-BLeWpuu1.js +1 -0
- package/apps/web/dist/assets/settings-DUXuOgWn.js +1 -0
- package/apps/web/dist/assets/settings-layout-xR4hrfte.js +1 -0
- package/apps/web/dist/assets/table-BQbZG4BK.js +1 -0
- package/apps/web/dist/assets/ticket-detail-B9M_eaVu.js +2 -0
- package/apps/web/dist/assets/ticket-editor-Chg1lDuU.js +1 -0
- package/apps/web/dist/assets/ticket-list-C8500tXj.js +5 -0
- package/apps/web/dist/assets/ticket-tags-cbwQ93Gt.js +1 -0
- package/apps/web/dist/assets/upload-5Wg5ou_Z.js +1 -0
- package/apps/web/dist/assets/workspace-settings-Dte44Kkx.js +1 -0
- package/apps/web/dist/brand/icon-192.png +0 -0
- package/apps/web/dist/brand/icon-512.png +0 -0
- package/apps/web/dist/brand/mark-blue.svg +1 -0
- package/apps/web/dist/brand/mark-white.svg +1 -0
- package/apps/web/dist/brand/mark.svg +1 -0
- package/apps/web/dist/brand/maskable-512.png +0 -0
- package/apps/web/dist/brand/social.png +0 -0
- package/apps/web/dist/favicon.ico +0 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/fonts/OFL.txt +93 -0
- package/apps/web/dist/fonts/ibm-plex-sans-latin.woff2 +0 -0
- package/apps/web/dist/icon.svg +1 -0
- package/apps/web/dist/index.html +31 -0
- package/apps/web/dist/site.webmanifest +29 -0
- package/apps/web/dist/third-party-icons.txt +64 -0
- package/apps/web/dist/third-party-themes.txt +548 -0
- package/bin/kanvil.mjs +27 -0
- package/compose.yml +66 -0
- package/docs/api.md +87 -0
- package/docs/branding.md +13 -0
- package/docs/design.md +37 -0
- package/docs/infrastructure.md +94 -0
- package/docs/mobile.md +77 -0
- package/docs/plan.md +111 -0
- package/docs/releasing.md +18 -0
- package/docs/self-hosting.md +204 -0
- package/docs/themes.md +38 -0
- package/docs/verification.md +83 -0
- package/docs/workspaces.md +21 -0
- package/package.json +45 -4
package/docs/api.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Kanvil interfaces
|
|
2
|
+
|
|
3
|
+
The HTTP API, MCP tools, and apps execute the same ticket operations and permission checks. HTTP schemas are available at `/api/openapi.json`.
|
|
4
|
+
|
|
5
|
+
Runnable MCP and durable realtime examples, plus the worker ownership sequence, are in [examples](../examples/README.md).
|
|
6
|
+
|
|
7
|
+
## Authentication
|
|
8
|
+
|
|
9
|
+
Browser and native clients use Better Auth sessions with local login or configured OIDC. Public registration is closed. Initial owner setup requires `KANVIL_SETUP_TOKEN` and is available only before any account exists.
|
|
10
|
+
|
|
11
|
+
Unattended consumers use revocable, project-scoped API credentials in `Authorization: Bearer <key>`. Create credentials through an authenticated account at `POST /api/v1/credentials`, with a name, project IDs, and `read`, `write`, `claim`, or `admin` scopes. A scope never grants more access than the owning account has. The secret is returned only on creation. Delete a credential at `/api/v1/credentials/{id}` to revoke it.
|
|
12
|
+
|
|
13
|
+
## Writes and ownership
|
|
14
|
+
|
|
15
|
+
Use the ticket's current `version` as `expectedVersion` for updates. A stale version returns `409 version_conflict` with `details.currentVersion`; fetch the current record and reconcile the change.
|
|
16
|
+
|
|
17
|
+
Reuse the same UUID `requestId` when retrying a write. Reusing it with a different operation or payload returns `409 idempotency_conflict`. Bulk edits either all commit or all roll back, and publish one `ticket.updated` event per ticket with consecutive IDs in a single batch.
|
|
18
|
+
|
|
19
|
+
Signed-in sessions may make `KANVIL_SESSION_WRITE_LIMIT` write requests per minute (120 by default) to `/api/v1`, and MCP OAuth clients get the same budget for write tools; exceeding it returns `429 rate_limited` with a `Retry-After` header. API keys keep the rate limit applied by the authentication layer. The counter is per application process.
|
|
20
|
+
|
|
21
|
+
Claims require an eligible ticket and current version. Successful claiming moves it to In progress. Renew an unexpired claim using its ID and the same credential. Once a ticket has been claimed, machine ticket updates and evidence submissions always require a current matching claim ID, including after release or recovery. Submit completion and evidence before releasing ownership. Human sessions can edit and reschedule work without a machine claim. Release unexpired ownership after the worker stops or completes. The ticket response exposes `canReleaseClaim` for the exact credential or client identity; sharing an account does not confer ownership. An expired claim remains blocking until a project administrator confirms the old worker stopped and explicitly recovers it; recovery does not automatically reschedule work.
|
|
22
|
+
|
|
23
|
+
Evidence records identify the submitting account and supplied commit. A reported passing outcome is an assertion by that identity, not independent verification by Kanvil.
|
|
24
|
+
|
|
25
|
+
Attachments use `upload_attachment` or the corresponding HTTP POST with `name`, `contentType`, and base64 `data`, up to 10 MB. Bytes and metadata commit together in PostgreSQL. Download content at `/api/v1/projects/{projectId}/attachments/{id}/content` using the same authorization. Filenames are metadata, never filesystem paths. Deleting an attachment deletes its bytes transactionally.
|
|
26
|
+
|
|
27
|
+
Tickets carry an optional `dueAt` ISO timestamp. `list_tickets` accepts `due` as `overdue` (past due), `today` (due within the next 24 hours), `week` (due within the next 7 days), `scheduled`, or `none`; the first three apply to open tickets only and use the database clock and `sort: "due"` for soonest first with undated tickets last. Other sorts: `priority` (default), `updated`, `created`, `title`, `labels`, `number`, `status`, `difficulty`, `kind`, and `assignee`. Each has a natural direction (newest first for `updated` and `created`, ascending otherwise); pass `order: "asc"` or `"desc"` to override it. Ties break by ticket number, and null values sort last in both directions. The `assigneeId` filter takes a user id or `none` for unassigned tickets. `list_projects` returns `openTickets` and `overdueTickets` per project (unarchived tickets not done or cancelled; overdue means a due date in the past).
|
|
28
|
+
|
|
29
|
+
`search_tickets` (`GET /api/v1/tickets/search?q=...`) finds unarchived tickets by exact key or text across every project the caller can read, optionally limited to one `workspaceId`, returning each ticket with its project `id`, `key`, and `name`. Exact key matches sort first.
|
|
30
|
+
|
|
31
|
+
Use `list_tickets` with `parentId` set to a parent UUID or ticket key to page through child tickets. Set `archived: "include"` to include archived children. `list_relations` also returns the current source/target keys and full titles, so consumers can show readable dependency context without separate lookups.
|
|
32
|
+
|
|
33
|
+
`list_ticket_labels` (`GET /api/v1/projects/{projectId}/labels`) returns distinct label names and ticket counts for the whole authorized project. Its `archived` option matches ticket listing. The `label` ticket filter matches an exact label; combine it with other filters or saved views. `sort: "labels"` orders by the first label alphabetically, ignoring case and label-array order, followed by ticket number. Tickets without labels come last.
|
|
34
|
+
|
|
35
|
+
`list_project_labels` (`GET /api/v1/projects/{projectId}/labels/catalog`) returns reusable definitions with stable IDs, names, colors, versions, and usage counts including archived tickets. Unused definitions remain available. Existing ticket label names are backfilled during upgrade; new freeform labels from ticket writes register automatically with gray as their color. Ticket payloads continue to use label-name arrays.
|
|
36
|
+
|
|
37
|
+
Project administrators can `create_project_label` (`POST .../labels`), `update_project_label` (`PATCH .../labels/{labelId}`), and `delete_project_label` (`DELETE .../labels/{labelId}`) through HTTP or MCP. Creation takes `name` and optional `color`; updates take `name`, `color`, and `expectedVersion`; deletion takes `expectedVersion`. Renames update every affected ticket, including archived tickets, and saved-view label filters. Deletion removes the label from tickets and removes its filter from saved views, retaining other filters. These changes commit atomically, preserve ticket write/claim checks, increment affected ticket versions, and emit normal `ticket.updated` events plus `label.created`, `label.updated`, or `label.deleted`. Recoloring emits a label event without changing ticket versions. Realtime consumers can refresh the label catalog on label events.
|
|
38
|
+
|
|
39
|
+
Label colors are a preset name (`default`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `gray`) or a custom `#rrggbb` hex value. Saving a label with a hex value adds it to the site-wide custom palette; `list_label_colors` (`GET /api/v1/label-colors`) returns that palette newest first and `remove_label_color` (`POST /api/v1/label-colors/remove`) drops an entry without recoloring the labels that use it; only an instance administrator or the person who added the color may remove it, and the removal publishes `label.color_removed` to every project the caller can write. Saving a label with a color it already had does not re-add a removed color.
|
|
40
|
+
|
|
41
|
+
`add_relation` accepts `blocks`, `related`, `duplicate`, `causes`, `clones`, `implements`, or `reviews`. `direction` defaults to `outgoing` for existing clients. Use `incoming` to express the inverse from the current ticket: for example, `{ticketId: "<current>", targetId: "<blocker>", type: "blocks", direction: "incoming"}` means the current ticket is blocked by the selected ticket. The server stores a single canonical edge, checks project access on both tickets, and rejects dependency cycles in either direction. Related links are symmetric. Only blocking dependencies add readiness blockers; duplicate sources remain ineligible for agent work. Other link types are informational. Parent/child hierarchy continues to use `parentId` rather than a second relationship edge.
|
|
42
|
+
|
|
43
|
+
## Realtime
|
|
44
|
+
|
|
45
|
+
`GET /api/v1/snapshot?projectId=<id>` returns a consistent project snapshot and event cursor. Connect to `/api/v1/events?projectId=<id>&cursor=<cursor>` to replay committed changes and continue receiving new ones.
|
|
46
|
+
|
|
47
|
+
The SSE feed sends `change` and `checkpoint` events with durable IDs. Persist checkpoints only after processing preceding changes. Delivery is at least once: handle duplicates. Reconnect with `cursor` or `Last-Event-ID`. A `410 snapshot_required` response means fetch a new snapshot. Consumers of the unfiltered feed can obtain `/api/v1/checkpoint`, then refresh their authorized queries before subscribing from that cursor. Without a cursor, the feed starts at the current position; wait for its first checkpoint before fetching initial query results.
|
|
48
|
+
|
|
49
|
+
Events are permission-filtered and contain ticket IDs, versions, and change metadata. Fetch authoritative records through the API. Membership, account, and credential access are rechecked while streaming. An SSE connection consumes one API request; internal frames and heartbeats do not consume more of the credential's rate allowance. Reverse proxies must preserve streaming and disable response buffering.
|
|
50
|
+
|
|
51
|
+
The portable `@kanvil/client` package supplies validated calls, one safe network retry for registered operations using the original request ID, and a parsed SSE consumer. Its `subscribe` method accepts an abort signal, processing/checkpoint callbacks, connection-state updates, and an optional fresh-snapshot callback for expired cursors. Its `watch` helper coordinates initial query refresh and expired-cursor recovery for query caches. Its query/action options work with TanStack Query without importing React into the SDK. Native clients can set `credentials: "omit"` and supply their securely stored session cookie through the headers callback. Custom `request` calls do not retry automatically, because arbitrary endpoints may lack idempotency. Actions execute immediately and are not queued for offline submission.
|
|
52
|
+
|
|
53
|
+
## Notifications
|
|
54
|
+
|
|
55
|
+
Subscribe to a ticket with `set_subscription`. Inbox entries are created atomically with subscribed ticket events and filtered by current project access when read. Human edits do not notify their author; agent credentials do notify their subscribing owner. Event data includes the ticket key/title and server-recorded `source.kind` and `source.id` for the session account or credential.
|
|
56
|
+
|
|
57
|
+
Signed-in accounts can read and update `/api/v1/notifications/preferences` with `{ "inApp": true, "push": false }` using GET/PUT. Disabling Inbox notifications affects future entries; existing history is retained. Read the latest 100 entries at `/api/v1/notifications` and mark selected UUIDs read at POST `/api/v1/notifications/read` with `{ "ids": [...] }`. Credentials cannot change personal preferences or mark Inbox entries read.
|
|
58
|
+
|
|
59
|
+
Omitting `push` from a preference update retains its current value. Explicitly unsubscribing cancels queued push notices for that ticket. Ticket deletion preserves already queued notices, including the deletion event, while account and project access still apply.
|
|
60
|
+
|
|
61
|
+
When `/api/v1/info` reports `push: true`, account sessions can POST or DELETE `/api/v1/notifications/device` with `{ "token": "ExpoPushToken[...]" }`. Registration is tied to the current login session. Reassigning a device token to another account removes its previous account's pending deliveries. Push preferences apply to all registered devices; unregistering affects only the supplied device. Push payloads contain `server`, `projectId`, `ticketId`, and `notificationId`; native routing accepts only the currently connected server. Tokens and server-held Expo credentials are never returned by a listing endpoint.
|
|
62
|
+
|
|
63
|
+
## MCP
|
|
64
|
+
|
|
65
|
+
The authenticated Streamable HTTP endpoint is `/mcp`. Tool inputs use the shared HTTP contracts, including expected versions, request IDs, and claim IDs. Successful structured results have a `result` property. Business failures return an error result with a stable code.
|
|
66
|
+
|
|
67
|
+
Clients supporting custom headers can connect using a scoped API credential. Interactive clients can discover OAuth authorization through the endpoint's protected-resource metadata, then sign in and consent through Kanvil. Resource scopes are `kanvil:read`, `kanvil:write`, `kanvil:claim`, and `kanvil:admin`. The web app supplies the login and consent pages. Configure OIDC callback URLs as `/api/auth/callback/oidc` and explicitly link an existing local account from Settings before using OIDC sign-in. Protocol compatibility is tested using the official SDK, including discovery, tool listing, and writes.
|
|
68
|
+
|
|
69
|
+
## Instance administration and profile photos
|
|
70
|
+
|
|
71
|
+
Human sessions use `GET/PATCH /api/v1/admin/instance` for versioned instance settings, and `POST /api/v1/admin/{storage,search,broker}/test` to verify an integration. These routes require the instance admin role; API keys and MCP tokens cannot configure the instance. `POST /api/v1/admin/search/reindex` queues a rebuild, and `POST /api/v1/admin/storage/cleanup` collects unused objects. Secret fields are write-only; omitted broker settings preserve the current provider for older callers. See [infrastructure](infrastructure.md) for schemas, configuration precedence, and recovery rules.
|
|
72
|
+
|
|
73
|
+
A signed-in user uploads their own photo with `POST /api/v1/profile/photo` using `{data: base64, contentType}` and removes it with `DELETE` on the same path. The response is `{image: string | null}`. Images are limited to 512,000 bytes and PNG/JPEG/WebP. `GET /api/v1/users/{id}/photo` requires a human session. Attachments and profile photos share the same configured storage service. These interfaces and their schemas appear in `/api/openapi.json`.
|
|
74
|
+
|
|
75
|
+
## Workspaces and project icons
|
|
76
|
+
|
|
77
|
+
The shared operation registry includes `list_workspaces`, `create_workspace`, `update_workspace`, `list_workspace_members`, and `set_workspace_member`; each is available through HTTP and MCP. See [workspace access](workspaces.md) for inheritance, private projects, guests, and safe upgrades. `list_projects` accepts an optional `workspaceId`; create projects with an explicit workspace and `visibility: "workspace" | "private"`. Older calls that omit a workspace choose a writable workspace, creating a personal one when necessary. Ticket prefixes remain unique across the instance.
|
|
78
|
+
|
|
79
|
+
`get_project` reads workspace and appearance metadata. `set_project_icon` accepts `expectedVersion` and `icon`: `{kind:"library", name:"rocket", color:"purple"}`, `{kind:"image", contentType:"image/png", data:base64}`, or `null` to reset. Library names are checked against the bundled Lucide collection. Image limits and supported formats match profile photos. `GET /api/v1/projects/{projectId}/icon` checks project read access, including project-bound API keys. Uploaded icons share blob storage, orphan cleanup protection, and backup requirements with attachments and profile photos.
|
|
80
|
+
|
|
81
|
+
## Account administration
|
|
82
|
+
|
|
83
|
+
Human instance administrators can search `GET /api/v1/admin/users` with `q` (name or email), `role` (`all`, `user`, `admin`), `status` (`all`, `active`, `disabled`), `limit`, and `offset`. Results contain display data and counts, never passwords or session tokens. `GET /api/v1/admin/users/{id}` adds workspace memberships, direct project grants, sign-in provider names, and an active-session count.
|
|
84
|
+
|
|
85
|
+
The web app uses Better Auth's existing account creation, profile, role, suspension, and session-management operations. Administrative role/status mutations share a serialized guard that preserves an active instance administrator, including the alternative user-update endpoint. `POST /api/v1/admin/users/{id}/password` accepts `{password}` (12–128 characters), sets a local password through the auth library, and revokes existing sessions. Password changes through the auth plugin's administrative endpoint also revoke sessions. Password reset keeps API keys; disabling the account blocks API-key access as well as sign-in.
|
|
86
|
+
|
|
87
|
+
The web client discards persisted administrator data when the account's instance role changes. Personal settings, workspace administration, and instance administration have separate navigation and server-side permissions.
|
package/docs/branding.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Kanvil branding
|
|
2
|
+
|
|
3
|
+
The approved anvil is traced vector artwork. `brandMark` in [theme.ts](../packages/contracts/src/theme.ts) is its source geometry. The web and native `Brand` components render that path directly. Edit this source and run `pnpm assets:generate` to regenerate the files; avoid editing individual exports.
|
|
4
|
+
|
|
5
|
+
The generator produces:
|
|
6
|
+
|
|
7
|
+
- Web: SVG and ICO favicons, a Safari pinned-tab mark, an Apple touch icon, a web manifest with regular/maskable icons, and a social-sharing image.
|
|
8
|
+
- Branding: transparent, blue, and white SVG marks in `apps/web/public/brand/`. The README selects a mark for the reader's theme.
|
|
9
|
+
- Native: the opaque application icon, adaptive and monochrome foreground, notification mark, and light/dark launch-screen images. Expo configuration connects these files to the native builds.
|
|
10
|
+
|
|
11
|
+
The app-icon exports have square backgrounds; the operating system supplies the corner mask. Standalone marks have transparent backgrounds. The anvil keeps its natural proportions, with optical centering for square icons and extra space for adaptive masking.
|
|
12
|
+
|
|
13
|
+
The self-hosted service serves these public files and fonts, with uncached HTML and an absolute social-image URL based on `KANVIL_URL`. Native icons and launch screens require a new binary; changing the image files alone does not update an installed app.
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Kanvil interface
|
|
2
|
+
|
|
3
|
+
Kanvil is a working surface for people supervising agent work. The ticket title and its requirements come first; status, ownership, and readiness remain separate facts. This redesign follows [Anthropic's frontend-design guidance](https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md).
|
|
4
|
+
|
|
5
|
+
## Direction
|
|
6
|
+
|
|
7
|
+
Use a precise, quiet workbench with blue selection and clear typographic hierarchy. Keep real tickets on screen instead of decorative dashboards. Desktop gets a narrow workspace rail and a generous reading surface; phones get full-width ticket rows with metadata beneath their titles.
|
|
8
|
+
|
|
9
|
+
Core light tokens: canvas `#f5f6f8`, surface `#ffffff`, ink `#202632`, secondary `#626c7c`, divider `#dce1e9`, blue `#3458c5`.
|
|
10
|
+
|
|
11
|
+
Core dark tokens: canvas `#171a21`, surface `#1e222c`, ink `#edf0f7`, secondary `#a2acc0`, divider `#343a49`, blue `#a4b8ff`.
|
|
12
|
+
|
|
13
|
+
Web typography uses self-hosted IBM Plex Sans, with its open counters and distinctive technical character. Ticket keys use the platform monospace font at a readable size. iPhone uses the native system family and Dynamic Type. Headings carry hierarchy through size and weight, not all-capital labels.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Desktop Phone
|
|
17
|
+
┌──────────┬─────────────────────────┐ ┌────────────────────────┐
|
|
18
|
+
│ Kanvil │ Project New ticket│ │ Kanvil Appearance│
|
|
19
|
+
│ Projects │ Search and filters │ │ Workspace navigation │
|
|
20
|
+
│ Inbox │ Saved views │ │ Project New ticket│
|
|
21
|
+
│ │ Key Full title Status │ │ Search and filters │
|
|
22
|
+
│ Projects │ Key Full title Status │ │ □ Key │
|
|
23
|
+
│ │ │ │ Complete ticket title│
|
|
24
|
+
│ Settings │ │ │ Status Priority Level│
|
|
25
|
+
└──────────┴─────────────────────────┘ └────────────────────────┘
|
|
26
|
+
|
|
27
|
+
Ticket detail: title → requirements + discussion | agent state + relationships.
|
|
28
|
+
On narrower screens the same sections form one reading column.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Reuse and checks
|
|
32
|
+
|
|
33
|
+
Extend the existing `Button`, `Status`, `Row`, form controls, table cells, and shared palette. Keep one ticket row renderer across desktop and phone layouts; preserve table semantics and selection. Reuse the existing brand treatment through a shared web component. Keep native navigation, pickers, sharing, and document selection.
|
|
34
|
+
|
|
35
|
+
The first concept retained the old wide table on phones; it failed the original requirement to read complete titles without sideways scrolling. The revised layout reflows the same cells. Avoid decorative counters, hero sections, gradients, cards, and animation that competes with work.
|
|
36
|
+
|
|
37
|
+
Validate real workspace, ticket, editor, Inbox, settings, and empty/error states in both themes. Check long titles, phone widths, zoom, keyboard focus, reduced motion, theme persistence, local/OIDC login, edits, attachments, and live conflict handling. Android runtime is outside this redesign goal.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Optional infrastructure
|
|
2
|
+
|
|
3
|
+
Kanvil needs only its application service and PostgreSQL. Administrators can configure optional S3-compatible storage, Elasticsearch-compatible search, and RabbitMQ under **Administration**. Local login and OIDC continue to share the same accounts and permissions.
|
|
4
|
+
|
|
5
|
+
## Configuration and credentials
|
|
6
|
+
|
|
7
|
+
Provider choices and integration credentials are saved in PostgreSQL. Credentials are encrypted with AES-256-GCM using an instance key derived from `BETTER_AUTH_SECRET`; they are never returned by the administration API. Empty secret fields retain saved credentials. Keep this secret with the database backup or the saved credentials cannot be decrypted after restoration.
|
|
8
|
+
|
|
9
|
+
An explicit `KANVIL_STORAGE_PROVIDER`, `KANVIL_SEARCH_PROVIDER`, or `KANVIL_BROKER_PROVIDER` overrides the corresponding admin selection and makes its fields read-only. Omit a provider variable to use the saved admin configuration. Setting `local` or `postgres` explicitly also overrides saved external settings. Deployment changes require an application restart; admin changes apply to running instances automatically.
|
|
10
|
+
|
|
11
|
+
Test connection buttons perform actual readback. Saving a changed external configuration also tests it before activation. Changing the instance name does not require an unchanged external service to be online. Concurrent settings edits use a version check and return HTTP 409 instead of overwriting each other.
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
Local storage keeps attachments and profile photos as database bytes. No extra filesystem volume or service is needed, and these bytes are included in PostgreSQL backups.
|
|
16
|
+
|
|
17
|
+
For S3-compatible storage, create a private bucket and choose **File storage → S3-compatible object storage**. Supply its endpoint, region, bucket, key prefix, and credentials. Leave the endpoint empty for Amazon S3. Omitted credentials use the AWS SDK credential chain, including workload identity when configured. Use path-style URLs for providers that require them.
|
|
18
|
+
|
|
19
|
+
The deployment equivalent is:
|
|
20
|
+
|
|
21
|
+
```dotenv
|
|
22
|
+
KANVIL_STORAGE_PROVIDER=s3
|
|
23
|
+
S3_ENDPOINT=https://s3.example.com
|
|
24
|
+
S3_REGION=us-east-1
|
|
25
|
+
S3_BUCKET=kanvil
|
|
26
|
+
S3_PREFIX=kanvil/
|
|
27
|
+
S3_FORCE_PATH_STYLE=true
|
|
28
|
+
S3_ACCESS_KEY_ID=your-access-key
|
|
29
|
+
S3_SECRET_ACCESS_KEY=your-secret-key
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Grant only object read/write/delete access under the selected prefix. Kanvil does not make the bucket public or expose signed bucket URLs. Downloads pass through Kanvil's authorization checks. Profile uploads accept PNG, JPEG, and WebP, and the browser crops and resizes them before upload.
|
|
33
|
+
|
|
34
|
+
Uploads commit their bytes and destination in the same database transaction as their metadata. A worker then uploads the object, reads it back, verifies SHA-256, and clears the database copy. Pending files remain readable from PostgreSQL during an outage. A crash after the remote upload safely retries the same object key. Slow external transfers do not hold the ticket mutation lock. Administration shows pending transfers and failures.
|
|
35
|
+
|
|
36
|
+
Changing providers affects new files. Existing files retain their original backend and prefix; switching to local storage does not make old S3 objects local. Saved credentials for an existing location can be rotated without rewriting file references. Unreferenced files are collected after a grace period, with a manual cleanup action in Administration. Keep old bucket access available while files still reference it.
|
|
37
|
+
|
|
38
|
+
Back up the database, `BETTER_AUTH_SECRET`, and every referenced S3 bucket. A database dump includes local bytes and pending transfers, but completed S3 transfers require a bucket backup. Restore both before reopening downloads. Search and broker data are rebuildable and are not file backups.
|
|
39
|
+
|
|
40
|
+
## Search
|
|
41
|
+
|
|
42
|
+
PostgreSQL searches ticket keys, titles, descriptions, acceptance criteria, labels, and scopes. It remains the default and the fallback when an optional search service is behind or unavailable.
|
|
43
|
+
|
|
44
|
+
Choose **Search → Elasticsearch-compatible search** and a dedicated, unused index name. Kanvil records its instance identity in the index mapping and refuses to reset an index owned by another application or instance. The connection test verifies index creation, object write/read/delete, and ownership. Select no authentication, an API key, or username/password authentication. Switching methods clears credentials from the previous method.
|
|
45
|
+
|
|
46
|
+
```dotenv
|
|
47
|
+
KANVIL_SEARCH_PROVIDER=elasticsearch
|
|
48
|
+
ELASTICSEARCH_URL=https://elasticsearch.example.com
|
|
49
|
+
# Omit the index variable to derive a unique name from the instance identity.
|
|
50
|
+
ELASTICSEARCH_INDEX=kanvil-instance
|
|
51
|
+
ELASTICSEARCH_AUTH=apiKey
|
|
52
|
+
ELASTICSEARCH_API_KEY=your-api-key
|
|
53
|
+
# For basic authentication, use ELASTICSEARCH_AUTH=basic and set
|
|
54
|
+
# ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD instead.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The implementation uses basic index, mapping, bulk, search, scroll, and delete-by-query APIs, without paid search features. Integration tests run against Elasticsearch 9.5.5. Other compatible distributions need the same APIs and should be validated against their version before deployment.
|
|
58
|
+
|
|
59
|
+
The index worker replays durable PostgreSQL events and checkpoints only acknowledged updates. A missing or expired checkpoint rebuilds the index. Full-text search adds fuzzy matching once the index catches up; requests fall back to PostgreSQL while it catches up or if external requests fail. Server-side project authorization and all other ticket filters still run in PostgreSQL, including when the external service returns an incorrect ticket ID. Use **Rebuild index** after restoring a database or changing an external index independently.
|
|
60
|
+
|
|
61
|
+
## RabbitMQ
|
|
62
|
+
|
|
63
|
+
Choose **Event delivery → RabbitMQ** to fan out event wakeups between application instances and wake background workers. Set an AMQP or AMQPS connection URL, including an existing virtual host, and an exchange name. Use a dedicated virtual host with permission to declare the exchange and temporary queues, bind them, publish, and consume. AMQPS uses the system trust store; a private CA can be supplied through `NODE_EXTRA_CA_CERTS`.
|
|
64
|
+
|
|
65
|
+
```dotenv
|
|
66
|
+
KANVIL_BROKER_PROVIDER=rabbitmq
|
|
67
|
+
RABBITMQ_URL=amqps://kanvil:password@rabbitmq.example.com:5671/kanvil
|
|
68
|
+
RABBITMQ_EXCHANGE=kanvil.events
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Percent-encode credentials and virtual host names in the URL. The default `/` virtual host can be written as `/%2F`. Administration masks the URL credentials and displays only its endpoint and exchange.
|
|
72
|
+
|
|
73
|
+
Kanvil declares a durable fanout exchange and an exclusive temporary queue for each running server. Messages contain only the Kanvil instance identity and a committed event cursor. There is no ticket content in the broker. Consumers acknowledge notifications and then read authorized durable data from PostgreSQL. Duplicate or dropped wakeups cannot change the event cursor or overwrite tickets.
|
|
74
|
+
|
|
75
|
+
A PostgreSQL advisory lock coordinates publishers, and a durable destination checkpoint advances only after RabbitMQ confirms publication. If publication succeeds but checkpointing is interrupted, the notification is published again. New or reconnected servers catch up from PostgreSQL. The AMQP client restores its connection, exchange, queue, and consumer after an outage. PostgreSQL notifications and periodic processors keep the application functioning while RabbitMQ is unavailable. Administration reports this fallback.
|
|
76
|
+
|
|
77
|
+
RabbitMQ improves notification distribution for operators who already run it. It does not replace the database, become a prerequisite for jobs, or change the HTTP, SSE, or MCP contracts. Pending uploads and indexing work remain recoverable without broker queue backups.
|
|
78
|
+
|
|
79
|
+
## Adapter interfaces
|
|
80
|
+
|
|
81
|
+
The application composition root is `apps/server/src/runtime-adapters.ts`. It selects authentication, push, realtime delivery, object storage, indexing, and background scheduling. HTTP and MCP share domain handlers and receive application services instead of importing provider SDKs.
|
|
82
|
+
|
|
83
|
+
`adapters/ports.ts` defines the external I/O interfaces. S3 drivers implement `ObjectStore`, search drivers implement `TicketIndex`, and message brokers implement `WakeupBus`. Provider registries select the driver. The blob service owns transactions, object references, integrity checks, and cleanup; the search service owns indexing checkpoints and SQL authorization; event delivery owns durable notification checkpoints and PostgreSQL fallback. Provider drivers perform only their protocol operations.
|
|
84
|
+
|
|
85
|
+
To add a provider, implement its matching interface, register its factory, add validated and sanitized configuration, and cover real I/O and failure recovery. Do not change ticket handlers or add provider checks to HTTP/MCP operations. PostgreSQL transactions, ordered event cursors, version checks, and claims are intentional durability contracts, not a generic database abstraction.
|
|
86
|
+
|
|
87
|
+
## Validation
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
pnpm db:up
|
|
91
|
+
pnpm test:services
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This starts disposable Elasticsearch and RabbitMQ instances on loopback, runs S3-compatible storage readback, upload failure recovery, external search permission/fallback tests, and broker fanout/outage/reconnect tests, then removes those containers. S3 uses an isolated S3rver fixture. Each suite creates and removes its own PostgreSQL database. CI supplies the same optional services to the standard test run. No real cluster credentials or endpoints are used.
|
package/docs/mobile.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Native app
|
|
2
|
+
|
|
3
|
+
Kanvil uses Expo SDK 57 with its compatible React Native and React versions. Web and native use the same contracts, ticket defaults, draft format, API client, and realtime recovery. Controls and navigation use their platform's renderer; the web DOM controls cannot provide native inputs, navigation, or document pickers.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
Activate the repository's Node and pnpm versions, then install dependencies. Start the backend and web app with `pnpm dev`. Start the native development server separately:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm --filter @kanvil/mobile dev
|
|
11
|
+
pnpm --filter @kanvil/mobile ios
|
|
12
|
+
# On a host with the Android SDK:
|
|
13
|
+
pnpm --filter @kanvil/mobile android
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Enter your server's origin in the app. An iOS simulator can reach the host through `http://localhost:5180`; a phone needs your reachable HTTPS address. Release builds require HTTPS. Local development allows HTTP. The server must expose its auth and API endpoints at the origin's root.
|
|
17
|
+
|
|
18
|
+
Native projects are generated from `app.config.ts` and its Expo plugins. Keep `ios/` and `android/` untracked. Expo's `enableSceneSupport` option is enabled for SDK 57 builds using Xcode 27; this uses Expo's scene lifecycle implementation. See the [Expo lifecycle guide](https://github.com/expo/fyi/blob/main/ios-scene-lifecycle.md).
|
|
19
|
+
|
|
20
|
+
## Sessions and cached data
|
|
21
|
+
|
|
22
|
+
Better Auth's Expo plugin stores session cookies and its session cache in SecureStore, with a separate namespace for each normalized server origin. Ticket caches and drafts use AsyncStorage, scoped to both that origin and account. Mutation state is never persisted for later automatic submission. Failed offline writes remain drafts and must be explicitly saved again.
|
|
23
|
+
|
|
24
|
+
Local login and explicitly linked OIDC accounts use the same project permissions. PocketID redirects to the server's `/api/auth/callback/oidc`; the Expo integration completes the return to `kanvil://auth/callback`. Ticket links use `kanvil://projects/<projectId>/tickets/<ticketId>`.
|
|
25
|
+
|
|
26
|
+
Attachment selection uses the native document picker. Uploads and downloads use the authenticated API. Downloaded files open through the system share sheet, and temporary attachment copies are removed afterward.
|
|
27
|
+
|
|
28
|
+
## Push notifications
|
|
29
|
+
|
|
30
|
+
Set the public `EXPO_PUBLIC_EAS_PROJECT_ID` UUID when generating/building the native project. This populates Expo's `extra.eas.projectId`; it is an identifier, not a secret. Use your Expo project and configure APNs and FCM credentials following the [Expo push setup guide](https://docs.expo.dev/push-notifications/push-notifications-setup/). Enable the optional server transport as described in [self-hosting](self-hosting.md).
|
|
31
|
+
|
|
32
|
+
Settings has a global push preference and an action to enable or disable this device. Permission is requested only when enabling. Device tokens are stored in SecureStore, refreshed after app activation/token changes, and bound to the server login session. Sign-out clears the local token marker and server session revocation removes the registration. Notification taps open a ticket only on the currently connected server; the normal API still checks access. Without a configured push project, the app keeps Inbox and realtime available and explains the missing build configuration.
|
|
33
|
+
|
|
34
|
+
Outbox commits, receipt processing, retry/restart behavior, membership revocation during delivery, account changes on a shared device, and session revocation are tested with an isolated transport. Native Inbox/push preference changes were checked against a second HTTP client on the iOS simulator. Real APNs/FCM delivery and notification-tap routing have not been validated; they need signing/push configuration and an authorized notification source. The unconfigured simulator rejected an injected notification with `UNErrorDomain 2003: Source is not authorized`.
|
|
35
|
+
|
|
36
|
+
## Checks
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm --filter @kanvil/mobile typecheck
|
|
40
|
+
pnpm --filter @kanvil/mobile exec expo install --check
|
|
41
|
+
pnpm --filter @kanvil/mobile export
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Export checks both iOS and Android JavaScript bundles. It does not replace native compilation or device validation. Build and simulator results are recorded separately from physical-device and store-release validation. Signing and App Store publication are separate release actions.
|
|
45
|
+
|
|
46
|
+
Initial simulator validation includes native owner/project/ticket creation, Markdown rendering, authenticated realtime, deep links, session restoration, persisted drafts across termination, and a concurrent server update preventing an overwrite. Switching between cached ticket deep links kept each comment draft with its original ticket.
|
|
47
|
+
|
|
48
|
+
An ad-hoc-signed iOS Release app was also compiled with Xcode 27.1 and run on iOS 27 with Metro stopped. It rejected HTTP, signed in through a verified HTTPS fixture, showed complete parent/child/relationship titles, explicitly linked PocketID, and signed out/in through the system browser to the same workspace. The provider used its login-code flow after a virtual passkey enrollment. Local password login remained available. Cached reading and cold startup after the backend stopped were checked separately.
|
|
49
|
+
|
|
50
|
+
The Android arm64 Release APK compiled in GitHub Actions and passed signature and embedded-JavaScript verification. Android JavaScript exports also pass. The development host has no Android SDK, and Android device execution has not been performed. Physical-device passkeys, distribution signing, and store publication remain separate validation. See the [verification record](verification.md).
|
|
51
|
+
|
|
52
|
+
## Release builds and signing
|
|
53
|
+
|
|
54
|
+
`eas.json` provides `development`, `preview`, `simulator`, and `production` profiles, sharing the repository's Node/pnpm pins. `preview` produces an Android APK or an internally distributed iOS build; `simulator` produces an iOS simulator build. `production` uses store distribution defaults. Configure your Expo project ID, signing credentials, and push credentials before running an EAS build. Follow the [EAS build configuration guide](https://docs.expo.dev/build/eas-json/). Local builds do not require an Expo hosting account.
|
|
55
|
+
|
|
56
|
+
Generate the native project and build an embedded iOS Release app locally:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
pnpm --filter @kanvil/mobile exec expo prebuild --platform ios
|
|
60
|
+
pnpm --filter @kanvil/mobile exec expo run:ios --configuration Release
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A Release app requires HTTPS and includes its JavaScript bundle, so Metro is unnecessary. Keep simulator ad-hoc signing enabled; removing signing also removes the entitlements SecureStore needs. For an isolated simulator backend using HTTPS, run `pnpm native:auth-fixture --https --simulator <udid>` and connect to `https://localhost:5188`. Only this test process and selected simulator trust the temporary certificate.
|
|
64
|
+
|
|
65
|
+
Increment `ios.buildNumber` and `android.versionCode` in `app.config.ts` for subsequent distributed builds. Keep signing material outside the repository. Building a simulator app or preview APK does not publish to an app store or install on a physical device.
|
|
66
|
+
|
|
67
|
+
## Native OIDC fixture
|
|
68
|
+
|
|
69
|
+
The Expo authentication proxy requires an HTTPS identity provider, including during local development. For simulator checks, start the disposable TLS provider and backend with:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm native:auth-fixture --simulator <booted-simulator-udid>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This generates a private temporary certificate, trusts its public certificate in the selected simulator, starts PocketID at `https://localhost:51411`, and runs an isolated Kanvil backend on port 5188. The backend trusts the certificate through its own `NODE_EXTRA_CA_CERTS` setting. Certificate trust is scoped to that simulator and test process. Stop the fixture with Ctrl-C to remove its container, database, and temporary private key. The simulator's temporary trust entry expires with the test certificate.
|
|
76
|
+
|
|
77
|
+
The fixture uses its own fixed test-only API key and accepts no real PocketID URL or credentials. It is exclusive with `pnpm test:oidc`, which uses the same loopback provider port. Native builds and Metro are started separately using the development commands above.
|
package/docs/plan.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Kanvil delivery plan
|
|
2
|
+
|
|
3
|
+
## Product goal
|
|
4
|
+
|
|
5
|
+
Make Kanvil the best open-source agent-focused ticketing system, released under the MIT license.
|
|
6
|
+
|
|
7
|
+
Judge the product by how reliably agents can discover work, obtain complete context, make authorized edits, coordinate ownership and dependencies, and recover after interruption. Keep the web and native apps clear and useful for the people working with those agents. Support external consumers through documented, standard interfaces without requiring a particular agent runner.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
Build a self-hosted ticketing system with web and native mobile apps, local login, PocketID-compatible OIDC, a documented HTTP API, durable realtime events, and MCP tools.
|
|
12
|
+
|
|
13
|
+
Start with a fresh workspace and native ticket storage. Kaneo compatibility, data import, and changes to T3 Code or Factory are outside this project. External consumers receive documented interfaces and example clients maintained in this repository.
|
|
14
|
+
|
|
15
|
+
Use established libraries and the smallest implementation that meets the acceptance criteria. Each ticket operation, permission check, validation rule, and event write has one shared implementation across HTTP and MCP. Web and mobile share contracts and client logic while using platform-appropriate rendering and navigation.
|
|
16
|
+
|
|
17
|
+
## Stack
|
|
18
|
+
|
|
19
|
+
- Workspace monorepo using TypeScript and pnpm.
|
|
20
|
+
- Web: React, Vite, TanStack Router, Query and Table, shadcn/ui with Base UI, and Tailwind v4.
|
|
21
|
+
- Mobile: Expo, React Native, React Navigation, and Legend List.
|
|
22
|
+
- Backend: Node.js, Hono, Zod/OpenAPI, PostgreSQL, and Drizzle.
|
|
23
|
+
- Authentication: Better Auth with local login, generic OIDC, Expo integration, and supported API-key and MCP authorization integrations.
|
|
24
|
+
- MCP: the official TypeScript SDK, with its version selected alongside the authentication integration and verified against representative clients.
|
|
25
|
+
- Deployment: one application service and PostgreSQL, including transactional attachment storage.
|
|
26
|
+
|
|
27
|
+
Pin the latest stable compatible releases during implementation. Reuse existing library primitives before adding custom code; add optional dependencies only when their feature is being delivered.
|
|
28
|
+
|
|
29
|
+
## Authentication and access
|
|
30
|
+
|
|
31
|
+
- First-owner setup, account management, local login, logout, and account recovery.
|
|
32
|
+
- Closed public registration by default.
|
|
33
|
+
- Local email/password and OIDC login available on web and mobile.
|
|
34
|
+
- PocketID setup through discovery URL, client ID, server-held secret, and documented callback URLs.
|
|
35
|
+
- Explicit account linking and consistent project authorization across login methods.
|
|
36
|
+
- Secure mobile session storage and tested browser-to-app authentication callbacks.
|
|
37
|
+
- Revocable credentials with project and operation scopes for unattended consumers.
|
|
38
|
+
- Authenticated and permission-filtered HTTP, realtime, attachment, and MCP access.
|
|
39
|
+
|
|
40
|
+
Validate PocketID mobile login and MCP client authentication early. These checks are implementation work owned by the agent; user input is needed only when essential access or credentials are unavailable.
|
|
41
|
+
|
|
42
|
+
## Ticket operations
|
|
43
|
+
|
|
44
|
+
- Projects and stable ticket keys with permanent links.
|
|
45
|
+
- Titles, Markdown descriptions, acceptance criteria, and attachments.
|
|
46
|
+
- Status, priority, kind, difficulty, scopes, labels, and assignment.
|
|
47
|
+
- Parent/child relationships, blockers, related tickets, and duplicate references.
|
|
48
|
+
- Comments, activity history, subscriptions, archiving, and deletion behavior.
|
|
49
|
+
- Search, filtering, sorting, pagination, and bulk edits.
|
|
50
|
+
- Optimistic concurrency checks that prevent silent overwrites.
|
|
51
|
+
- Idempotent writes that remain safe when a client retries a request.
|
|
52
|
+
|
|
53
|
+
Keep ticket status, automation readiness, ownership, and supplied execution or validation evidence distinct. Store external references and their provenance without implementing an execution scheduler or PR automation.
|
|
54
|
+
|
|
55
|
+
## HTTP and realtime interfaces
|
|
56
|
+
|
|
57
|
+
- Versioned HTTP API with generated OpenAPI documentation and consistent errors.
|
|
58
|
+
- Eligibility queries with explicit ready, blocked, and not-ready reasons.
|
|
59
|
+
- Dependency validation and cycle prevention.
|
|
60
|
+
- Atomic claim, renew, and release operations, with safe expiration and recovery semantics.
|
|
61
|
+
- Ticket holds, readiness updates, external references, and evidence records.
|
|
62
|
+
- Authenticated SSE feed with project filters, ordered event cursors, ticket versions, reconnect, and replay.
|
|
63
|
+
- Events for ticket, dependency, readiness, and ownership changes.
|
|
64
|
+
- Explicit snapshot recovery when a replay cursor has expired.
|
|
65
|
+
- At-least-once delivery and documented duplicate handling.
|
|
66
|
+
|
|
67
|
+
Commit each ticket mutation and its durable event together. Use PostgreSQL notifications as wakeups; persisted records remain the authority for replay. Define snapshot-to-stream handoff and cursor ordering so concurrent commits cannot hide events.
|
|
68
|
+
|
|
69
|
+
Deliver reference consumers and contract tests within Kanvil. External applications are responsible for adopting these interfaces.
|
|
70
|
+
|
|
71
|
+
## MCP
|
|
72
|
+
|
|
73
|
+
- Authenticated HTTP MCP endpoint using the official SDK.
|
|
74
|
+
- Project discovery, ticket search/read/create/update, comments, relationships, readiness, claims, and evidence tools.
|
|
75
|
+
- Shared schemas and operation handlers with the HTTP API.
|
|
76
|
+
- Stable identifiers and structured results.
|
|
77
|
+
- Supported OAuth authorization for interactive clients and scoped credentials for supported headless connections.
|
|
78
|
+
- Connection documentation, configuration examples, and compatibility tests.
|
|
79
|
+
|
|
80
|
+
## Web and mobile
|
|
81
|
+
|
|
82
|
+
- Web list with readable wrapping titles, adjustable columns, filters, selection, bulk actions, and keyboard navigation.
|
|
83
|
+
- Ticket detail view containing requirements, relationships, discussion, attachments, activity, and supplied evidence.
|
|
84
|
+
- Project settings, account settings, and notification preferences.
|
|
85
|
+
- Native mobile lists and navigation, ticket editing, comments, attachments, deep links, and notifications.
|
|
86
|
+
- Live updates while connected, recovery after disconnection, cached reading, and locally saved drafts.
|
|
87
|
+
- Shared client operations, validation, formatting, design tokens, and status presentation rules.
|
|
88
|
+
- Accessible controls, responsive layouts, useful loading/error states, and explicit connection status.
|
|
89
|
+
|
|
90
|
+
Start with Markdown editing and an established renderer. Rich-text editing remains an optional scope decision. Defer automatic submission of offline edits and custom workflow designers. Optional external storage, search, and broker adapters are described in [infrastructure](infrastructure.md); the default deployment still requires only PostgreSQL.
|
|
91
|
+
|
|
92
|
+
## Self-hosting and verification
|
|
93
|
+
|
|
94
|
+
- Docker Compose, configuration examples, and HTTPS/reverse-proxy instructions.
|
|
95
|
+
- Persistent PostgreSQL storage for both tickets and attachment bytes, health checks, logs, and database schema migrations.
|
|
96
|
+
- Backup and tested restoration of the database, including attachment bytes, with the instance configuration kept securely.
|
|
97
|
+
- CI, native builds, signing configuration, and documented mobile delivery.
|
|
98
|
+
- Local and PocketID login validated on web and native mobile.
|
|
99
|
+
- Permission isolation across HTTP, MCP, events, and attachments.
|
|
100
|
+
- Regression coverage for concurrent edits, retried writes, competing claims, dependency changes, and expired ownership.
|
|
101
|
+
- Replay coverage for interrupted streams, service restarts, concurrent commits, and expired cursors.
|
|
102
|
+
- Protocol validation using representative MCP clients and the reference realtime consumer.
|
|
103
|
+
|
|
104
|
+
## Build order
|
|
105
|
+
|
|
106
|
+
1. Establish the workspace, self-hosted service, shared contracts, and both login methods. Prove web and iPhone sign-in, plus an authenticated MCP handshake.
|
|
107
|
+
2. Deliver ticket operations and usable web/mobile views through the shared backend.
|
|
108
|
+
3. Deliver durable realtime events, readiness queries, ownership operations, and reference consumers.
|
|
109
|
+
4. Complete MCP tools, compatibility checks, documentation, backup restoration, and release packaging.
|
|
110
|
+
|
|
111
|
+
Overall difficulty: high, driven by authentication across platforms and reliable concurrent writes.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Releases
|
|
2
|
+
|
|
3
|
+
PRs and `main` pushes run `.github/workflows/ci.yml`. Independent suites and browser/native/Docker matrices run in parallel; only jobs consuming build artifacts wait for the build. The always-running `Checks` status requires every suite to succeed. Releases start on a `vX.Y.Z` tag and reuse that workflow against the tagged commit. Publishing jobs have registry write permissions; PR checks have read-only repository access.
|
|
4
|
+
|
|
5
|
+
The public npm application package is `@maudecode/kanvil`; the workspace root and internal packages remain private. The release tag supplies the version for both npm and Docker: `v1.2.3` publishes `@maudecode/kanvil@1.2.3` and `ghcr.io/maudecode/kanvil:1.2.3`. No `package.json` version bump is needed. PR and local builds use the development version in `package.json`.
|
|
6
|
+
|
|
7
|
+
## Publish a version
|
|
8
|
+
|
|
9
|
+
1. Merge the intended changes to `main` through a reviewed PR with green `Checks`.
|
|
10
|
+
2. Fetch `main`, inspect the intended commit, then create and push an annotated tag: `git tag -a vX.Y.Z COMMIT -m "Kanvil X.Y.Z"` and `git push origin vX.Y.Z`. Repository release-tag rules apply. Creating the tag authorizes public publication.
|
|
11
|
+
3. Check the `Release` run. It rejects malformed or prerelease tags, `v0.0.0`, and commits outside `main` history. The full CI workflow must pass on that commit.
|
|
12
|
+
4. Verify `npm view @maudecode/kanvil@X.Y.Z dist.integrity` and `docker buildx imagetools inspect ghcr.io/maudecode/kanvil:X.Y.Z`. The image includes Linux amd64 and arm64. Confirm anonymous Docker pulls work.
|
|
13
|
+
|
|
14
|
+
The workflow publishes the exact npm tarball installed by CI to the `latest` dist-tag, while separate amd64/arm64 publishers push the exact image archives tested by CI. Docker builds and restoration tests run on matching native runners; release publishing does not rebuild or emulate either image. A final Docker manifest job joins those immutable digests under `ghcr.io/maudecode/kanvil:X.Y.Z` and `:latest`. Stable releases only; prerelease versions are rejected. Do not tag an older version to move `latest` backward.
|
|
15
|
+
|
|
16
|
+
If either registry publication fails, rerun the failed release jobs. npm and Docker publication are independent after validation. The npm step accepts an existing version only when its SHA-512 integrity matches the tested tarball; different bytes fail and require a new version. Registry publication is not atomic across npm and GHCR. Never move a published tag or reuse a version for changed bytes. Avoid rerunning an old release after a newer release, since Docker's `latest` tag would move backward.
|
|
17
|
+
|
|
18
|
+
Tag creation and package publication are separate from deploying to any running instance. Operators choose when to upgrade using [self-hosting](self-hosting.md).
|