Vancetope Application — app: kanban
Self-contained Kanban-board pattern built on the
kind: applicationfoundation (seedoc-kind-application.md). One folder = one board. Sub-folders = columns. Onekind: cardfile per ticket. Derived artifacts (_board.md,_stats.yaml) regenerate from the source cards.
1. Why a Kanban application
After Calendar proved out the kind: application foundation (folder-as-app, manifest-driven, deterministic Java-driven create/refresh), Kanban is the second concrete app — same pattern, different domain. It exists because:
- Workflow-state tracking is fundamentally different from time-anchored planning (the Calendar app). Cards move between columns; events sit on a timeline.
- Boards are a high-leverage primitive in Vancetope because most “what should I do?” sessions resolve into “promote X from todo to doing, decompose Y in backlog into smaller cards”. A board representation is closer to that vocabulary than a calendar or a checklist.
- Cards are description-heavy: a Kanban card has acceptance criteria, design notes, links, discussion. That argues for one file per card (Markdown body + structural front-matter), not one Map entry inside a single big YAML.
2. Folder layout
boards/sprint-q3/ ← suite folder
├── _app.yaml ← manifest (kind: application, app: kanban)
├── _board.md ← auto-generated (kind: diagram, Mermaid)
├── _stats.yaml ← auto-generated (kind: data)
├── backlog/ ← column = sub-folder
│ ├── search-feature.md
│ └── notifications.md
├── todo/
├── doing/
│ └── login-flow.md
├── review/
└── done/
└── logo-refresh.md
Column resolution rule: the leaf folder of a card’s path relative to the suite root is its column. Files directly in the root land in backlog. Deeply nested (a/b/c/card.md) → column = c. This mirrors the Calendar rule (Calendar uses default as the fallback; Kanban uses backlog because that’s the most natural “where does a new card with no explicit column go” answer).
Underscore prefix is system-managed: _app.yaml, _board.md, _stats.yaml. KanbanFolderReader excludes these from the card list so a rebuild stays idempotent.
3. Card schema (kind: card)
Markdown is the primary mime type — cards have prose bodies. YAML and JSON are also supported (for tooling).
---
kind: card
title: Login-Flow implementieren
priority: high
assignee: alice
labels: auth, sprint-q3
dueDate: 2026-07-15
estimate: 5
blocked: false
---
Email + Passwort, JWT-basiert.
## Akzeptanzkriterien
- [x] Registrierung
- [ ] Login
- [ ] Logout
- [ ] Password-Reset
| Field | Type | Notes |
|---|---|---|
title |
string | Headline; falls back to filename stem when missing. |
priority |
string | Free-form. critical / high render as standouts on the board. |
assignee |
string | User identifier (login / email / name). |
labels |
string list | Free tags. The manifest’s blockedLabel (default blocked) flags the card. |
dueDate |
string | ISO date (yyyy-MM-dd). Pass-through. |
estimate |
number | Story points / hours. Renderer doesn’t pretend to know the unit. |
blocked |
boolean | True flags the card; equivalent to having the blocked label. |
| Markdown body | string | Free description. GFM checkboxes (- [ ] / - [x]) feed the progress.subtasks stat. |
Cross-format round-trip: Markdown front-matter is flat strings, so list-valued fields (labels) are comma-separated on disk. JSON/YAML store them as native arrays. Converting between mime types is lossy by design — commas inside label names get split.
4. Manifest schema
$meta:
kind: application
app: kanban
title: "Sprint Q3 Board"
description: "Auth + Search"
kanban:
columns: # Map keyed by column-name, NOT an array.
backlog: { title: "Backlog", order: 1 }
todo: { title: "To Do", order: 2, wipLimit: 5 }
doing: { title: "In Progress", order: 3, wipLimit: 3 }
review: { title: "Review", order: 4, wipLimit: 2 }
done: { title: "Done", order: 5 }
board:
outputPath: "_board.md"
style: "mermaid" # mermaid (default) | table
maxCardsPerColumn: 20 # truncate beyond this with a "+N more" marker
columnOrder: [] # optional explicit order; else `order:` per column
stats:
outputPath: "_stats.yaml"
blockedLabel: "blocked"
staleThresholdDays: 14 # 0 = disable stale-detection
wipEnforce: "soft" # soft (default — warns) | hard (blocks moves)
Columns referenced by cards but missing from kanban.columns are auto-added during refresh with order after the declared ones (so an LLM that dispatches a card into a column it forgot to declare doesn’t silently lose the card).
5. Derived artifacts
5.1 _board.md
Two output styles via board.style:
Mermaid (default). Native kanban diagram (Mermaid 11.3+). Wrapped in a kind: diagram Markdown body with the mermaid fence so the standard diagram viewer renders it.
kanban
Backlog
cardId1[Search feature]
cardId2[Notifications]
In Progress (4/3)
cardId3[Login-Flow]@{ assigned: 'alice', priority: 'high' }
Done
cardId4[Logo refresh]@{ assigned: 'bob' }
WIP-exceeded columns get a (count/limit) suffix. Overflow (more than maxCardsPerColumn) becomes a +N more placeholder node.
Table. Markdown table with one column per board column, one card per row, inside a kind: text body. Better for hard-copy / long card titles where Mermaid wraps badly.
Card ordering inside each column: priority desc → dueDate asc → title asc.
5.2 _stats.yaml
kind: data body with deterministic structure:
$meta:
kind: data
folder: projects/website/board
columns:
backlog: { count: 4 }
todo: { count: 2, wipLimit: 5 }
doing: { count: 4, wipLimit: 3, wipExceeded: true }
review: { count: 1, wipLimit: 2 }
done: { count: 5 }
blocked:
- projects/website/board/doing/db-migration.md
stale:
- projects/website/board/backlog/old-idea.md
progress:
totalCards: 16
done: 5
open: 11
ratio: 0.31
subtasks: { total: 22, done: 14, ratio: 0.64 }
donecounts cards in any column whose name matchesdone/completed/closed/erledigt(case-insensitive). The board owner doesn’t have to special-case “done”.staleuses the document’screatedAtas a proxy becauseDocumentDocumentdoesn’t track per-update timestamps yet. Known limitation — over-counts cards that were touched in place. Improving this needs amodifiedAtonDocumentDocument.subtasksonly appears when at least one card has GFM checkboxes in its body.
6. Tools
| Tool | Purpose | Where the logic lives |
|---|---|---|
kanban_app_create |
One-shot bootstrap: manifest + cards + auto-rebuild. The recommended entry point. | KanbanApplication.create(...) via VanceApplication contract. |
kanban_card_create |
Single-card add. Doesn’t auto-rebuild. | KanbanCardCreateTool direct write through DocumentService. |
kanban_move |
Move a card between columns. Respects WIP limits (soft warns, hard blocks). Optional rebuild: true. |
KanbanMoveTool via DocumentService.update(..., newPath=...). |
kanban_aggregate |
Read-only query — column / assignee / labels / blocked / priority filters. | KanbanAggregateTool via KanbanFolderReader.scan. |
app_rebuild |
Generic — works for any Vancetope application. Dispatches to KanbanApplication.refresh() via the registry. |
AppRebuildTool + VanceApplicationRegistry. |
The Java services own the schema. The LLM doesn’t get to invent it.
7. WIP-limit enforcement
wipEnforce controls kanban_move:
- soft (default) — over-limit moves succeed. The result carries
warnings: ["wip-exceeded:<column>:<count>/<limit>"]. The LLM should surface that warning in the chat reply. - hard — over-limit moves are rejected with a
ToolException. Recovery: move another card out of the target column first.
_stats.yaml always carries the wipExceeded flag per column regardless of enforcement mode — it’s pure observation, not gating.
8. Relationship to other apps
| Use case | Use this | Why |
|---|---|---|
| Workflow states (backlog → done) | app: kanban |
Cards move between columns. |
| Time-anchored plan with milestones / dependencies in time | app: calendar |
Events sit on a timeline; lanes are organizational, not states. |
| Free-form todo with no workflow | kind: checklist |
Kanban manifests are overhead. |
| Project management with resource allocation, burndown | Out of scope | Export to Linear / Jira / GitHub Projects. |
A folder cannot be both app: calendar and app: kanban. Each app folder hosts exactly one _app.yaml. The forward-compatibility design of ApplicationDocument (nested config.<app> blocks) supports an eventual “multi-face folder” but no concrete app uses that today.
9. Web-UI editor
Kanban gets a dedicated interactive editor in the web UI, mounted by the shared Cortex / Notepad shell via the kind-registry. The kanban addon’s ./register federation expose registers an application:kanban kind whose view is KanbanAppKind.vue — a thin wrapper that adapts the kind-registry mount contract (single document prop) to the existing KanbanBoard.vue’s (projectId, folder, title) interface.
Routing: clicking an _app.yaml file in the file tree opens it as a regular tab in cortex.html?doc=…/_app.yaml (or notepad.html?…). docTypeRegistry.resolveBinding sees kind: application, reads the app: discriminator from the manifest headers, calls resolveKind('application:kanban'), and mounts KanbanAppKind immersively (sidebar / tab strip / shell toolbar suppressed in App view-mode; see doc-kind-application §7.2). The earlier dedicated app.html MPA entry + AppEditor.vue dispatcher are gone.
Board view (KanbanBoard.vue):
- Horizontal scroll, one column per
KanbanColumnView. Card sort = priority desc → dueDate asc → title asc. - Drag-and-drop via
vue-draggable-plus. Cross-column drop emits an optimistic local mutation, thenPOST /kanban/move; on error the local move rolls back. - WIP-limit display in the column header (
count/limit, red when exceeded). Soft-overflow warnings from the move response surface as aVAlertbanner. - Per-column “+” button opens a new-card modal.
- Card click opens the right-panel
KanbanCardDetail.vue. - Owns the self-write quiet window + card array. It exposes
reload(changedPaths?)which theKanbanAppKindwrapper drives ondocuments.changedpushes. - Each tile is tinted by the card’s accent color (a subtle
bg-<color>-500/10full-tile wash), in addition to the priority left-border. Neutral cards keep the default surface.
Live updates + auto-save. Both content levels — attributes (front-matter fields) and the Markdown body — are auto-saved; there is no explicit Save/Discard button. The panel debounces edits (~800 ms) and PATCHes attributes + body in a single request so the two levels never race on the server’s read-modify-write merge. The whole app folder (everything under _app.yaml) is subscribed via useDocumentPrefixReaction, so a remote change reloads the board and re-seeds the currently-open card (fields + editor source). A self-write quiet window (3 s, keyed per card path) suppresses the refresh for the board’s own write-echoes so the editor cursor never jumps. Last-Writer-Wins — no CRDT, no per-field merge.
Card detail (KanbanCardDetail.vue):
- Edit all attribute fields (title, priority, assignee, labels, dueDate, estimate, blocked, color) inline; changes auto-save.
- Color is the document-level accent (
DocumentDocument.color, the 12-valueAccentColorpalette — not card front-matter). The panel uses the sharedVColorPicker; the value rides the same debounced patch as acolor/clearColorfield and is applied server-side via the atomicDocumentService.setColor/clearColor(the content merge preserves it). On the wire it is the enum name string, because the addon’s TS generator doesn’t emit cross-package imports for thevance-apienum. - The body is edited in a roomy modal via the shared
WorkPageEditor(bodyOnly). The modal runs the editor withautoSaveMs=0and drives saving through the panel’s single debounced save loop (pulls the latest markdown viaeditorRef.save()on each flush) — no local buffer, no “Save to persist” step. GFM checkboxes in the body feed the board’s subtask progress badge. - The content editor wires the block-editor’s compose callbacks (
runCompose/pollCompose/cancelCompose→ the shared@vance/sharedcompose helpers, plus the host-injectedcompose-output-component), so/composeblocks run inside a card just like in the workbook. Relativevance:paths resolve against the card’s app folder; runs bind to the active cortex session when present. - A save-status indicator (
Bearbeitet… / Speichern… / Gespeichert / error) sits in the panel header and the content modal footer. Delete action remains explicit (confirm dialog). - The client never serializes the card — the body markdown is sent as
{ body }and the server merges it into the card’s front-matter viaCardCodec.
REST surface — KanbanBoardController:
| Endpoint | Purpose |
|---|---|
GET /brain/{tenant}/kanban/board?projectId&folder |
Full board view (KanbanBoardView). |
POST /brain/{tenant}/kanban/move?projectId&folder |
Move a card. Body { card, toColumn }. WIP-soft → warnings; WIP-hard → 400-style error. |
POST /brain/{tenant}/kanban/cards?projectId&folder |
Create a card. Body = KanbanCardCreateRequest. |
PATCH /brain/{tenant}/kanban/cards?projectId&folder&path |
Patch fields (any subset). |
DELETE /brain/{tenant}/kanban/cards?projectId&folder&path |
Soft-delete (trash). |
POST /brain/{tenant}/kanban/rebuild?projectId&folder |
Regenerate _board.md + _stats.yaml. |
The controller is a thin adapter over KanbanApplication.moveCard() + KanbanFolderReader.scan() + DocumentService — no business logic. The move logic is shared between the REST controller and the kanban_move tool by a public KanbanApplication.moveCard(...) method.
What the UI does NOT do (v1):
- No live cursors / presence on the board. The board + open card live-refresh over the
documentschannel (see auto-save section above), but there is no per-user cursor overlay or viewer roster like the workpage editor has. Conflicting edits resolve Last-Writer-Wins. - No inline body editing on the card tile. Click into the right panel, then into the content modal, to edit the body. Inline edit was tempting but adds rich-text-editor weight (CodeMirror or Tiptap) to every visible card.
- No swimlanes / row grouping. Columns only.
- No card-ID stability. Filename is the de-facto id. Renaming a card breaks any external references.
10. Non-goals (v1)
- No conflict resolution beyond Last-Writer-Wins. The board + open card live-refresh over the
documentschannel (§9), but two writers touching the same card race on optimistic locking; the loser’s in-flight change is overwritten on the next refresh. No CRDT, no per-field merge. Acceptable for v1 — the average user is solo, with the LLM as the second writer. - No card history / audit log.
DocumentArchiveDocumentcaptures versions of the card body if the document-archive feature is enabled, but there’s no “card X moved from todo to doing at 14:32” timeline. - No mermaid-kanban interactivity in the static
_board.md. The Mermaid block is a fallback for chat embeds / non-app-aware viewers. The interactive web-UI board is the canonical view.
11. Open questions / future work
- Card-ID stability. v1 uses the filename as the implicit ID. Renaming breaks references. A
$meta.id(e.g.KAN-42) would help cross-card linking and make external integrations easier. Decision deferred — wait for a concrete need (kanban_aggregatefilter by ID, sibling-card references). modifiedAton DocumentDocument. Required for accurate stale-detection. Currently approximated withcreatedAt.- Subtask promotion. A card’s GFM checkboxes could be promoted to sibling cards via
kanban_subtask_promote(card, subtask). Useful when a checklist item grows into its own workstream. Not in v1. - Linked cards (depends-on / blocks). Out of scope for v1. Cross-card relationships likely become a
kind: graphoverlay rather than card-internal references. - Multi-face folders.
config.kanban+config.calendarcoexisting in one_app.yamlwould let a folder be both a board and a timeline of the same items. Possible because of the nested-config design but no concrete user yet.