boardown

September 2, 2026 · View on GitHub

CI License: MIT pnpm Node

A local-first task board that stores its data as plain markdown files inside your project's git repo. Releases, epics and tasks live in .boardown/ next to your code, so they version, branch and diff with the rest of the project — no cloud, no server, no account.

boardown ships as a VS Code extension that reads .boardown/ from the open workspace and a standalone desktop app (Windows / macOS / Linux) that opens any project folder — both reuse the same board UI and read the same markdown files. A headless CLI (@grinev/boardown-cli) rounds it out for scripts and AI agents, driving the same .boardown/ files from the command line.

AI-agent friendly. The board is plain markdown in your repo, so an AI coding agent (Claude Code, Cursor, …) already sees it next to your code. The CLI gives it a first-class way to drive that board: every command speaks JSON, so an agent can read the backlog, pick up the current release, and add or move tasks — the very same board you see in the editor. Plan with your agent, watch the board update live.

boardown board view

boardown task details

boardown backlog view

See PRODUCT.md for the full spec. What's planned next lives on boardown's own board in .boardown/.

Installation

boardown comes as a VS Code extension, a desktop app, and a command-line tool — pick whichever fits your workflow. The extension is published to the VS Code Marketplace and Open VSX; the desktop app is attached to each GitHub Release; the CLI is on npm. All of them read and write the same .boardown/ board.

VS Code extension

Install boardown from either registry:

Open the Extensions view (Ctrl+Shift+X / Cmd+Shift+X), search for boardown, and click Install. Or from the command line:

code --install-extension grinev.boardown

Prefer to install from a .vsix? Each GitHub Release attaches one:

  1. Download boardown-<version>.vsix from the latest release's Assets.
  2. Open VS Code and go to the Extensions view (Ctrl+Shift+X / Cmd+Shift+X).
  3. Click the menu at the top of the Extensions panel and choose Install from VSIX….
  4. Select the downloaded .vsix file.

Once installed, open your project folder and click the board icon in the top-right corner of the editor, or run Boardown: Open Board from the Command Palette (Ctrl+Shift+P / Cmd+Shift+P). If the workspace has no .boardown/ folder yet, an onboarding screen walks you through creating the board.

The open board refreshes itself when its .boardown/ files change on disk — switching branches, pulling, editing a file, or running the CLI all update the board in place, without the Reload button. Turn it off with the boardown.autoRefresh setting.

Desktop app

Each GitHub Release also ships a standalone desktop app (Electron). Grab the file for your OS from the release's Assets:

  • Windowsboardown-<version>-win-setup.exe (installer) or the portable boardown-<version>-win.zip (unzip and run boardown.exe).
  • macOS.dmg (drag to Applications) or .zip. Pick the arm64 build for Apple Silicon or the x64 build for Intel Macs.
  • Linux.AppImage (chmod +x boardown-*.AppImage && ./boardown-*.AppImage) or .deb (sudo dpkg -i boardown-*-linux-amd64.deb).

The desktop builds are not code-signed yet, so the OS warns on first launch. To run anyway:

  • Windows — on the SmartScreen prompt, click More info → Run anyway.
  • macOS — right-click the app → Open (then confirm), or clear the quarantine flag: xattr -dr com.apple.quarantine /Applications/boardown.app.
  • Linux — no prompt; just make the .AppImage executable as shown above.

On launch the app shows recent project folders and an Open Folder… button; pick a folder and the board loads from its .boardown/.

The board refreshes itself when those files change on disk — from git, an editor, or the CLI — updating in place without the Reload button. Toggle it under Settings → Auto-refresh on file changes in the sidebar.

Command-line interface (CLI)

A headless boardown command for scripts and AI agents, published to npm as @grinev/boardown-cli. It reads and writes the same .boardown/ markdown files as the apps, so every change is a reviewable git diff.

Install it globally, or run it on demand with npx:

npm i -g @grinev/boardown-cli   # installs the `boardown` command
boardown --help

npx @grinev/boardown-cli release current  # or run without installing

