Your local-first AI agent, reachable from anywhere. A headless daemon plus a mobile-first dashboard, glued to Claude Code and exposed publicly via your own Cloudflare Tunnel.
Friday is a local-first AI orchestrator. A headless Node daemon runs the Claude Agent SDK on your machine. Postgres is the canonical store; Zero (by Rocicorp) is the sync engine that mirrors settled state to each of your devices' local caches so phone, laptop, and tablet all stay in lock-step. A SvelteKit dashboard sits in front of it all as the only public-facing process — auth-gated by BetterAuth, exposed through a Cloudflare Tunnel — and is fully usable from phone, tablet, or laptop.
No servers to deploy beyond your laptop. No third-party identity. Single user, multi-device. One persistent chat with Friday at /, and any sub-agents she spawns become first-class chats you can switch into.
Topology:
flowchart LR
cft["Cloudflare Tunnel<br/>public"]
subgraph local["Your Machine"]
direction LR
dash["SvelteKit Dashboard<br/>BetterAuth + reverse proxy"]
daemon["Friday Daemon<br/>127.0.0.1"]
zcache["zero-cache<br/>(sync sidecar)"]
sdk["Claude Agent SDK<br/>Pro / Max auth"]
pg[("Postgres<br/>canonical store")]
end
cft --> dash
dash -->|"/api/events SSE"| daemon
dash -->|"/api/sync WS"| zcache
dash -->|"/api/mutators"| pg
daemon --> sdk
daemon --> pg
zcache -->|"logical replication"| pg
The daemon binds to 127.0.0.1. zero-cache binds to 127.0.0.1. The dashboard is the only thing the public internet ever sees, and it gates every request through BetterAuth before forwarding — with one deliberate sessionless surface, POST /api/capture, gated by a write-scoped Capture key instead of a login cookie so the Apple Watch Shortcut / quick-add can fire a thought at Friday without a session (ADR-047). Daemon and dashboard are peer writers to Postgres — neither owns the DB, each survives the other's reboot.
- One persistent chat with Friday at
/. No conversation list, no "new chat" button — memory and compaction handle long-term context. Sub-agents are first-class chats you switch into via clickable references in the transcript. - Mobile-first dashboard. Priority+ navigation, virtualized lists, PWA install with offline shell, tap-to-insert autocomplete, image and PDF attachments via the native file picker. Fully usable on phone over cellular.
- Markdown-first rendering. Streamed responses render with Shiki syntax highlighting (Catppuccin Latte / Mocha) and DOMPurify-hardened output. KaTeX and Mermaid render inline.
- Local-first sync, live deltas. Each device runs Zero's reactive cache, so opening Friday on your phone is instant — your chat history, tickets, memory, and unread badges are already there. Settled state flows over a sync WebSocket; live token streaming rides a narrow per-agent SSE side-channel. The connectivity widget honestly distinguishes Internet / Sync / Daemon health.
- Compaction in place, never session rotation. As a chat grows, Friday compacts it instead of starting over — a nightly maintenance sweep (03:30 local, orchestrator/helper/bare over 100K context) keeps long-lived agents lean while persona-continuity instructions preserve open commitments, in-flight work, tone, and the reasoning behind recent decisions. Each compaction leaves a durable full-width "Context compacted" divider you can scroll past into pre-compaction history, and a pre-compaction memory flush saves any context that summarization would otherwise lose. While a compaction is running, a live "Compacting context…" indicator with an elapsed-time readout and a distinct cyan sidebar dot show it's in progress — backed by durable state, so the signal is there even if you reload or reconnect mid-compaction, or open the dashboard while a post-restart compaction is running (rather than the UI just looking frozen).
- Builders, Helpers, Planners, Bare, Scheduled. The orchestrator (Friday herself) spawns isolated Builder agents in their own git worktrees for project work, short-lived Helpers for delegated tasks, read-only Planners for deep research that ends in a handoff document, Bare agents for ad-hoc
/scratchsessions, and Scheduled agents for cron / one-shot autonomous work. Builders are confined to their worktree by a PreToolUse workspace guard. - Per-role and per-task model selection. Route each agent role — and each evolve internal pass — to its own Claude model from the settings page, so an Opus planner can design the work while Sonnet builders execute and Haiku handles the scans. Roles without an override fall through to the global default.
- Mail as the universal delivery primitive. Every user-visible reply, every cross-agent message, every scheduled-agent escalation flows through the same
mailtable. Priority field on each row;priority='critical'mid-turn-injects into a live worker so an interruption actually interrupts. - Scheduled agents with state continuity. Cron and one-shot runs persist
state.mdbetween fires (auto-injected on the next prompt) andlast-run.mdwritten by the daemon. Missed runs catch up on restart; cooperative abort on shutdown. - Stateless capture & intake routing. Fire a raw thought at Friday from anywhere — an Apple Watch Shortcut, the dashboard quick-add box, eventually voice — without opening the chat or burning an orchestrator turn. A key-gated
POST /api/capturehands the text to a cheap single-turn classifier (Haiku) that cleans it, picks a route target (reminder, habit check-in, memory, ticket, or an agent), and decides whether to act now or stage for approval. High-confidence actions run immediately and land as Done (with a one-tap Undo); ambiguous ones land as Proposed for you to approve; anything it can't confidently place lands as Unsorted — nothing is ever silently dropped. Everything surfaces in an Inbox bell (a two-tone dot: attention when something needs you, low-key when it's just FYIs that auto-resolve on view). Triage is yours: tell Friday "work through my inbox" and she reads and acts on it via the orchestrator-onlyfriday-inboxtools — there's no nightly auto-triage. Mint a Capture key withfriday capture-key create. - User-facing scheduled reminders. Any agent can set a cron, one-shot, or day-scoped reminder (e.g. "thaw the chicken at midday Thursday", or just "remind me Friday" — a bare day fires at a configurable default hour, 09:00 local) that lands straight into your chat as a notification — no worker spawns, no turn, no tokens. The unread badge lights up cross-device; the reminder lands as a
mail-like chat block without waking the orchestrator, and can be snoozed by name./schedulesshows an upcoming-reminders agenda alongside the full schedule list. - Native push notifications, presence-aware. Machine→human events — a Builder finished, mail arrived, a schedule surfaced a result, an
evolvecritical fired, a low-confidence Capture needs a look — reach you whether or not the app is open: a native PWA Web Push + app-icon badge when you're away, an in-app toast when you're present. The daemon owns the routing — it consults a per-event × per-channel policy, a Do-Not-Disturb window (with a master toggle that lets a critical class punch through), and server-side presence (any fresh, foregrounded device suppresses the push down to a toast) — and holds the only copy of the Web Push VAPID private key. Notifications are storageless: they're transient deliveries, never rows — the Inbox bell stays the one durable surface, and the badge count is derived from it. Pick presets (Auto / Always push / Toast only / Off) per event type in Settings → Notifications; "Forget this device" drops its push subscriptions. (ADR-048)
Orchestrator. Friday herself — the single long-lived agent behind the main chat, and the only role that can spawn anything without stating a reason. She is never archived; compaction and memory keep her one continuous conversation.
Builder. Spawned by the orchestrator (only) for project work, each Builder gets an isolated git worktree and a PreToolUse workspace guard that confines writes to it. A Builder lives for the duration of a ticket and is archived when the work ships or is abandoned.
Helper. A short-lived delegate for a bounded task — research a question, run a check, draft a document. Any role except a Planner can spawn one with a stated reason, and the spawner owns its archive.
Planner. A long-lived deep-research role that designs work but never executes it: read-only on the filesystem, it inherits its parent's working directory, converges on a plan, and mails the parent a handoff document. It is a leaf — it cannot spawn other agents — and its parent archives it once the plan is locked.
Scheduled. An autonomous agent fired by cron or a one-shot timer, persisting state.md between runs so each fire resumes where the last one left off. No other agent spawns one — the scheduler owns its lifecycle — and missed runs catch up on daemon restart.
Bare. A minimal ad-hoc session created by /scratch, with a stripped-down prompt stack and the daemon's working directory. It lives until you archive it.
Who can spawn whom:
| Spawner ↓ \ Spawned → | builder | helper | bare | planner | scheduled |
|---|---|---|---|---|---|
| orchestrator | ✅ | ✅ | ✅ | ✅ | n/a (cron) |
| builder | ❌ | ✅ (reason) | ❌ | ✅ (reason) | n/a |
| helper | ❌ | ✅ (reason) | ❌ | ✅ (reason) | n/a |
| bare | ❌ | ✅ (reason) | ❌ | ✅ (reason) | n/a |
| scheduled | 🔒 evolve carve-out | ✅ (reason) | ❌ | ✅ (reason) | n/a |
| planner | ❌ | ❌ | ❌ | ❌ | n/a |
- Habit tracking with streaks, as a first-class surface. Define recurring habits — daily, specific weekdays, or a quota per week/month/year ("20 workouts a month", "one blog post a month") — and check them off from a Today card on the dashboard or from any agent via the core
friday-habitMCP. Streaks are computed on read (consecutive satisfied periods, never a stored counter that goes stale at midnight) and shown as filled-square strips in a per-habit color drawn from a 7-stop palette ramp. A/habitsroute gives a Sun–Sat calendar history and manages ongoing habits alongside bounded ones ("run a 5K every day in June"), which auto-archive as completed or expired when their window closes. Separate from tasks and reminders by design — a reminder about a habit is just a reminder you set independently.
- Folder-as-app, one tool call to install. An app at
~/.friday/apps/<id>/is a manifest plus optional prompt overlays, optional stdio MCP servers, and an optional.envfor app-scoped secrets. Friday's installer registers the app's agents and schedules, scopes the MCP servers to those agents only, and runs each app's workers with the app folder ascwd. - Symmetric install / uninstall / reload.
friday app install <path>,friday app uninstall <id>,friday app reload <id>. Default uninstall renames the folder so reinstall un-archives everything; the--folder=deleteflag is irreversible and prompts unless--yes. The orchestrator can do the same through thefriday-appsMCP server.
- File-based memory + auto-recall. Markdown entries with Postgres full-text search (
tsvector+ GIN), recall-frequency boosting, and an audit log. The daemon prepends a<memory-context>block at the major dispatch sites (user prompts, mail-driven turns, scheduled fires, scratch, agent spawn) — nomemory_searchtool call required for those paths. Amemory_searchMCP tool is still available for cases that build their own prompts. - Slash commands and skills. Daemon-registered system commands —
/kill,/restart,/status,/inspect,/clear,/scratch— are TypeScript-deterministic and instant;/jumpand/archiverun client-side. Skills are markdown files in~/.friday/skills/(user-additive); the built-in slot atpackages/shared/src/prompts/skills/is empty in v1. - Evolve pipeline. Scans daemon logs, transcripts, usage, and feedback for friction signals; ranks proposals; surfaces them in the dashboard at
/evolvefor review and apply, with confident critical signals auto-spawning read-only triage helpers. - Nightly memory dreaming. As part of the daily maintenance pass, Friday re-reads the day's chat and consolidates it into memory — proposing the facts, preferences, and corrections worth keeping, auto-saving the confident ones (dedup-extending rather than duplicating on re-runs), and surfacing the rest — including anything about a person — for your review at
/evolveinstead of writing it blind. The same pass keeps the memory corpus from rotting (merging near-duplicates, flagging cold entries) without ever deleting anything, and journals each night's run to~/.friday/evolve/dream-diary.md.
- System-agnostic tickets (
FRI-1234). Comments, relations, and external links live as sibling tables — adding a ticket integration is a new package, not a schema change. - Linear integration (optional). Archiving an agent that owns a Friday ticket propagates the close state (
done/canceled/ failure) to the linked Linear issue. Direct ticket edits stay local; cross-system reconcile is read-side. GitHub Issues is on the roadmap.
- Three-layer prompt stack.
CONSTITUTION.md(inviolate, source-only) →SOUL.md(your editable identity, copied to~/.friday/SOUL.mdon first boot, never overwritten on upgrade) → role-specific agent base. Then per-turn: skill context, memory recall, user message.
- Postgres as canonical store. Host-installed via Homebrew (
brew install postgresql@18), Friday provisions a dedicatedfridaydatabase. Daemon and dashboard both write directly — daemon for runtime state (block closes, agent status, mail), dashboard for user-driven mutations via Zero mutators. Each survives the other's reboot. See ADR-023. - Row-as-intent dispatch. Every mutation writes the row it cares about with a status field encoding "side effect needed." The daemon LISTENs on Postgres NOTIFY, dispatches, and transitions the row. Boot recovery scans the same WHERE clauses, so live path and recovery path are the same code. Latency-critical ops (abort, mail-wakeup, cancel-queued) get an additional localhost fast-path; both paths converge on the same idempotent handler.
- Two-phase bootstrap, generous client cache. First-time device load fetches the orchestrator's recent chat and active-state metadata in ~2s; background Phase 2 fills the full history for active agents, tickets, memory, and recent mail. Blocks for agents archived >30 days expunge from the client cache; the server keeps everything.
- Boot recovery.
~/.claude/projects/.../sessionId.jsonlis walked once on startup to back-fill any blocks lost between workerblock-completeand the Postgres write. Idempotent on(session_id, message_id, kind)for text/thinking and(session_id, tool_use_id)for tool blocks. - Continuous invariant auditor. Every 60s the daemon checks builder-worktree presence and
status=working ⇒ live worker mapagainst the canonical source. Self-heals quietly; loud only when it has to be. - Age-encrypted secrets vault. Integration secrets in committed
secrets/vault.enc; machine-local autogen secrets in.env.local.friday secretsCLI +friday-secretsMCP for on-demand fetch (ADR-038). - Optional PostHog analytics. Set
POSTHOG_API_KEYviafriday secrets set … --daemonto light up instrumentation across the stack — daemon business + exception events (posthog-node) and dashboard product analytics, autocapture, session replay, and client/server error tracking (posthog-js). Off by default; silent no-op with no key set. Seedocs/setup.md§ Analytics.
Requires Homebrew. The curl install in step 2 doesn't clone the repo, so stream the Brewfile straight from GitHub:
curl -fsSL https://raw.githubusercontent.com/sethvoltz/friday/main/Brewfile | brew bundle --file=-(Contributors with a local checkout can just brew bundle --file=Brewfile.) Installs:
postgresql@18— Friday's canonical store. Managed bybrew services, lifecycle-independent offriday start/stop.pgvector— Postgres extension backing semantic memory recall (embedding vector(384)onmemory_entries). The Brewfile installs the extension binaries;friday provision(run by hand, or automatically byfriday updateafter it flips to the new version) creates thevectorextension in thefridaydatabase via an admin connection (it requires superuser, so it can't be created by daemon migrations). If the binary or extension is missing, the supervisor parks the whole stack with a clear remedy instead of crash-looping andfriday statustells you to runfriday provision(ADR-044). The embedding runtime is onnxruntime-web (WASM) — its.wasmfiles ride in the release tarball'snode_modules, so there's no per-platform native binary to fetch and it runs cross-platform (Intel x64 and Apple Silicon). Only the quantized ONNX model + tokenizer (~30MB, under~/.friday/models/) are fetched at install/update time, and recall degrades fail-open to full-text-only if they're absent.gh— GitHub CLI for Builders to clone and open PRsfnm— Fast Node Manager; resolves the pinned Node from.node-version(22.21.1) and is how the launchd-supervised stack launches Node, ABI-matched to the pre-baked native modules (ADR-034)pnpm— build/dev-time package manager (CI packs the release tarball, contributors build from source); not on Friday's runtime pathcloudflared— Cloudflare Tunnel client (optional, for public reachability)
After installing, wire your tools into ~/.zshenv — the one zsh file sourced by every shell agents spawn (interactive or not, login or not). ~/.zshrc/~/.zprofile are skipped by the non-interactive shells agents use, so PATH lines there won't reach them:
# ~/.zshenv, then open a NEW terminal
eval "$(fnm env)" # node (brew prints no caveat for this)
eval "$(/opt/homebrew/bin/brew shellenv)" # gh + brew tools (Apple Silicon)This is required, not cosmetic: the daemon captures your shell env ($SHELL -ilc) and hands it to each agent's Claude Code process, so without node/gh on that PATH agent turns silently complete with no reply or builders can't open PRs. The curl installer surfaces this and friday doctor flags it (node in shell / gh CLI rows, with a warning when a tool is reachable only via your interactive rc).
Install Claude Code separately (not in the Brewfile, since the cask shadows Anthropic's own installer):
# Anthropic's official installer
curl -fsSL https://claude.ai/install.sh | bash
# …or via brew
brew install --cask claude-codeSee docs.anthropic.com/en/docs/claude-code. friday doctor verifies which claude regardless of install method.
tmux is no longer required — Friday's prod supervision moved to launchd, with the plist written directly by the installer (see ADR-028 and ADR-034). Contributors who want it for the dev workflow can brew install tmux separately.
Built and tested against Node 22 and pnpm 10. Start Postgres if it isn't already running:
brew services start postgresql@18Sign in to Claude Code once before running Friday:
claude(the CLI walks you through the OAuth flow). Friday's workers spawn the SDK as a child process and inherit that login.
curl -fsSL https://raw.githubusercontent.com/sethvoltz/friday/main/install.sh | bashFriday runs on macOS — Apple Silicon (arm64) is the primary target, Intel (x64) is supported as legacy. The installer downloads the pre-baked release tarball for your Mac's architecture (no on-device build), verifies its shasum -a 256, ensures the pinned Node via fnm install (reading .node-version), extracts to ~/.local/share/friday/versions/<version>/, flips the ~/.local/share/friday/current symlink, drops a ~/.local/bin/friday shim on your PATH, and writes + bootstraps the launchd plist (com.sethvoltz.friday) directly. It finishes in seconds — there's no pnpm install/pnpm -r build step on your machine. Re-running it updates in place; friday update does the same from the CLI (see CLI).
The shim lands at
~/.local/bin/friday. If that's not already on yourPATH, the installer prints the line to add to your shell profile (export PATH="$HOME/.local/bin:$PATH") — add it and open a new shell (orsourceyour profile) before runningfridayin step 3.
Source-editing contributors who don't want the curl installer can clone + pnpm install + pnpm build from the repo — see Developing Friday below. The dev workflow doesn't need the release tarball.
friday setupProvisions the friday Postgres database and role, runs initial Drizzle migrations, creates ~/.friday/, copies the default SOUL.md, and creates your primary account (email + password). Idempotent — re-run anytime. Use friday setup --reset-password to change the password.
friday start # bootstrap/kickstart the launchd job (com.sethvoltz.friday)
friday status # supervisor + per-service status + probed ports
friday attach daemon # tail ~/.friday/logs/<service>.jsonl (Ctrl-C exits)
friday logs --follow # tail the daemon's structured logfriday start starts the launchd-supervised stack (daemon + dashboard + zero-cache, owned by one supervisor process — ADR-028) by writing + bootstrapping the com.sethvoltz.friday launchd job directly (launchctl bootstrap/kickstart, ADR-034) — no brew services. The supervisor's RunAtLoad: true means Friday comes back up automatically after Mac reboot/login; you don't have to friday start again.
friday start prints the local dashboard URL (http://localhost:7615) on launch — open it and sign in.
For dev hot-reload, use the pnpm dev:* wrappers — see Developing Friday.
friday setup --cloudflare # paste connector token + public URL (sets serve-intent on)
friday start # daemon + dashboard + tunnel
friday tunnel up | down | status # flip "serve here" intent without re-prompting for the tokenfriday setup --cloudflare saves the connector token to the secrets vault, sets the serve-intent (tunnel.serve) on, and installs cloudflared as its own user launch agent (com.cloudflare.cloudflared). From then on friday start reconciles that agent declaratively against tunnel.serve + token presence (FRI-166): serve-intent on with a token → the agent is installed and running; serve-intent off or no token → the agent is torn down. So a restored config brings the tunnel back on the same box automatically (DR works), and removing the token tears it down — no manual cloudflared surgery. If cloudflared is missing or no token is set, the tunnel is skipped — daemon and dashboard come up regardless.
friday tunnel up/down are the explicit serve-intent lever (no token re-prompt); friday tunnel status reports intent, token, agent state, and public URL. Migration note: friday restore deliberately leaves the tunnel dark (forces tunnel.serve off) even though the bundle carries the source machine's live token — this avoids two connectors serving one hostname (split-brain). Cut over by stopping the tunnel on the source box, then friday tunnel up on the new one. See docs/setup.md for the full Cloudflare walkthrough.
The friday CLI manages services and inspects state. Inspection commands work read-only against Postgres directly when the daemon is down.
# Lifecycle
friday setup [--cloudflare] [--reset-password]
friday doctor # data dir, db, account, external CLIs (incl. pgvector + the vector extension)
friday provision # install/repair runtime deps: pgvector + vector extension + embedding model (idempotent)
friday start # preflight deps, then bootstrap/kickstart the launchd job (whole stack atomically)
friday stop # bootout the launchd job (cascade-stops every child; auto-launches again on reboot)
friday restart # launchctl kickstart -k the launchd job
friday disable # stop + remove the plist: keeps the install, NO auto-launch on reboot
friday enable # re-arm auto-launch (writes the plist; does not start now)
friday status # supervisor + service state + probed ports
friday tunnel <up|down|status> # flip Cloudflare tunnel serve-intent (FRI-166)
friday attach <daemon|dashboard|zero-cache> # `tail -F ~/.friday/logs/<service>.jsonl`
friday logs [daemon|dashboard|zero-cache|tunnel] [--follow]
# Install lifecycle (ADR-034)
friday update [--check] [--rollback] # download + verify + extract latest; flip symlink; provision new deps; restart ONLY if it was running
friday uninstall [--data=keep|delete] [--yes] # remove the install tree + launchd job; ~/.friday preserved by default
# Inspection (read-only; daemon optional)
friday agents ls
friday sessions ls
friday memory ls | show <id>
friday tickets ls | show <id>
friday mail inbox <agent>
friday schedules ls
# Mutations (daemon required)
friday agents archive <name> # builders: also drops worktree + branch
friday tickets create --title ... --body ...
friday tickets update <id> --status ...
friday tickets comment <id> --author ... --body ...
friday mail send --from ... --to ... --type ... --body ...
friday schedules <create|pause|resume|trigger|delete> ...
# Capture keys (ADR-047) — write-scoped keys for the sessionless POST /api/capture
friday capture-key create [--label "X"] # mint a key; the plaintext is printed ONCE
friday capture-key list # list keys (metadata only, never the secret)
friday capture-key revoke <id|label> [--force] # disable a key immediately
# Memory / Evolve
friday memory <ls|show|add|edit|delete>
friday evolve <list|show|scan|enrich|cluster|apply|dismiss>
# Friday Apps (ADR-021)
friday app install <path> [--adopt]
friday app uninstall <id> [--folder=archive|keep|delete] [--yes]
friday app list | inspect <id> | reload <id>
# Secrets vault (ADR-038)
friday secrets init | unlock --check
friday secrets set <name> [--app <id>] [--mode env|on-demand] [--daemon] [--agents a,b]
friday secrets get|list|unset|edit|audit|migrate-from-env|public-key
# Backup & restore
friday backup [output-path] [--include-age-key] # pg_dump + curated filesystem → portable .tar.gz
friday backup --full [--include-age-key] # complete migration bundle: whole ~/.friday
# (incl .git) + Claude SDK sessions
friday restore <bundle> [--force] # auto-detects selective/full/legacy bundles; full
# restores the whole tree + Claude sessions
friday export-legacy-sqlite [output] [--source <path>]
# one-shot SQLite → Postgres cutover bundleSee docs/setup.md §8 for the routine backup/restore flow and the one-time SQLite → Postgres cutover procedure.
agent-friday/
├── packages/
│ ├── shared/ @friday/shared — types, config, logger, DB (Drizzle),
│ │ wire schema, prompts, services, markdown plugins
│ ├── cli/ @friday/cli — citty + clack + picocolors
│ ├── memory/ @friday/memory — file store + tsvector FTS + auto-recall
│ ├── evolve/ @friday/evolve — self-improvement pipeline
│ └── integrations/
│ └── linear/ @friday/integrations-linear (optional)
├── services/
│ ├── daemon/ @friday/daemon — owns Claude SDK, agent registry,
│ │ MCP servers, EventBus, SSE, scheduler, watchdog
│ └── dashboard/ @friday/dashboard — SvelteKit + Svelte 5 (runes),
│ BetterAuth, adapter-node, PWA
├── bin/ friday + friday-supervisor shims — exec through fnm (ADR-034)
├── packaging/ pack.mjs — builds the pre-baked release tarball (ADR-034)
├── install.sh Curl-installable installer (ADR-034)
├── .node-version Pinned Node (22.21.1) — single Node-pin source of truth
└── docs/ Architecture, setup, ADRs, sandbox, UX, roadmap
Operational files live at ~/.friday/. Canonical state (blocks, mail, tickets, agents, memory entries, schedules, apps, sessions/users, etc.) lives in the Postgres friday database, host-managed by brew services.
~/.friday/
├── config.json, .env, SOUL.md Settings + secrets + identity
├── agents/<name>/ Per-agent home (orchestrator/helper/scheduled cwd; ADR-029)
├── apps/<id>/ Installed Friday Apps (ADR-021)
├── workspaces/<name>/ Builder git worktrees
├── evolve/ Evolve proposals + dream-diary.md (nightly memory-dreaming journal; ADR-046)
├── models/ ONNX embedding-model cache (all-MiniLM-L6-v2; FRI-24)
└── logs/*.jsonl Structured logs, rotated at 1 MiB
Full layout reference: docs/running.md#data-location.
Override the location with FRIDAY_DATA_DIR=$HOME/.friday-v2. Backups: pg_dump friday > friday.dump.sql for canonical state; cp -r ~/.friday somewhere for operational files.
Stack: TypeScript (ESM), pnpm workspaces, Turborepo, Vitest, SvelteKit + Svelte 5 runes, BetterAuth, Drizzle ORM (Postgres), Zero (Rocicorp) for client sync, Claude Agent SDK.
pnpm install
pnpm build # Turborepo: shared first, then services in parallel@friday/shared is consumed via its built dist/. When you edit shared source, rebuild it before exercising the change downstream:
pnpm --filter @friday/shared buildDev mode runs directly from the repo with three pnpm scripts (each in its own terminal) — not the friday CLI:
pnpm dev:zero # zero-cache — binds 0.0.0.0:4848 (LAN/mobile reachable)
pnpm dev:daemon # tsx watch on the daemon — binds :7444
pnpm dev:dashboard # vite dev on the dashboard — binds :5173dev:daemon / dev:dashboard set FRIDAY_DAEMON_PORT=7444 so the dev dashboard's SvelteKit server-side fetches reach the dev daemon (:7444) rather than the prod daemon (:7610). dev:zero is required unless a prod zero-cache is already running on :4848 — dev used to borrow the prod one, so a box that no longer hosts prod had nothing there and the dashboard hung on "Syncing your data". Prod (friday start) and dev (pnpm dev:*) can run side-by-side without TCP collisions — prod uses :7610 / :7615, dev uses :7444 / :5173 (zero-cache is single-port on :4848, so run only one). They share ~/.friday/ (and the prod Postgres friday DB) by default; for full isolation, prefix with FRIDAY_DATA_DIR=$HOME/.friday-dev. See docs/running.md for the parallel-Postgres + parallel-zero-cache caveat.
The --dev CLI flag was retired (FRI-83) — friday start --dev now exits with citty's unknown-flag error.
pnpm test # unit suite (fast — no subprocesses)
pnpm test:e2e # multi-subprocess e2e (daemon + dashboard + zero-cache against a scratch PG)
pnpm test:playwright # browser-driven user-visible round-trip (slowest; chromium must be installed)
pnpm test:clean # reclaim leaked test artifacts (orphan tmp data dirs + idle scratch DBs)
pnpm --filter @friday/daemon run test # one package
pnpm --filter @friday/daemon exec vitest run src/foo.test.ts # one fileTests are co-located with source as *.test.ts, deterministic, no network. Files named *.e2e.test.ts are the heavy multi-subprocess suites — excluded from pnpm test, run via pnpm test:e2e. The Playwright browser suite lives in services/dashboard/e2e/. See docs/architecture.md for testing conventions.
pnpm drizzle:generate # after editing packages/shared/src/db/schema.tsThe daemon applies pending migrations on boot.
friday doctorVerifies the data dir, config, db migrations, account presence, external CLIs, and (when configured) tunnel state.
| Doc | What's in it |
|---|---|
| docs/architecture.md | System overview, topology, prompt stack, block model, wire protocol, agent lifecycle |
| docs/chat-ux.md | Single-chat UX, sidebar, focus model, slash commands, attachments, markdown |
| docs/mobile-ux.md | Priority+ nav, virtualization, PWA, mobile autocomplete |
| docs/mcp.md | MCP server surface (Friday-internal + user-configured) |
| docs/sandbox.md | Worker isolation: M1–M5 rollout (PreToolUse rules, sandbox-exec, pgrp containment, stall watchdog) + residual risk |
| docs/decisions.md | Architecture Decision Records (ADRs) + watch list |
| docs/roadmap.md | Open work, sequenced for execution |
| docs/setup.md | Full setup including Cloudflare Tunnel walkthrough |
| docs/running.md | Daily commands, modes, data layout, cutover from old Friday |
| docs/self-hosted-runner.md | Intel x64 self-hosted GitHub Actions runner that builds the darwin-x64 release leg |
| docs/ui-conventions.md | Cross-cutting UI patterns and icon map |