Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

355 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tandem

Real-time project-scoped kanban for humans and AI agents.

Website: tandemcloud.cc

Tandem is a single Rust backend with four first-class clients:

  • Desktop app (Tauri + Vue + shadcn-vue)
  • Web UI (Vue + shadcn-vue)
  • CLI (tandem)
  • MCP server (mcp/) for LLM agents

Screenshot

Tandem desktop board

What You Get

  • HTTP API for task and project operations
  • Project-scoped WebSocket stream for live board updates
  • Optional bearer auth (TANDEM_AUTH_TOKENS)
  • SQLite persistence with in-memory cache
  • Pluggable storage boundary (BoardPersistence) for future backend swaps (for example libSQL)
  • Integrated developer workflow for local, remote, and Tauri-oriented UI development

Architecture

flowchart LR
  subgraph Clients["Client Surfaces"]
    UIBrowser["Vue UI\n(Browser)"]
    UIDesktop["Vue UI\n(Tauri WebView)"]
    CLI["tandem CLI"]
    MCPClient["MCP Host\n(Claude, Codex, etc)"]
  end

  subgraph Desktop["Desktop Shell (Tauri v2)"]
    Runtime["Desktop runtime manager\n(workspace settings + mode switch)"]
    Settings["workspace-settings.v1.json"]
    LocalProc["Local server subprocess\n(--tandem-local-server)"]
  end

  subgraph MCP["MCP Adapter (mcp crate)"]
    MCPServer["tandem-mcp\nstdio server"]
    MCPSubs["Resource subscription manager\nclient-id aware subscribe/unsubscribe"]
    MCPWS["WebSocket listener\n(reconnect + fan-out)"]
  end

  subgraph Server["Tandem Server (axum)"]
    HTTP["REST API\n/api/tasks /api/projects\n/api/agent-runs"]
    WS["WebSocket /ws\nproject-filtered events"]
    Access["Access control\nAuthorization + X-Tandem-Project"]
    State["AppState\nbroadcast + webhook + storage"]
    Presence["Presence registry\n(online/recent clients)"]
    Runs["Agent run state\n(in-memory queue/history)"]
  end

  subgraph Storage["Persistence Layer"]
    Trait["BoardPersistence trait"]
    Sqlite["SqliteBoardPersistence"]
    DB[("tandem.db (SQLite)")]
    Future["Future backend\n(e.g. libSQL)"]
  end

  subgraph SessionRuntime["Session Runtime (tandem-session)"]
    Main["main\ncoordinator + SSE aggregator"]
    Worker["worker\nproject-local executor"]
    PI["pi-coding-agent\n(pi-rpc / pi-sidecar)"]
  end

  Webhook["Webhook endpoint (optional)"]

  UIBrowser -->|HTTP| HTTP
  UIBrowser -->|WS| WS
  UIDesktop -->|invoke| Runtime
  Runtime --> Settings
  Runtime -->|start/stop| LocalProc
  UIDesktop -->|effective HTTP| HTTP
  UIDesktop -->|effective WS| WS
  LocalProc -->|hosts| HTTP
  LocalProc -->|hosts| WS
  CLI -->|HTTP| HTTP
  MCPClient -->|MCP stdio| MCPServer
  MCPServer -->|HTTP tools| HTTP
  MCPWS -->|WS events| WS
  MCPServer --- MCPSubs
  MCPSubs --- MCPWS

  HTTP --> Access
  WS --> Access
  Access --> State
  State --> Presence
  State --> Runs
  State --> Trait
  Trait --> Sqlite
  Sqlite --> DB
  Trait -.swappable boundary.-> Future
  State -->|broadcast WsMessage| WS
  State -->|task notifications| Webhook

  Runs -->|POST /run| Main
  Main <-.SSE /event.-> State
  Main -->|POST /run by project| Worker
  Worker -->|POST /worker/event| Main
  Worker -->|POST /worker/heartbeat| Main
  Worker -->|spawns + control channel| PI
  PI -.events / stream.-> Worker
Loading

Quick Start

1. Install dependencies

git clone https://github.com/nasedkinpv/tandem-oss
cd tandem-oss
cargo build
npm install

2. Run local server + UI

make ui

This starts Tandem on http://127.0.0.1:8080 and serves UI from ./dist when available (or ./static as fallback).

3. Use CLI

# add
target/debug/tandem add "Review API contract" --priority high --tag api --tag docs

# create/list projects
target/debug/tandem project create "Platform Core"
target/debug/tandem project list
target/debug/tandem project update platform-core --context-file ./AGENTS.md