It finds the board by walking up from the current directory to a .boardown/ folder (like git finds .git), or takes --data-dir <path>. Output is a stable JSON envelope when piped (or with --json) and human-readable in a terminal, which makes it a good surface for automation. It reads the way the app does — release current, backlog and archive mirror the three tabs and list tasks as compact summaries; task get <id> is the full drill-down. See the CLI README for the full command list and the machine-readable schema contract.

Local server (boardown-web)

A local HTTP server that serves the board UI in an ordinary browser tab, for one project or for several at once. It is not published to npm: you build a tarball from a checkout and install that, which is the whole of its distribution story.

pnpm install
pnpm --filter @boardown/web build
cd packages/web && npm pack          # → boardown-web-<version>.tgz
npm i -g ./boardown-web-<version>.tgz

With no argument it serves your registry of projects — the list of boards you keep on this machine. Point it at a single board instead with --data-dir, or at another registry file with --registry:

boardown-web                                      # your registry of projects
boardown-web --data-dir /path/to/project/.boardown
boardown-web --registry /home/me/projects.yaml
boardown-web --port 7777                          # otherwise the OS picks the port
boardown-web --no-watch                           # do not watch; Reload only

The default registry file lives where your OS keeps user configuration:

OSFile
Windows%APPDATA%\boardown-web\projects.yaml
macOS~/Library/Application Support/boardown-web/projects.yaml
Linux and the rest${XDG_CONFIG_HOME:-~/.config}/boardown-web/projects.yaml

It prints the address it is listening on, and the registry file it opened. It binds the loopback interface only and refuses a request from anywhere else, so it is a board on your machine, not a board on your network.

An open board refreshes itself when its files change on disk — from git, an editor, or the CLI — updating in place without the Reload button, as in the VS Code and desktop shells. Pass --no-watch to turn that off for the run.

Nothing is created for you up front: until that file exists the server starts anyway and / lists no projects — the file, and the folder above it, are written the first time you add a project. A file that is there but does not parse refuses the start instead, naming the reason — the same as one you passed with --registry.

The registry is a list of projects, whether it is the default file or one you name with --registry:

# projects.yaml
projects:
  boardown: /home/me/code/boardown
  shop: /home/me/work/shop

Each key is the id that appears in the URL — lowercase letters, digits and dashes — and each value is an absolute path to the project folder, whose board is the .boardown/ inside it. The boards are then at http://127.0.0.1:<port>/b/boardown/ and /b/shop/, and / lists them with each project's name taken from its own config.yaml. A project whose board cannot be read still gets a row, with the reason on it, so one bad line never costs the others. Editing the file is enough: a project added to it shows up without a restart, and one removed from it stops being served, and reloading / is what re-renders the list.

You do not have to edit it by hand, though. The list page keeps it for you: every row has a Remove button, and under the list an Add project button opens a dialog taking a path and an id, filling the id in from the folder's name as you type. A folder with no board yet is fine — it is registered like any other, and opens onboarding when you click it. Removing a project takes it out of the list and nothing else; your files stay exactly where they are. Both write one line of the registry file and leave the rest of it, comments included, alone — so the file is still yours to edit, and a project you add by hand and one you add from the page look the same.

Custom task statuses (beta)

Beta. Statuses are declared by hand in config.yaml and there is no UI for managing them yet. The on-disk format may still change before 1.0 — expect to edit your config when it does.

A board can replace todo / in-progress / done with its own columns. Declare them in .boardown/config.yaml:

statuses:
  - key: backlog
    label: Not started   # optional — the key is shown when absent
  - key: dev
  - key: review
  - key: shipped

It is all-or-nothing: absent keeps the three built-ins, present replaces the whole set. Between 2 and 8 entries; key follows the same rule as a custom field's — 1–40 characters, starts with a letter, then letters, digits, _ or - — and keys must be unique.

