Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wikanban

A kanban board for any agentic-wiki bundle.

Your backlog is a folder of Markdown files. An agent files them, links them, and keeps them honest, and the wiki CLI answers questions about them like a database. What you don't get from a terminal is the thing a backlog is actually for: a glance. Which column is overloaded, what's blocked on what, what nobody has touched in three weeks.

wikanban is that glance. It reads a bundle, groups the entries into columns by whichever frontmatter field you nominate, and lets you drag a card to change it. Nothing else. The files stay the source of truth, an agent keeps editing them underneath you, and the board follows along within a second.

cd my-backlog && wikanban     # opens a board on http://localhost:8730

No configuration required. Point it at a folder and it serves that folder.

What it is, and what it deliberately isn't

The whole design follows one rule: wikanban renders the files, it does not own them.

An agent will edit this bundle while the board is open. Someone will edit it in Obsidian. wiki move will relocate files. So the board cannot hold opinions the files don't, cannot cache state the files don't have, and cannot enforce policy the files can violate. Every consequence below falls out of that:

  • It shows, it never prevents. A card blocked by unfinished work says so, loudly, and still drags anywhere you want. The board reports the state of your files the way wiki check reports a broken link: as information, not as a veto.
  • It invents no state. There is no manual card order, because the format has no order: field and inventing one would make every agent responsible for maintaining it. Ordering is derived from data you already keep.
  • It edits surgically. A drag rewrites exactly one line of one file. It never reserializes an entry (see Writes).
  • It has no database. Restart it, delete it, point it at a different folder. There is nothing to migrate and nothing to lose.