# list (optional tag/column filters)
target/debug/tandem list --column todo --tag api

# cross-project dashboard
target/debug/tandem overview
target/debug/tandem overview --stale-days 10 --stuck-days 5 --quiet

# update
target/debug/tandem edit <id> --column doing --add-tag in-progress

# update task inside a specific source project scope
target/debug/tandem edit <id> --project platform-core --title "Renamed from CLI"

# move task between projects (source scope + explicit target project)
target/debug/tandem edit <id> --project default --to-project platform-core

# move command supports the same project flags
target/debug/tandem move <id> done --project default --to-project platform-core

# delete
target/debug/tandem delete <id> -y

Dev Workflows

Local integrated mode

make ui

Remote backend UI mode

TANDEM_REMOTE_URL=http://<remote-host>:8080 make ui-remote

When Vite is available, /api and /ws are proxied to TANDEM_REMOTE_URL, so UI Workspace Settings can stay default in dev mode.

Fallback mode (no Vite) serves static files only. In that case set UI Workspace Settings manually:

  • API Base URL: http://<remote-host>:8080
  • WebSocket mode: Auto (or Manual with explicit URL)
  • Auth Token: optional bearer token
  • Project: project slug or * (UI fetches all projects and aggregates)

MCP server

# local backend
make mcp

# remote backend
TANDEM_URL=http://<remote-host>:8080 make mcp

Tauri-ready UI scripts

npm run ui:dev:tauri
npm run ui:build:tauri

Native desktop shell (Tauri v2)

# run desktop app in dev mode
npm run tauri:dev

# build desktop app bundle
npm run tauri:build

Desktop runtime behavior:

  • Default mode is local: app launches a managed Tandem subprocess on startup and UI auto-connects.
  • If workspace mode is remote, desktop app does not start local subprocess.
  • Settings are persisted per desktop workspace in workspace-settings.v1.json (app config dir).
  • Connection mode can be switched to remote; optional stopWhenRemote stops local subprocess automatically.
  • WebSocket config is explicit: wsMode=auto|manual with wsUrlManual persisted separately (no implicit loss).
  • Workspace Settings are grouped into categories: Connection, Agent Runtime, Identity, Defaults, Advanced.

Optional build-time default backend for desktop shell:

VITE_TANDEM_DEFAULT_API_BASE_URL=http://127.0.0.1:8080 npm run ui:build:tauri

Build standalone CLI/MCP binaries

# build for current host target and copy to ./bin
make build-bin

# explicit macOS Apple Silicon output
make build-bin-macos-arm64

Outputs are written to ./bin with platform suffixes (for example tandem-macos-arm64, tandem-mcp-macos-arm64).

API Contract

Endpoints