The meaning is positional. The first status is the one a new task takes, and the only one a task may be created with outside an active release. The last is the terminal one: a link to a task in it is struck through, and completing a release counts everything else as unfinished. The columns between are what the WIP limit caps — one number in wipLimits, applied to each of them independently. Colours follow the same order, so the first column stays grey and the last stays green whatever you call them.

Editing the list under an existing board never rewrites your files. Tasks whose status you dropped keep it and gather in a read-only Unknown column at the end of the board, which you can drag them out of; the CLI answers USAGE naming the board's own list if you try to set a status it does not declare, and boardown schema reports the list so an agent reads it up front.

Custom task fields (beta)

Beta. This is the first slice of a larger customization story: only the string type exists, fields are declared by hand in config.yaml, and there is no UI for managing them yet. The on-disk format and the CLI flag may still change before 1.0 — expect to edit your config when they do.

A board can carry extra per-task fields that boardown itself knows nothing about — who reported a bug, which environment it reproduces on, a ticket number in another system. Declare them in .boardown/config.yaml:

idPrefix: BD
nextId: 47
projectName: My Board

customFields:
  - key: reporter       # the frontmatter key, and the CLI's --field name
    label: Reporter     # optional — the key is shown when absent
    type: string        # the only type today
  - key: env
    type: string

key is 1–40 characters, starts with a letter and continues with letters, digits, _ or -. Keys must be unique, and may not reuse a name task frontmatter already has (id, type, priority, status, epic, order, checklist, notes, links). A bad declaration makes the config invalid — the app shows its config error screen and the CLI returns BOARD_INVALID, rather than ignoring the line.

Every declared field becomes a row in the task dialog's Details panel, under Type / Priority / Epic / Release, edited in place like everything else there. Values land in the task's own frontmatter as ordinary keys:

## Fix flaky sort order

---
id: BD-12
type: bug
status: in-progress
order: 300
reporter: alice
env: staging
---

