DOCS

The SayDo Manual

From install to first conversation, to tuning your models and understanding the safety model — all in one place. Status is labeled honestly: Available now / In progress / Planned.

Contents

0. How to read this document

  • Just want to get it running: read §1 (what it is) → §4 (quickstart) → §5.3 (the three setup plans); look up the rest as needed.
  • Want to know how it actually works: §3 (how it works) → §7 (projects & Git) → §8 (approvals & safety) → §9 (memory & the ledger) → §10 (review & delivery).
  • Want to combine multiple model vendors / CLIs for the best results: read all of §5 (models & providers), then §6 (executors).
  • Care about where your data goes and what it costs: §14 (privacy & data), §15 (cost).
  • A section title carrying Available now / In progress / Planned marks the current state of the capability it describes; sections without a badge are available now as a whole, with individual exceptions noted inline.
  • This document tracks the current version of the macOS desktop service; none of the mobile apps are in stores yet, so everything phone-related is written as "how you can connect today", not "download from a store" (§13).

1. What SayDo is

SayDo is a chief of staff you can talk to out loud, running on your own Mac. You just talk things through — even think out loud while you talk. It first builds deep context on your projects and keeps every item in a ledger; once it understands and things are ready, it proposes to start work and drives the AI tools already on your computer; when execution and checks have run, it calls you back to review; only your nod makes it delivered.

Its mental model is a chief of staff: talks with you every day, understands and gets things done on its own, then reports back — not an instruction translator of "the clearer you speak, the better I do". The core competitiveness is not which model it uses, but context engineering + the human–AI collaboration workflow.

1.1 Three core promises

  • Said means recorded: hold to talk (or type) and think out loud. A casual "note this for me" goes straight into the ledger; things it promised, things awaiting your sign-off, things the AI is working on, things waiting on others — every item has an owner and a destination.
  • Understand first, then act: the first time it meets a project, it runs a grounding pass — scans the project structure and key files, absorbs the conventions already in your repo, and builds a durable project knowledge base; every later conversation carries its understanding of your project instead of starting from zero. This is the dividing line between SayDo and "opening a new chat window". (In the current version grounding is a deterministic mechanical pipeline; deep research by the thinking-slot model is the next tier, in progress — §9.2.)
  • Only your sign-off counts: before work starts you get a decision pack (what will be done, how, which parts it does and which need you, what the result will look like); afterwards it only says "ready for your review", never "done"; merge and delivery always take effect only when you approve.

1.2 What it is not

  • Not a voice input method. It doesn't do "speech-to-text then paste"; voice is only the entry point — behind it is a full loop of understanding context, opening ledger entries, judging readiness, driving execution, and organizing review.
  • Not a cloud runner. There are no SayDo servers and no account signup. The heavy work runs on your own computer and data stays on your own devices; the third-party AI you connect (subscription CLIs or API keys) is called directly by your computer — the developer never touches or sees it.
  • Not another chat client. Speech recognition, chat, and coding agents each have mature point solutions; what SayDo does is govern "talk it through → start work automatically → call you to review when it ran" into one closed loop — with approvals, budgets, isolation, callbacks, review, and retention.
  • Not a general voice assistant. No alarms, small talk, or music; it doesn't replace your IDE — diffs, logs, and long documents always go to the screen; voice carries only decisions and summaries.

1.3 What it's for

It serves "things with a concrete deliverable": code changes / documents / research reports / articles / proposals / action plans. Coding is the main scenario (an engineer dictates requirements → the AI develops in an isolated worktree → you review and merge); but the same upstream capabilities (grounding, interviewing, readiness judgment, callbacks, retention) also serve non-code work like writing, life planning, and research. Current capability gates by project type: coding (available now), writing (a narrow version has landed, reusing coding's worktree + strong-authentication merge chain; citation-level referencing, attribution contracts and the full scope are still planned); dedicated execution contracts for research / marketing / planning / general are planned — but the upstream capabilities "open a Focus item and track it, record obligations, interview, research" work for anything (§9.4).

1.4 Current status in one breath

  • macOS desktop service (daemon + Web console + optional voice pipeline): available now, open source and free (see §18 Open source & contributing).
  • Drives the AI you already have: reasoning slots can use the subscription login of Claude Code / Codex / Cursor / Grok / Gemini CLI / Qwen Code / Copilot CLI, or any OpenAI-compatible API (§5); the executor (the agent that actually edits code) is currently Cursor CLI, the Claude Code executor is in progress, and the Codex execution route is planned (§6).
  • Project memory & the four-color ledger: available now.
  • iOS / Android / HarmonyOS apps: engineering shells exist; none submitted to stores. Today a phone can connect to the desktop service over the LAN via an app shell or the mobile browser (§13).
  • Call-style voice briefings: planned.

1.5 Cost in one breath

The desktop service itself is free. AI costs go directly to the vendors you choose — use your existing CLI subscriptions (zero extra cost within quota) or your own API keys on pay-as-you-go. SayDo adds no markup, never touches the money, and doesn't top up for you. Details in §15.

2. Core concepts at a glance

ConceptIn one sentenceDetails
Communication surface / execution surfaceThe phone and browser are the communication surface (talk, watch progress, sign off, approve); your Mac is the execution surface (grounding, memory, driving AI, background work). Communication is naturally mobile; execution is naturally stationary§3.1, §13
daemonThe core resident process of the desktop service: holds all ledgers and state, serves the Web console, executes tool calls, dispatches tasks, fires callbacks. It stays alive even when a voice session drops§3.1
Brain (dialog brain)The model behind conversation turns: interviews, answers, spoken summaries; it has no direct execution power — every side effect goes through the daemon§3.1
FocusThe spine of the ledger: one continuously tracked item (can link 0..N projects, or belong to none); obligations (who owes what) and expectations (what "done right" looks like) hang under it§9.4
Four-color ledgerThe four color zones of the Today page: awaiting your call (orange) / owed by you (blue) / AI working (green) / waiting externally (gray). Every item has an owner and a destination§9.4
Project / workspaceA project = the boundary carrying one kind of deliverable; a coding project's workspace is a git repo folder on your machine; non-code projects default to a system-managed folder§7.1
Grounding (Foundation)The blocking deep study on first contact with a project; builds the project knowledge base (M1)§9.2
M0–M3 memoryM0 cross-project user profile / M1 project knowledge base / M2 accumulated artifacts / M3 session transcript§9.1
ReadinessThe "enough to start?" judgment: confirmed item by item against the project type's readiness checklist, re-checked by an independent evaluator (a different model family from the dialog model)§3.3, §5.4
Decision packOnce ready: a deliverable preview + implementation plan (each step marked AI-executed / needs you) + estimated cost and cap, plus a lightweight demo (a web preview mechanically rendered from the same source as the plan) and one question: "start now?"§3.3
Execution modesstep-confirm (asks you before every out-of-circle action and step boundary; factory default, currently the only active mode) / direct-to-review (runs straight through to "ready for your review"; S2-class effects pass via a read-aloud-signed pre-authorization list; in progress)§8.4
S0–S3Risk grading of actions (computed from effects, not action names): S0 read / S1 write inside the worktree / S2 out-of-circle but reversible / S3 irreversible or external impact. S3 only ever goes through an authenticated screen — voice never approves it§8.1
Gate 0The safety gate that must be closed before autonomous dispatch opens (identity & authorization, idempotency, independent acceptance oracle, secret/egress, intent audit, deletion/consent propagation); there is no bypass branch in the code§8.2
worktree isolationEach task gets its own git worktree; the agent never touches your main working tree§7.2
ready_for_reviewThe state where execution and checks have run, artifacts are on disk, and it's ready for your review; callbacks fire from here. It does not mean done — only after merge / archive is it "delivered"§10.1
Reasoning slots (four)dialog / thinking / cheap / evaluator — each independently supplied by API or subscription CLI§5.1
Dev slot (executor)The fifth slot [models.dev]: which agent edits the code when a task is dispatched (a different question from "which model talks")§6
BYOA (bring your own agent)Use CLIs already logged in on your machine (Codex / Claude Code / Cursor / Grok / Gemini / Qwen / Copilot) as reasoning supply — zero API keys§5.2
cap-tokenThe console's local capability token (~/.saydo/.cap-token), carried in the console URL; deleting it rotates it§4.3, §14

3. How it works

3.1 Overall architecture: three layers

You (speak / listen / occasionally look at the screen; sign off / approve)
   │ voice or text (browser, phone)              callback (voice / desktop notification / phone push)
┌──▽─────────────────────────────────────────────────────────────────┐
│ Communication surface: Web console (desktop browser) / phone        │
│   (app shell or mobile browser, LAN)                                │
│   · Voice engine (optional Python voice pipeline: ASR → LLM → TTS)  │
│   · Brain: interview / readiness self-check / decision packs /      │
│     spoken summaries; no direct execution power                     │
├─────────────────────────────────────────────────────────────────────┤
│ Control plane: daemon (Node 22, resident) — sole owner of all       │
│   durable state                                                     │
│   · Ledger: Focus items / obligations / four-color attention /      │
│     memory event ledger / approval receipts                         │
│   · Tool execution, Context Pack assembly, task dispatch, event     │
│     reconciliation, callbacks, cost accounting                      │
│   · Local HTTP/WS (default 127.0.0.1:47100, capability-token auth), │
│     serves the console static pages                                 │
├─────────────────────────────────────────────────────────────────────┤
│ Execution surface: local AI executor (Tier 1, currently Cursor CLI) │
│   — edits code in an isolated worktree; every shell command goes    │
│   back to the daemon through the approval gate; three circuit       │
│   breakers: budget / duration / turns                               │
│   (a separate batch execution route via Hopper: contract settled,   │
│   production binding dormant, §6.3)                                 │
└─────────────────────────────────────────────────────────────────────┘

Three iron rules run through the entire design:

  1. The Brain's model process is disposable and has no direct execution power — voice sessions or reasoning processes can die and be rebuilt at any time while tasks keep running; all side effects happen in the daemon / executor.
  2. Voice sessions are short-lived; tasks are long-lived — a session is an event-driven window (suspended after 45 idle seconds or when you say "that's it for now / leave it here / wrap up", at zero cost); tasks keep executing in the background, and callbacks pull you back.
  3. Every piece of state has exactly one owner — dialog-domain state belongs to the daemon, execution-domain state to the executor / backend; boundaries are connected by durable protocols (idempotency keys, event cursors, receipts), not a shared database.

Component overview (everything runs on your Mac, no cloud dependency):

ComponentWhat it isNotes
daemonCore resident process (TypeScript / Node 22): sole owner of dialog and task state; serves the console pages; the approval gate, ledgers, and audit all live hereDefault port 47100; listens on loopback only
consoleWeb console (Vite + React); one codebase shared by desktop and phone (mobile shell route)Served directly by the daemon; open in a browser and use
pipelineVoice pipeline (Python): cloud hearing and speaking for the desktop browser (Volcano Doubao ASR / TTS)Optional; without it, type or use browser system voice
saydo CLIsaydo up / status / open: holds the daemon in the foreground, health-checks, opens the consoleOptional; an alternative to starting the daemon directly
contractsData contract package (zod schemas, state machines, digests): the single type source that keeps components from forkingFor developers
ExecutorThe agent that actually edits code in a worktree after sign-off (currently Cursor CLI)Optional; conversation and ledger work without it

3.2 One full loop: from one sentence to "come review it"

StepWhat happensMechanism behind itYou
1 Start talkingOn the Today page tap "start talking", hold to talk or type; a casual "note this for me" goes straight to the ledger; it interviews one question at a timeContext Pack injection (relevant slices from M0+M1+M2 + recent turns); interview discipline: one question at a time, multiple-choice first, a question budgettalk
2 Readiness callEach turn it self-checks "enough knowledge + clear requirements", filling whichever dimension is short; only then does it propose to startThe project type's readiness checklist (coding: goal / verifiable acceptance criteria / scope / constraints / key parts of the codebase understood), item by item; "confirmed" = you said it yourself + it plays it back and you nod; an independent evaluator (different model family) re-checks before any proposalhear the proposal
3 Decision packDelivers the deliverable preview, implementation plan (each step marked AI / needs you), in/out of scope, acceptance criteria, risks, plus estimated cost and cap; a lightweight demo (web preview, mechanically rendered from the same source as the plan) ships with the pack — tap "view demo" in the console; in a local conversation it appears on your screen in the same turnThe plan lands on disk as an editable artifact; the approved version freezes into the "contract"; the demo freezes with the pack (edit the pack and it regenerates); a decision pack is valid for 24 hourslook
4 Your sign-offSay "that's it" / "start"; the execution mode is currently step-confirm (read-aloud signing for the "run it straight through" mode is in progress, §8.4)Two keys: "enough evidence" ≠ "an authorized person approved this pack + scope + budget + validity"; the dispatch receipt is digest-bound and single-use; if Gate 0 isn't closed, dispatch is refusedsign off
5 Background executionRuns in its own git worktree; your main working tree is never touched; every shell command passes the approval gate; three circuit breakers: budget / active duration / turns; you can walk away — the voice session suspendsCommands are graded S0–S3 by effect; S0/S1 pass automatically, S2 escalates for your confirmation (refused after 45 s without an answer), S3 is refused and demands on-screen strong authenticationwalk away
6 Calls you when it ranExecution and checks have run, artifacts confirmed on disk → it calls you on its own: "execution and checks have run — ready for your review"The callback fires only after settle reconciliation (the runner exiting doesn't mean done); priority: blocked > failed > pending approval > ready for review > progress (silent by default); delivery follows the escalation chain: you're at the console with voice ready ⇒ it speaks to you online (first sentence is the callback reason; escalates after 30 s with no response) → macOS desktop notification + ntfy phone push; inside the do-not-disturb window it sends one low-interruption push and calls back after the window (§11)get called back
7 Review & retainSpoken summary (one sentence / 150-character walkthrough / the list of decisions it made itself) + evidence view (diffs, tests, decisions, unverified items); you nod → strong-authentication merge → "delivered"; knowledge and artifacts are retainedMerge = a Touch ID / passkey on-screen approval card signing a single-use receipt, or you merge in the terminal yourself (manual handoff); at session end, stable conclusions are mechanically nominated as candidates and enter M1 when you approvereview

3.3 Interview, readiness, decision packs: why you never write a requirements doc

  • Instructions are lossy compression; conversation is progressive unfolding. SayDo doesn't require you to be good at prompting: you just talk, and when it understands and is ready, it proposes to start. The spec is its output, not your input.
  • Cognitive load shifts from "expressing" to "judging": instead of conjuring complete requirements from thin air, you look at concrete things (deliverable preview + plan + demo) and say "right / wrong / adjust".
  • Readiness is typed: development can't start without acceptance criteria; writing can't form without a core thesis. So it first identifies what kind of thing this is, then judges readiness with the matching checklist. If critical items aren't confirmed as covered, it mechanically cannot produce a pack (not left to the model's self-discipline).
  • Guarding against "correlated error chains": if the same model produces facts, judges gaps, builds the demo, and then self-assesses "good enough", the errors are highly correlated. So "enough to start" is re-checked by an independent evaluator — a different model family from the dialog / thinking models, reading only the evidence ledger, never the Brain's self-defense (this is where §5.4's different-family rule comes from).
  • Three orthogonal dials (design principle; landing varies): conversation lane (Quick: a sentence or two of confirmation then go / Guided: standard interview / Explore: open brainstorm — planned, currently only instrumented) × execution mode (step-confirm available now / direct-to-review in progress) × governance depth (S0–S3 decided by action effects, available now). The guard sentence: you can speed up the conversation and thin out confirmations, but you can never lower any action's risk grade or the S3 threshold.