Not in scope: authentication (put it behind a proxy), git operations, multi-user collaboration, reporting and burndowns (that's a skill over wiki list --format json), or being a Markdown editor.

Requirements

The wiki CLI on PATH, and a bundle. Any bundle: wikanban has no required workflow and no required fields. If your entries carry a status, you have a board.

wiki init --workflow project-backlog scaffolds a bundle that works with zero configuration beyond root.

Dependencies

Blockers are read from a frontmatter field of paths, which is one of the two dependency recipes project-backlog's WORKFLOW.md documents:

blockers: [/active/tz-bug.md, /backlog/dedup.md]

wikanban resolves relative spellings too, but write them root-absolute. wiki's --where is exact string equality, so the same target spelled ./tz-bug.md from one folder and ../active/tz-bug.md from another cannot both be found by one query, and wiki list --where blockers=/active/tz-bug.md stops being a reliable way to ask what is waiting on something. One spelling per target is the whole reason to use a field rather than a body link. wiki move --include-frontmatter normalizes to it.

The board computes the reverse edge itself, so a card shows both what blocks it and what it blocks. A blocker naming a file that does not exist is shown as unresolved rather than hidden: that is the same "promised but not yet written" case wiki unresolved treats as a feature.

Which folder it serves

The board's location is a command-line concern, never configuration. In order:

  1. --path <dir>, if given.
  2. The directory containing --config <file>, if that was given instead. A config normally lives inside the folder it describes, so wikanban --config ~/kb/backlog/wikanban.toml serves ~/kb/backlog.
  3. The working directory. So cd my-backlog && wikanban just works.

Note what rule 2 is: the config file's directory, not something derived from its name. If you keep several board configs side by side in one folder, they all point at that one folder, so pass --path as well and let --config do only its own job. The startup log always names the folder being served, so there is no guessing.

That folder is the board. Everything under it is on the board and nothing else is, so a subfolder of a big knowledge base is served by pointing at the subfolder rather than by configuring a filter.

The bundle root is discovered, not configured. wikanban asks the CLI (wiki status) which bundle contains the target, so the walk-up rule stays the CLI's to define, exactly as git finds .git. It's needed for two things you never have to think about: scoping the query (--prefix is a bundle path, so it depends on where the target sits inside the bundle) and turning the bundle-relative paths in wiki's output back into files on disk.

If the folder isn't inside a bundle, wikanban says so and suggests wiki init.

Configuration

Entirely optional. wikanban.toml says how to interpret a backlog; it never says where one is.

wikanban looks for it beside the backlog, then at the bundle root, so one file can serve every board in a bundle while a particular folder still overrides it. --config names one explicitly.

Every key below is already the default, so this file is what wikanban does with no config at all. Nothing here needs writing; it is the list of what you can change.

filter   = ["type=task"]      # `--where` expressions, ANDed: which entries are cards
group_by = "status"           # the frontmatter field that becomes the columns
blockers = "blockers"         # field holding root-absolute paths an entry waits on

[order]                       # each list runs most-important first
priority = ["urgent", "high", "medium", "low"]

[server]
addr = "0.0.0.0:8730"

The rest is opt-in, and off unless you ask for it:

columns = ["todo", "in-progress", "blocked", "done"]  # replaces the inferred set
lane_by = "assignee"          # split the board into horizontal bands
lanes   = ["john", "mary"]    # their order; inferred from the values in use

[create]                      # quick-add is ON by default; this only tunes it
type    = "issue"             # defaults to the type your `filter` names
dir     = "/inbox"            # defaults to the served folder
enabled = false               # …or turns quick-add off entirely

Two more field names are fixed rather than configurable, because the format already defines them: title names a card, and tags are shown on it. Those are OKF standard fields, so wikanban follows the format instead of inventing a setting.

Every key is derived if you omit it, and wikanban logs whatever it assumed at startup, so you can see what was decided for you and pin only the parts that are wrong.

Columns: a baseline, plus whatever is in use

An unconfigured board always offers backlog, todo, in-progress, done, and adds every other value your entries actually use.

Detection alone would not be enough. A backlog where nothing is in progress would have no in-progress column, so there would be nowhere to drag a card to, and the state you most want to record is the one you couldn't. An empty backlog would show no board at all. So the two coexist as a union:

Your entries use Columns you get
nothing yet backlog todo in-progress done
only todo backlog todo in-progress done
in-review, blocked, cancelled backlog todo in-progress in-review blocked done cancelled

Ordering comes from a built-in vocabulary; anything it doesn't recognize sorts after it, alphabetically, and is marked as unconfigured in the UI so a typo'd stauts: done stays visible.

Setting columns yourself replaces the baseline entirely — no merging, no surprises.

The baseline is specific to status, because that is the only field wikanban has an opinion about. A board grouped by tags, or by some field of your own, gets no invented vocabulary: its columns are exactly the values in use.

create.dir is a bundle path (rooted at the bundle, not at your disk), the same address space the wiki CLI uses for file arguments so it means the same thing wherever you run from.

Why this lives outside wiki.toml

It would be natural to put board config in the bundle's own wiki.toml. Don't: wiki check warns on unknown wiki.toml keys, so every wikanban user would get permanent lint noise in an unrelated tool. Keeping the file separate also keeps the layering clean, wiki.toml describes the bundle, wikanban.toml describes one view of it, and a bundle can have several boards.

Lanes: a second axis

lane_by splits the board into horizontal bands by a second field, so assignee gives you a row per person and a horizon field gives you Now / Soon / Someday. Lanes follow exactly the same rules as columns (configured order first, values in use after, unknown ones marked, collapsible), because they run through the same code.

Dragging within a lane changes the column. Dragging across lanes changes both fields, in one atomic write rather than two, so a card can never end up half-moved with its column updated and its lane not.

Bands stack vertically and each scrolls its columns horizontally, rather than forming a true 2D grid. On a phone the columns already snap one per swipe, and a second scroll axis on top of that is unusable.

Omit lane_by and the board is one unnamed lane, rendered through the same path, so there is no separate lanes layout to drift.

Where new entries land

Quick-add is on by default. Both of its settings already have defaults, so the [create] section configures the feature rather than enabling it, and enabled = false turns it off. Requiring config to add a card would be an odd line to draw in a tool that already rewrites a card's status with no config at all.

New entries get the type your filter names (type=task by default), because an entry the filter excludes would vanish the instant you made it. Set create.type to override.

They are written into the folder being served, which is almost always what you want, so most configs never set create.dir at all.

Set it only when the two genuinely differ: the folder the board shows versus the folder quick-add writes to. The project-backlog workflow is the case that wants it, with the board on committed work in active/ while a quick capture belongs in the parked backlog/. Know the trade-off first — a card written outside the served folder is not on this board, so it appears to vanish the moment you create it. wikanban warns about that at startup rather than letting you discover it one confusing capture at a time.

Point dir at a folder your wiki.toml lists under ignore_orphans. A new entry nothing links to yet is otherwise reported by wiki orphans, which is correct behavior you don't want to trip on every capture.

Ordering is by position, not by number

columns and each [order] list run most-important first: the first value outranks the second. There are no numeric weights, deliberately — a number invites arguing about whether high is 80 or 100 without answering a question that position doesn't. (It also avoids the trap of "weight", where a bigger number would suggest higher priority and a list would read backwards.)

Cards sort by the configured order of their priority field, then by timestamp (newest first), then by path. Entirely derived, so a human dragging cards and an agent rewriting files can never disagree about what the order "really" is. A card with no value for an ordering field sorts last rather than first, so a missing priority doesn't jump the queue.

A value not in columns gets its own column at the right, marked as unconfigured. A typo'd stauts: done should be visible, not swallowed; hiding cards is how a board starts lying to you.

If group_by names a list-valued field such as tags, a card appears in every matching column. That is the honest rendering, and it makes a tag board useful.

How it reads the bundle

  fsnotify                  wiki list --format json          in-memory board
  on the bundle    ───▶     (one spawn per change)    ───▶   ───▶ HTTP + SSE

The server holds the parsed board in memory and rebuilds it only when the filesystem says something changed (debounced). Requests are served from memory, so per-request cost is zero, and the same rebuild that answers the next request also pushes an SSE event to every open board. Edit a file in your editor and the column re-sorts about a second later.

Work is split by who owns the meaning:

  • wiki list --format json decides what an entry is. Bundle discovery, ignore globs, the frontmatter subset, --where semantics. That output already carries every frontmatter field plus _path, which is a complete board in one call. Shelling out means the board can never disagree with wiki list, which is the entire reason to shell out rather than re-implement.
  • wikanban reads bodies directly from disk when you open a card. wiki read would be a whole process spawn to strip a --- header, and the write path already needs frontmatter-boundary logic, so this adds no new surface.

Filtering

Filter chips use exactly the --where vocabulary: key=value, key!=value, repeatable and ANDed, with an empty value testing emptiness. They live in the URL, so a filtered board is a link you can send someone.

They execute in the browser, over the single fetch the page already has. That is faster than re-spawning the CLI per keystroke and lets the UI offer things --where cannot express, such as selecting several values of one field (an OR). To keep that honest rather than a silent divergence, every board has a copy as CLI action that prints the equivalent command:

wiki list --root /home/user/data/kb --prefix /backlog --where type=task --where status!=done

If that command and the board ever disagree, one of them is a bug.

Writes

A drag changes one frontmatter field. wikanban writes it the way wiki itself does in check --fix: find the --- fence, replace exactly the one matching line, leave every other byte untouched. Then write to a temp file and rename it into place, so a reader never sees a half-written entry, and take a lock file so two writes can't interleave.

It never parses an entry into a struct and reserializes it. The frontmatter parser both tools share is lossy on purpose, it skips nested maps and anchors, drops comments, and normalizes quoting. A round-trip would silently delete anything the subset doesn't model, on every single drag. Surgical editing cannot lose data it doesn't understand.

After each write the server runs wiki check and surfaces anything it reports. The board is not the authority on whether your bundle is healthy.

This is the one place wikanban duplicates logic that belongs upstream. wiki has no command to set a frontmatter field, so a UI has no primitive to call. That gap is filed as wiki#016 wiki set; when it lands, this code becomes a shell-out and the tests carry over unchanged.

Creating entries

With a [create] section configured, quick-add takes a title and writes an entry: the title slugged into a filename (lowercase, punctuation dropped, spaces to hyphens, collisions suffixed -2), type from config, and the group_by field set to whichever column you created it in.

The slug rules are deliberately stricter than wiki tidy --slug, which only collapses whitespace and preserves case. Being stricter means what wikanban creates is both check-clean and something tidy will never want to rename.

Point create.dir at a folder your wiki.toml lists under ignore_orphans (a parked backlog/, say). A new entry that nothing links to yet is otherwise reported by wiki orphans, which is correct behavior you don't want to trip on every quick capture.

There is no delete. Retiring work has a documented order in the project-backlog workflow (take it off the board first, then archive or delete the file, because wiki move faithfully rewrites board links into archive/), and without git there is no undo. That is a considered step, not a button.

Security

wikanban binds 0.0.0.0 by default and has no authentication. Anyone who can reach the port can rewrite your files. Put it behind a reverse proxy or bind 127.0.0.1.

Write endpoints do check Origin/Sec-Fetch-Site. That is not authentication, it stops a different attack: without it, any website your browser visits could POST to a board on your own network and edit your bundle.

The interface

Two form factors, two genuinely different gestures rather than one compromised one:

  • Desktop: every column visible, real drag-and-drop between them.
  • Mobile: columns snap-scroll one at a time; tap a card and change its column from the sheet. Dragging with a finger is worse than picking from a list, so it isn't offered.

A card shows its title, its priority, its tags, and how many entries block it. Priority is coloured by position in your configured order, never by name, so any vocabulary works: the first value is the most urgent. Column headers carry a count, and a column whose value isn't in your config is shown and marked rather than hidden.

Opening a card gives the rendered body, every frontmatter field, and the blocker graph in both directions: what blocks this, and what this blocks. A blocker naming an entry that doesn't exist yet is shown as pending, not broken.

Develop

just          # list recipes
just dev      # Vite dev server + Go server, hot reload against a real API
just build    # build the frontend, embed it, produce ./bin/wikanban
just check    # vet + lint + test, both sides
just smoke    # boot a server against a throwaway bundle and drive the API

Vite plus React plus TypeScript, compiled to static assets and go:embeded into the binary. One binary, no runtime dependency but wiki on PATH, the same shape wiki itself ships in. Not Next.js: there is no SSR and no build-time data, so a static export would be overhead with no return.

The dependency list is deliberately short. Tailwind earns its place for tokens, dark mode, and responsive layout. Beyond that, the browser already has what a board needs: native <dialog> for the sheet (focus trapping, Esc-to-close, and page inertness, correct and free), native HTML5 drag (bad on touch, which is exactly where we don't use it), and CSS scroll-snap for mobile columns. The only other runtime dependency is markdown-it, loaded lazily on first card open, and configured with html: false so raw HTML in a body is escaped rather than rendered.

The backlog is a wiki

wikanban's own backlog lives in backlog/ and is an agentic-wiki bundle, like the ones it renders. It is also the first board wikanban is pointed at.

cd backlog && wiki list --where type=task --where status!=done

About

A kanban UI for wiki bundles configured as a backlog

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages