00
Orientation
What You Are Operating
Shipping build · macOS · notarized direct download below · $99 one-time planned

TULKAS is a macOS desktop console for working with language models. It is organized like a DAW: each track is a conversation bound to a provider; the mixer shows one channel strip per provider with a budget fader that actually enforces; the arrangement is a timeline of finished turns. Two permission systems gate what models may do: tool-exec grants (may a model's proposed command run?) and autonomy grants (may the console dispatch turns with nobody present?). Both are time-boxed and charge-limited; both are visible as lit controls, not settings.

Everything in the app travels one JSON-RPC dispatcher. The same daemon serves the GUI, a Unix socket for the tulkas CLI, and a LAN WebSocket for paired devices — anything you can click, you can script (§08–§09). All state is local: SQLite databases and a key vault under your own Application Support directory (§10). No telemetry, no phone-home; provider keys are yours (BYOK).

DL
Download
Notarized · Direct Install

TULKAS ships as a Developer-ID-signed, Apple-notarized disk image. Download, open, drag TULKAS.app to Applications, launch. No account, no telemetry, no phone-home — provider keys are yours (BYOK).

Download for Mac
Apple Silicon + Intel macOS 14+ Notarized by Apple v1.0.0

Release manifest (JSON) — version, build, size, and SHA-256 of the exact artifact this button serves.

01
Getting Started
Launch, First Run, First Send
  1. Launch TULKAS.app. The app boots its daemon in-process and connects; the status chip at the top-right of the command bar reads BRIDGE UP when ready (usually under a second). The bottom status bar shows the durable event sequence number — if it advances, the store is live.
  2. You start with one track, MAIN. Type into the Console prompt box and press TRANSMIT ▸. With no key configured, pick the fake provider in the Console's provider strip first — it is a built-in, zero-cost echo vendor that exercises the whole pipeline, so you can verify the instrument works before spending a token.
  3. To use a real provider, open the Mixer (06 MIXER pill) and read the health LEDs (§07). Add a key with the CLI — tulkas rpc tulkas.vault.set '{"vendor_id":"deepseek","token":"…"}' — then press REFRESH in the Mixer header; the strip should flip to KEYED.
  4. Watch a turn arrive three ways at once: streaming text in the Console, a growing clip in the Arrangement, and rows in the wire log. If those three disagree, that is a bug — report it.

Headless alternative: tulkas serve --port 8787 runs the same daemon without the GUI; every operation in this manual then works over tulkas rpc (§08).

02
Command Bar Reference
Top Rail, Left To Right
ControlBehavior
WORKSPACE atlas · ops · harness · mix · dawPane presets. A preset lights only when EVERY pane matches it exactly. daw = console + mixer + arrange + terminal.
MODULES 01 ATLAS … 07 ARRANGEIndividual pane toggles. Numbers are stable IDs used throughout this manual.
FREEZEThe global autonomy halt. Click → confirm popover → every autonomy grant on every track is revoked in one call. Amber AUTO pills go dark as the daemon's per-track events arrive. Tool-exec arms are NOT affected. Always visible, in every workspace.
EDITAtlas card edit mode toggle (affects pane 01 only).
HINTSOne switch for every ? button in every pane. Off = no question marks anywhere.
SPECOpens the live PROTOCOL document — the exact wire contract this build ships, embedded in the binary. When this manual and SPEC disagree, SPEC wins.
BRIDGE UP / DOWN chipDaemon connection state. DOWN shows a reason and a reconnect affordance; on reconnect the app replays exactly the durable events it missed.

The bottom status bar shows: last durable seq, an AUTOSAVED flash (driven by real store-write events, not a timer), and a one-line summary of the last wire exchange (method, status, latency, tokens).

03
Console & Tracks
Sending, Stopping, Focusing, Punch-In
OperationHow
Send a promptType in the prompt box → TRANSMIT ▸. Goes to the FOCUSED track, on that track's bound provider unless the provider strip overrides.
Stop a turn■ STOP — cooperative cancel of the active turn. The partial text is kept byte-complete and the clip renders red (aborted).
Create a trackThe + affordance in the track area (auto-names TRK 2, TRK 3, …). Focus follows the new track.
Focus a trackClick its name. The Console lane swaps to that track's history; unfocused tracks accumulate quietly and raise an unseen dot.
Punch-in (resume an abort)In the Arrangement, click a red clip → CONTINUE ▸. The model resumes from the exact stored partial, on the same track. The partial rides history exactly once.
Run a proposed commandWhen a model proposes a shell command it STAGES in the hosted terminal pane (05) and waits. RUN executes it and feeds output back; DISMISS declines (the model is told). RUN on an armed track burns one tool-exec charge; unarmed tracks cannot execute at all.

Busy rule: one streaming turn per track. A send into a busy track is rejected with busy — nothing queues silently. Budget rejections behave the same way (§04) and flash the mixer's rejection LED.

04
Mixer Reference
Pane 06 — Strips, Fader, Arms

One strip per provider. Top to bottom: name chip · transport LED + live VU (streaming bytes) · the budget fader · MUTE · today's readout (tokens · est $; a free vendor shows for dollars, never a fake $0.0000) · rejection LED with count · the health LED (§07).

The fader is admission control. Detents top-to-bottom: ∞ · 1M · 500k · 200k · 100k · 50k · 20k · 10k · 5k · 1k · 0 tokens/day. Drag commits on release; the thumb renders the daemon-confirmed value, so a failed set snaps back visibly. A send that would exceed the cap is rejected at admission — an already-streaming turn is never cut mid-flight; ■ STOP is the in-flight control. Caps reset at 00:00 UTC. MUTE blocks all sends on that provider.

The TRACKS column (right side) carries per-track arms:

ControlBehavior
Record-arm (red circle)Toggles the tool-exec grant: default 4 hours, optional charge limit. Armed = filled red, pulsing, with a live countdown (3:58) and a charge readout (2/10). At 0:00 or exhaustion it renders disarmed on its own — the countdown is a projection of the same expiry the enforcement check reads.
AUTO pill (amber)The autonomy grant. Arming is never a bare toggle: the popover requires a run count (stepper, 1–99) because unbounded autonomy does not exist in this product. Armed shows used/max + a m:ss TTL countdown (default 1 h). Click while armed = disarm.

The two grants are independent: an AUTO-armed track can dispatch by itself but still cannot execute a tool result unless it is ALSO record-armed.

05
Arrangement & Session Grid
Pane 07 — Reading The Timeline
ElementMeaning
Amber clipCompleted turn. Width ≈ real latency from the wire log.
Red clipAborted turn. Click for CONTINUE ▸ (punch-in, §03).
Cyan clipTurn that ended in a staged tool proposal.
Hatched clip (45° stripes)The turn was dispatched by a TRIGGER, not a human. The hatch is stored with the turn (its origin) and survives relaunch.
Pulsing outlineStreaming right now, growing at the playhead.
Automation sub-laneUnder each track: schedule-trigger tick marks (clock glyph; dimmed once fired), follow-trigger arrows (source lane → target lane), a red flash + reason when a trigger is skipped, and an amber AUTO tag while the track holds an autonomy grant.
Amber vertical lineThe playhead (now). Scrolling away pauses auto-follow; JUMP TO NOW resumes.

Session grid (bottom rail): prompt presets. Click a pad = launch that prompt on the focused track (arm/budget gates apply; a rejection flashes the pad red for 3 s). Long-press = overwrite the pad with the current Console prompt text. + SAVE mints a new pad from it.

06
Autonomy Procedures
Triggers, Step By Step

To schedule an unattended run:

  1. Arm autonomy on the target track: Mixer → its AUTO pill → set the run count (e.g. 2) → ARM. This is the budget of unattended dispatches you are granting; each fire burns one.
  2. Arrangement header → + TRIGGER. Kind SCHEDULE, pick the target track, enter the prompt, optionally a vendor (blank = the track's bound vendor), set date/time. CREATE.
  3. A tick mark with a clock glyph appears on the track's automation lane at the fire time. The scheduler sweeps every 30 s, so firing precision is ±30 s — "around this time unattended," not cron.
  4. On fire: one autonomy charge burns, the turn runs, and the clip renders hatched. The pill's readout drops (e.g. 0/2 → 1/2). On the last charge the pill goes dark by itself.

To chain tracks (follow / side-chain): same form, kind FOLLOW, choose the SOURCE track. When the source finishes a human-initiated turn, the trigger fires on the target. Turns started by triggers never fire further follows — chains are capped at one hop by construction, so A→B→A loops cannot exist. A trigger targeting its own source is refused at creation.

Skip reason (red flash on the lane)Meaning
not_armed_autonomyNo live autonomy grant covered the fire. NOTE: an unarmed schedule trigger stays active and re-skips every 30 s sweep until you arm the track or delete the trigger.
busyThe target track was mid-turn. The charge is still spent — you armed N unattended attempts, and a collision is one of them.
vendor_missingThe trigger's vendor no longer exists.

To stop everything: FREEZE in the command bar (§02) — one confirmed click revokes all autonomy everywhere. In-flight turns are stopped with ■ STOP as usual.

07
Providers & Keys
The Vault, The Router, The Health LEDs

Keys live in the vault (macOS Keychain–backed, service TULKAS-VendorVault). Set one per provider:

tulkas rpc tulkas.vault.set '{"vendor_id":"deepseek", "token":"sk-…"}' // write-only: no vault-read RPC exists. Keys are masked out of every // logged event. Environment variables (ANTHROPIC_API_KEY etc.) are NEVER // read — an ambient shell key cannot decide which account pays.
vendor_idKey sourceNotes
fakenoneBuilt-in echo sandbox. Free. Use it to test anything.
claude-cliits own loginDrives your installed claude CLI. Reports real token usage. Respects your model choice.
deepseekvaultDirect API.
antigravityvaultGoogle managed agents.
philabnone (local)Local Phi-class small models; needs a PHILAB install.
thorondorvault (bearer, optional)The built-in router: one endpoint fronting ollama / openai / anthropic / deepseek. Upstream keys live in the ROUTER's environment, not in TULKAS.

The router path. Run thorondor serve (default 127.0.0.1:8787) with the upstream keys you want in its env. TULKAS asks it what it fronts (GET /v1/providers) and, when the router covers a provider, dispatch for that vendor_id rides the router automatically — including trigger-fired turns. If the router sets THORONDOR_API_TOKEN, vault the same value under thorondor.

LEDMeaning
ROUTED (green)The router fronts this provider right now; sends will travel through it.
KEYED (green)Direct key present (vault or the vendor's own login).
NO KEY (amber)Reachable but unkeyed — vault a key.
DOWN (red)Probe failed or timed out (5 s bound); the row carries the error.
(idle)Not probed yet. Health is checked on pane-open and on REFRESH only; the header shows the probe's age. It is a snapshot, not a live feed.
08
The CLI
Everything The GUI Does, Scriptable
# run the daemon headless tulkas serve --port 8787 # list every RPC method with its one-line contract tulkas rpc --list # call any method (default socket: ~/Library/Application Support/TULKAS/tulkas.sock) tulkas rpc tulkas.track.list '{}' tulkas rpc --socket /path/to/tulkas.sock tulkas.mixer.get '{}' # send and stream the turn's events until it completes tulkas rpc --follow tulkas.llm.send '{"vendor_id":"fake","prompt":"hello"}' # vendor health sweep / audit tail tulkas rpc tulkas.vendor.health '{}' tulkas wirelog --limit 20

The CLI talks to the same dispatcher over the same socket as the GUI — a turn sent from the terminal appears in the app's Console, Arrangement, and wire log identically. This parity is load-bearing: the product's own simulation batteries drive it this way.

09
RPC Method Reference
Condensed — SPEC Has The Full Contracts
MethodDoes
tulkas.llm.send / llm.abortDispatch a prompt (ack {turn_id}, events follow) / cancel a turn.
tulkas.tool.resultFeed a staged command's output back. executed:true burns a tool-exec charge or is refused.
tulkas.track.arm / disarmTool-exec grant on/off (TTL default 4 h, optional max_runs).
tulkas.autonomy.arm / disarm / freezeAutonomy grant (max_runs REQUIRED, TTL default 1 h) / revoke one / revoke ALL.
tulkas.trigger.create / list / deleteSchedule (when_spec=RFC3339) or follow (when_spec=source track) triggers.
tulkas.track.list / create · conversation.getTracks with arm state · full turn history (each turn carries origin).
tulkas.mixer.get / setStrips + budgets / partial budget update (max_tokens_day: null clears).
tulkas.vendor.healthOn-demand probe; per-vendor {reachable, authenticated, source: routed|vault|builtin|none, error}. Never polled.
tulkas.vault.setStore a provider key (write-only). Honest ack: a failed store is an error, never accepted:true.
tulkas.preset.list / save / deleteSession-grid pads.
tulkas.schedule.*The on-board day-planner engine (getDay, createTask, generateDayPlan, acceptProposal).
tulkas.atlas.* · connectors.* · spec.get · term.logAtlas board · connector rows + probes · the embedded PROTOCOL text · terminal audit rows.
tulkas.bus.subscribeTransport-level: stream every event on this connection (what --follow uses).

Errors are JSON-RPC standard: -32001 busy · -32002 budget-rejected · -32003 exec-unarmed · -32602 bad params / unknown vendor · -32603 internal.

10
Files & Environment
Where Your Data Lives
PathContents
~/Library/Application Support/TULKAS/audit.dbThe durable store: turns, grants, triggers, budgets, wire log, events. SQLite; back this up and you have your history.
…/TULKAS/schedule.dbThe on-board planner engine's store.
…/TULKAS/tulkas.sockThe CLI socket (created at daemon boot, mode 0600).
Keychain service TULKAS-VendorVaultProvider keys (one item per vendor_id).
TULKAS_DATA_DIR (env)Relocates the socket + databases for the headless daemon — scratch/test instances. Keep it SHORT (see §11).
TULKAS_VAULT_DIR (env)File-based vault override (<vendor>.token files, 0600) instead of Keychain — test isolation.
11
Troubleshooting
Symptom → Cause → Fix
SymptomCauseFix
Headless daemon exits: PathTooLongmacOS limits socket paths to 103 bytes; your TULKAS_DATA_DIR is too deep.Use a short dir (e.g. /tmp/tulkas-x). The error names the limit — this is deliberate; the daemon refuses to boot socketless.
Daemon exits: DaemonAlreadyRunningAnother instance holds the socket (the app's in-process daemon counts).Quit the other instance, or give the new one its own TULKAS_DATA_DIR + --port.
A trigger's lane flashes a skip every 30 sAn unarmed one-shot stays active by design (so arming later still fires it) — past-due + unarmed = a skip per sweep.Arm the track, or delete the trigger.
Vendor shows DOWN with a timeoutProbe hit the 5 s bound (router down, blackholed host).Start/repoint the router (connector thorondor base_url), then REFRESH.
Set a key but the LED stays NO KEYKeys are per-vendor_id; a typo'd id stores under the wrong name. The vault.set ack is honest — check it wasn't an error.Re-run tulkas.vault.set with the exact id from §07, then REFRESH.
Sent from the CLI but the app shows nothingDifferent daemons: your rpc hit another socket (env leaked a scratch TULKAS_DATA_DIR).Pass --socket explicitly at the default path, or clear the env var.
Autonomy stopped firing overnightBy design: the grant's charges exhausted or its TTL lapsed.Re-arm with a new run count. Check the hatched clips + wire log to see what ran before it stopped.
Keychain permission dialog at first key usemacOS guards the vault items.Approve "Always Allow" for TULKAS; headless daemons should use TULKAS_VAULT_DIR files instead.