A value that mentions another task (BD-12), a doc page ([[guides/release-process]]) or a web address (https://example.com) renders those as links, the same way the task description does — clicking one opens the task, the doc page, or the URL in the system browser. Typing [[ while editing a field offers the doc-page suggestion list.

From the CLI, set them with a repeatable --field, and ask schema which fields a board declares:

boardown task edit BD-12 --field reporter=alice --field env=staging
boardown task edit BD-12 --field reporter=        # clear it
boardown task add "Fix login" --type bug --field env=prod

boardown schema --json     # includes the board's customFields

Two things to know while this is beta. Values are stored as plain top-level keys, so removing a field from customFields drops its stored values the next time each of those files is written — git is the recovery path. And a board that declares nothing sees no change at all: no new rows, no new keys on disk, no new CLI output.

Working with branches

The board is text in your repo, so it merges like any other file — no server arbitrates writes. Two habits keep merges clean.

On a branch, touch only the task that branch is about — its status, title, description, checklist, notes. That is one task block in one file, and a status change is a two-line diff: task sections never move around, so nothing else in the file shifts. Leave the neighbouring tasks alone even if you spot something worth fixing; note it down for later.

Reshape the board on the main branch: creating tasks (a new task bumps nextId in config.yaml and two branches end up with the same ID, which nothing detects yet), dragging cards, moving a task between the backlog, an epic and a release (a delete in one file and an insert in another — merged badly, the task lands in both), and the release lifecycle.

Skim the .boardown/ diff before merging: a duplicated task block is easy to spot in markdown and far cheaper to fix there. Conflict markers left inside a task's frontmatter only drop that task — the parser flags it and the rest of the board keeps working.

Building the .vsix from sources

To build an installable .vsix yourself instead of downloading it:

pnpm install
pnpm --filter boardown package

This produces packages/vscode/build/boardown-<version>.vsix, which you can install with the steps above.

Try it from sources

Install dependencies once:

pnpm install

Start boardown against this repo's sample .boardown/:

pnpm dev

Or open another project by pointing --data-dir at that project's .boardown/ directory:

pnpm dev -- --data-dir /path/to/project/.boardown

Each run writes a log file to logs/ at the repo root (gitignored), named for the run's start time — logs/web-2026-07-19T14-32-08-123Z.log. At the default info level it records every action taken in the app (with its arguments), every write to the board, and every failure on either side, which makes it the thing to attach to a bug report. BOARDOWN_LOG_LEVEL=debug adds each individual read/list/stat; warn or error narrows it to problems. The folder keeps the 10 most recent runs. Only this dev shell writes logs — the VS Code, Electron and CLI builds a user installs write none.

Then open http://localhost:5173 in a browser. In VS Code, run Simple Browser: Show from the Command Palette, enter http://localhost:5173, and pin the tab if you want it to behave like a local board panel.

If the selected .boardown/ has no config.yaml, the web shell creates the default structure automatically with idPrefix: TASK. Create config.yaml manually before first launch if you want a different prefix.

Development

Requirements:

  • Node.js >= 20 (the repo pins 20 via .nvmrc; Node 22 also works)
  • pnpm 10+ (npm install -g pnpm or via corepack)

Install dependencies once if you skipped the quick start above:

pnpm install

The repo is a pnpm workspace with six packages:

  • packages/core — platform-agnostic logic (schemas, parser, board operations). Pure TypeScript, runs in Node.
  • packages/ui — the React app: components, Zustand store, UI flow. Takes an FsAdapter as input, knows nothing about the host. Source-only (consumed directly by the shell's bundler).
  • packages/web — the browser shell, in two roles over one set of endpoints: the Vite dev app that mounts @boardown/ui over a middleware serving a local .boardown/ data directory, used for iterating on the UI from sources, and boardown-web, a local server for one board or a registry of several. Nothing here is published to a registry.
  • packages/vscode — the primary MVP distribution target, a VS Code extension shell next to web (extension host via esbuild + webview via Vite), reusing @boardown/ui unchanged. Packages into an installable .vsix (see Building the .vsix from sources above).
  • packages/electron — a cross-platform desktop shell (macOS / Windows / Linux): an Electron main process + preload behind the same FsAdapter, with a Vite-built renderer that reuses @boardown/ui. See Desktop app (Electron) below.
  • packages/cli — a headless command-line / agent-facing shell: it does not mount @boardown/ui, mapping commands onto @boardown/core board operations over a Node FsAdapter, with machine-readable JSON output. Bundled with esbuild and published to npm as @grinev/boardown-cli. See Command-line interface (CLI) above.

Common scripts (run from the repo root)

CommandWhat it does
pnpm devStart the web dev server against this repo's .boardown/ (Vite, http://localhost:5173)
pnpm dev:sandboxStart the web dev server against a throwaway copy of the test fixture (http://localhost:5199) — see Browser testing
pnpm buildBuild the shells that have a build script (web → Vite bundle, vscode → host + webview); core and ui are source-only and skipped
pnpm testRun Vitest across all packages
pnpm typecheckRun tsc --noEmit in every package
pnpm lintRun ESLint over the workspace
pnpm formatApply Prettier in-place
pnpm format:checkCheck Prettier formatting without writing
pnpm iconsRegenerate every shell's app icon from assets/brand/boardown.svg (run after changing the logo)

Running a single package

Use pnpm's --filter:

pnpm --filter @boardown/web dev      # only the web dev server
pnpm --filter @boardown/core build   # only the core build
pnpm --filter @boardown/core test    # only core tests
pnpm --filter @boardown/ui test      # only ui tests
pnpm --filter @grinev/boardown-cli test   # only cli tests

The dev server runs in any modern browser — it talks to the selected .boardown/ over a local Vite middleware, so no File System Access API or Chromium-only feature is involved.

To open another boardown data directory from sources, pass --data-dir. The path must point to the .boardown directory itself, not to the project root:

pnpm dev -- --data-dir /path/to/project/.boardown

If --data-dir is omitted, boardown uses this repository's .boardown/, same as before. Relative --data-dir paths are resolved from the directory where you run the command.

Desktop app (Electron)

packages/electron is a cross-platform desktop build (macOS / Windows / Linux). It reuses @boardown/ui unchanged behind an Electron FsAdapter and boots to a sidebar of recent project folders — pick one, or Open Folder…, and the board loads from that folder's .boardown/.

Run it from sources in dev (Vite HMR for the renderer, esbuild watch for the main process and preload):

pnpm --filter @boardown/electron dev
# open a specific folder on launch:
pnpm --filter @boardown/electron dev -- /path/to/project

Bundle and package it:

pnpm --filter @boardown/electron build   # main + preload (esbuild) + renderer (Vite) → dist/
pnpm --filter @boardown/electron dist    # package for the current OS via electron-builder → release/

pnpm install downloads the Electron binary automatically (it is allow-listed in the root pnpm.onlyBuiltDependencies).

Per OSelectron-builder packages for the host OS, so run dist on the OS you're targeting (or in a CI matrix, one runner per OS):

  • macOS.dmg + .zip
  • Windows → a Setup .exe installer (NSIS) + a portable .zip
  • Linux.AppImage (run directly) + .deb

Cross-building from another OS is fiddly (Windows would need Wine), so the Release workflow runs a per-OS matrix to build all three and attach them to each GitHub Release (see Releasing). Signed / notarized artifacts (Apple notarization, Windows code-signing) need certificates and are deferred, so distributed builds are unsigned for now — end users see a SmartScreen (Windows) / Gatekeeper (macOS) warning on first launch (how to bypass it).

App icons

Every shell's app icon derives from a single master, assets/brand/boardown.svg. pnpm icons rasterizes it (via sharp + png2icons) into the per-shell binaries each build expects — the VS Code Marketplace icon.png, and Electron's build/icon.{ico,icns,png} (Windows / macOS / Linux) — and also writes the VS Code command/tab button mark packages/vscode/media/board.svg as a small colored copy of the master, so it never drifts from the app icon. Those outputs are committed, so normal builds and CI never need the rasterizer. To change the logo, replace the one SVG and re-run pnpm icons.

Sample board for the dev server

The repo ships a .boardown/ folder at the root with a minimal config and a couple of empty releases / epics. pnpm dev reads the selected data directory via a small Vite middleware that exposes /api/fs/{read,list,stat,write} over HTTP, and @boardown/ui mounts on top of an HttpFsAdapter that talks to those endpoints — the same pair boardown-web serves. This is the working environment for UI development and local use from sources; a production browser deployment (folder picker, FS Access API or otherwise) is not in the MVP scope.

When the selected data directory has no config.yaml, the shell does not seed a config or a starter release. Instead @boardown/ui shows an onboarding modal that collects the project name and ID prefix and writes .boardown/config.yaml on submit (nextId starts at 1). After onboarding the board starts empty and opens on the Backlog tab — create your first release from the UI. The web dev shell only ensures the board root directory exists.

Browser testing

There is no e2e suite. UI changes are exercised by driving a real browser — either by hand or by an AI agent through the Playwright MCP server configured in .mcp.json (first run needs npx playwright install chromium).

pnpm dev:sandbox

This copies tests/fixtures/board/.boardown/ into a fresh temp directory, prints its path, and serves it at http://localhost:5199. The copy is what makes the board safe to poke at: every click writes markdown through /api/fs/write, so driving the repo's own .boardown/ would corrupt the real board (and loading a board can rewrite config.yaml on its own, via the nextId check). Nothing is cleaned up on exit — the temp copy stays around so you can inspect what the UI actually wrote.

The fixture covers the interesting states: a finished, a current and a future release, tasks in every status and of every type, an epic with unscheduled tasks, a task with a checklist and notes, and a pair of linked tasks.

Board columns, task cards, backlog rows and sections carry data-testid attributes because they have no accessible name of their own; everything else (dialogs, form fields, buttons) is reachable through roles and labels.

Releasing

The whole monorepo ships under one lockstep version: the same number lives in every package.json, with the root package.json as the single source of truth. Each release attaches the VS Code .vsix plus the Electron desktop installers for all three OSes (Windows / macOS / Linux), and publishes the CLI to npm; future shells (web, JetBrains) will release together under the same version.

Releases are driven by a version bump on main, not by pushing tags by hand:

  1. Bump the version:

    pnpm release:prepare patch     # or minor / major / an explicit 0.3.0
    pnpm release:rc                # cut a 0.3.0-rc.1 prerelease
    

    This updates the root version and mirrors it into every package. For a stable version it also seeds docs/release-notes/vX.Y.Z.md with a draft (the same notes the workflow would auto-generate). It does not commit and does not tag.

  2. Curate the release docs, then commit them together:

    • Rewrite docs/release-notes/vX.Y.Z.md into user-facing notes — the workflow publishes it verbatim as the GitHub Release body (and falls back to generating from the commit log if the file is absent; RC prereleases always generate).
    • Add a ## X.Y.Z section to packages/vscode/CHANGELOG.md and, if the release adds a headline capability, a bullet to the extension README's Features list — these are the VS Code Marketplace listing.
    • Run the gates (pnpm lint && pnpm typecheck && pnpm build && pnpm test), then stage the version bump plus those docs and commit as a single chore(release): vX.Y.Z (this scope is excluded from generated notes).
  3. Push to main:

    git push origin main
    
  4. The Release workflow notices that the tag vX.Y.Z for the current version does not exist yet. It runs in three stages: a determine job decides whether the bump is releasable; a build matrix then runs the checks and builds the .vsix once on Linux and the desktop installers on one runner per OS (electron-builder packages only for its host); finally a release job gathers every artifact, resolves the release notes (the committed docs/release-notes/vX.Y.Z.md if present, otherwise generated from the commit log), creates and pushes the tag, and publishes a GitHub Release with the .vsix and the desktop installers in Assets. If the tag already exists (no version bump), the workflow skips the release.

  5. Once the GitHub Release exists, Release calls the reusable Publish to Marketplace workflow, which downloads the released .vsix and pushes it to the VS Code Marketplace (vsce publish). It needs a VSCE_PAT repository secret (an Azure DevOps Personal Access Token with the Marketplace → Manage scope for the grinev publisher); prerelease (-rc.N) versions and versions already on the Marketplace are skipped. If the store publish fails after the release is cut, re-run it on its own from Actions → Publish to Marketplace → Run workflow against the same tag — no rebuild needed.

  6. Release then also calls the reusable Publish to Open VSX workflow, which publishes the same released .vsix to the Open VSX registry (ovsx publish) — the open marketplace used by VSCodium, Gitpod, Cursor and others. It needs an OVSX_PAT repository secret (an Open VSX access token whose owner owns the grinev namespace, created once via ovsx create-namespace grinev); prerelease (-rc.N) versions and versions already on Open VSX are skipped. It can likewise be re-run on its own from Actions → Publish to Open VSX → Run workflow against any tag.

  7. Release finally calls the reusable Publish to npm workflow, which builds and publishes the CLI package (@grinev/boardown-cli, the boardown command) to the npm registry. Auth is tokenless via npm Trusted Publishing (OIDC) — the repo is registered as a trusted publisher for the package, so no npm secret is needed; the workflow only mints an id-token for auth and build provenance. Stable versions publish under the latest dist-tag and RC prereleases under next; versions already on npm are skipped. Re-runnable on its own from Actions → Publish to npm → Run workflow against any tag.

boardown tracks its own work on a board stored in .boardown/. Commits that only touch that board data use the chore(board): … scope and are excluded from the generated release notes (just like the chore(release): … bump commit), so dog-fooding the board never clutters a user-facing changelog.

Preview the notes that would be generated for the current version with:

pnpm release:notes:preview

This is also the starting point curated notes are seeded from: the preview and docs/release-notes/vX.Y.Z.md share the same generator.

Every push and pull request to main also runs the CI workflow (lint, typecheck, build, test).

License

MIT