scope-kanban
v0.9.0
Published
Local-first kanban for projects, epics, stories, and bugs — built for agents. CLI + web UI.
Maintainers
Readme
scope
Local-first kanban for epics, stories, and bugs — built so coding agents and humans can plan and track work without leaving the command line.
Ships as a CLI, a GitHub-Projects-style web UI, and a hub daemon
(scope serve) that fans changes out to every viewer over SSE. Everything
lives in a .scope/ directory in your repo (SQLite, WAL mode), so it works
offline, syncs through git if you want, and needs no external service.
brew install briannadoubt/tap/scope # macOS / Linuxbrew
npm install -g scope-kanban # any platform with Node ≥20
npx scope-kanban --help # one-shot, no installQuick start
cd ~/my-app
scope init # prompts for workspace key + name on a TTY
scope workspace set --key MA --name "My App" --description "Short blurb"
scope auth login # browser-approved login for Scope Cloud
scope connect # creates/joins a hosted project and syncs
scope ticket create "Auth refactor" -t epic -p high
scope ticket create "OAuth login" -t story --parent MA-1
scope serve # → https://localhost:4321 (also https://scope.local:4321)
scope ca trust # one-time: trust the local CA so browsers stop warningscope init accepts --key MA --name "My App" if you want to skip the prompts
(e.g. from another agent or a non-interactive shell). New workspaces keep their
event log and SQLite cache in machine-local Scope storage by default, with a
small committed .scope/workspace.json marker in the repo. Use
scope init --git-events only when you intentionally want the event log carried
through git.
LAN security
scope serve listens on HTTPS with a leaf cert signed by a local
certificate authority (generated on first run, persisted to
~/.scope-hub/ca/). Authentication is layered:
- Browser path — a bearer token stored in a cookie. Bookmark
https://scope.local:4321/?token=…once (printed at startup) and the cookie does the rest. Loopback connections from the same machine bypass the token check entirely soscopeCLI commands work without configuration. - Native path (SwiftUI app, etc.) — clients pair with
scope pairand get a client certificate signed by the local CA. mTLS replaces the bearer token for those connections; seescope devices list.
To clear the browser cert warning, trust the local CA once:
scope ca trust # System keychain, sudo (recommended)
scope ca trust --user # login keychain, no sudo (per-user only)
scope ca fingerprint # print SHA-256 for out-of-band verification
scope ca untrust # reverses `scope ca trust`The CA's private key lives at ~/.scope-hub/ca/ca.key (mode 0600) and
never leaves the machine. The cert at ~/.scope-hub/ca/ca.crt is what gets
trusted by the keychain.
What ships today
- CLI —
workspace / ticket / epic / artifact / link / status / branch / pr / boardwith--jsonoutput on every command for agent consumption. - Web UI — configurable kanban columns, drag-and-drop, ticket drawer with inline edit and sandboxed agent-authored HTML visualizations, workspace overview, epic filter, swimlanes (group by epic / assignee / priority / type), live updates via SSE.
scope servehub — one long-lived process that serves the UI, the REST API, and the SSE event stream onhttps://localhost:4321(loopback HTTP also bound for CLI traffic). Multiple agents and a human in the browser all share the workspace's SQLite DB; writes from any source push to every viewer over Server-Sent Events within ~100ms.- Self-healing federated hub — every
scope serveinvocation auto-discovers a running hub (default port4321, walks forward to4330if taken by a non-scope process) and registers its local.scope/workspace with it. First one to start binds the port; the rest idle with a watchdog that promotes a survivor if the hub-owning process dies. Concurrent Claude Code sessions / previews / repos all converge on the same UI, no port flags required. Each repo keeps its own.scope/scope.db(so it travels withgit clone). - iOS app — SwiftUI client that discovers the hub over Bonjour, pairs via
mTLS, and renders the same board + ticket detail + live updates. Lives in
App/in this repo.
The web UI
scope serveThe Group by picker in the topbar turns the board into swimlanes — one
horizontal row per epic, assignee, priority, or type, each with its own
status columns. Manage columns in that same popover lets you add, rename,
reorder, recolor, and classify workspace-specific statuses. State (group
choice, collapsed lanes) persists in localStorage.
The little dot next to the refresh button is your live indicator: green = SSE connected, blue flash = just applied a change, gray = paused (drawer/modal/input/drag), red = disconnected. Clicking refresh during a red indicator triggers an active hub re-probe and rebuilds the SSE connection.
Programmatic API
Beyond the CLI, scope-kanban publishes its data layer as an importable
library. Install it as a dependency and drive a workspace from Node directly —
no shelling out to the scope binary:
import { openWorkspace, TICKET_TYPES } from 'scope-kanban';
const ws = openWorkspace(); // opens the nearest existing .scope/ (like the CLI)
const epic = ws.createTicket({ type: 'epic', title: 'Auth refactor', priority: 'high' });
const story = ws.createTicket({ type: 'story', title: 'OAuth login', parent: epic.id });
ws.updateTicket(story.id, { status: 'in_progress' }, 'claude');
ws.listTickets({ status: 'in_progress' }); // → [{ id: 'MA-2', ... }]
ws.epicProgress(epic.id); // → { total, counts, done, percent }
ws.close();openWorkspace() is the one entry point — every operation hangs off the
returned handle, so there's a single obvious way to do things and the storage
engine stays hidden. It mirrors exactly what the CLI does on each command (open
the SQLite cache, ensure the append-only event log, replay the log if it's
ahead), so writes persist to both the cache and the log — identical to
scope ticket create. Each handle method forwards to the data layer with the
workspace's db pre-bound; the raw db is not exposed, since writing to it
directly would bypass the event log and be lost on the next cache rebuild.
Like the CLI, opening never creates a workspace implicitly — it throws if none
is found, so library writes can't land in an accidental board. Pass
{ create: true } to bootstrap one:
const ws = openWorkspace('./.scope', { create: true });
ws.updateWorkspace({ key: 'MA', name: 'My App' });Alongside the handle, the root also exports the domain vocabulary
(TICKET_TYPES, STATUSES, PRIORITIES, RELATION_TYPES, DEFAULT_COLUMNS) —
enough to build UIs and validate input without reaching into internals.
Published entry points
| Entry | For |
|---|---|
| scope-kanban | The library above — openWorkspace plus the domain vocabulary. |
| scope-kanban/cli | run / buildProgram, for embedding the commander program. |
The surface is intentionally small: one runtime entry, not a function per table.
Everything else under src/ is private, so internals can move without a
breaking change.
Agent integration
Agents call scope via the CLI — every command supports --json for
machine-readable output. No extra config; if scope is on
$PATH it works.
// example tool use from an agent
scope --json ticket list --status todo
scope --json ticket create "Fix CSRF on /signup" -t bug --priority high
scope --json status MA-7 in_progress --by claude
scope artifact add MA-7 ./latency-dashboard.html --by claudeShip the "how to use scope" skill into Claude Code, Codex, or Cursor:
scope skills install # uses bundled copy from your install
curl -fsSL https://raw.githubusercontent.com/briannadoubt/scope/main/skills/install.sh | bash # remoteForce a subset or target a specific repo:
scope skills install --tool claude
scope skills install --tool cursor --project /path/to/repoThe skill teaches the agent when to reach for Scope (multi-step work,
status updates, bug tracking), how to invoke it (CLI with --json), the
data model, and a handful of guardrails (e.g. read state before writing
state when multiple agents share a board).
Previewing in Claude Code
If you want the kanban available in Claude Code's preview pane, use
scope preview --port <unique> in .claude/launch.json — never plain
scope serve for previews:
{
"version": "0.0.1",
"configurations": [
{
"name": "scope-myproject",
"runtimeExecutable": "scope",
"runtimeArgs": ["preview", "--port", "4322"],
"port": 4322,
"autoPort": false
}
]
}Why: Claude Code's preview_start enforces one tracked server per port.
If two projects both register port: 4321 (the hub), opening the preview in
the second pane forcibly stops the first pane's tracked process — the iframe
goes blank with "The preview server stopped." Even with unique server names
this happens, because the collision is on port.
scope preview --port <N> works around this with a tiny per-pane reverse
proxy: each project picks its own port (e.g. 4322, 4323, ...), and every
proxy forwards to the single shared hub on 4321. Each pane gets its own
preview-tracked server (no collision), all viewers see the same federated
kanban. The first scope preview to run lazily starts the hub via the
usual ensureHub() path; subsequent ones just proxy.
Pick a different port for every project (suggested range: 4322–4399).
Data model
| | |
|---|---|
| Hub | The scope serve daemon. Discovers and brokers traffic across one or more workspaces on a machine / LAN. |
| Workspace | A .scope/ directory: owns the key prefix (e.g. MA), name, description, and overview. Each workspace is one SQLite database. |
| Ticket | Epic, story, or bug. Belongs to one workspace. IDs are <KEY>-<n> (e.g. MA-3). |
| Epic | High-level work. Parents stories and bugs. |
| Story | Unit of work toward an epic. |
| Bug | Defect. Can live under an epic. |
| Status | backlog → todo → in_progress → in_review → done (+ cancelled) |
| Priority | low / medium / high / urgent |
| Relation | blocks, blocked_by, relates_to, duplicates, duplicate_of (inverse auto-created) |
Ticket IDs are immutable — once a ticket is created, its prefix is baked into its ID. Changing the workspace key after the fact leaves old tickets with the old prefix.
Collaboration and storage
Scope is event-sourced. The source of truth is an append-only log — one JSON
file per change, named by a time-sortable ULID. By default, new workspaces store
that log and the rebuildable scope.db cache under ~/.scope/workspaces/<id>/
so Scope does not flood your codebase with event files. The repo carries a tiny
.scope/workspace.json marker plus optional .scope/remote.json; secrets never
go there.
The normal sharing path is Scope Cloud:
scope auth login # stores a machine-local credential
scope connect # defaults to https://scope-hub.fly.dev
scope remote show # explains cloud target, auth, and local storageIf you see two boards with the same name, check the subtitle in the board
switcher. A local workspace is the editable repo marker on this machine; a
cloud project is the hosted copy that remote clients and the iOS app read.
scope remote show tells you whether the local workspace is bound to that cloud
project.
Git-carried events remain available as an advanced mode:
scope init --git-events
scope events move-to-git
scope events move-to-local
scope events statusIn git-events mode, the log lives at .scope/events/ and scope.db remains a
cache that must never be committed. Because the log is append-only and every
file name is globally unique, merging is just the union of new event files.
Conflicts resolve deterministically without coordination:
- Concurrent field edits → last-writer-wins by timestamp (ULID breaks ties).
- New tickets / comments / relations → grow-only union; both survive.
- Ticket numbers (
SCP-42) are display values de-collided at replay — the earliest creator keeps the number; a colliding offline create is bumped.
Git-events mode works over any dumb file sync — git, iCloud Drive, Dropbox, Syncthing — because all any of them has to do is deliver new files.
When you do want sub-second live updates, run scope serve: the hub brokers
changes over SSE/mTLS on a machine or LAN. That's an optimization on top of the
same log — the log remains the source of truth, so going offline and syncing
later loses nothing. (Real-time across the open internet with zero
infrastructure is the one thing that's out of scope — that always needs a
meeting point.)
See docs/event-log-format.md and docs/adr/0001-decentralized-ticket-identity.md for the format and conflict semantics.
Command reference
| Command | What it does |
|---|---|
| scope init [--key KEY --name NAME] [--git-events] | Create .scope/ in the current directory. Defaults to machine-local event storage; --git-events opts into .scope/events. |
| scope workspace show | Print the current workspace (key, name, description, overview). |
| scope workspace set [--key KEY] [--name NAME] [--description ...] [--overview ...] | Edit workspace metadata. --key only affects future tickets. |
| scope workspace rekey <KEY> | Change the key and reprefix every existing ticket (MA-1 → APP-1). The correct way to rename a key. |
| scope workspace add / list / remove | Manage which workspaces the running hub knows about. |
| scope ticket create <title> -t <type> [--parent <epic>] | New ticket in the current workspace. |
| scope ticket list / show / edit / delete | Manage tickets. edit accepts a comma-separated id list (atomic). |
| scope status <ids> <status> [--by <name>] | Move a ticket to any status id configured in the workspace columns. ids may be comma-separated to move several atomically. |
| scope batch [-f ops.json] | Apply many ops as one atomic transaction (or pipe the JSON array on stdin). Supports $ref to reference a ticket created earlier in the batch. The supported path for bulk/compound edits — never edit scope.db directly. |
| scope branch <id> [<name>] [--in-progress] | Get/set branch, optionally flip status. |
| scope pr <id> [<url>] [--in-review\|--merged] | Get/set PR, optionally flip status. |
| scope link add <from> <type> <to> | Relate two tickets. |
| scope epic list / children <id> | Epic-focused views. |
| scope comment <id> <body> [--by <name>] | Add a comment. |
| scope history <id> | Change log for a ticket. |
| scope board [--epic <id>] | Terminal kanban view. |
| scope serve [-p <port>] | Run the hub (auto-attaches to a running hub if one exists). |
| scope preview --port <N> | Run a per-pane proxy to the hub. For Claude Code's .claude/launch.json — each pane uses a unique port so preview_start doesn't make panes stop each other. |
| scope auth login [--remote <url>] | Browser-approved hosted login for this machine. Stores the key outside the repo so future syncs just work. |
| scope connect [--new NAME\|--project ID] | Connect the workspace to Scope Cloud by default, write safe .scope/remote.json, and run the first sync. |
| scope events status / move-to-local / move-to-git | Inspect or migrate event storage between quiet local mode and git-events mode. |
| scope ca fingerprint / trust / untrust / path | Manage the local certificate authority. |
| scope pair | Pair a new native client (prints a one-time 6-digit code). |
| scope devices list / rename | Inspect or rename paired native clients. |
| scope skills install [--tool ...] [--project ...] | Install agent skill. |
Deprecated.
scope project create / show / list / editare kept as aliases that route to thescope workspacecommands and print a yellow warning.scope project deleteerrors out — there's nothing to delete now that each workspace owns exactly one project.scope ticket createstill accepts--project <KEY>but ignores it with a deprecation warning.
Every command accepts --json for machine-readable output.
Architecture
- Storage — SQLite via
better-sqlite3, in.scope/scope.db. WAL mode for safe multi-process writes; serialization happens at the SQLite layer. Each DB has a singletonworkspacerow (key, name, description, overview) and aticketstable — the oldprojectstable has been folded intoworkspace. Existing DBs migrate on first open. - CLI — Node 20+ ES modules,
commanderfor parsing. - Server — Express. Mounts the REST API and an SSE
/eventschannel.GET /api/workspacesreturns{id, scope_dir, label, key, name, description, overview, columns};GET /api/boardreturns{columns, terminal_columns, buckets}so web, hosted, and iOS clients render the same workspace-defined status pipeline.GET /api/projectsis kept as a back-compat shim that synthesizes one project per workspace for older clients. - Realtime — in-process
EventEmitterbus emits on every mutation;fs.watchon.scope/plus aPRAGMA data_versioncheck catches writes from other processes (CLI, sibling serve processes) and feeds them into the same bus. UI subscribes viaEventSource, debounces refresh, and diffs by hash to skip no-op renders. - Hub coordination — discovery file at
~/.scope-hub/hub.json, workspace registry at~/.scope-hub/workspaces.json. The watchdog in every long-lived process polls/api/metaand re-runsensureHub()if the current hub stops answering, so the UI never goes blank for surviving workspaces.
Releasing
npm run release bumps the patch version, tags, and pushes. From there,
.github/workflows/release.yml takes over:
- Verifies tag matches
package.json. npm publish --provenance --access publicto the npm registry.- Fetches the GitHub source tarball and computes its sha256.
- Patches
Formula/scope.rband pushes it intobriannadoubt/homebrew-tapvia an SSH deploy key. - Creates a GitHub release with auto-generated notes.
Bump types:
npm run release # patch
npm run release minor
npm run release major
npm run release 1.0.0 # explicitRepo layout
.
├── bin/scope.js # CLI entrypoint
├── src/
│ ├── index.js # public library API (openWorkspace + vocab)
│ ├── cli.js # commander wiring
│ ├── db.js # SQLite schema, migrations, id generation
│ ├── repo.js # data layer (emits change events)
│ ├── events.js # in-process bus
│ ├── server.js # Express: REST + SSE + UI
│ ├── hub.js # auto-discovery + watchdog
│ ├── workspaces.js # workspace registry
│ ├── format.js # terminal table / board renderers
│ └── web/ # vanilla-JS SPA (no build step)
├── App/ # SwiftUI iOS client
├── skills/ # agent skills (Claude / Codex / Cursor)
├── Formula/scope.rb # Homebrew formula
├── .github/workflows/ # tag-driven release
└── scripts/release.sh # local bump + tag + push wrapperLicense
MIT — see LICENSE.
