scope-kanban
v0.8.2
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 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).
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 / link / status / branch / pr / boardwith--jsonoutput on every command for agent consumption. - Web UI — kanban columns, drag-and-drop, ticket drawer with inline edit, 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. 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.
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 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 — deploy nothing, just git pull
Scope is event-sourced. The source of truth is an append-only log under
.scope/events/ — one JSON file per change, named by a time-sortable ULID.
scope.db is a cache rebuilt from that log on demand; it is gitignored (via
.scope/.gitignore, written by scope init) and must never be committed.
Because the log is append-only and every file name is globally unique, two
people (or agents) working in parallel never write the same file. Merging is
just the union of each side's event files — exactly what git pull does for a
directory of new files. There is no binary SQLite merge, so there is nothing to
corrupt:
# commit the log (NOT the cache)
git add .scope/events && git commit -m "scope: plan auth work"
git pull # brings in teammates' event files
# next `scope` command rebuilds scope.db from the merged log — automatically
scope boardConflicts 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.
This works over any dumb file sync — git, iCloud Drive, Dropbox, Syncthing — because all any of them has to do is deliver new files. No server to deploy.
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] | Create .scope/ in the current directory. Prompts on a TTY if flags are omitted. |
| 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. 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 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};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/
│ ├── 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.
