This guide covers:
- how to build and start
tandem-engine - how to run automated tests
- how to run the end-to-end smoke/runtime proof flow on Windows, macOS, and Linux
From tandem/:
pnpm install
pnpm engine:stop:windows
cargo build -p tandem-ai
New-Item -ItemType Directory -Force -Path .\src-tauri\binaries | Out-Null
Copy-Item .\target\debug\tandem-engine.exe .\src-tauri\binaries\tandem-engine.exe -Force
pnpm tauri devFrom tandem/:
pnpm install
# Kill any existing engine instance
pkill tandem-engine || true
cargo build -p tandem-ai
mkdir -p src-tauri/binaries
cp target/debug/tandem-engine src-tauri/binaries/tandem-engine
pnpm tauri devPowerShell equivalent (Windows):
pnpm install
Get-Process | Where-Object { $_.ProcessName -in @('tandem-engine','tandem') } | Stop-Process -Force -ErrorAction SilentlyContinue
cargo build -p tandem-ai
New-Item -ItemType Directory -Force -Path .\src-tauri\binaries | Out-Null
Copy-Item .\target\debug\tandem-engine.exe .\src-tauri\binaries\tandem-engine.exe -Force
pnpm tauri devFrom tandem/:
cargo build -p tandem-ai
cargo run -p tandem-ai -- serve --host 127.0.0.1 --port 39731
cargo test -p tandem-server -p tandem-core -p tandem-aicargo build -p tandem-ai --profile fast-release
sudo install -m 755 target/fast-release/tandem-engine /usr/local/bin/tandem-engine
sudo systemctl restart tandem-enginecd packages/tandem-control-panel && pnpm build && sudo systemctl restart tandem-control-panel.serviceVerify token-gated API behavior:
cargo run -p tandem-ai -- serve --host 127.0.0.1 --port 39731 --state-dir .tandem --api-token tk_test_tokenIn a second terminal:
# Health remains public
curl -s http://127.0.0.1:39731/global/health | jq .
# Non-health endpoints require token
curl -i -s http://127.0.0.1:39731/config/providers
curl -s http://127.0.0.1:39731/config/providers -H "X-Tandem-Token: tk_test_token" | jq .Desktop/TUI token checks:
- Desktop Settings now shows
Engine API Tokenmasked by default withRevealandCopy. - TUI supports
/engine tokenfor masked output and/engine token showfor full output plus storage backend/path. - Storage backend is reported as
keychain,file,env, ormemory.
Validate that engine-detected environment is surfaced to every client path:
curl -s http://127.0.0.1:39731/global/health | jq .environmentExpected fields:
osshell_familypath_stylearch
Optional: toggle environment prompt block:
TANDEM_OS_AWARE_PROMPTS=0 cargo run -p tandem-ai -- serve --host 127.0.0.1 --port 39731On Windows, validate shell guardrails:
- Unix-only commands that cannot be safely translated are blocked with
metadata.os_guardrail_applied=trueandguardrail_reason. - Translatable commands (for example
ls -la,find ... -type f -name ...) includemetadata.translated_command.
serve supports:
--hostor--hostname(same option)--port--state-dir
State directory resolution order:
--state-dirTANDEM_STATE_DIR- canonical shared storage data dir (
.../tandem/data)
Use the tool subcommand to invoke built-in tools directly with JSON input.
webfetch is especially useful because it converts noisy HTML into clean Markdown,
extracts links + metadata, and reports size reductions. It should work against any public
HTTP/HTTPS webpage (subject to site limits, auth, or anti-bot protections).
Use webfetch_html when raw HTML is explicitly required.
The Markdown output is returned inline on stdout as JSON in the output field:
output.markdownholds the Markdownoutput.textholds the plain-text fallbackoutput.statsincludes raw vs Markdown size
Size savings example (proven from the Frumu.ai run above):
- raw chars: 36,141
- markdown chars: 7,292
- reduction: 79.82%
- bytes in: 36,188
- bytes out: 7,292
@'
{"tool":"webfetch","args":{"url":"https://frumu.ai","return":"both","mode":"auto"}}
'@ | cargo run -p tandem-ai -- tool --json -@'
{"tool":"mcp_debug","args":{"url":"https://mcp.exa.ai/mcp","tool":"web_search_exa","args":{"query":"tandem engine","numResults":1}}}
'@ | cargo run -p tandem-ai -- tool --json -cat << 'JSON' | cargo run -p tandem-ai -- tool --json -
{"tool":"webfetch","args":{"url":"https://frumu.ai","return":"both","mode":"auto"}}
JSONRun:
cargo test -p tandem-server -p tandem-core -p tandem-aiCoverage includes route shape/contracts like:
/global/health/provider/api/sessionalias behavior/missioncreate/list/get/apply-event/routinescreate/list/patch/delete/run-now/history/events/session/{id}/message/session/{id}/run/session/{id}/run/{run_id}/cancel- SSE
message.part.updated prompt_async?return=run(202withrunID+ attach stream)- same-session conflict (
409with nestedactiveRun) - permission approve/deny compatibility routes
Mission/routine policy-focused tests:
cargo test -p tandem-server mission_ -- --nocapture
cargo test -p tandem-server routines_ -- --nocapture
cargo test -p tandem-server routine_policy_ -- --nocapture
cargo test -p tandem-server routines_run_now_ -- --nocaptureContract-focused tests (JSON-first orchestrator parsing):
cargo test -p tandem test_parse_task_list_strict -- --nocapture
cargo test -p tandem test_parse_validation_result_strict_rejects_prose -- --nocaptureDesktop/CLI runtime contract closure tests:
# Desktop sidecar tests (includes reconnect recovery + conflict parsing + run-id cancel)
cargo test -p tandem sidecar::tests::recover_active_run_attach_stream_uses_get_run_endpoint -- --nocapture
cargo test -p tandem sidecar::tests::test_parse_prompt_async_response_409_includes_retry_and_attach -- --nocapture
cargo test -p tandem sidecar::tests::cancel_run_by_id_posts_expected_endpoint -- --nocapture
cargo test -p tandem sidecar::tests::mission_list_reads_engine_missions_endpoint -- --nocapture
cargo test -p tandem sidecar::tests::mission_get_reads_engine_mission_endpoint -- --nocapture
cargo test -p tandem sidecar::tests::mission_create_posts_to_engine_mission_endpoint -- --nocapture
cargo test -p tandem sidecar::tests::mission_apply_event_posts_event_payload -- --nocapture
# CLI (tandem-tui) run-id cancel client path
cargo test -p tandem-tui cancel_run_by_id_posts_expected_endpoint -- --nocaptureExample MCP auth challenge shape:
Authorization is required before I can continue with this action.
Tool mcp.arcade.jira_getboards result: Authorization required for mcp.arcade.jira_getboards.
Authorize here: https://example.com/oauth/start
Use a sanitized example in docs. Do not paste live OAuth challenge payloads with real redirect URIs, scopes, or state values into committed documentation. MCP-focused regression checks:
# Runtime MCP auth challenge extraction + schema normalization coverage
cargo test -p tandem-runtime mcp::tests::extract_auth_challenge_from_result_payload -- --nocapture
cargo test -p tandem-runtime mcp::tests::normalize_mcp_tool_args_maps_clickup_aliases -- --nocapture
# Full runtime MCP test module
cargo test -p tandem-runtime mcp::tests -- --nocaptureManual MCP smoke checks:
- Connect MCP server and verify tools are present in
/tool. - Trigger an auth-gated tool and verify
mcp.auth.requiredis emitted. - Complete auth and retry tool call (no engine restart required).
- Force refresh/disconnect failure and verify stale MCP tools are not left active.
- In web quickstart, verify run failures are rendered (no blank chat state on failure).
TUI mission quick-action commands (manual runtime validation):
/missions
/mission_create Mission Demo :: Validate quick actions :: Initial task
/mission_get <mission_id>
/mission_start <mission_id>
/mission_review_ok <mission_id> <work_item_id>
/mission_test_ok <mission_id> <work_item_id>
/mission_review_no <mission_id> <work_item_id> needs_revision
This is the automated version of the manual proof steps and writes artifacts to runtime-proof/.
./scripts/engine_smoke.ps1Optional args:
./scripts/engine_smoke.ps1 -HostName 127.0.0.1 -Port 39731 -StateDir .tandem-smoke -OutDir runtime-proofPrerequisites:
jqcurlpspkill
Run:
bash ./scripts/engine_smoke.shOptional env vars:
HOSTNAME=127.0.0.1 PORT=39731 STATE_DIR=.tandem-smoke OUT_DIR=runtime-proof bash ./scripts/engine_smoke.sh- engine starts and becomes healthy
- session create/list endpoints
- session message list endpoint has entries
- provider catalog endpoint
- SSE stream emits
message.part.updated - idle memory sample after 60s
- peak memory during tool-using prompt (with permission reply)
- cleanup leaves no rogue
tandem-engineprocess
cargo run -p tandem-ai -- serve --host 127.0.0.1 --port 39731 --state-dir .tandemTauri dev must be able to find the tandem-engine sidecar binary in a dev lookup path.
Use the binary built in target/ and copy it into src-tauri/binaries/.
Important: the filename is the same (tandem-engine or tandem-engine.exe), but the directories are different.
Desktop and TUI now use shared engine mode by default:
- default engine port:
127.0.0.1:39731 - clients attach to an already-running engine when available
- closing one client detaches instead of force-stopping the shared engine
- set
TANDEM_ENGINE_PORTto override the shared default for both desktop and TUI
Disable shared mode (legacy single-client behavior) by setting:
$env:TANDEM_SHARED_ENGINE_MODE="0"If the app is stuck on Connecting... or fails to load, do a clean dev restart:
pnpm tauri:dev:cleanManual equivalent:
Get-Process | Where-Object { $_.ProcessName -in @('tandem','tandem-engine','node') } | Stop-Process -Force -ErrorAction SilentlyContinue
pnpm tauri devStrict planner/validator contract mode can be forced via env:
$env:TANDEM_ORCH_STRICT_CONTRACT="1"
pnpm tauri devTANDEM_ORCH_STRICT_CONTRACT=1 pnpm tauri devBehavior in strict mode:
- planner uses strict JSON parse first
- validator uses strict JSON parse first
- prose fallback is still allowed by default (
allow_prose_fallback=true) during phase 1 - contract degradation/failures emit
contract_warning/contract_errororchestrator events
From tandem/:
cargo build -p tandem-ai
mkdir -p ./src-tauri/binaries
cp ./target/debug/tandem-engine ./src-tauri/binaries/tandem-engine
chmod +x ./src-tauri/binaries/tandem-engine
pnpm tauri devcargo run -p tandem-ai -- serve --host 127.0.0.1 --port 39731 --state-dir .tandemcargo build -p tandem-aicargo build -p tandem-aiGet-Process | Where-Object { $_.ProcessName -like 'tandem-engine*' } | Stop-Process -Forcepkill -f tandem-engine || trueAccess is denied (os error 5)on Windows build usually meanstandem-engine.exeis still running and locked by the OS loader.- Stop rogue engine processes, then rebuild.
- If bind fails, verify no process is already listening on your port.
- If startup log shows
Another instance tried to launch, a previous app instance is still running. Close/kill alltandemprocesses and relaunch. - For writable state/config, use
--state-dirwith a project-local directory.
Windows copy/paste recovery for os error 5:
Get-Process | Where-Object { $_.ProcessName -in @('tandem-engine','tandem') } | Stop-Process -Force -ErrorAction SilentlyContinue
cargo build -p tandem-ai
New-Item -ItemType Directory -Force -Path .\src-tauri\binaries | Out-Null
Copy-Item .\target\debug\tandem-engine.exe .\src-tauri\binaries\tandem-engine.exe -ForceNote: mkdir -p and cp target/debug/tandem-engine ... are macOS/Linux commands. On Windows, use New-Item and Copy-Item with the .exe filename.
When upgrading from builds that used ai.frumu.tandem, verify canonical migration:
- Ensure
%APPDATA%/ai.frumu.tandemexists with legacy data. - Ensure
%APPDATA%/tandemis empty or absent. - Start Tandem app or engine once.
- Verify
%APPDATA%/tandem/storage_version.jsonand%APPDATA%/tandem/migration_report.jsonexist. - Verify sessions/tool history are visible without manual copying.
- Verify
%APPDATA%/ai.frumu.tandemremains intact (copy + keep legacy).
- Seed legacy data under either:
%APPDATA%/ai.frumu.tandem%APPDATA%/opencode%USERPROFILE%/.local/share/opencode/storage- Launch Tandem and unlock vault.
- Verify full-screen migration overlay appears and blocks interaction until completion.
- Verify progress updates through phases:
- scanning -> copying -> rehydrating -> finalizing.
- On success/partial, verify summary card shows repaired session/message counts.
- Click Continue and verify chat history loads for migrated sessions.
- Open Settings -> Data Migration.
- Run
Dry Runand verify result status reportsdry_run. - Run
Run Migration Againand verify counters update. - Verify
migration_report.jsontimestamp updates and report path is shown.
For an existing workspace that contains legacy metadata:
- Ensure
<workspace>/.opencode/plansand/or<workspace>/.opencode/skillexists. - Start Tandem and set/switch active workspace to that folder.
- Verify
<workspace>/.tandem/plansand<workspace>/.tandem/skillare created. - Verify plan list and skills list still include legacy entries.
- Create a new plan and install/import a skill; verify new files are written under
.tandem/*. - Confirm legacy
.opencode/*remains untouched (read-compatible window).
- With multiple projects configured, switch active folder in the project switcher.
- Verify sidebar session list shows only sessions belonging to that folder.
- Create a new chat in the active folder; verify it appears immediately in sidebar.
- Verify sessions with legacy
directory = "."still appear under current workspace.
After launching desktop (pnpm tauri dev) and sending one prompt:
- Open
%APPDATA%\\tandem\\logsand verify files exist:tandem.desktop.YYYY-MM-DD.jsonltandem.engine.YYYY-MM-DD.jsonl
- Search for
provider.call.startin engine JSONL. - Search for
chat.dispatch.startin desktop JSONL. - Verify a matching
correlation_idexists across desktop dispatch and engine provider events. - If stream fails, verify one of:
stream.subscribe.errorstream.disconnectedstream.watchdog.no_events
PowerShell helpers:
Select-String -Path "$env:APPDATA\tandem\logs\tandem.desktop.*.jsonl" -Pattern "chat.dispatch.start"
Select-String -Path "$env:APPDATA\tandem\logs\tandem.engine.*.jsonl" -Pattern "provider.call.start"
Select-String -Path "$env:APPDATA\tandem\logs\tandem.desktop.*.jsonl" -Pattern "stream.subscribe.error|stream.disconnected|stream.watchdog.no_events"