scope-kanban
v0.10.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, agent execution state, and sandboxed agent-authored HTML visualizations; a visual coordination center for agent presence, durable conversations, delivery state, leases, attempts, and conflicts; workspace overview, epic filter, swimlanes (group by epic / assignee / priority / type), and 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.
The connected-agents button in the topbar opens Agent coordination. It shows heartbeat presence, pending delivery counts, active leases, attempt and conflict health, and every durable conversation available to the selected agent. Threads can be filtered by ticket, acknowledged, replied to, or started from the browser. Ticket cards show the current execution phase and agent; ticket drawers expand that into lease expiry, attempt status, verification, evidence, repository intent, handoffs, and conflicts. Message bodies remain durable workspace data. Agent rows and the message composer distinguish a live model-session connection from a durable mailbox that cannot currently wake its recipient.
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
Scope's primary API is an executable agent protocol. Start by discovering the runtime contract rather than assuming statuses, fields, or features:
scope --json capabilities
scope --json ready --capabilities node,postgres
scope --json claim --agent codex --capabilities node,postgres --files src/auth.js,test/auth.test.jsEvery JSON response is a versioned envelope. Read data; on failure inspect
error.code, error.retryable, and error.details. Mutations support two
global reliability controls:
# Exactly-once retry: normally replays the original receipt; after an
# interrupted receipt write, the event-carried request fence prevents a repeat.
scope --json --request-id run-42 status MA-7 in_progress --by codex
# Compare-and-swap: reject the write if state changed since the last read.
scope --json --if-revision sha256:abc... ticket edit MA-7 --priority highThe normal execution lifecycle is ready → claim → context → discover/plan →
complete. Claims atomically create a renewable lease and an attempt; completion
atomically records verification/evidence, finishes the attempt, releases the
lease, and moves the ticket to the workspace's done column. Contracts can make
capabilities, evidence, or passing verification mandatory.
scope --json contract set MA-7 \
--acceptance '["token expires correctly"]' \
--verify '["npm test -- auth"]' \
--policy '{"requireVerification":true}' --by planner
scope --json context MA-7 --budget 3000
scope --json lease renew LEASE_ID --agent codex
scope --json discover MA-7 fact "Expiry is enforced in middleware" --by codex
scope --json complete MA-7 --attempt 01... --agent codex \
--verification '[{"command":"npm test -- auth","ok":true}]'Capture data.lease.leaseId and data.attempt.attemptId from claim and keep
them distinct from the ticket id. Stop renewing after completion, handoff, or
release. A stale lease error means re-read execution state, not retry the old
id; an invalid argument means correct the command shape before retrying.
When Codex or Claude can spawn native subagents, the host remains the harness and Scope is the shared coordination state. Ask Scope for a conflict-aware parallel plan, then atomically claim one ticket for each native child:
scope --json ready --plan --capabilities node,postgres
scope --json claim MA-7 --agent codex:worker-1 \
--files src/auth.js,test/auth.test.jsready --plan groups tickets whose known repository intent is disjoint and
defers work that overlaps an active lease. Each child reads context, owns one
claimed ticket, and uses its native prompt from the parent. The host handles
spawning, model-session wakeup, waiting, cancellation, sandboxing, and worktrees.
Scope records the durable lease, attempt, observed files, discoveries,
verification, outcome, structured handoff, and addressed messages:
scope --json handoff create MA-7 --agent codex:worker-1 \
--summary "Middleware updated; expiry test still needed" \
--remaining '["add the regression test"]'Agents can register heartbeat presence and exchange durable threaded messages.
When registration runs inside Codex or Claude, Scope binds the current session
locally by default. The bridge owned by scope serve resumes the addressed
session, retries transient failures, and acknowledges after the provider accepts
the turn:
scope --json agent register codex:sol --provider openai --ttl 2m
scope --json message send --from codex:sol --to claude:opus \
--ticket MA-7 --kind review_request --body "Review commit abc123"
scope --json bridge statusInstall native Codex and Claude Code lifecycle hooks to register a private random identity automatically, reuse it on resume, renew presence, and mark it offline on session end:
scope bridge hooks install
scope --json bridge hooks statusThe supported-host matrix and mailbox-only fallbacks are documented in docs/session-lifecycle.md.
Explicit binding and the provider-neutral listener/SSE adapter contract are documented in docs/agent-messaging.md.
Humans can inspect the same state visually from the connected-agents button in
the scope serve topbar. The coordination center shows agent presence,
ticket-linked threads, pending/acknowledged delivery, active leases, attempts,
and conflicts; execution badges and details also appear on ticket cards and in
the ticket drawer.
Claims and attempt outcomes derive ticket lifecycle automatically. Agent-readable
ticket, board, context, and readiness JSON include a coherent execution
projection, so a parent can verify durable state instead of trusting a child's
final chat message.
Dependencies determine readiness; file/worktree/branch/base-SHA intent warns or
blocks overlapping claims; expired leases are reclaimable; causal sibling
writes become explicit conflicts; and scope watch --since <event-id> provides
a resumable JSONL changefeed. scope doctor --json verifies the authoritative
event log and cache, while --repair rebuilds only the disposable cache.
The same coordination methods are available from the published Node workspace
handle and under /api/agent/* on the hub. The generated command reference is
docs/agent-protocol.md.
Mixed-version agents
All agents sharing a workspace must use a compatible Scope reader. Check
scope --json capabilities: current builds write event format 2, read formats
1 and 2, and expose the requirement under
data.eventFormat.minimumReaderVersion. Upgrading preserves and reads existing
format-1 history without rewriting it. After a format-2 event is written, do
not reopen or sync that workspace with a format-1-only binary; scope doctor
reports the mismatch as an incompatible reader requirement rather than log
corruption. See docs/event-log-format.md.
Ship the agent 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 agents to discover capabilities, claim leases, use compact context packs, publish typed discoveries, attach verification, and recover from stale revisions or contention.
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 checks that source and documentation are clean, bumps the
patch version, refreshes the generated agent reference, commits the
release-owned files, tags, and pushes. Active,
untracked .scope/events/*.json and .scope/receipts/*.json runtime records do
not block a release and are never staged by the wrapper; tracked changes and
all other untracked files still block it. From there,
.github/workflows/release.yml takes over:
- Runs the full Node 20/22/24 suite plus the live PostgreSQL integration suite.
- Verifies the tag matches
package.jsonand inspects the npm payload. - Uses a short-lived GitHub OIDC identity to run
npm stage publish; no long-lived npm token is stored in GitHub. - Waits for npm's malware scan and an explicit maintainer approval with 2FA before treating the package as released.
- 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.
- Calls the hosted deployment workflow only after the release succeeds.
The npm package is configured with a stage-only trusted publisher for
briannadoubt/scope's release.yml workflow and the strict publishing-access
policy. Direct token publishing is intentionally disabled. During a release,
approve the scanned version from npm's Staged Packages view; the workflow
will then continue automatically.
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.