4. Quickstart (the normal user path)

Goal: one Mac, one AI CLI you're logged into (or one API key) — up and talking in about fifteen minutes.

4.1 Prerequisites

ItemRequirementNotes
SystemmacOS (Apple Silicon or Intel)Residency, notifications, opening the browser etc. are macOS implementations; other systems unsupported
Node22.x (>=22 <23)Runtime of the desktop service and CLI; install via nvm / fnm / Homebrew
pnpm10.xcorepack enable is enough
gitany recent versionNeeded for builds and task isolation (worktrees)
One AI supplyEither: ① any wired CLI already logged in locally (Codex / Claude Code / Cursor / Grok / Gemini CLI / Qwen Code / Copilot CLI); or ② an OpenAI-compatible API key (OpenRouter / OpenAI / Anthropic / DeepSeek)Chatting, ledger entries, and research need no key (they use CLI subscriptions); see §5.3
Optional · Python 3.11+ with uvOnly needed for desktop-browser cloud voice (Volcano Doubao ASR/TTS)Without it you can still talk: browser system voice is the fallback (§12)
Optional · cursor-agent login + jqOnly needed to dispatch execution tasks (letting the AI actually edit code)Its absence doesn't block conversation — sign-off just won't launch an executor (§6)

4.2 Install

Currently distributed as source (no npm / Homebrew package yet, see §16):

git clone https://github.com/Octo-o-o-o/SayDo.git && cd SayDo
pnpm install && pnpm -r build      # build is required: the console pages served by the daemon come from packages/console/dist

4.3 Start the desktop service

Two equivalent ways — pick one (the same data directory ~/.saydo allows only one instance at a time):

# Option 1: start the daemon directly (development form; run from the repo root)
mkdir -p ~/.saydo
(cd packages/daemon && SAYDO_HOME=$HOME/.saydo nohup ./node_modules/.bin/tsx src/index.ts >> ~/.saydo/daemon.log 2>&1 &)

# Option 2: use the saydo CLI (distributable form; build once first)
pnpm --filter @saydo/cli build
node packages/cli/dist/cli.mjs up        # holds the daemon in the foreground; Ctrl+C exits gracefully and tasks resume
node packages/cli/dist/cli.mjs status    # health probe: 0=connected / 1=port free / 2=port conflict
node packages/cli/dist/cli.mjs open      # opens the console
  • Default port 47100 (override with the SAYDO_DAEMON_PORT environment variable when starting the daemon directly; the saydo CLI only accepts --port); listens on the loopback address by default. saydo up opens the browser automatically by default; --no-open disables that.
  • The data directory defaults to ~/.saydo (override with --home or SAYDO_HOME). First launch auto-generates: config.toml (placeholder template), .cap-token (console token), saydo.db, sessions/, logs/, projects/, etc.
  • Health probe: curl -s http://127.0.0.1:47100/health (ok:true means it's up; a tsx cold start takes about 10–15 seconds).
  • Open the console: open "http://localhost:47100/?token=$(cat ~/.saydo/.cap-token)" (this is exactly what saydo open does). Note the localhost: for the WebAuthn binding of the Touch ID merge card, page requests to 127.0.0.1 are redirected to localhost. After the first injection the token lives in browser-local storage; later links don't need to carry it.

4.4 First run: the resource profile wizard

The first thing you see is the resource profile: it inventories the model sources available on this machine (which CLIs are installed, which are logged in, which keys are in .env), then offers plan cards (the three plans of §5.3). A typical zero-key flow:

  1. You see a one-tap card like "all on Codex (zero cost within subscription)" (one card per logged-in CLI; the list sorts logged-in > installed-only > not-installed, and by usage count within a tier);
  2. Tick two informed confirmations (evaluator same family as dialog / evaluator cage isolation not provable, §5.4);
  3. Tap "confirm and start" → writes the pending configuration → self-check 1/2 (one real call per slot) → restarts the service → self-check 2/2 (re-verified after taking effect) → lands on "start talking". For an all-CLI plan each slot takes 15–25 seconds per check, about 1–3 minutes total for both rounds (varies by CLI, can be longer), with progress and elapsed time shown;
  4. Afterwards you can switch plans in "settings" anytime, or configure slot by slot (advanced mode).

Until the dialog slot is configured, every page bounces you into the wizard (with a "just look around" escape hatch); on the very first run with an empty ledger, it opens with a fixed greeting asking you to "name any three things you need to do this week" to show you how the ledger works.

4.5 Start talking: the Today page

Console left rail: start talking (main entry) / Today (the four-color ledger, the only "needs you" entry) / Overview board / an "ongoing items" group (Focus items listed by space) / a "records" group: memory / artifacts / cost / settings; the legacy Dashboard, approval center, notifications, tasks, Focus list (old), and board (old) live in a collapsed "legacy" group.

  • On the Today page, tap "start talking": type, or hold to talk (§12); think out loud.
  • When a conversation ripens, it asks whether to open a Focus item; things you want it to do, things you owe it, and things waiting on others all appear in the four color zones of the Today page.
  • Development requests really walk the whole chain "interview → readiness → decision pack → sign-off → dispatch" (§3.2); before sign-off it quotes the estimated cost and cap.
  • Known feel (not broken): 15–25 seconds of thinking per turn is normal in CLI dialog mode — don't double-click (the desktop only errors after a 120-second per-call cap); occasionally it answers only half of what you asked — just ask again; casual "note this for me" items are usually proposed for the ledger in the same turn, occasionally they degrade into the memory store first (visible in the menu) and can be attached to the ledger once the item exists.

4.6 Optional: desktop browser voice

  • Zero config: if the browser has system speech recognition (Chrome / Safari SpeechRecognition), you can hold to talk and have replies read aloud; quality is average and recognized text goes through the text channel.
  • Cloud voice (more accurate, more natural): fill the Volcano Doubao keys VOLC_APP_ID + VOLC_ACCESS_TOKEN (streaming recognition with hot-word bias toward your project terminology) and DOUBAO_TTS_API_KEY (seed-tts-2.0 synthesis) in ~/.saydo/.env, then start the Python voice pipeline:
cd pipeline && uv sync
SAYDO_HOME=$HOME/.saydo SAYDO_DAEMON_PORT=47100 nohup uv run python -m saydo_pipeline >> ~/.saydo/pipeline.log 2>&1 &

Once configured, the console switches to cloud voice automatically; when the pipeline is absent, /readyz reports voiceReady=false (text and the control plane keep working — not a startup failure). Details in §12.

4.7 Optional: phone connection (LAN)