Method Path Notes
GET /api/health Readiness endpoint with DB check (200 {"status":"ok","db":"ok"} or 503 {"status":"degraded","db":"error"})
GET /api/capabilities Runtime feature flags (for example agentRuns.executorEnabled)
GET /api/overview Cross-project aggregates (todo/doing/done/stale/stuck) with ?stale_days and ?stuck_days thresholds
GET /api/tasks Cursor paging (?limit, ?cursor) + filters (?tag=..., ?column=..., `?sort=asc
GET /api/tasks/search Cursor-paged search (?q required) with optional tag/column/sort/limit/cursor
POST /api/tasks Create task
GET /api/tasks/:id Fetch one task (project-scoped)
PATCH /api/tasks/:id Partial update with validation (including optional project move)
DELETE /api/tasks/:id Delete task
GET /api/agent-runs List run states for active project (task_id, limit)
POST /api/agent-runs Dispatch a new agent run for a task (optional model override; optionally forwards webhook to external executor)
POST /api/agent-runs/:run_id Update run status (`queued
GET /api/projects List projects
POST /api/projects Create project (name, optional slug, optional context)
PATCH /api/projects/:slug Update project metadata (name, context)
DELETE /api/projects/:slug Delete empty project (default project is protected)
POST /api/projects/:slug/copy Clone project metadata + tasks (include_done option), reset task authorship
GET /api/projects/:slug/members List project members (`owner
PATCH /api/projects/:slug/members/:member_id Update member role (owner only)
DELETE /api/projects/:slug/members/:member_id Remove member (owner only, cannot remove last owner)
GET /api/projects/:slug/invites List invites for project (owner only)
POST /api/projects/:slug/invites Create invite + one-time claim (owner only)
POST /api/invites/:id/revoke Revoke invite (owner only)
POST /api/invites/accept Accept invite by claim and upsert membership
GET /api/tokens List token metadata for active project (owner only)
POST /api/tokens Issue scoped token (plaintext returned once)
POST /api/tokens/:id/revoke Revoke token
POST /api/tokens/:id/rotate Rotate token (revokes old, returns new plaintext once)
GET /api/ws/replay Replay buffered project-scoped websocket events since cursor (project, since, limit)
GET /api/ws/presence Presence snapshot + timeout diagnostics for one project (project)
GET /api/project-chat/events Replay buffered project-chat websocket events (project, since, limit, optional session_id)
GET /ws WebSocket stream (project-scoped)

Agent runtime capability semantics

GET /api/capabilities includes agentRuns flags:

  • executorEnabled: executor webhook is configured on server.
  • executorMode: executor mode (session_runtime).
  • executorReachable: last cached probe indicates endpoint is reachable.
  • executorAuthFailed: last probe returned 401/403 (auth mismatch).
  • executorLastError: sanitized probe error text (no token values).
  • executorCheckedAt: RFC3339 timestamp of latest probe sample.
  • sessionRuntimeSse: runtime SSE bridge snapshot:
    • enabled: bridge configured and started.
    • mode: shadow|active when configured.
    • connected: current stream connectivity state.
    • lastEventId / lastEventAt: last observed runtime SSE event.
    • lastError / checkedAt: last stream failure and observation timestamp.
  • sessionRuntimeMain: runtime main /health snapshot:
    • reachable, service, at, workers, mainSessionWake.
    • components[] with per-component status, lastError, lastOk, updatedAt, restartCount.
  • sessionRuntimeWorkers: runtime main /worker snapshot:
    • reachable, checkedAt, lastError.
    • workers[] entries: workerId, project, baseUrl, directory, activeRuns, lastSeen, agentRunAvailable.
  • sessionRuntimePollRecovery: SSE fallback poll status:
    • enabled, lastAttemptAt, lastSuccessAt, lastError, lastRecoveredRuns.
  • sessionRuntimeWsReplay: websocket replay endpoint snapshot:
    • enabled, lastAttemptAt, lastSuccessAt, lastErrorAt, lastError,
    • lastTruncatedAt, truncatedCount, lastReplayedEvents, lastReplayedPages.

UI policy:

  • Show Run Agent only when executor is configured.
  • If probe sample exists and executorReachable=false, show disabled/warning state before dispatch.
  • In session_runtime, show Run for a task only when current project has discovered worker(s) with agentRunAvailable=true.
  • Runtime settings should expose topology/discovery panel:
    • main health summary,
    • worker topology (host/project/lastSeen/activeRuns/event lag),
    • discovery coverage for known projects,
    • SSE + poll-recovery diagnostics.

Request context

  • Authorization: Bearer <token> when TANDEM_AUTH_TOKENS is configured
  • Issued tokens from /api/tokens are also accepted:
    • stored as SHA-256 hash
    • reject immediately when revoked/expired
    • enforce declared scope on API + WebSocket paths
  • X-Tandem-Project: <slug> for project scope
  • Optional client identity headers:
    • X-Tandem-Client-Id
    • X-Tandem-Client-Name
    • X-Tandem-Client-Type
    • X-Tandem-Avatar-Seed
  • WebSocket supports query fallbacks for browser clients:
    • /ws?token=<token>&project=<slug>&client_id=<id>&client_name=<name>&avatar_seed=<seed>&client_type=<type>

Task payloads now include actor metadata fields:

  • created_by (who created task)
  • created_by_user_id (stable creator user id)
  • updated_by (who last edited task)
  • updated_by_user_id (stable last-editor user id)

Validation rules (server)

  • Title: required, max 500 chars
  • Content: max 10,000 chars; empty string clears content on update
  • Tags: max 20 tags, each non-empty, max 50 chars
  • Priority: low|medium|high (default: medium)
  • Project slug: lowercase letters, digits, -, max 64 chars
  • Project context: optional markdown, max 10,000 chars (empty string clears on update)
  • PATCH /api/tasks/:id with project requires target project to exist
  • Invite target_email: optional, max 320 chars
  • Invite expires_in_minutes: optional, defaults to 1440, max 10080
  • Token label: optional, max 120 chars
  • Token expires_in_minutes: optional, defaults to 1440, max 10080 (0 means immediate expiry)
  • Token scopes: workspace:* or project:<slug>:read|write|admin (project scopes must match active project)

WebSocket event types

All websocket frames include monotonic event_id cursor field for replay/recovery.

  • tasks_loaded
  • task_added
  • task_updated
  • task_moved
  • task_deleted
  • presence_snapshot
  • presence_changed
  • agent_run_dispatched
  • agent_run_updated
  • agent_run_output_delta
  • project_chat_message

Storage Model

SQLite file defaults to ./tandem.db.

Main tables:

  • projects
  • users
  • api_tokens
  • project_memberships
  • project_invites
  • audit_events
  • tasks

The runtime keeps an in-memory board cache under RwLock and persists every mutation through BoardPersistence. Agent run state is intentionally in-memory only (bounded per project).

Environment Variables

Server

  • TANDEM_BIND (default 127.0.0.1:8080)
  • TANDEM_DB_FILE (default ./tandem.db)
  • TANDEM_STATIC_DIR (default ./dist if present, else ./static)
  • TANDEM_CORS_ORIGIN (default *)
  • TANDEM_AUTH_TOKENS (comma-separated bearer token allowlist)
  • TANDEM_DEFAULT_PROJECT (default default)
  • TANDEM_WEBHOOK_URL
  • TANDEM_WEBHOOK_TOKEN
  • TANDEM_AGENT_EXECUTOR_URL (optional outbound endpoint for agent_run_dispatched payloads)
  • TANDEM_AGENT_EXECUTOR_TOKEN (optional bearer for executor endpoint)
  • TANDEM_AGENT_EXECUTOR_TIMEOUT_MS (optional timeout, default 10000)
  • TANDEM_AGENT_EXECUTOR_MODE (optional, currently session_runtime)
  • TANDEM_SESSION_RUNTIME_SSE_MODE (optional: off, shadow, active; if unset and executor mode is session_runtime, defaults to active)
  • TANDEM_SESSION_RUNTIME_SSE_URL (optional override for runtime main SSE endpoint; default is derived from executor URL as <base>/event)
  • TANDEM_SESSION_RUNTIME_SSE_TOKEN (optional bearer for runtime SSE stream; defaults to executor token)
  • TANDEM_SESSION_RUNTIME_SSE_RECONNECT_MS (optional SSE reconnect delay, default 750)
  • TANDEM_WS_EVENT_HISTORY_LIMIT (optional in-memory per-project websocket replay buffer size, default 4000, clamp 100..20000)
  • TANDEM_AGENT_RUN_QUEUE_TIMEOUT_SECS (optional stale queue timeout, default 180)
  • TANDEM_AGENT_RUN_RUNNING_TIMEOUT_SECS (optional stale running timeout, default 900)
  • RUST_LOG

Legacy executor aliases were removed:

  • TANDEM_EXECUTOR_URL -> TANDEM_AGENT_EXECUTOR_URL
  • TANDEM_EXECUTOR_TOKEN -> TANDEM_AGENT_EXECUTOR_TOKEN

CLI / MCP context

  • TANDEM_URL (default http://localhost:8080)
  • TANDEM_TOKEN
  • TANDEM_PROJECT (default project scope; CLI --project overrides it per command)
  • TANDEM_CLIENT_NAME (optional actor label override for CLI/MCP)
  • TANDEM_CLIENT_ID (optional stable client id override)
  • TANDEM_CLIENT_ID_FILE (optional path for persisted client id file)
  • TANDEM_WS_URL (MCP only; optional explicit WS override)

UI build/runtime

  • VITE_TANDEM_DEFAULT_API_BASE_URL
  • TANDEM_REMOTE_URL (used by Vite proxy in remote dev mode)
  • TANDEM_UI_HOST, TANDEM_UI_PORT

Quality Gates

# tests
cargo test
cd mcp && cargo test

# strict lint
cargo clippy --all-targets -- -D warnings
cd mcp && cargo clippy --all-targets -- -D warnings

# dead-code scans (UI + optional rust dependency audit)
make deadcode
TANDEM_REQUIRE_UDEPS=1 ./scripts/check-rust-udeps.sh

Integration coverage includes:

  • tests/api_integration.rs (API behavior, validation, project scoping)
  • src/cli/http_client.rs (CLI cursor pagination for list/search + /api/tasks/search 404 fallback)
  • mcp/src/client.rs (MCP cursor pagination for list/search + defensive cursor-loop checks)
  • token lifecycle and scope isolation (issue/list/revoke/rotate, revoked/expired rejection)
  • mcp/tests/smoke.rs (MCP end-to-end list/filter and project create/update/move smoke against mock Tandem API)

Additional Docs

License

Elastic License 2.0 (ELv2). See LICENSE. Third parties may use/modify the software, but may not offer Tandem itself as a hosted or managed service under ELv2.

About

Real-time project-scoped kanban for humans and AI agents

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages