Skip to content

Configuration

Tandem’s configuration surface stays small on purpose. Five things to know: agent definitions (agents.json), the mc_settings toggles surfaced under /admin, the environment variables the backend reads, the three ports services bind to, and the targeted restart commands. Everything else is convention.

agents.json

The agent roster lives in project-workspace/Tandem/app/agents.json as a single JSON document with a top-level "agents": [...] array. Each entry is one named agent. The schema below is enforced by the backend on read; missing required fields cause that agent card to be skipped (with a warning in the API log).

Schema (per-agent fields)

FieldTypePurpose
idstringStable slug. Used by mc.py claim-task <id> <agent-id>, in URLs (/agents/<id>), and as the worktree branch suffix.
namestringDisplay name on the Agents page.
descriptionstringOne-line summary. Renders below the card name.
projectstringOwning project. Use "General" for cross-project agents.
default_toolstringDefault LLM (e.g. opus, sonnet, haiku, codex, ollama/qwen3.6:35b).
system_promptstringFull role prompt. Multiline strings allowed. Required.
toolsstring[]Whitelist of tools this agent may invoke (e.g. Read, Grep, Bash, mc.py). Enforced by the per-agent ACL.
constraintsstring[]Human-readable rules surfaced in the agent card and prepended to dispatch prompts.
skillsstring[]Skill names loaded for this agent (matches .claude/skills/ entries).
autonomous_configobjectPer-agent ACL. Keys: worktree_isolation (bool), allowed_dirs (path[]), network_access (local/none/full), max_sessions (int).

Example: a minimal writing agent

{
"id": "doc-updater",
"name": "Doc Updater",
"description": "Keeps README, CHANGELOG, and wiki pages aligned with code.",
"project": "Tandem",
"default_tool": "sonnet",
"system_prompt": "You are a documentation writer. Read the diff, identify which docs are affected, and update them in the same PR. Never rewrite history — only patch.\n\nTASK TRACKING: Use `python3 mc.py update-task <id> --status active` when starting, and append to `## Activity Log` when done.",
"tools": ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
"constraints": [
"Only edit files under docs/, README.md, CHANGELOG.md, or wiki/",
"Never delete a doc — archive it under project-workspace/archive/",
"Cite the source commit hash in every wiki edit"
],
"skills": ["documentation-and-adrs", "obsidian-markdown"],
"autonomous_config": {
"worktree_isolation": true,
"allowed_dirs": [
"project-workspace/Tandem/app",
"wiki",
"docs"
],
"network_access": "local",
"max_sessions": 2
}
}

mc_settings

Live toggles that change runtime behavior without a restart. Stored in the mc_settings table in tandem.db; surfaced and edited under /admin. Every toggle is also exposed via GET /api/mc-settings/<key> + POST /api/mc-settings/<key>.

KeyValuesWhat it controls
agent_restart_modegated · freeWhether agents may restart services without an explicit user prompt. gated (default) routes via the MiniFixService popover or asks y/n; free lets the agent pick the targeted restart but never run a full mc.py fix while a long-running job is in flight.
sandbox_enabledoff · onAdmin-gated bubblewrap sandbox for agent-generated code (off by default). When on, code runs inside bwrap via systemd-run; verify the host with python3 -m sandbox.prereq.
auto_snapshoton · offPer-agent post-action snapshot hook (.claude/hooks/post-agent-snapshot.sh). When on, dirty worktrees auto-commit on Agent tool return with prefix auto-snapshot: as a safety net.
approvals_defaultdeny · promptDefault disposition for agent actions outside their allowed_dirs. deny short-circuits silently; prompt opens an approval card under /approvals.
cost_panel_provideranthropic · codex · bothWhich provider’s JSONL transcripts the /costs page reads. anthropic pulls claude --print output; codex reads the Codex CLI cache.
spark_worker_enabledoff · onWhether spark-worker.py picks queued briefs. When off, briefs queue up but are never dispatched — useful for staging template changes.
scheduled_jobs_enabledoff · onMaster switch for spark-scheduler.py cron-style triggers. Off pauses every scheduled brief at the clock-match step without losing state.

Environment variables

Read once at process start. Set in ~/.tandem/env, the systemd unit’s Environment= stanza, or export inline before tandem.sh.

Core