The phone apps are not in stores; today you can connect to the desktop service over the LAN using a mobile browser (or a self-built app shell):

  1. Start the daemon with SAYDO_MOBILE_LAN=1 (listens on 0.0.0.0, behind three gates: RFC1918 private-network sources only + capability token + Host/Origin checks);
  2. Find your LAN IP (ipconfig getifaddr en0), turn http://<LAN-IP>:47100/?token=<cap-token> into a QR code (don't paste the token into third-party online tools) and scan it with the phone; opening the URL directly in the mobile browser works too (mobile shell route #/m);
  3. On the phone, hold the capsule at the bottom to talk (iPhone uses system speech recognition — no Python pipeline needed), swipe up to choose which Focus item it belongs to; see the four colors of Today, watch progress, nod or retract ledger-type confirmation cards. Approvals of running commands (S2) and merge / delete (S3) only ever appear on the local trusted screen — a LAN phone surface cannot rule on them (the phone has no paired device identity).

This is a dogfood-grade temporary boundary (cleartext LAN HTTP + a long-lived capability token) — do not expose it to untrusted networks; device pairing / end-to-end encryption / store apps are covered in §13 and §16. Deleting ~/.saydo/.cap-token and restarting rotates the token; every connected device drops and must re-scan.

4.8 Residency, backup, shutdown, reset

  • Residency (launchd): just daemon install | start | stop | restart | status | logs | deploy [sha] — auto-start at login + auto-restart on crash, with the runtime tree separated from the development tree (~/.saydo/runtime~/.saydo/releases/<sha>). install also generates and loads the voice pipeline's residency config (requires uv); without uv, use just daemon install --without-pipeline to install only the daemon (desktop cloud voice unavailable; system voice and text work as usual); uninstall removes both symmetrically. For daily use the foreground forms in §4.3 are enough; residency suits long-running setups.
  • Update: git pull && pnpm install && pnpm -r build, then restart the daemon (foreground form: Ctrl+C and start again; launchd form: just daemon deploy — restarts the daemon and the voice pipeline, pick an idle moment).
  • Backup: the daemon snapshots automatically every day to ~/.saydo/backups/<timestamp>/ (online SQLite backup + sessions/ + the user profile + each project's knowledge base and grounding artifacts), retained for [params].backup_retention_days (default 30 days); just backup triggers one manually.
  • Shutdown: lsof -t -iTCP:47100 -sTCP:LISTEN | xargs kill; pkill -f saydo_pipeline (in the saydo up foreground form just Ctrl+C: it first stops new dispatches, writes "to be resumed" markers for active tasks, then closes down; the next start picks up where it left).
  • Full reset (back to a brand-new user): after stopping, rm -rf ~/.saydo — this rotates the token and also deletes all ledgers and memory; <repo>/.saydo/ inside project workspaces is outside this directory and must be handled separately.

5. Models & providers (required reading for advanced users)

SayDo splits model usage into five slots, each with its own supply method and model. This is the same mechanism behind both "one subscription does everything" and "combining multiple vendors for the best results".

5.1 The five slots

SlotConfig keyWhat it doesWhat it needsExample (repo template / wizard "one key" card)
dialog[models].dialogEvery conversation turn: interview, Q&A, spoken summariesLow latency, good spoken Chinese, strong tool-call disciplineAPI (OpenRouter, openai/gpt-5.6-luna)
thinking[models].thinkingTask card drafting, decision pack / plan generation, grounding distillation (the lightweight demo is mechanically rendered and uses no model)Deep reasoning, async without blocking dialogAPI (OpenRouter, openai/gpt-5.6-terra-pro)
cheap[models].cheapSummary narration, hot-word extraction, event tagging and other high-frequency structured callsCheap, fastAPI (OpenRouter, google/gemini-3.1-flash-lite)
evaluator[models].evaluatorIndependently judges "enough to start" (readiness deep assessment)A different family from dialog / thinking (§5.4)API (OpenRouter, anthropic/claude-sonnet-5)
dev (executor)[models.dev]Which agent edits code on dispatch, and which of its modelsSee §6agent = "cursor" (matches the repo template; the only production executor today)

Why split: dialog wants latency (you're waiting for it to speak), thinking wants quality (its output is the "contract"), cheap wants cost (high-frequency calls), evaluator wants independence (a referee from another vendor). One model for all four slots is either slow, expensive, or not independent.

5.2 Two supply methods

Method A · direct API ({ provider = "api", via = "<endpoint name>", model = "..." })

  • Any OpenAI-compatible endpoint: declare base_url + api_key = "env:VAR" under [providers.api.<name>] (keys may only be referenced as environment variables — never written into the config in plaintext).
  • Recommended gateway: OpenRouter — one key reaches many vendors (it's the default template); direct single-vendor connections also work (OpenAI / Anthropic / DeepSeek / Moonshot / Alibaba Bailian… the template has examples).
  • When the model name carries a vendor prefix (openai/…, anthropic/…, google/…, deepseek/…, x-ai/…, qwen/…), the family resolves automatically; on single-vendor endpoints whose model names have no prefix, the endpoint must declare family = "deepseek" or similar explicitly — used by §5.4's different-family check.
  • A dialog slot on API runs in realtime mode: full multi-turn tool loop, streaming, sub-second returns — voice conversations rely on it.

Method B · subscription CLI (BYOA) ({ provider = "<x>_cli", model = "..." })

Use AI command-line tools already logged in on your machine as reasoning supply — zero API keys, billed to your subscription quota. Currently wired CLIs:

providerCommandNotes
codex_clicodexFamily always GPT; optional reasoning (none/minimal/low/medium/high/xhigh/max); model may be omitted
claude_cliclaudeFamily always Claude; model may be omitted
cursor_clicursor-agentmodel required (e.g. claude-fable-5-thinking-max, cursor-grok-4.6-high-fast); family resolved from the model name
grok_cligrokFamily always grok; model may be omitted
gemini_cligeminiFamily from the explicit model name or in-stream reporting; if it can't be resolved, the slot is blocked
qwen_cliqwenSame (transport-style CLI)
copilot_clicopilotSame (transport-style CLI)

(kimi_cli / opencode_cli are only "recognized" in the resource profile and cannot be used as supply — local testing showed zero-tool operation cannot be proven before execution, so for safety they are not wired.)

Login is between you and the vendor: log in with each vendor's official method; SayDo only probes "installed? / logged in? / which models can it list?", and the wizard offers paste-ready fix commands when one isn't logged in (follow the wizard's hints):

CLILogin
cursor-agentcursor-agent login
codexcodex login
claudeclaude auth login
grokgrok login
geminiRun gemini in a terminal to complete the Google login
qwenConfigure an OpenAI-compatible endpoint or OPENAI_API_KEY (it bills through the upstream API, and the ledger copy says so honestly)
copilotcopilot login

How BYOA works and its discipline (the part you'll feel in the UI):

  • One shot, stateless: every call spawns a fresh CLI subprocess; sessions are never reused (resume is deliberately avoided — session persistence conflicts with privacy switches, and in testing resume gained no latency either).
  • Dialog slot on CLI = slow text mode (dialog_cli_oneshot): about 15–25 seconds per turn, returning one reply plus up to 8 structured actions (record a note, open a task, check status, propose to start…), which the daemon executes one by one; it does not enter the voice realtime loop (give the dialog slot an API for voice conversations). This is not a hang — the UI shows a slow-mode badge and progress.
  • Takes effect only after real self-checks: when the config is saved, every CLI-backed slot makes one real call (the dialog slot must return a valid envelope, the cheap slot must pass schema, the evaluator must pass a deep-assessment request of the same shape as production and report the actual model); all four slots must pass before the restart takes effect; any failure marks only that slot red with a plain-language reason — no fake success.
  • The cage: CLI calls for reasoning run in a fresh empty isolated directory each time, the prompt header hard-bans "execute commands / read files", and each CLI gets its strictest zero-tool flags (Claude --tools "", Codex read-only sandbox, Grok --tools "", Gemini/Qwen/Copilot their own deny policies + isolated HOME); if a tool-call event appears in the stream, the call is terminated and voided immediately (tripwire). Cursor CLI has no provable zero-tool flag — only "ask mode + tripwire detection", the weakest cage of the three. Precisely because CLI cages are "write-limited but not read-limited" (disk reads can't be blocked, so an evaluator could in theory read the Brain's self-defense), any evaluator slot on any CLI requires you to personally tick two informed confirmations (same-family + isolation, §5.4).
  • Actual-model verification: every call records requestedModel / observedModel; if the model family reported in-stream doesn't match the config, that call is voided; fixed-family CLIs like Codex / Claude may pass under their registered family when binary identity verification (absolute path + content digest registration) succeeds.

5.3 Three setup plans (the three cards in the first-run wizard)

The first time you enter the console you see the "resource profile wizard": it probes which CLIs are installed, which are logged in, and which keys are already in .env, then offers one-tap plan cards —

PlanWho it fitsHow the four reasoning slots are configuredExperience
All on one CLI (e.g. "all on Codex")One subscription, don't want to apply for keysdialog / thinking / cheap / evaluator all on that CLIZero keys, zero terminal; 15–25 s per turn (slow text mode); zero extra cost within subscription quota. The evaluator on CLI and same family as dialog requires ticking both the "same-family informed confirmation" and the "isolation informed confirmation"
Dialog on API + the rest on CLIHave a subscription, willing to pay a little API for realtime dialogdialog on API (sub-second, voice-capable); thinking / cheap / evaluator on CLIThe recommended "fast and cheap" combination; the evaluator on CLI also needs the two confirmations
One key for everythingNo CLI subscriptionsAll four slots on the OpenRouter API, models assigned per slot specSub-second returns; pay-as-you-go per the provider's bill

The wizard's "advanced mode" allows slot-by-slot manual configuration: each slot independently picks an API endpoint / any logged-in CLI / a specific model; both routes require all four self-checks green before the restart takes effect. Advanced combinations, for example:

  • dialog on API (e.g. a low-latency model like google/gemini-3.1-flash on OpenRouter, family gemini) + thinking on cursor_cli (claude-fable-5-thinking-max, family claude) + cheap on grok_cli + evaluator on codex_cli (reasoning = "max", family gpt) — fast dialog, strong thinking, the evaluator in a different family from both dialog and thinking, and thinking and evaluator don't share the same vendor's subscription window (the evaluator on CLI requires the two confirmations).
  • All-API but split vendors: dialog / thinking on one vendor, the evaluator on another; or one OpenRouter key with different vendors' models per slot.
  • Project-level overrides (console project settings): a project can switch the dialog / thinking slots to a stronger (or cheaper) API model, change the dev-slot model, or change the budget cap; the project layer only allows API bindings, and the evaluator slot cannot be overridden (it's the guardrail anchor).

5.4 The different-family rule: the evaluator must be another vendor

If the "enough to start" judgment were self-assessed by the same model (or family), errors would correlate strongly with generation. Therefore:

  • The evaluator slot's model family must not match dialog or thinking (fail-closed: same family without confirmation → evaluator unarmed, the daemon warns at startup; conversation still works, but readiness deep assessment won't arm).
  • Family is judged by the resolved actual model: openai/…→gpt, anthropic/…/fable/sonnet/opus→claude, google/…→gemini, deepseek, grok, qwen, composer (Cursor's own)…; codex_cli is always gpt, claude_cli always claude, grok_cli always grok; Cursor CLI resolves from the model name you fill in.
  • Two informed exemptions (you must tick them yourself; the UI won't sign for you): evaluator_same_family_ack (accept same-family evaluation, at the cost of reduced independence), evaluator_isolation_ack (accept a "write-limited but not read-limited" CLI cage as evaluator, at the cost that "the evaluator can't read the Brain's self-defense" being unprovable). Whenever the evaluator slot uses any CLI (any vendor, regardless of family), both must be true; an evaluator on API with a different family needs neither. The "all on one CLI" plan inherently needs both.
  • Arming conditions for an evaluator on CLI = both acks true + self-check passed + actual model family provable; anything missing means unarmed (the reasons show as same_family_blocked / isolation_ack_required / cli_self_test_required / cli_self_test_failed).
  • Adjacent rule: when a thinking / cheap slot's CLI is unregistered or fails self-check, it honestly falls back to the dialog slot's supply (status shows fallback_dialog with the reason) — the service doesn't break and doesn't pretend to be active.

5.5 Billing & accounting

  • Subscription CLI calls produce no "yuan": the cost ledger records them as source = subscription, shown as "within subscription quota" (call counts appear in the weekly report) — not ¥0, not "unknown", and no quota forecasts. Only when the CLI's authentication is verified to be genuinely subscription-based (e.g. Gemini personal OAuth, Copilot GitHub login) is it written as "zero cost within subscription"; if the CLI's upstream is actually API-key billing (e.g. Qwen with a key configured), the copy honestly changes to "billed per the CLI's upstream method — SayDo doesn't pay for you".
  • API calls are estimated from the price table: models without an entry in [pricing] show "no exact figure yet" (never invented numbers, never 0). What actually stops you is the per-task triple circuit breaker of cost / duration / turns (§8.5); the cost cap only constrains the API-billed part — subscription calls are backstopped by wall-clock + turn count; [budget].monthly is currently just a config item (no enforcement or display point), not a hard gate.
  • Subscription rate limits never silently convert to billing: when a weekly / 5-hour window rate limit hits, that call returns a retryable rate-limit error and tells you honestly, never auto-switching to API billing. Two contracts — "stop and ask whether to switch to pay-as-you-go or wait for the reset + a single-use billing-switch receipt (valid 10 minutes)" and "durable queue with automatic replay" — have landed but are not yet wired into production (in progress).

5.6 Configuration file reference

Global config ~/.saydo/config.toml (non-secret) + ~/.saydo/.env (the actual secrets; never enters Git); copyable templates live in the repo's templates/. The first-run wizard writes exactly these two files; edit by hand and restart the daemon to apply (with invalid config the daemon enters "recovery-only" mode: the health probe and the settings self-rescue page stay up, dialog and dispatch are disabled — §17).

[models]
profile   = "default"                # default | dev (dev also needs the SAYDO_DEV=1 env var; two switches)
dialog    = { provider = "api", via = "openrouter", model = "openai/gpt-5.6-luna" }     # family gpt, realtime voice loop
thinking  = { provider = "codex_cli", reasoning = "high" }                            # family always gpt, billed to the ChatGPT subscription
cheap     = { provider = "grok_cli" }                                                 # family always grok; model omitted = CLI default
evaluator = { provider = "api", via = "openrouter", model = "anthropic/claude-sonnet-5" } # family claude, different from both above, no ack needed
# evaluator_same_family_ack = true   # both lines are required when the evaluator uses any CLI (otherwise readiness deep assessment stays unarmed); evaluator on API with the same family needs only the first
# evaluator_isolation_ack   = true

[models.dev]                         # dev slot = executor (§6)
agent     = "cursor"
transport = "cli"
model     = "grok-4.6"               # use a model name currently listed by cursor-agent

[providers.api.openrouter]
base_url = "https://openrouter.ai/api/v1"
api_key  = "env:OPENROUTER_API_KEY"  # always an env reference; multi-vendor gateways declare no family
# provider_order = ["deepinfra"]     # optional: pin the upstream routing order on an aggregator gateway and disable automatic fallback (deterministic routing)
# [providers.api.deepseek]           # single-vendor direct example: model names have no prefix ⇒ family is required
# base_url = "https://api.deepseek.com/v1"
# api_key  = "env:DEEPSEEK_API_KEY"
# family   = "deepseek"

[voice]
engine = "cascade"                   # cascade: ASR → text model → TTS
asr    = "volc"                      # Volcano Doubao streaming recognition (with hot-word bias)
tts    = "volc"                      # Doubao seed-tts-2.0 v3 bidirectional streaming

[privacy]
store_audio          = false         # recording and transcripts are consented separately
store_transcript     = true
audio_retention_days = 0

[budget]
monthly  = 200                       # currently just a config item (no enforcement point), not a hard gate; the real breakers are per-task (§8.5)
currency = "CNY"
task_max_default = 20                # default per-task cost cap (yuan; constrains only the API-billed part)

[dnd]
window = "23:00-08:00"               # do not disturb: no outbound calls inside the window; snoozed to the window end, then called back in queue order

[tier1]                              # executor (Cursor CLI) version pinning; enabled only when both keys are present
cursor_agent_bin            = "/Users/<you>/.local/share/cursor-agent/versions/<ver>/cursor-agent"  # must be an absolute path; ~ is not accepted
cursor_agent_pinned_version = "<ver>"

[gate0]
enabled = true
bypass  = false                      # always false; true actually refuses all dispatch (guards against misconfiguration)

[params]                             # tunable parameters (excerpt)
foundation_budget_min  = 5           # grounding wall-clock cap (minutes)
interview_question_budget = 8        # interview question budget
park_aging_hours       = 72          # park aging: waiting on a human for over 72h cancels and converts to a draft
proposed_ttl_hours     = 24          # decision pack validity
backup_retention_days  = 30
enabled_project_types  = ["coding"]  # type capability gate; may enable ["coding","writing"]

Common .env keys: OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY (fill per the endpoints you create), DOUBAO_TTS_API_KEY + VOLC_APP_ID + VOLC_ACCESS_TOKEN (desktop browser voice, §12), NTFY_TOPIC + NTFY_SERVER (phone push, §11).

The whitelist of project-level overridable keys is in §7.1 (<workspace>/.saydo/project.toml): only [project]/[git]/[verify]/[setup] and budget / dnd / params subsets; models / providers / gate0 / hopper / privacy / voice at the project layer are refused outright — a file shipped with a repo is untrusted input and must not point your keys at someone else's endpoint.

6. Executors (the dev slot): who actually edits code

6.1 Two vocabularies — don't mix them

  • CLI supply for reasoning slots (§5.2) = letting the Codex / Claude / Cursor / Grok / Gemini / Qwen / Copilot CLIs speak, think, and evaluate for the Brain — they do one-shot text reasoning only and never touch your repo.
  • Executors (dev slot, [models.dev]) = the agent that, after sign-off, actually edits code and runs commands in a worktree. The vocabulary is claude_code | cursor | codex, but only cursor has a production implementation today; configuring anything else makes the daemon refuse to start the executor (fail-closed — it won't quietly substitute the Cursor binary); conversation is unaffected.

6.2 Currently available: the Cursor CLI executor Available now

  • Driven as: cursor-agent -p --force --trust --output-format stream-json --model <model> [--resume <chatId>] <prompt>, running in the task's worktree; model = any model name currently listed by cursor-agent (Grok 4.6 and Composer 2.5 verified in testing; Claude-family models also work); if the actual model family reported in the event stream doesn't match the config, that round is voided.
  • Approval gate: the executor writes .cursor/hooks.json per worktree; the beforeShellExecution hook blocks synchronously and calls back to the daemon's local unix socket; the daemon rules by command effect (§8.1/§8.3); hook timeout, malformed JSON, or an unreachable socket all mean refuse; a shell call in the event stream without a matching gate request ⇒ the gate is judged bypassed and the task is terminated immediately (canary).
  • Version pinning: [tier1].cursor_agent_bin must be the absolute path of the locked copy (…/versions/<ver>/cursor-agent) plus cursor_agent_pinned_version exactly equal; the executor is enabled only when both keys are present — guarding against behavior drift after cursor-agent self-updates. Upgrading = change both keys + rerun the gate.
  • Credential stripping: the agent process environment carries only ten variables — PATH/HOME/USER/LOGNAME/SHELL/LANG/LC_ALL/LC_CTYPE/TERM/TMPDIR — no API keys or tokens; its login state lives in its own HOME storage.
  • Honest boundary: on the Cursor backend only the shell channel passes the gate; built-in file read/write / networking tools don't go through the hook — so its network egress capability is honestly marked egress = uncontrolled; isolation relies on "separate worktree + credential stripping + command gate", not a sandbox; for tasks you don't trust, use step-confirm mode and watch S2 closely.
  • Changing requirements mid-run (steer): no backend currently supports live injection into a running task; a running task = terminate the current round and resume with the new instructions (cancel_resume, worktree kept), a queued / parked task = injected at its next run (queued_delta). It tells you honestly which case applies.

6.3 In progress & planned

BackendStatusNotes
Claude Code executor (agent = "claude_code")In progressPlan settled: the official claude -p --output-format stream-json subprocess as transport, Claude Code's PreToolUse hooks for S1–S3 rulings (file tools pass the gate too — one layer more than Cursor), hooks injected from the command line rather than files inside the worktree; subscriptions consumed only through the claude login, zero API keys; no bypass permissions, no live steer. Landing in three batches: batch one (real-machine capability testing against the official CLI + approval-gate pure functions) is closed; the wiring batch is underway
Codex executorPlannedDesigned via the Hopper batch route (below); agent = "codex" is refused today
Hopper batch route (heavy tasks dropped into the standalone Hopper task system: worktree / event sourcing / crash recovery / budget / review gates)Contract settled, production binding dormantThe daemon's main flow doesn't start it; the locked-version copy and dedicated vault conventions are written in the config template but currently not consumed; merges on the Hopper route are always manual handoff, never the Touch ID card. For users it is not a usable feature today
provider-neutral native executor (any OpenAI-compatible backend)PlannedThe direction recommended by the 2026-08 decision (replacing "make the DeepSeek Harness the default runner" — the latter was rejected for conflicting with fail-closed red lines: approvals defaulting to allow, hook failures not blocking, etc.)
Gemini CLI as executorNot plannedGemini CLI serves reasoning slots only

7. Projects & Git: how it works with your repos

7.1 Projects & workspaces

  • A coding project = a folder on your machine (a git repo), registered by absolute path (~/ allowed; symlinks resolved; filesystem identity recorded to prevent drift). Hard constraints: must be inside your user directory, must not overlap ~/.saydo, must not be a parent/child of another active project.
  • No "give me a remote repo URL and clone it for me": the whole implementation assumes a local folder; clone remote repos yourself first.
  • How to register: name the local path in conversation (it parses only the single path literal in your own current sentence, read-only verifies the directory exists, plays back "this local project, type X" and adopts it only after your closed confirmation — denial, ambiguity, a nonexistent path, or a path you didn't say yourself all mean zero writes; the model never invents a path and attaches it automatically); or pick an existing project at the console's trusted terminal. Non-code work (writing, research, life planning) defaults to the system-managed directory ~/.saydo/projects/<id> and won't stop you to "pick a folder first".
  • In-project directory <workspace>/.saydo/: knowledge/ (the M1 knowledge base, Markdown, human-readable and editable, with its own git history) / foundation/ (grounding artifacts and manifest) / worktrees/<taskId>/ (task isolation trees) / project.toml (project-layer config). The artifact store, source snapshots, readiness deep-assessment records, and session transcripts all live in the global ~/.saydo/{artifacts,snapshots,assessments,sessions}, not in the project directory. Adding .saydo/ to the project's .gitignore is recommended (the daemon won't edit .gitignore for you; to share the knowledge base with a team, commit knowledge/ explicitly).
  • project.toml (shipped with the repo, treated as untrusted input): only [project] (type / exec_mode_default), [git].protected (protected branches, unioned with the default ["main","master"] — can only add, never remove), [[verify.entries]] (acceptance command registration, §7.4), [setup].command (dependency install command), and budget / dnd / params subsets are accepted; models / providers / gate0 / hopper / privacy / voice / pricing / tier1 appearing means refusal (keys are refused, not the file; refusals land in the audit). A broken file = the project's task pickup turns blocked and calls a human — it never plows ahead with lenient defaults.
  • AGENTS.md interop: grounding reverse-absorbs the AGENTS.md / CLAUDE.md / .cursor/rules already in your repo; on the output side it places an idempotent pointer block in AGENTS.md pointing at the knowledge base (<!-- saydo:knowledge:begin/end -->), so the knowledge base's value doesn't zero out when you switch tools.

7.2 One worktree per task

  • After sign-off and dispatch, the executor runs git worktree add -b saydo/<taskId> … HEAD under <repo>/.saydo/worktrees/<taskId>/branch name saydo/<taskId>, cut from the main repo's current HEAD; rework / resume of the same task reuses the same worktree. Your main working tree is never touched during execution (the agent's cwd is locked to the worktree; out-of-circle commands escalate or are refused per S2/S3).
  • Installing dependencies: [setup].command only accepts pnpm / npm / yarn in install / i / ci form with a flag whitelist, and --ignore-scripts is force-appended (postinstall belongs to the supply-chain execution surface; escalate as S2 when genuinely needed).
  • The agent's prompt states explicitly: only make changes inside the current working directory, do not git push, and do not merge your changes.
  • Tasks in the same repo run serially; tasks across repos run in parallel; heavy repos can serialize on a single worktree.
  • Worktrees are never auto-deleted (kept even on cancel, so work can be recovered); <repo>/.saydo/worktrees/ accumulates on disk — git worktree remove / clean up periodically yourself.

7.3 Completion, review, merge: it never pushes for you, and never opens PRs

  1. When execution finishes → the daemon (not the agent) snapshots the worktree tree (git add -A excluding .cursor/, then write-tree for the treeSha) → re-runs the frozen acceptance commands in the worktree (§7.4) → produces the settle proof (task / round / decision pack version / treeSha / verify digest / transcript cursor) → the task enters ready_for_review → calls you. Any drift afterwards is detected.
  2. On the review surface you pick one of three: approve (records the approved treeSha, enters "awaiting merge") / needs changes (a new round of the same task, reusing the worktree, your feedback goes into the next prompt) / void (cancel this round).
  3. Two merge paths, both human-triggered:
    • Touch ID / passkey approval card (Tier1 default): one WebAuthn strong authentication on screen signs a single-use receipt binding task / round / decision pack version / expected tree; with the receipt the daemon: re-checks the worktree's current tree == receipt tree (drift refuses) → re-runs the frozen verify in an isolated environment (failure ⇒ merge_failed, to manual) → asserts the main repo hasn't moved (merge-base == HEAD) and its tracked files are clean (uncommitted changes ⇒ to manual) → git commit-tree + git merge --ff-only into the main repo's current branch (fast-forward only, no history rewrite; commit message like saydo: merge <taskId> (S3 approved)) → asserts the HEAD tree once more → "delivered". First use requires a one-time "register approval fingerprint" on the machine.
    • Manual merge handoff (no passkey registered, or you prefer to do it yourself): it gives you a handoff entry; you merge in the terminal yourself; then tap "verify" and the daemon re-reads the main repo's HEAD tree and reconciles it against the treeSha stored at approval — match advances to "delivered", mismatch refuses and leaves a trace (guards against "rolled back but shown as done").
  4. It never pushes and never opens PRs: no code path anywhere pushes to a remote or creates a Pull Request; if the agent types git push itself, it escalates or is refused per S2 (feature branches) / S3 (protected branches or force). Pushing and opening PRs are things you do yourself after reviewing and merging.
  5. The voice surface never renders merge buttons; merge actions invoked from a LAN / tailnet phone surface are refused and audited.

7.4 Acceptance commands only from registered templates (against command injection and "self-certified passes")

  • Register in project.toml: [[verify.entries]] name="test" source="package_script" ref="test" (source is only package_script / justfile); the Brain can only choose among registered entries — any assembled string is refused.
  • Content freeze: at dispatch / pickup, each verify's argv + script content digest + digests of related framework config files (known configs like vitest / vite / playwright / jest / tsc / eslint / pytest / ruff / mocha) are frozen; re-read and re-computed before execution, mismatch ⇒ fail-closed. If the agent legitimately modified the test scripts ⇒ blocked and calls you back: "the verification commands need to change — your re-sign-off is needed" (Plan Delta); never silently allowed and never silently dead.
  • A coding task with no registered verify does not settle — it turns blocked and calls a human: the independent acceptance oracle is a gate, not an option.
  • Verify runs in an environment narrower than the agent's (HOME replaced with a one-time empty directory). Honest boundary: absolute-path reads and network egress still can't be blocked; full isolation = containers / sandboxes later.
  • The frozen verify is run once more before merging.

8. Approvals & safety

8.1 Risk graded by effect, S0–S3

Voice is a weak authentication channel (anyone present can speak; recognition has an error rate), so approval power is graded by risk; grades are computed from effects, not action names: effect × target (inside / outside the circle) × data sensitivity touched × identity credentials × downstream triggered (CI / deploy) × cost.

LevelTypical (defaults; effects can upgrade)Approval
S0 readReading code, read-only commandsAutomatic
S1 write (inside worktree)Editing code, running registered verify, local commitAutomatic
S2 out-of-circle but reversibleInstalling dependencies, pushing to a feature branch, unrecognized commandsstep-confirm: on-screen / voice confirmation (45-second window by default, timeout = refuse); direct-to-review: passes if it matches the read-aloud-signed pre-authorization list (not currently enabled on the voice chain, §8.4)
S3 irreversible / external impactMerging to a protected branch, force push, deploy, spending money, deleting data, messaging the outside worldAn authenticated screen (Touch ID / passkey) is mandatory; voice never approves — a playback into the same microphone is not an independent authentication factor

Upgrade rules: touching .env / credentials / customer data ⇒ at least S2; dependencies with postinstall ⇒ at least S2 and must produce a list; push triggering preview deploys ⇒ S3; push to protected branches ⇒ S3.

8.2 Gate 0: the master switch for whether work may start

Gate 0 is not a runtime command gate but six safety gates that must be closed before autonomous dispatch opens. Before every dispatch it checks [gate0] enabled = true && bypass = false, and there is no bypass branch in the code (a corrupted config file is treated as "not closed" — dispatch is refused rather than continuing with lenient defaults):

GateContent
G1 identity / authorizationSingle-user assumption made explicit; S3 only on screen; console capability token + Host/Origin whitelist + DNS rebinding protection; approval-gate integrity (socket rulings, canary)
G2 cross-boundary transaction idempotencyUnique callback outbox, event cursors + crash replay
G3 independent acceptance oracleverify whitelist + content freeze + Plan Delta; the generator never grades itself
G4 secret / egress isolationAgent environment stripped of credentials; egress capability honestly declared per adapter (Cursor = uncontrolled, no fake green); setup defaults to --ignore-scripts; TTS redaction
G5 intent auditTranscript → receipt → task traces connected end to end; the audit table is immutable (triggers)
G6 deletion / consent propagationA hard-delete path for "forget this"; recording and transcripts consented separately; backups expire whole per retention

8.3 How each shell command passes the gate

  • Physical chain: .cursor/hooks.json inside the worktree → the gate script supplied by the daemon and placed outside the worktree at ~/.saydo/tier1/ (same user as the agent, currently not guaranteed unwritable by the agent — the honest account) → unix socket → daemon ruling; compensating control = the script's integrity is recomputed on every gate request, and drift ⇒ all active tasks cancelled.
  • The classifier conservatively derives effects from commands: anything unrecognized escalates as S2; commands lexically containing .env / credential / secret / .pem / .key / id_rsa / id_ed25519 / .npmrc / .netrc / keychain / token ⇒ sensitive upgrade; force push / absolute-path deletion / piping remote scripts into execution ⇒ S3; compound commands (&& / ; / | / subshells) are classified by their highest-risk segment, and any unjudgeable segment makes the whole command unjudgeable. Arbitrary code execution entries like node / python / tsx / just are also S2 (node -e "fetch(…)" and node build.js are lexically indistinguishable); registered verify (e.g. just test) matches before the classification table at the gate and still passes automatically.
  • Rulings: S0/S1 pass; S2 escalate (45 seconds, timeout = refuse); S3 refuse outright; risk-calculation errors = refuse. Every command is approved independently; one approval never goes long-lived.
  • A fourth action, "edit and approve": you may modify the command before approving; the agent retries the edited command verbatim and hits this single-use receipt.
  • Each S2 gets one receipt, bound to the parent decision pack's digest and the sign-off round, consumed on acceptance.

8.4 Two execution modes

ModeSemanticsStatus
step-confirm (factory default; colloquially "ask me every step / keep an eye on it")Every out-of-circle action (S2) confirmed beforehand + confirmation at plan step boundaries; agent questions escalate immediately; S0/S1 never ask in any mode (the worktree already backstops them)Available now — all tasks currently execute in this posture
direct-to-review (colloquially "run it straight through")Sign-off authorizes the whole pack: the decision pack declares item by item and reads aloud this task's expected S2 effect classes (with constraint parameters: which packages to install / which branch pattern to push), signed with the pack's digest; during the run, matches on the list pass automatically (a pre-authorization receipt is recorded and validity re-checked at the enforcement point), misses or upgrades are refused with feedback to the agent to take another routeIn progress — the pre-authorization list's contract and matching logic have landed, but the voice sign-off loop isn't wired for read-aloud signing yet, so packs are always assembled as step-confirm; choosing "straight through" at sign-off is honestly answered with a redirect to the screen or to step-confirm

Invariants (no mode may touch): effect grading, S3 on-screen strong authentication, the three circuit breakers, the verify whitelist, Plan Delta re-authorization, callback priority, status-word discipline, the two keys, Gate 0. The pre-authorization scope always belongs to the decision pack — heard and signed by you; the mode is only the enabling switch for that list — otherwise "switch to full auto" would become a one-sentence privilege-escalation channel. Defaulting tight (step-confirm) hedges automation bias; "trust graduation" was explicitly rejected.

8.5 Three circuit breakers & park aging

  • Three circuit breakers (any one trips ⇒ cancel ⇒ enters blocked with context and calls you): active wall-clock (default 45 minutes; the clock stops while parked for approvals / questions) + turn count (default 80) + API cost (from the decision pack's cap, default 20 yuan; constrains only the API-billed part — subscription calls are backstopped by the first two). All three are project-overridable. "No events for 15 minutes" only catches hangs, not "actively burning money in circles" — so all three are indispensable.
  • Park aging: waiting on your review or on you to unblock beyond park_aging_hours (default 72 hours) ⇒ cancel and convert to a draft card (a parked task holds a worktree and the same-repo queue — it can't hang forever); resuming from a long park forces receipt-validity re-checks.
  • Decision pack validity proposed_ttl_hours (default 24 hours), void on expiry; a new proposal for the same project supersedes the old.

8.6 Other safety discipline

  • TTS redaction: tokens, keys, customer data, and full local paths never enter voice (a path is spoken as "a config file", a key as "a credential").
  • Console caller identity: every request / WS handshake validates the capability token, with a Host / Origin whitelist, stopping malicious web pages from bypassing voice authorization to call the daemon directly; listens on loopback only by default.
  • Memory write path: third-party content (repos / web pages / documents) can never define "what you like / what you allow / where your credentials are"; candidate → trusted requires human approval (§9.3).
  • Audit / log separation: logs may rotate; the audit table is immutable; sensitive payloads are recorded as digests only, never in the clear.

9. Memory & the ledger

9.1 Four layers of memory (M0–M3)

LayerNameStorageLifecycleContent
M0User profileThe cross-project tier of the memory ledger (~/.saydo/saydo.db; ~/.saydo/profile.md is covered by backup protection, and a human-readable projection is planned)Cross-project, long-termPreferences, tech stack, communication style, terminology hot words (incidentally improves recognition accuracy)
M1Project knowledge base<workspace>/.saydo/knowledge/ (Markdown, human-readable; m1-notes.md is the ledger projection)Project-level, long-term stableProject goals / constraints, codebase understanding (architecture / modules / terminology), key decisions, domain knowledge
M2Accumulated conversation knowledgeArtifact store (~/.saydo/artifacts/ + main-database index, attributed by project)Project-level, grows with conversationsNew plans / decisions and research material from each conversation (versioned, recallable)
M3Session working memoryContext window + transcript (~/.saydo/sessions/<id>.jsonl)Single sessionThe last N turns, the current task

Context Pack = each turn picks relevant slices from M0 + M1 + M2 plus M3's recent turns into the window (versioned; re-fetched on mismatch); retrieval uses SQLite FTS5 (trigram, BM25) to answer "what did we decide?", while code facts are always read live with rg and never enter the knowledge base (no staleness tax; terms shorter than 3 characters are backstopped by rg).

Source of truth: the memory domain's truth is the append-only memory event ledger; Markdown files and the SQLite index are rebuildable projections; source code and Git are always the final authority on code facts. m1-notes.md is a one-way projection — editing the file directly doesn't write back to memory; to change it, tell SayDo (P0 has no file watching).

9.2 Grounding (Foundation) Available now · mechanical pipeline

Triggered on first contact with a project, blocking ("learning" is a first-class session state — it says what it's learning, progress, and how long remains):

  • Scans the file inventory and language distribution; reads key files by whitelist (README / AGENTS.md / CLAUDE.md / package.json / pyproject.toml / justfile / Makefile / Cargo.toml / go.mod etc., 6000-character excerpt cap per file);
  • Reverse-absorbs the AGENTS.md / CLAUDE.md / .cursor/rules already in your repo;
  • Generates four knowledge documents: core.md / inventory.md / build-test-run.md (marked "not run-verified") / conventions.md;
  • Budget: 5 minutes wall-clock + 200k tokens by default; over budget produces a partial and honestly writes "haven't finished reading — answering from what was read";
  • Atomic switch by generation (knowledge/gen-N/ + a current link; failures keep the old generation), and the knowledge/ directory carries its own git history; after a grounding generation change, readiness bindings that depended on old-generation knowledge automatically lapse (no starting work on stale understanding);
  • Places an idempotent pointer block in your AGENTS.md (<!-- saydo:knowledge:begin … end -->) pointing at the knowledge base; write failures (read-only repo / permissions) degrade silently without flipping the published generation.
  • Session warm-up (every time, lightweight): incremental refresh seeded by four git diff groups (committed / staged / unstaged / untracked); rebase / force-push is never misjudged as "everything deleted".

Honest note: grounding today is a deterministic mechanical pipeline (inventory + excerpts + document generation), without LLM deep research by the thinking-slot model — the latter is the next tier, in progress.

9.3 The memory write path: candidate → trusted (a safety boundary)

The top risk of persistent memory isn't staleness but source pollution and cross-project leakage. The write path is:

raw evidence (untrusted) → candidate (with source / citation / trust level / expiry) → conflict / taint / policy checks
→ low-impact project-local facts may enter automatically; preferences / decisions / external facts need your approval → trusted
  • Only what you said yourself (user_stated) and low-impact mechanical facts (git-tracked in-repo files / your edits, and factual statements) enter trusted directly; anything hitting instruction / side-effect words (curl | sh, rm -rf, sudo, git push, "deploy / go live / drop the database / migrate"…) is demoted to candidate; anything from web pages / agent output / imports is always a candidate.
  • The M0 red line: third-party content can never define "what you like / what you allow / where your credentials are"; M0 accepts only your own words or what you approve.
  • Post-session distillation = mechanical nomination + your approval (not automatic retention): when a session suspends, up to 5 candidates are nominated by a decision-word vocabulary ("decided / settled / let's use / from now on / standardize on / signed off"…); you approve / reject them one by one on the memory page (rejection = soft forgetting; a denial is never resurrected).
  • Forgetting: invalidation markers (rows kept, excluded from projections) / soft forgetting / hard delete ("forget this": tombstone + historical content overwritten in place with [forgotten] + purged from the full-text index, projections, source snapshot bodies, and assessment records). Honest boundary: automatic snapshot backups are immutable whole files — hard delete doesn't purge backups entry by entry; they close out by expiring whole per the retention period (default 30 days).

9.4 Focus, obligations & the four-color ledger Available now

The console's spine is the Focus: one continuously tracked item, lifecycle captured / active / dormant / closed / abandoned / archived; it can link 0..N projects or belong to none (life planning and research are fine); under it hang obligations (who owes what: owner = you / AI / external; status open / in_progress / waiting / deferred / blocked → resolved / superseded) and expectations (acceptance criteria for what "done right" looks like — changes require the AI to play back the impact and re-confirm).

The four-color ledger (Today page) is not a separate table but a "whose ball is it" view computed live from the ledger:

ColorMeaningSource
Orange · awaiting your callNeeds your answer / decisionPending confirmation cards, tasks ready for your review, obligations owned by you that need a decision / input, anything terminated upstream (blocked)
Blue · owed by youNeeds your actionObligations owned by you that need action
Green · AI workingBall with the AIQueued / running tasks, open obligations owned by the AI
Gray · waiting externallyBall with someone elseOpen obligations owned by external parties
  • "Note this for me / jot this down / don't forget" ⇒ proposed for the ledger immediately in the same turn (a decision = decision, a to-do by attribution, something to verify = check), written via a confirmation card (your nod or a countdown); multiple items in one sentence can be confirmed on one card, all-or-nothing. If the current dialog's tool surface has no ledger tool, it degrades into the memory store first and tells you "it can be attached to the ledger once the item exists".
  • Clearing entries: obligations are resolved (done / abandoned / no longer applicable) by the AI's proposal with your confirmation; the AI marking its own obligation done must carry verifiable evidence (three checks: artifact / task / event); narrative revisions don't clear entries. Green / gray entries can be "got it"-ed away from the inbox; orange / blue cannot be acked and only disappear when the source data changes; when an entry upgrades (green → orange) it always reappears, ignoring historical acks.
  • Confirmation cards don't evaporate on expiry: an on-screen confirmation card that times out (10 minutes) degrades by attribution into the matching color zone (a "waiting for one more word from you" form); the voice mode's 5-second countdown auto-execution is by design; storm guard: at most 5 cards per session per item per day, aggregated beyond that.
  • Multi-step continuation: after you confirm one step, the AI automatically continues to the next, at most 3 consecutive automatic steps, then stops and parks on the ledger (runaway guard).

10. Review & delivery

10.1 Status-word discipline (hard constraint)

StatusMeaningWhat it says
run.completedThe agent process exited (the gate / artifacts may not have settled yet)Never announces "done"
ready_for_reviewThe acceptance gate ran, artifacts confirmed on disk, ready for your review"Execution and checks have run — ready for your review"
task.doneAfter your review + merge / archive"This item is delivered"

Execution tasks never estimate remaining time, only report elapsed ("ran for 18 minutes"); unknown cost says "no exact figure yet", never 0.

Task state machine (coding type, main trunk excerpt; there are also states like confirmed / paused_step_boundary / merge_failed / cancel_settled / superseded):

queued → running → ready_for_review → review_approved_waiting_merge → merging → task_done
              ↑            │ needs changes: new round of the same task (reuses the worktree)
              └────────────┘
  at any stage: blocked (waiting on you: S2 timeout / question / circuit breaker / missing verify) · failed (retryable) · cancel_requested (voided, worktree kept)

10.2 The evidence view

The task review surface is organized by acceptance criteria: criteria on the left, evidence on the right (diffs, test results, the list of decisions it made itself, unverified items). Discipline: agent-self-reported evidence is marked hollow to show downgrading and can be overturned; items with no bound evidence are honestly marked unknown — no fake precision; the decision list and the spoken summary are same-sourced (the persisted decisions), so what's spoken matches what's on screen.

10.3 Three layers of spoken summary

  1. One sentence (≤40 characters, pure rules): "changed 5 files, tests 12/12 passing, 1 decision awaiting confirmation" — file counts from git, test counts from the independent gate, unknowns marked unknown, never written as 0;
  2. Walkthrough (a 150-character spoken script, when you say "tell me more");
  3. Decision list (each entry = decision + reason + overturnable action, ≤5 entries; mechanically extracted from the event stream → lazily distilled once by the cheap slot → persisted).

The raw event stream never goes directly into the dialog model; the narration model only phrases — it may not produce numbers the rules layer didn't.

10.4 Review tri-state & merge

Approve / needs changes (new round of the same task, reusing the worktree, feedback into the next prompt) / void; after approval, merge per §7.3's two paths. The writing type has an extra "section-by-section ruling" barrier: settle doesn't mean all green, and approval is impossible without per-item rulings.

11. Callbacks & notifications

  • Trigger: fired only by post-settle states (ready_for_review / blocked / failed etc.) — the runner exiting doesn't count; durable outbox, idempotent replay, one call only across daemon restarts.
  • Priority: blocked (waiting on a human) > failed > pending approval > ready_for_review > progress (silent by default).
  • Escalation chain Available now: L0 online voice callback — when the console is online, the voice pipeline healthy, and you're not speaking, it speaks up directly, and the first sentence is the callback reason; escalates after 30 seconds without a response. L1 macOS desktop notification (system notification center) + ntfy phone push — either one delivered counts as notified (the first desktop notification may need to be allowed in system settings); the orange zone of the Today page stays lit throughout. L2 phone calls and call-style mobile briefings are planned. A sweep scans the outbox every 15 seconds; delivery failures stay queued and retry.
  • ntfy: fill NTFY_TOPIC (a random string; subscribe to the same topic in the ntfy phone app) and NTFY_SERVER (default https://ntfy.sh, self-hostable) in .env; messages are JSON POSTed (Chinese titles go in the body), priority blocked / failed = 4, ready-for-review = 3; deep links carry only routes, never the capability token; titles go through the same redaction as TTS (a public topic is plaintext-subscribable). These two keys aren't in the wizard whitelist today — edit .env by hand.
  • PagerDuty-style state machine: pending → notified → acked → resolved; ack only stops escalation — it doesn't mean resolved, and certainly doesn't authorize any action; unresolved 30 minutes after ack (callback_resolution_timeout_min) re-escalates. Ways to ack: tap "got it" on the notifications page, or simply speak back (speaking after a voice callback counts as answered); when a task leaves ready-for-review / blocked as resolved, the entry clears automatically.
  • Do not disturb [dnd].window (default 23:00-08:00, supports crossing midnight): inside the window it neither speaks nor pops desktop notifications — it sends one low-priority ntfy (marked as DND hours) and snoozes to the window's end; after the window it calls back in queue order; an unreachable device is not DND — it goes to short-cycle retries.
  • Intercepting isn't calling you: dangerous actions first feed the reason back to the agent to take another route; the same intent must be blocked ≥ 2 times before entering the callback chain.
  • Output arbitration Available now: it doesn't interrupt with voice while you're still mid-sentence — the item degrades to desktop + push; "meeting software holding the mic" detection has no data source yet and is always treated as not held (honestly).

12. Voice

12.1 Tiers of voice capability

TierRequirementNotes
Typing (always available)NoneType anytime; the keyboard's built-in system dictation works too
Browser system voice (zero config)Browser supports SpeechRecognition (Chrome / Safari)Hold to talk → browser recognition → text channel into the conversation; replies read with speechSynthesis. Average quality
Cloud cascade voice (more accurate, more natural)The three Volcano Doubao keys in .env + the Python voice pipeline runningASR = Volcano Doubao sauc streaming (with hot-word bias: terminology you've corrected and project symbols auto-injected, +10 points of term recall in testing); TTS = Doubao seed-tts-2.0 v3 bidirectional streaming (first sentence plays early, interruptible anytime); current recognition form = hold to talk, whole-utterance recognition on release (live captions in progress)
Phone native voiceiOS / Android shellUses the system speech recognition and synthesis (on-device); no Python pipeline needed

12.2 Capture & turns

  • Three desktop triggers: click - click again (toggle) / keyboard long-press (hold space) / hands-free VAD (energy RMS + hangover state machine + a semantic "are they done?" vocabulary + an explicit "I'm done" button backstop; mixed Chinese-English speech defaults to a longer 900 ms silence threshold); noisy environments → hold to talk.
  • After recording, pick one: "send" (the voice bubble enters the conversation first, the transcript attaches asynchronously, the AI starts immediately) / "transcribe and edit" (the transcript goes only into the input box; edit, then send); "cancel" discards completely. An in-flight transcript always shows an explicit "transcribing" state; empty transcript / timeout gives "didn't catch that".
  • Interrupting: TTS stops immediately; unplayed text is marked heard=false and never enters conversation facts; an interrupted authorization-class announcement (S2 / pre-authorization list) immediately voids the current confirmation, and a subsequent bare "yes / okay" doesn't consume the old receipt.
  • Replay the last question: replays the original audio — never regenerates.
  • Correcting mishearing: say "not X, it's Y" ⇒ the correction enters the M0 hot words (hotword: X->Y) and biases the next recognition; repeated mishearing degrades to typing.
  • A session suspends after 45 idle seconds or when you say "that's it for now / leave it here / wrap up" (zero cost); the closing words always carry status: "the task is running — I'll call you at the review point".

12.3 Spoken discipline

Persona = chief of staff: brief, colloquial, in Chinese, never reads code aloud, conclusion before details; spoken turns ≤ 30 seconds; lists longer than 3 items go to the screen; at most 3 options read at a time. The TTS redaction red line: tokens / keys / customer data / full local paths never enter voice; PEM blocks, sha256:, Bearer, sk-/ghp_-style prefixes, cloud access keys, absolute paths, 11–19-digit number strings, and bare key strings of ≥20 characters are automatically replaced.

13. Phone & remote

13.1 What you can do today

  • Direct LAN connection (SAYDO_MOBILE_LAN=1, §4.7): a mobile browser or self-built app shell opens http://<LAN-IP>:47100/?token=… (the desktop console can show a QR code; codes are only offered for RFC1918 private addresses — never for public / CGNAT addresses). The phone surface is the mobile shell route (#/m): Today's four colors, Focus item details, recent conversation replay, recent memory; send text / native speech-to-text into the conversation; nod or retract ledger-type confirmation cards. The LAN surface's HTTP route whitelist is extremely narrow (read-only projections + the first-run greeting); write endpoints, settings write endpoints, S3, and full-screen text are all refused.
  • The tailnet (Tailscale) surface: config.toml [t2].tailnet_hosts = [...] (an explicit whitelist of bare hostnames / IPs — wildcards banned, any illegal entry keeps the whole surface closed) + [t2].listen; just t2-pair produces the pairing URL (one-time injection of the phone's local session; deep links carry no token afterwards). The tailnet surface can view tasks and approve S2-level approvals (review / decide / memory candidate approval); merge-chain actions and the /dev/* injection channel get 403 with guidance back to the desktop.
  • S3 is never on the phone: four assertions (socket must be the local loopback, Origin exactly http://localhost:<port>, source surface local, rpId fixed to localhost), with the refusal copy "S3 operations only happen at the local trusted terminal — go back to the desktop screen"; S3 tools aren't in the dialog model's tool surface either.
  • Honest boundary: the LAN surface today is a dogfood-grade temporary boundary of "cleartext LAN HTTP + a long-lived capability token" — no device pairing / per-device identity / end-to-end encryption; deleting the token and restarting drops everyone. Do not expose it to untrusted networks.

13.2 App status

PlatformStatusNotes
iOSNot submittedNative SwiftUI shell (QR pairing, native speech recognition / synthesis, web container) engineering exists; App Store record created (Chinese store name "说到"), no submittable build
AndroidNot submittedKotlin shell builds; Play app record created (com.octoooo.saydo), no submittable build
HarmonyOSNot submittedHarmonyOS NEXT ArkTS shell; AppGallery record and release certificate ready, Profile not yet created

The shared reason none can be submitted: no production pairing / trust layer, cleartext LAN, review fixtures not executed. Before store release: device pairing (one-time ticket + Noise XX mutual authentication + manual desktop confirmation + trusted-device confirmation-free reconnect), push privacy contract (payload carries only opaque ids), call-style briefings (PushKit wake + CallKit incoming-call UI). All planned.

13.3 Design principles

Communication is naturally mobile; execution is naturally stationary: the phone is only a communication surface (talk, watch progress, sign off, approvals capped at S2); heavy work stays on your Mac; a phone disconnect never affects backend execution. Remote channels are read-only by default; phone DTMF (planned) only does ack / snooze / refuse — never any approval.

14. Privacy & data

  • No servers, no accounts, no tracking: the code contains no telemetry / analytics / advertising SDKs; the developer cannot access — and does not collect — your data.
  • Where the data lives:
    • ~/.saydo/saydo.db (the main database: tasks / approvals / costs / memory events / audit…), ~/.saydo/sessions/*.jsonl (transcripts, directory permission 0700), ~/.saydo/.cap-token (console token, 0600), ~/.saydo/logs/, ~/.saydo/backups/;
    • <workspace>/.saydo/ (knowledge base / grounding / worktrees / project.toml); ~/.saydo/{artifacts,snapshots,assessments} (artifacts / source snapshots / readiness deep-assessment records);
    • Third-party AI is called directly by your computer (CLI subscriptions or API keys), under your own agreements with those providers.
  • Where voice data goes (the two ends differ — keep them straight): the phone end and browser system voice recognize on the device / browser side; on the desktop end with Volcano Doubao keys configured, audio travels over WebSocket to the Volcano Engine cloud for recognition and synthesis — a third-party service you configured yourself, connected directly from your computer. Defaults: store_audio = false (the current version has no code path that writes audio files at all), store_transcript = true (can be turned off; off means in-turn flow works as usual, just nothing lands on disk — marked honestly).
  • What traffic leaves the machine (only two classes, both upstreams you configured yourself): ① the AI upstreams you connect — CLI subscription tools go through their official channels, API keys connect directly to your configured endpoints; ② speech recognition and synthesis (Volcano Doubao, only when you enable desktop cloud voice). Optional ntfy push is a third class (your configured topic, titles redacted).
  • Immutable audit: database triggers ban UPDATE / DELETE on the audit table; sensitive payloads are recorded as digests only, never in the clear; every entry carries an actor. Logs may rotate; audit may not.
  • Caller identity: console capability token + Host / Origin whitelist + DNS rebinding protection; listens on loopback only by default; LAN / tailnet surfaces have their own source gates (§13).
  • Backup: automatic daily snapshots (online SQLite backup + sessions + each project's foundation / knowledge), default 30-day retention; just backup manually.
  • Deletion: "forget this" hard-delete propagates to indexes / projections / source snapshots / assessment records; backups expire whole per retention (stated honestly); full reset = delete ~/.saydo (token rotated, devices dropped).
  • Migration: after setting up a new machine, copy ~/.saydo/ (or one of the backups) back; the token and the executor's locked copies need re-registration; each project's <workspace>/.saydo/ (knowledge base / grounding / worktrees) travels with the project directory.
  • Full privacy policy at /privacy/.

15. Cost

  • Software: the desktop service is open source and free; the apps are free with no in-app purchases (planned).
  • Where AI costs go:
    • Subscription CLIs (BYOA): billed to your existing Codex / Claude / Cursor / Grok / Gemini / Qwen / Copilot subscription quota, zero extra cost; the ledger shows "within subscription quota"; hitting a rate limit errors honestly for that call — never silently converts to billing.
    • API keys: per each vendor's bill; the ledger estimates from the [pricing] price table (unfilled by default = shows "no exact figure yet", no invented numbers); every task has a cost cap (default 20 yuan) + the triple circuit breaker of 45 active minutes + 80 turns.
    • Voice (optional): Volcano Doubao ASR / TTS bills to your Volcano account; browser system voice is free.
    • Push (optional): the ntfy public server is free, or self-host.
  • SayDo adds no markup, never touches the money, doesn't top up for you. Before letting others use it, check your quota: development requests really dispatch execution tasks that consume subscription or API quota.

16. Roadmap & status

CapabilityStatusNotes
macOS desktop service (daemon + console)Available nowSource distribution; npm / Homebrew packages and a desktop app shell planned
Four reasoning slots on API supply (OpenAI-compatible / OpenRouter)Available now
Four reasoning slots on CLI subscription supply (Codex / Claude / Cursor / Grok / Gemini / Qwen / Copilot)Available nowDialog slot on CLI is slow text mode
First-run resource profile wizard (three plan cards + advanced per-slot)Available now
Focus / obligations / expectations / four-color ledger / confirmation card degradation / multi-step continuationAvailable now
Grounding (mechanical pipeline) + M1 knowledge base + AGENTS.md interop + post-session nomination & approval + forgettingAvailable nowLLM deep-research tier planned (design in progress)
Typed readiness checklists + rules layer + different-family deep assessment + playback-confirmation bindingAvailable now
Decision packs (preview / plan / cost / risk / acceptance criteria / lightweight demo)Available nowThe demo is mechanically rendered from the same source as the plan; "view demo" in the console + same-turn on-screen locally
Cursor CLI executor + worktree isolation + command effect gate + three circuit breakers + verify freezeAvailable now
Review evidence view + three-layer spoken summary + Touch ID merge card + manual merge verificationAvailable nowLive human pass on the Touch ID card pending the owner's touch
step-confirm modeAvailable now
direct-to-review mode (read-aloud-signed pre-authorization list)In progressContract and matching logic landed; voice sign-off loop not wired
Callback escalation chain: online voice callback → macOS desktop notification + ntfy phone push; do not disturb; ackAvailable nowVoice callback needs console online + voice pipeline healthy; escalates after 30 s unanswered; output arbitration landed in the same batch
Cloud cascade voice (Doubao ASR/TTS) + browser system voice + hot-word correctionAvailable nowLive captions, Silero VAD in progress
coding typeAvailable now
writing type (narrow)Available now (capability gate required)Citations / attribution contracts and full scope planned; full human pass pending
research / marketing / planning / general execution contractsPlannedUpstream interview / ledger / research usable
LAN phone surface (browser / shell)Available now (dogfood boundary)No pairing / no E2E
tailnet thin versionAvailable now (manual config)
Claude Code executorIn progressThree batches: batch one (real-machine capability testing against the official CLI + approval-gate pure functions) closed; wiring batch underway
Codex executor / Hopper batch routePlanned / dormant
provider-neutral native executorPlanned
Device pairing / E2E encryption / push privacy contractPlanned
iOS / Android / HarmonyOS store releasePlannedStore records created, shell engineering exists
Call-style voice briefings (PushKit + CallKit)Planned
Phone callbacks / S2S voice engine / local full-stack voice / wake wordPlanned
Conversation lanes Quick / Guided / ExplorePlannedOnly instrumented
Proactive check-ins / sitting in on multi-person meetingsPlanned

17. FAQ & troubleshooting

Is this a voice input method? No. Voice is only the entry point; behind it is the loop of understanding context, opening ledger entries, judging readiness, driving execution, and organizing review.

Does it upload my project to the cloud to run? No. There are no cloud servers; heavy work runs on your computer; the third-party AI you connect is called directly by your computer.

What are the prerequisites? macOS + Node 22 + one logged-in AI CLI (or one API key). Voice and execution are optional increments (§4).

Can I use it without any keys? Yes: any logged-in CLI on the machine means zero-key chatting (15–25 s per turn in slow text mode); voice via browser system voice or typing.

How does it know "it's done"? When execution and checks have run and artifacts are on disk, it says "ready for your review" — never "done"; your nod plus strong-authentication merge makes it "delivered".

Can it secretly spend money? Subscription calls cost nothing and rate limits only error honestly without converting to billing; API calls have a per-task cost cap + active duration + turn count triple circuit breaker; S3 (spending / deploying / deleting data / messaging outside) requires your on-screen strong authentication.

Will it push to GitHub or open PRs? No. It edits code in a worktree on the saydo/<taskId> branch; merging into your current branch takes your Touch ID or your own merge; pushing / PRs are yours afterwards.

Each turn takes fifteen to twenty seconds — is it stuck? No. A dialog slot on CLI subscription is slow text mode (15–25 s); the UI shows a slow badge; the desktop errors explicitly after a 120-second per-CLI-call cap (the phone page suggests resending at 90 s). For sub-second dialog, give the dialog slot an API key (§5.3 plan two).

The UI says "recovery-only mode". The active config is invalid (broken TOML, out-of-range parameters, project override conflicts…): the daemon doesn't exit — it keeps only the health probe, console static pages, and the settings self-rescue write endpoint; dialog, dispatch, merge, and task resume all stop. Go to the settings page, rebuild a safe config or clean up invalid project overrides as prompted, then restart.

The wizard says a CLI is "recognized but cannot be used as model supply". That CLI (e.g. kimi / opencode) cannot prove zero-tool before execution, so for safety it isn't wired; switch vendors or use an API.

The evaluator stays unarmed. Check the reason: same family unconfirmed (tick evaluator_same_family_ack or change family) / CLI evaluator missing the isolation confirmation (tick evaluator_isolation_ack) / self-check failed / actual model family reported unknown. unarmed doesn't affect dialog — readiness deep assessment just won't arm.

The console opens blank / unresponsive. First curl -s http://127.0.0.1:47100/health; if not ok:true, check ~/.saydo/daemon.log (tsx cold start takes 10–15 s); confirm the URL carries ?token= and uses localhost.

Windows / Linux support? Not for now — it targets macOS (residency, notifications, browser opening etc. are macOS implementations); no clear timeline, no promises.

Port conflict. saydo status exit code 2 = 47100 is held by another service or a daemon of another data directory; it won't auto-switch ports or kill processes — use --port or stop the other side.

Phone can't connect by QR / everything dropped. Confirm the daemon runs with SAYDO_MOBILE_LAN=1 and the phone and computer are on the same private network; a deleted .cap-token rotates the token — scan again.

No executor started after sign-off. [models.dev]'s agent must be cursor (or empty — cursor is the default) with a non-empty model; both [tier1] keys (locked binary absolute path, ~ not accepted + version) present; jq and curl on PATH (hard dependencies of the gate script — without jq even the refusal receipt can't be produced, so everything counts as refused); cursor-agent logged in. Otherwise queued tasks stay in queued with prescriptive logs; dialog is unaffected.

Task stuck in blocked. Check the reason: S2 confirmation timed out / no verify registered / project.toml broken / workspace or .git gone / agent asked a question / a circuit breaker tripped. Resolve it, then "retry / answer" on the task page.

Worktrees piling up. By design they're not auto-deleted (for recovery); clean up yourself with git worktree remove <repo>/.saydo/worktrees/<taskId>.

Residency (launchd) won't install. just daemon install needs uv to also install the voice pipeline residency; when it reports "uv not found", use just daemon install --without-pipeline as prompted to install only the daemon (cloud voice unavailable; system voice and text work as usual), or install uv first and rerun (§4.8).

18. Open source & contributing

  • Repository: https://github.com/Octo-o-o-o/SayDo (the desktop service — contracts / daemon / console / cli / pipeline — with all design documents, evidence, and process archives in the same repo); the executor-backend candidate Hopper is a separate repo by the same author. The public repo is a snapshot repo (since 2026-08-20): the commit-by-commit process history is kept in a private archive, and historical commit SHAs referenced in documents resolve there (see "process history & archive" in the repo README).
  • License: Apache License 2.0 (with patent grant); the "说到" / "SayDo" names, the seal logo, and the assets/ brand assets are not covered by the license (Apache 2.0 §6) — derivative works may not present themselves under the original project's name; see LICENSE / NOTICE in the repo.
  • Engineering conventions: zero emoji repo-wide; status-word discipline; no Gate 0 bypass; contracts never fork (types are only imported from @saydo/contracts); audit / log separation; the two-commit method (code commit + evidence commit).
  • Local development: pnpm install && pnpm -r build, just dev (daemon + pipeline + console with hot reload), just ci (node + python dual matrix + emoji / color / migration gates); distribution acceptance pnpm --filter @saydo/cli verify:distribution.
  • Design document map: docs/01–11 (vision / product definition / architecture / key mechanisms / roadmap / references / selection / modules / data contracts / voice scripts / UI) + docs/adr/.
  • Feedback & support: [email protected] (please state your OS version and whether QR pairing is done; do not paste API keys, pairing tokens, or full logs). Report security issues privately per the repo's SECURITY.md (mark the email subject SECURITY) — don't disclose details publicly.

19. Terminology (Chinese–English)

中文EnglishNotes
说到SayDoProduct name
沟通面 / 执行面communication surface / execution surface
一件事FocusThe ledger's spine
义务obligationWho owes what
期待expectationThe management layer of acceptance criteria
四色账本four-color ledger等你拍板 awaiting your call / 你欠的动作 owed by you / AI 正在办 AI working / 等外部回音 waiting externally
奠基grounding / foundationBuilding the project knowledge base
知识底座knowledge base (M1)
就绪readiness
决策包decision pack
拍板sign-off
逐步确认 / 直达验收step-confirm / direct-to-reviewExecution modes
等你验收ready for your reviewStatus word — never "done"
交付了deliveredAfter merge / archive
回叫callback
停靠parkedWhile waiting on a human
熔断circuit breaker
推理槽位 / 开发档reasoning slots / dev slot (executor)
订阅 CLI 供给BYOA (bring your own agent)
异族different model familyThe evaluator rule
能力令牌capability token

Appendix · Cheat sheet

ItemValue
daemon port47100 (daemon direct: SAYDO_DAEMON_PORT; saydo CLI: --port)
Console entryhttp://localhost:47100/?token=$(cat ~/.saydo/.cap-token) (saydo open)
Health / readiness probesGET /health, GET /readyz (voiceReady=false is not a startup failure)
Data directory~/.saydo (--home / SAYDO_HOME, absolute path)
Config / secrets~/.saydo/config.toml / ~/.saydo/.env (templates in the repo's templates/)
In-project directory<workspace>/.saydo/{knowledge,foundation,worktrees,project.toml}; artifacts / snapshots / deep-assessment records / transcripts in ~/.saydo/{artifacts,snapshots,assessments,sessions}
Environment variablesSAYDO_HOME, SAYDO_DAEMON_PORT, SAYDO_MOBILE_LAN=1 (LAN phone surface), SAYDO_DEV=1 (dev profile double switch)
Common .env keysModels: OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY; voice: VOLC_APP_ID / VOLC_ACCESS_TOKEN / DOUBAO_TTS_API_KEY; push: NTFY_TOPIC / NTFY_SERVER
Residencyjust daemon install / start / stop / restart / status / logs / deploy [sha] (install also generates the voice pipeline residency config; without uv use --without-pipeline, §4.8)
Backupjust backup~/.saydo/backups/<timestamp>/; automatic daily; 30-day retention
Shutdownlsof -t -iTCP:47100 -sTCP:LISTEN | xargs kill; pkill -f saydo_pipeline
Circuit breaker defaults45 active minutes / 80 turns / 20 yuan; park aging 72 hours; decision pack validity 24 hours; S2 confirmation window 45 seconds
Branch / directorysaydo/<taskId> @ <repo>/.saydo/worktrees/<taskId>
Back to top ↑