VariableDefaultPurpose
TANDEM_API_BASEhttp://127.0.0.1:4200API base URL. Every helper (mc.py, spark-worker, spark-scheduler, terminal-server) reads it.
MC_FRONTEND_PORT3000Override the Next.js port. Always bind 127.0.0.1.
MC_API_PORT4200Override the API port.
MC_TERMINAL_PORT4201Override the WebSocket terminal port.
TANDEM_VAULT~/Desktop/claude-brainAbsolute path to the Obsidian vault. Used by tasklib.py, the wiki ingest pipeline, daily-notes append.
TANDEM_DB<tandem-dir>/tandem.dbSQLite path. Override for testing.

Spark (queue + scheduler)

VariableDefaultPurpose
SPARK_VAULT$TANDEM_VAULTVault root passed into bwrap-spark.sh.
SPARK_TANDEM_DIRdir of spark-worker.pyWhere bwrap-spark.sh + spark-settings.json live.
SPARK_AGENTS_JSON<tandem>/agents.jsonUsed to resolve brief.template_id → spark-* system_prompt.
SPARK_PHASE_TIMEOUT900Wall-clock cap (seconds) per author phase. Accommodates cold qwen3.6:35b load.
SPARK_OLLAMA_FALLBACKhttp://127.0.0.1:11434Used if /api/settings/ollama-primary-url is missing.
SPARK_NETWORK_ALLOW1Forwarded to bwrap-spark.sh; set 0 to enforce --unshare-net.
SPARK_LOG_FILE$TANDEM_VAULT/tandem-outputs/spark-worker.logAppend-only log path.
SPARK_SCHEDULER_STATE<tandem>/data/spark-scheduler-state.jsonLast-fire-date persistence so restart-same-day doesn’t double-fire.

Offline / local-only

VariableDefaultPurpose
CLAUDE_OFFLINEunsetWhen 1, agents must not pass a model param on Agent tool calls and must skip Anthropic-only built-in subagents. Local model required.
TANDEM_NPM_TIMEOUT_SECS900Max seconds for the npm install step on macOS install. Bump for slow links.
TANDEM_BROWSER(prompt)Auto-pick browser for macOS install — safari, brave, chrome, or skip.

Port config

Three services. All bind 127.0.0.1 only — the install hardening refuses 0.0.0.0.

PortServiceProcessOverride
3000Frontend (Next.js + Tailwind + shadcn/ui)npm run dev via tandem.shMC_FRONTEND_PORT
4200API (HTTP, REST)python3 server.pyMC_API_PORT
4201Terminal (WebSocket, node-pty, tmux-persistent)node terminal-server.jsMC_TERMINAL_PORT

Verify they are bound

Terminal window
ss -tln | grep -E ':(3000|4200|4201)\b'
# Expected output — three rows, all 127.0.0.1:
# LISTEN 0 ... 127.0.0.1:3000 0.0.0.0:*
# LISTEN 0 ... 127.0.0.1:4200 0.0.0.0:*
# LISTEN 0 ... 127.0.0.1:4201 0.0.0.0:*

If any row shows 0.0.0.0:PORT instead of 127.0.0.1:PORT, that service was started without the right bind flag — stop it and re-launch via tandem.sh (which enforces the bind).

Service restart

Use the most targeted restart command possible. Never run a full mc.py fix when only one service needs bouncing — it nukes the frontend Next.js cache and costs ≈60 s every time.

CommandRestartsTimeWhen to use
mc.py fixBackend + frontend + caches~60 sCorrupted state, first boot, total reset.
mc.py fix backendBackend only (server.py)~3 sAfter editing server.py, tasklib.py, config.py or any Python backend file.
mc.py fix frontendFrontend only (Next.js dev server)~10–60 sAfter editing frontend/ files that need a dev-server restart.
mc.py fix terminalTerminal server only~2 sAfter editing terminal-server.js, or when WebSocket connections drop.

Aliases

  • mc.py repair — same as mc.py fix.
  • mc.py debug — same as mc.py fix, with extra stderr noise.
  • Both aliases accept the same subcommands: backend, frontend, terminal.

Rules for agents

  • If you edited server.pymc.py fix backend (NOT mc.py fix).
  • If you edited frontend components → mc.py fix frontend.
  • If you edited terminal-server.jsmc.py fix terminal.
  • Full mc.py fix only when multiple services are broken or caches are corrupted.

From the Tandem UI

If you’re in the MiniFixService popover (sidebar header), every restart above is a button:

  • Terminalmc.py fix terminal
  • Backendmc.py fix backend
  • Frontendmc.py fix (frontend implies the full path)
  • Fullmc.py fix

Agents emit a =======RESTART <AREA> REQUEST======= banner and fire POST /api/ui/highlight {target, button} to open the popover with the button pulsing. No permission prompt, no CLI paste.