diff --git a/canvas/developers.mdx b/canvas/developers.mdx
index de108a4af..48c239621 100644
--- a/canvas/developers.mdx
+++ b/canvas/developers.mdx
@@ -19,7 +19,8 @@ Two packages own this territory:
## The big picture
-
+
+
Both packages follow the same layered pattern, shared with every state
manager in the repo:
@@ -129,7 +130,8 @@ Context paths resolve through three private helpers in `DataManager`:
`federated_ranked_search`, and `federated_reduce` from
[`unify/common/federated_search.py`](https://github.com/unifyai/unify/blob/main/unify/common/federated_search.py).
-
+
+
`ContextRegistry`
([`unify/common/context_registry.py`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py))
@@ -315,7 +317,8 @@ bindings; *the Actor never writes bridge API calls*.
## Life of a live tile
-
+
+
The `create_tile` pipeline in `DashboardManager`, step by step:
diff --git a/communication/behavior.mdx b/communication/behavior.mdx
index cb8c3aa96..5a44c0ddd 100644
--- a/communication/behavior.mdx
+++ b/communication/behavior.mdx
@@ -28,9 +28,10 @@ Your assistant isn't purely reactive:
tells you on an appropriate channel.
- **Scheduled work** — [tasks](/tasks/overview) you've scheduled can end
with a message, an email, or a call at the time you chose.
-- **Gentle follow-ups** — if a conversation goes quiet while something needs
- your input, it may send a friendly follow-up email. Ask it to stop and it
- stops.
+- **Gentle follow-ups** — if a conversation with your T-W1N goes quiet while
+ something needs your input, it may send a friendly follow-up email. This
+ re-engagement is specific to your T-W1N; hired teammates don't do it. Ask it
+ to stop and it stops.
- **Ringing you** — when a live conversation beats a wall of text, it can
ring you on [Unify Meet](/communication/unify-meet), and falls back to chat
if you don't pick up.
diff --git a/communication/developers/architecture.mdx b/communication/developers/architecture.mdx
index c58e752e2..86e935629 100644
--- a/communication/developers/architecture.mdx
+++ b/communication/developers/architecture.mdx
@@ -25,7 +25,8 @@ The communication stack is two cooperating pieces:
back through the gateway via
[`unify/comms/primitives.py`](https://github.com/unifyai/unify/blob/main/unify/comms/primitives.py).
-
+
+
## The wire contract: envelopes
diff --git a/communication/developers/conversation-manager.mdx b/communication/developers/conversation-manager.mdx
index ce4641612..60eb24ea9 100644
--- a/communication/developers/conversation-manager.mdx
+++ b/communication/developers/conversation-manager.mdx
@@ -10,7 +10,8 @@ state, consumes events, runs the reasoning loop, and delegates outbound
I/O. Its package README describes it as the layer that interfaces with
users while delegating complex reasoning to the `Actor`.
-
+
+
## The event model
@@ -23,7 +24,10 @@ Subclasses auto-register via `__init_subclass__`, serialize with
The catalogue is broad and symmetric: for most channels there's a
`*Received` and a `*Sent` class (`SMSReceived`/`SMSSent`,
-`EmailReceived`/`EmailSent`, `UnifyMessageReceived`/`UnifyMessageSent`…),
+`EmailReceived`/`EmailSent`, `UnifyMessageReceived`/`UnifyMessageSent`, and —
+for the org-installed Teams app — separate DM and channel pairs
+`MsTeamsBotMessageReceived`/`MsTeamsBotMessageSent` and
+`MsTeamsBotChannelMessageReceived`/`MsTeamsBotChannelMessageSent`…),
plus call-lifecycle events (`PhoneCallReceived`, `PhoneCallAnswered`,
`PhoneCallEnded`, `UnifyMeetStarted`…), per-utterance voice events
(`InboundPhoneUtterance`, `OutboundUnifyMeetUtterance`…), reactions,
@@ -52,7 +56,11 @@ Two mapping layers connect the world to handlers:
defines the `Medium` enum — the single source of truth for channel types
(`UNIFY_MESSAGE`, `EMAIL`, `SMS_MESSAGE`, `WHATSAPP_MESSAGE`,
`WHATSAPP_CALL`, `PHONE_CALL`, `UNIFY_MEET`, `GOOGLE_MEET`, `TEAMS_MEET`,
-Slack/Discord/Teams variants…). Each medium registers a `MediumInfo` with a
+Slack/Discord variants, and both the delegated-Graph
+(`TEAMS_MESSAGE`, `TEAMS_CHANNEL_MESSAGE`) and org-installed bot
+(`MS_TEAMS_BOT_MESSAGE`, `MS_TEAMS_BOT_CHANNEL_MESSAGE`) Teams mediums…).
+The two org-installed bot send tools are gated behind the assistant's
+`has_ms_teams_bot` capability. Each medium registers a `MediumInfo` with a
**mode**: `TEXT`, `CALL`, or `MEET`
([`cm_types/mode.py`](https://github.com/unifyai/unify/blob/main/unify/conversation_manager/cm_types/mode.py)).
Mode is what gates behavior — voice tools like `guide_voice_agent` only
diff --git a/communication/developers/gateway.mdx b/communication/developers/gateway.mdx
index 7180e4807..5d42a21d2 100644
--- a/communication/developers/gateway.mdx
+++ b/communication/developers/gateway.mdx
@@ -51,7 +51,8 @@ its router(s):
| [`gmail/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/gmail/views.py) | `send_email` (threading headers), `watch_email` (Pub/Sub push watch), attachment fetch | `googleapiclient`, `google.oauth2` |
| [`outlook/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/outlook/views.py) | Same shape over Microsoft Graph subscriptions | `msgraph` via [`common/graph.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/common/graph.py) |
| [`email/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/email/views.py) | Provider-agnostic dispatcher — routes `send_email` to Gmail or Outlook based on how the assistant's mailbox is connected | — |
-| [`teams/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/teams/views.py) | Chats, group chats, channels, watches, and meeting creation (`create_meeting.py`: instant links vs scheduled calendar events) | `msgraph` |
+| [`teams/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/teams/views.py) | Delegated Microsoft Graph Teams: chats, group chats, channels, watches, and meeting creation (`create_meeting.py`: instant links vs scheduled calendar events) | `msgraph` |
+| [`ms_teams_bot/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/ms_teams_bot/views.py) | Org-installed Bot Framework Teams app: pending-install / tenant-bind / install upsert, `send` (DM + channel/group), Bot Connector token minting from the bot's home tenant | Bot Framework Connector |
| [`slack/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/slack/views.py) | Workspace install upsert, `send_slack_message`, user lookup by email | Slack Web API |
| [`discord/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/discord/views.py) | `send_discord_message`, bot pool registration and status | Discord REST |
| [`drive/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/drive/views.py) / [`sharepoint/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/sharepoint/views.py) | File browsing for the workspace file picker (not messaging) | Google Drive API, `msgraph` |
@@ -63,7 +64,8 @@ its router(s):
|---|---|---|
| [`twilio.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/twilio.py) | SMS, voice calls, call status, WhatsApp messages/reactions/calls/call status | `msg`, `call`, `call_answered`, `call_not_answered`, `whatsapp`, `whatsapp_reaction`, `whatsapp_call`, … |
| [`google.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/google.py) | Gmail Pub/Sub push notifications, Google OAuth callback and revoke | `email` |
-| [`microsoft.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/microsoft.py) | Outlook and Teams Graph notifications, Microsoft OAuth callback, a shared `/microsoft/router` delegator | `email`, `teams_chat`, `teams_channel` |
+| [`microsoft.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/microsoft.py) | Outlook and delegated-Teams Graph notifications, Microsoft OAuth callback, a shared `/microsoft/router` delegator | `email`, `teams_chat`, `teams_channel` |
+| [`ms_teams_bot.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/ms_teams_bot.py) | Bot Framework activity webhook for the org-installed Teams app; validates the inbound Bot Framework JWT ([`ms_teams_bot_auth.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/ms_teams_bot_auth.py)), resolves the assistant via the dispatcher (token addressing + coordinator fallback) | `ms_teams_bot` |
| [`slack.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/slack.py) | Slack Events API, with a two-pass sender-identity resolution for coordinator routing and message dedup | `slack` |
| [`internal.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/internal.py) | Console/runtime routes: chat messages, reactions, attachments, Unify Meet rings, system events, pre-hire chat logging, wake-ups | `unify_message`, `unify_meet`, `api_message`, `unity_system_event`, … |
diff --git a/communication/developers/voice.mdx b/communication/developers/voice.mdx
index 95714bc65..9b5e718b4 100644
--- a/communication/developers/voice.mdx
+++ b/communication/developers/voice.mdx
@@ -8,7 +8,8 @@ sub-second conversational reflexes *and* deep reasoning, and no single
loop provides both. The runtime splits the job between two brains that
share one conversation.
-
+
+
## The two brains
diff --git a/communication/email.mdx b/communication/email.mdx
index 2ccf4241c..533f691da 100644
--- a/communication/email.mdx
+++ b/communication/email.mdx
@@ -7,6 +7,13 @@ Your assistant can send and receive email from a real mailbox — replying in
threads, handling attachments, and looping in cc'd colleagues, just like any
teammate.
+
+ **If this is your T-W1N** (your digital twin): its email is platform-managed
+ and already set up — there's nothing to create or configure. T-W1N acts on
+ *your* behalf, so it uses your own connected account rather than a mailbox of
+ its own. The setup below is for **hired teammates**.
+
+
## How your assistant gets an email address
Your assistant uses a **Google (Gmail) or Microsoft (Outlook / Microsoft 365)
@@ -32,12 +39,6 @@ mailbox, which means email from your assistant lands like email from a person.
-
- **T-W1N, your digital twin, is the exception**: it's meant to act on *your*
- behalf, so you connect your own account to it. Its contact details are
- platform-managed — there's nothing to create or configure.
-
-
Connecting email is free — there are no setup or monthly credit costs for
email contacts.
diff --git a/communication/microsoft-teams.mdx b/communication/microsoft-teams.mdx
index 19122912c..50e378f95 100644
--- a/communication/microsoft-teams.mdx
+++ b/communication/microsoft-teams.mdx
@@ -1,35 +1,61 @@
---
title: "Microsoft Teams"
-description: "Chats, group chats, channels, and meetings with your assistant"
+description: "Bring your assistants into Teams with the org-installed Unify app"
---
-If your organization runs on Microsoft Teams, your assistant fits right in —
-one-on-one chats, group chats, channel threads, and even creating and joining
-Teams meetings.
+If your organization runs on Microsoft Teams, the **Unify Teams app** brings
+your assistants right into it — one-on-one chats, group chats, and channel
+threads. It's an org-wide app your admin installs once, so everyone can reach
+your assistants without any per-assistant setup. This is the Teams equivalent
+of the [Slack app](/communication/slack): the assistant shows up as the shared
+Unify bot, not as a person.
+
+
+ There's a second, separate way to use Teams: an assistant can act through
+ its **own connected Microsoft 365 account** (the same account as its
+ [mailbox](/workspace/email)) — appearing as that user rather than the shared
+ app. That path is part of the workspace connection and is documented under
+ [Workspace → Teams](/workspace/teams).
+
## Setup
-Teams comes with the **Microsoft 365 connection** — the same one used for
-[Outlook email](/communication/email). When you connect a Microsoft account
-for your assistant, grant the **Teams** feature and Teams chat is enabled.
+A Microsoft admin installs the **Unify Teams app** into your organization's
+Teams. Once it's installed, every assistant in the org — and your coordinator
+— is reachable through it; there's nothing to configure per assistant.
## Messaging
-- **1:1 chat** — message your assistant in Teams like any colleague.
-- **Group chats** — your assistant can take part in group conversations with
- two or more people, and can start them too.
-- **Channels** — @mention your assistant in a channel and it replies in the
- thread; it can also post to channels and even create new ones.
-- **Attachments** — your assistant can send files in Teams messages.
-
-## Meetings
-
-Your assistant is unusually capable with Teams meetings:
-
-- **Create instant meetings** — ask for a meeting link and it generates one
- on the spot.
-- **Schedule meetings** — it can put a Teams meeting on the calendar with
- invites sent to attendees.
-- **Join meetings** — send your assistant a Teams meeting link and it joins
- the call and participates with its voice, like any remote attendee. See
- [Meetings](/communication/meetings).
+- **1:1 chat** — message the Unify app in Teams and your coordinator picks up
+ the conversation.
+- **Group chats** — add the app to a group chat and @mention it to bring it
+ in; it replies in the same conversation.
+- **Channels** — @mention the app in a channel and it replies in the thread,
+ keeping channels tidy.
+
+## Reaching a specific teammate
+
+Because the app is shared, addressing works like Slack:
+
+- In a **channel or group chat**, @mention the app and include the teammate's
+ name to reach a specific assistant (for example, `@Unify Alex, can you…`).
+ @mention it with no name and the message goes to your **coordinator**, who
+ can hand off from there.
+- In a **1:1 chat**, messages go to your coordinator — there's no name to add,
+ so naming a teammate isn't used here.
+
+
+ Teams only delivers channel and group-chat messages to the app when it's
+ **@mentioned**, so remember the mention when you want the assistant to see a
+ message in a shared space. In a 1:1 chat every message is delivered.
+
+
+## Good to know
+
+- Your assistant only speaks in a channel when it's mentioned or directly
+ relevant — it won't spam your team's conversations.
+- It's the same assistant across every surface: a task you discussed in Teams
+ can be delivered over email, and vice versa.
+- **Meetings** are separate from the app. Your assistant can join and host
+ Teams meetings — see [Meetings](/workspace/meetings) for joining, and
+ [Workspace → Teams](/workspace/teams) for creating and scheduling them.
diff --git a/communication/overview.mdx b/communication/overview.mdx
index e49078dfe..4ab369fd9 100644
--- a/communication/overview.mdx
+++ b/communication/overview.mdx
@@ -20,9 +20,9 @@ assistant with the same memory of everything you've discussed.
| [WhatsApp](/communication/whatsapp) | Yes | Yes | WhatsApp contact |
| [Email](/communication/email) | Yes | Yes | Connected Gmail or Outlook |
| [Slack](/communication/slack) | Yes | Yes | Slack app install |
-| [Microsoft Teams](/communication/microsoft-teams) | Yes | Yes | Microsoft 365 connection |
+| [Microsoft Teams](/communication/microsoft-teams) | Yes | Yes | Teams app install ([or Microsoft 365 connection](/workspace/teams)) |
| [Discord](/communication/discord) | Yes | Yes | Discord ID on your profile |
-| [Google Meet & Teams meetings](/communication/meetings) | Invite it to a meeting | It can create Teams meetings | Meeting link |
+| [Google Meet & Teams meetings](/workspace/meetings) | Invite it to a meeting | It can create Teams meetings | Meeting link |
## Two channels work out of the box
@@ -103,12 +103,12 @@ A few things that hold everywhere:
DMs and channel mentions in your workspace.
} href="/communication/microsoft-teams">
- Chats, group chats, channels, and meetings.
+ Chats, group chats, and channels via the org-installed Unify app.
DMs and server channels.
- } href="/communication/meetings">
+ } href="/workspace/meetings">
Your assistant joins Google Meet and Teams meetings.
diff --git a/communication/slack.mdx b/communication/slack.mdx
index dcce45356..4b794c4a0 100644
--- a/communication/slack.mdx
+++ b/communication/slack.mdx
@@ -9,7 +9,7 @@ DM, @mention, and collaborate with in channels.
## Setup
Connect Slack from the Console — your assistant can open the setup flow for
-you, or you'll find it in the integrations area of your assistant's profile.
+you, or you'll find it under your assistant's **Contact Details → Slack**.
A workspace admin approves the Unify Slack app, and your assistant appears in
your workspace.
diff --git a/communication/voice.mdx b/communication/voice.mdx
index e8147491e..6d40b4c70 100644
--- a/communication/voice.mdx
+++ b/communication/voice.mdx
@@ -6,7 +6,7 @@ description: "Choose how your assistant sounds on calls"
Whenever your assistant speaks — on [Unify Meet](/communication/unify-meet),
[phone calls](/communication/phone-calls),
[WhatsApp calls](/communication/whatsapp), or in
-[meetings](/communication/meetings) — it uses a voice you choose.
+[meetings](/workspace/meetings) — it uses a voice you choose.
## Choosing a voice
diff --git a/hiring/overview.mdx b/hiring/overview.mdx
index 285ba773f..76eb1e5bf 100644
--- a/hiring/overview.mdx
+++ b/hiring/overview.mdx
@@ -63,6 +63,12 @@ For most organizations the right sequence is:
+When you do hire, the dialog asks one ownership question up front: an
+**owning team** (the default — the teammate belongs to the team itself,
+everyone on it can work with it, and everything it learns is shared
+there), or **Personal** (it reports to you alone). The trade-offs are
+covered in [Team assistants](/teams/team-assistants).
+
Your T-W1N actively helps with this judgment — it's briefed to spot the
moment:
diff --git a/images/developers/architecture-flow-dark.svg b/images/developers/architecture-flow-dark.svg
new file mode 100644
index 000000000..6eceb4525
--- /dev/null
+++ b/images/developers/architecture-flow-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/architecture-flow-light.svg b/images/developers/architecture-flow-light.svg
new file mode 100644
index 000000000..e3f473ed4
--- /dev/null
+++ b/images/developers/architecture-flow-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/datamanager-architecture-dark.svg b/images/developers/datamanager-architecture-dark.svg
new file mode 100644
index 000000000..ce07eef43
--- /dev/null
+++ b/images/developers/datamanager-architecture-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/datamanager-architecture-light.svg b/images/developers/datamanager-architecture-light.svg
new file mode 100644
index 000000000..ec611248e
--- /dev/null
+++ b/images/developers/datamanager-architecture-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/datamanager-architecture.png b/images/developers/datamanager-architecture.png
deleted file mode 100644
index 4c921a9c3..000000000
Binary files a/images/developers/datamanager-architecture.png and /dev/null differ
diff --git a/images/developers/demo-clarification-bubbling-technical-dark.svg b/images/developers/demo-clarification-bubbling-technical-dark.svg
new file mode 100644
index 000000000..5b6055a4c
--- /dev/null
+++ b/images/developers/demo-clarification-bubbling-technical-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/demo-clarification-bubbling-technical-light.svg b/images/developers/demo-clarification-bubbling-technical-light.svg
new file mode 100644
index 000000000..4bfb480ec
--- /dev/null
+++ b/images/developers/demo-clarification-bubbling-technical-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/demo-course-correct-technical-dark.svg b/images/developers/demo-course-correct-technical-dark.svg
new file mode 100644
index 000000000..e3a3f45b2
--- /dev/null
+++ b/images/developers/demo-course-correct-technical-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/demo-course-correct-technical-light.svg b/images/developers/demo-course-correct-technical-light.svg
new file mode 100644
index 000000000..c399b8919
--- /dev/null
+++ b/images/developers/demo-course-correct-technical-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/destination-scopes-dark.svg b/images/developers/destination-scopes-dark.svg
new file mode 100644
index 000000000..2afa8d32e
--- /dev/null
+++ b/images/developers/destination-scopes-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/destination-scopes-light.svg b/images/developers/destination-scopes-light.svg
new file mode 100644
index 000000000..2a978b91f
--- /dev/null
+++ b/images/developers/destination-scopes-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/destination-scopes.png b/images/developers/destination-scopes.png
deleted file mode 100644
index c31211b74..000000000
Binary files a/images/developers/destination-scopes.png and /dev/null differ
diff --git a/images/developers/dev-computer-control-stack-dark.svg b/images/developers/dev-computer-control-stack-dark.svg
new file mode 100644
index 000000000..b4e8e9fa4
--- /dev/null
+++ b/images/developers/dev-computer-control-stack-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-computer-control-stack-light.svg b/images/developers/dev-computer-control-stack-light.svg
new file mode 100644
index 000000000..5f127ee31
--- /dev/null
+++ b/images/developers/dev-computer-control-stack-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-computer-control-stack.png b/images/developers/dev-computer-control-stack.png
deleted file mode 100644
index 1cafeaec1..000000000
Binary files a/images/developers/dev-computer-control-stack.png and /dev/null differ
diff --git a/images/developers/dev-desktop-lifecycle-dark.svg b/images/developers/dev-desktop-lifecycle-dark.svg
new file mode 100644
index 000000000..189860aa4
--- /dev/null
+++ b/images/developers/dev-desktop-lifecycle-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-desktop-lifecycle-light.svg b/images/developers/dev-desktop-lifecycle-light.svg
new file mode 100644
index 000000000..5fc955e25
--- /dev/null
+++ b/images/developers/dev-desktop-lifecycle-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-desktop-lifecycle.png b/images/developers/dev-desktop-lifecycle.png
deleted file mode 100644
index 4cb291f2a..000000000
Binary files a/images/developers/dev-desktop-lifecycle.png and /dev/null differ
diff --git a/images/developers/dev-screen-share-flow-dark.svg b/images/developers/dev-screen-share-flow-dark.svg
new file mode 100644
index 000000000..be38ef032
--- /dev/null
+++ b/images/developers/dev-screen-share-flow-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-screen-share-flow-light.svg b/images/developers/dev-screen-share-flow-light.svg
new file mode 100644
index 000000000..679603ec2
--- /dev/null
+++ b/images/developers/dev-screen-share-flow-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/dev-screen-share-flow.png b/images/developers/dev-screen-share-flow.png
deleted file mode 100644
index 4ca0a8e68..000000000
Binary files a/images/developers/dev-screen-share-flow.png and /dev/null differ
diff --git a/images/developers/event-lifecycle-dark.svg b/images/developers/event-lifecycle-dark.svg
new file mode 100644
index 000000000..742dc6756
--- /dev/null
+++ b/images/developers/event-lifecycle-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/event-lifecycle-light.svg b/images/developers/event-lifecycle-light.svg
new file mode 100644
index 000000000..f277bcfe8
--- /dev/null
+++ b/images/developers/event-lifecycle-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/event-lifecycle.png b/images/developers/event-lifecycle.png
deleted file mode 100644
index 42d06ff02..000000000
Binary files a/images/developers/event-lifecycle.png and /dev/null differ
diff --git a/images/developers/federated-scopes-dark.svg b/images/developers/federated-scopes-dark.svg
new file mode 100644
index 000000000..845fc442f
--- /dev/null
+++ b/images/developers/federated-scopes-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/federated-scopes-light.svg b/images/developers/federated-scopes-light.svg
new file mode 100644
index 000000000..84c893b34
--- /dev/null
+++ b/images/developers/federated-scopes-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/federated-scopes.png b/images/developers/federated-scopes.png
deleted file mode 100644
index c24ede8dd..000000000
Binary files a/images/developers/federated-scopes.png and /dev/null differ
diff --git a/images/developers/gateway-architecture-dark.svg b/images/developers/gateway-architecture-dark.svg
new file mode 100644
index 000000000..be7395d8c
--- /dev/null
+++ b/images/developers/gateway-architecture-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/gateway-architecture-light.svg b/images/developers/gateway-architecture-light.svg
new file mode 100644
index 000000000..e28c36fa8
--- /dev/null
+++ b/images/developers/gateway-architecture-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/gateway-architecture.png b/images/developers/gateway-architecture.png
deleted file mode 100644
index 1fec1a0d2..000000000
Binary files a/images/developers/gateway-architecture.png and /dev/null differ
diff --git a/images/developers/knowledge-memory-dark.svg b/images/developers/knowledge-memory-dark.svg
new file mode 100644
index 000000000..efcbe77c0
--- /dev/null
+++ b/images/developers/knowledge-memory-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/knowledge-memory-light.svg b/images/developers/knowledge-memory-light.svg
new file mode 100644
index 000000000..70ba4c3d2
--- /dev/null
+++ b/images/developers/knowledge-memory-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/knowledge-memory.png b/images/developers/knowledge-memory.png
deleted file mode 100644
index ef65da3a8..000000000
Binary files a/images/developers/knowledge-memory.png and /dev/null differ
diff --git a/images/developers/learning-loop-dark.svg b/images/developers/learning-loop-dark.svg
new file mode 100644
index 000000000..2807baa5a
--- /dev/null
+++ b/images/developers/learning-loop-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/learning-loop-light.svg b/images/developers/learning-loop-light.svg
new file mode 100644
index 000000000..27979e4a5
--- /dev/null
+++ b/images/developers/learning-loop-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/learning-loop.png b/images/developers/learning-loop.png
deleted file mode 100644
index baab4b838..000000000
Binary files a/images/developers/learning-loop.png and /dev/null differ
diff --git a/images/developers/live-tile-lifecycle-dark.svg b/images/developers/live-tile-lifecycle-dark.svg
new file mode 100644
index 000000000..bb6c88308
--- /dev/null
+++ b/images/developers/live-tile-lifecycle-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/live-tile-lifecycle-light.svg b/images/developers/live-tile-lifecycle-light.svg
new file mode 100644
index 000000000..88e3deff4
--- /dev/null
+++ b/images/developers/live-tile-lifecycle-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/live-tile-lifecycle.png b/images/developers/live-tile-lifecycle.png
deleted file mode 100644
index b76ca64ae..000000000
Binary files a/images/developers/live-tile-lifecycle.png and /dev/null differ
diff --git a/images/developers/nested-steering-sequence-dark.svg b/images/developers/nested-steering-sequence-dark.svg
new file mode 100644
index 000000000..0f4268f3d
--- /dev/null
+++ b/images/developers/nested-steering-sequence-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/nested-steering-sequence-light.svg b/images/developers/nested-steering-sequence-light.svg
new file mode 100644
index 000000000..b8664f6ee
--- /dev/null
+++ b/images/developers/nested-steering-sequence-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/storage-check-dark.svg b/images/developers/storage-check-dark.svg
new file mode 100644
index 000000000..e029cfcad
--- /dev/null
+++ b/images/developers/storage-check-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/storage-check-light.svg b/images/developers/storage-check-light.svg
new file mode 100644
index 000000000..0f297bd5a
--- /dev/null
+++ b/images/developers/storage-check-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/storage-check.png b/images/developers/storage-check.png
deleted file mode 100644
index 597818e53..000000000
Binary files a/images/developers/storage-check.png and /dev/null differ
diff --git a/images/developers/tasks-activation-lanes-dark.svg b/images/developers/tasks-activation-lanes-dark.svg
new file mode 100644
index 000000000..815490406
--- /dev/null
+++ b/images/developers/tasks-activation-lanes-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-activation-lanes-light.svg b/images/developers/tasks-activation-lanes-light.svg
new file mode 100644
index 000000000..78215774d
--- /dev/null
+++ b/images/developers/tasks-activation-lanes-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-activation-lanes.png b/images/developers/tasks-activation-lanes.png
deleted file mode 100644
index 84b274cce..000000000
Binary files a/images/developers/tasks-activation-lanes.png and /dev/null differ
diff --git a/images/developers/tasks-anatomy-dark.svg b/images/developers/tasks-anatomy-dark.svg
new file mode 100644
index 000000000..4a01360e0
--- /dev/null
+++ b/images/developers/tasks-anatomy-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-anatomy-light.svg b/images/developers/tasks-anatomy-light.svg
new file mode 100644
index 000000000..836888dfd
--- /dev/null
+++ b/images/developers/tasks-anatomy-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-anatomy.png b/images/developers/tasks-anatomy.png
deleted file mode 100644
index e8dc83660..000000000
Binary files a/images/developers/tasks-anatomy.png and /dev/null differ
diff --git a/images/developers/tasks-lifecycle-dark.svg b/images/developers/tasks-lifecycle-dark.svg
new file mode 100644
index 000000000..2c502a93d
--- /dev/null
+++ b/images/developers/tasks-lifecycle-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-lifecycle-light.svg b/images/developers/tasks-lifecycle-light.svg
new file mode 100644
index 000000000..0adc7242e
--- /dev/null
+++ b/images/developers/tasks-lifecycle-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/tasks-lifecycle.png b/images/developers/tasks-lifecycle.png
deleted file mode 100644
index 047849c86..000000000
Binary files a/images/developers/tasks-lifecycle.png and /dev/null differ
diff --git a/images/developers/teams-membership-runtime-dark.svg b/images/developers/teams-membership-runtime-dark.svg
new file mode 100644
index 000000000..dfafd8b44
--- /dev/null
+++ b/images/developers/teams-membership-runtime-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-membership-runtime-light.svg b/images/developers/teams-membership-runtime-light.svg
new file mode 100644
index 000000000..c7d1ee0b5
--- /dev/null
+++ b/images/developers/teams-membership-runtime-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-membership-runtime.png b/images/developers/teams-membership-runtime.png
deleted file mode 100644
index d1e61b556..000000000
Binary files a/images/developers/teams-membership-runtime.png and /dev/null differ
diff --git a/images/developers/teams-scope-model-dark.svg b/images/developers/teams-scope-model-dark.svg
new file mode 100644
index 000000000..6cd1714a6
--- /dev/null
+++ b/images/developers/teams-scope-model-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-scope-model-light.svg b/images/developers/teams-scope-model-light.svg
new file mode 100644
index 000000000..0b7c237ec
--- /dev/null
+++ b/images/developers/teams-scope-model-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-scope-model.png b/images/developers/teams-scope-model.png
deleted file mode 100644
index 2e877a148..000000000
Binary files a/images/developers/teams-scope-model.png and /dev/null differ
diff --git a/images/developers/teams-write-read-paths-dark.svg b/images/developers/teams-write-read-paths-dark.svg
new file mode 100644
index 000000000..8e3caa70c
--- /dev/null
+++ b/images/developers/teams-write-read-paths-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-write-read-paths-light.svg b/images/developers/teams-write-read-paths-light.svg
new file mode 100644
index 000000000..039c3b219
--- /dev/null
+++ b/images/developers/teams-write-read-paths-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/teams-write-read-paths.png b/images/developers/teams-write-read-paths.png
deleted file mode 100644
index 1a50cc028..000000000
Binary files a/images/developers/teams-write-read-paths.png and /dev/null differ
diff --git a/images/developers/user-desktop-architecture-dark.svg b/images/developers/user-desktop-architecture-dark.svg
new file mode 100644
index 000000000..ec058af07
--- /dev/null
+++ b/images/developers/user-desktop-architecture-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-architecture-light.svg b/images/developers/user-desktop-architecture-light.svg
new file mode 100644
index 000000000..c4dd4a19f
--- /dev/null
+++ b/images/developers/user-desktop-architecture-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-architecture.png b/images/developers/user-desktop-architecture.png
deleted file mode 100644
index de4d2d01a..000000000
Binary files a/images/developers/user-desktop-architecture.png and /dev/null differ
diff --git a/images/developers/user-desktop-consent-gates-dark.svg b/images/developers/user-desktop-consent-gates-dark.svg
new file mode 100644
index 000000000..8a30bdb62
--- /dev/null
+++ b/images/developers/user-desktop-consent-gates-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-consent-gates-light.svg b/images/developers/user-desktop-consent-gates-light.svg
new file mode 100644
index 000000000..11ffc9725
--- /dev/null
+++ b/images/developers/user-desktop-consent-gates-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-consent-gates.png b/images/developers/user-desktop-consent-gates.png
deleted file mode 100644
index dbd6ecd26..000000000
Binary files a/images/developers/user-desktop-consent-gates.png and /dev/null differ
diff --git a/images/developers/user-desktop-files-flow-dark.svg b/images/developers/user-desktop-files-flow-dark.svg
new file mode 100644
index 000000000..3b63299e0
--- /dev/null
+++ b/images/developers/user-desktop-files-flow-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-files-flow-light.svg b/images/developers/user-desktop-files-flow-light.svg
new file mode 100644
index 000000000..84db9d814
--- /dev/null
+++ b/images/developers/user-desktop-files-flow-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/user-desktop-files-flow.png b/images/developers/user-desktop-files-flow.png
deleted file mode 100644
index 0792a522f..000000000
Binary files a/images/developers/user-desktop-files-flow.png and /dev/null differ
diff --git a/images/developers/voice-dual-brain-dark.svg b/images/developers/voice-dual-brain-dark.svg
new file mode 100644
index 000000000..d30495026
--- /dev/null
+++ b/images/developers/voice-dual-brain-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/voice-dual-brain-light.svg b/images/developers/voice-dual-brain-light.svg
new file mode 100644
index 000000000..d3f5c69d8
--- /dev/null
+++ b/images/developers/voice-dual-brain-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/voice-dual-brain.png b/images/developers/voice-dual-brain.png
deleted file mode 100644
index 6ab24e0cc..000000000
Binary files a/images/developers/voice-dual-brain.png and /dev/null differ
diff --git a/images/developers/workspace-email-identities-dark.svg b/images/developers/workspace-email-identities-dark.svg
new file mode 100644
index 000000000..05e75c0f3
--- /dev/null
+++ b/images/developers/workspace-email-identities-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-email-identities-light.svg b/images/developers/workspace-email-identities-light.svg
new file mode 100644
index 000000000..5e0686b66
--- /dev/null
+++ b/images/developers/workspace-email-identities-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-email-identities.png b/images/developers/workspace-email-identities.png
deleted file mode 100644
index 8efcef979..000000000
Binary files a/images/developers/workspace-email-identities.png and /dev/null differ
diff --git a/images/developers/workspace-oauth-plumbing-dark.svg b/images/developers/workspace-oauth-plumbing-dark.svg
new file mode 100644
index 000000000..e60195908
--- /dev/null
+++ b/images/developers/workspace-oauth-plumbing-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-oauth-plumbing-light.svg b/images/developers/workspace-oauth-plumbing-light.svg
new file mode 100644
index 000000000..4d7aea7b8
--- /dev/null
+++ b/images/developers/workspace-oauth-plumbing-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-oauth-plumbing.png b/images/developers/workspace-oauth-plumbing.png
deleted file mode 100644
index fa9b30f44..000000000
Binary files a/images/developers/workspace-oauth-plumbing.png and /dev/null differ
diff --git a/images/developers/workspace-proxy-policy-dark.svg b/images/developers/workspace-proxy-policy-dark.svg
new file mode 100644
index 000000000..bc81cb759
--- /dev/null
+++ b/images/developers/workspace-proxy-policy-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-proxy-policy-light.svg b/images/developers/workspace-proxy-policy-light.svg
new file mode 100644
index 000000000..7848cf95f
--- /dev/null
+++ b/images/developers/workspace-proxy-policy-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/developers/workspace-proxy-policy.png b/images/developers/workspace-proxy-policy.png
deleted file mode 100644
index 86ea43a35..000000000
Binary files a/images/developers/workspace-proxy-policy.png and /dev/null differ
diff --git a/images/integrations-dev-execution-dark.svg b/images/integrations-dev-execution-dark.svg
new file mode 100644
index 000000000..e22c42e1b
--- /dev/null
+++ b/images/integrations-dev-execution-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-execution-light.svg b/images/integrations-dev-execution-light.svg
new file mode 100644
index 000000000..e35e5e22d
--- /dev/null
+++ b/images/integrations-dev-execution-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-execution.png b/images/integrations-dev-execution.png
deleted file mode 100644
index dd957da18..000000000
Binary files a/images/integrations-dev-execution.png and /dev/null differ
diff --git a/images/integrations-dev-lanes-dark.svg b/images/integrations-dev-lanes-dark.svg
new file mode 100644
index 000000000..7da890f30
--- /dev/null
+++ b/images/integrations-dev-lanes-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-lanes-light.svg b/images/integrations-dev-lanes-light.svg
new file mode 100644
index 000000000..7f0e709e8
--- /dev/null
+++ b/images/integrations-dev-lanes-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-lanes.png b/images/integrations-dev-lanes.png
deleted file mode 100644
index ba865230a..000000000
Binary files a/images/integrations-dev-lanes.png and /dev/null differ
diff --git a/images/integrations-dev-sync-dark.svg b/images/integrations-dev-sync-dark.svg
new file mode 100644
index 000000000..31d35f6b7
--- /dev/null
+++ b/images/integrations-dev-sync-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-sync-light.svg b/images/integrations-dev-sync-light.svg
new file mode 100644
index 000000000..02231bf19
--- /dev/null
+++ b/images/integrations-dev-sync-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/integrations-dev-sync.png b/images/integrations-dev-sync.png
deleted file mode 100644
index 14355b907..000000000
Binary files a/images/integrations-dev-sync.png and /dev/null differ
diff --git a/images/unify-docs-banner-dark.svg b/images/unify-docs-banner-dark.svg
new file mode 100644
index 000000000..bdafeeeeb
--- /dev/null
+++ b/images/unify-docs-banner-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/images/unify-docs-banner-light.svg b/images/unify-docs-banner-light.svg
new file mode 100644
index 000000000..a0292d3ab
--- /dev/null
+++ b/images/unify-docs-banner-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/integrations/developers.mdx b/integrations/developers.mdx
index 837e64447..e7f86583e 100644
--- a/integrations/developers.mdx
+++ b/integrations/developers.mdx
@@ -23,7 +23,8 @@ on one rule: **FunctionManager search is the single discovery surface for
executable tools**. Whatever lane a capability arrives through, the actor
finds it the same way it finds everything else.
-
+
+
| Lane | Discovery | Activation | Execution |
|---|---|---|---|
@@ -135,7 +136,8 @@ What it triggers is **materialization**: provider tools from the Builtins
catalog are written into the assistant's own `Functions/Primitives` context
as first-class FunctionManager rows, so ordinary FM search finds them.
-
+
+
### The materialized row
@@ -213,7 +215,8 @@ At call time, the runtime is a thin, honest client — every policy decision
happens server-side, and every non-`ok` outcome comes back as a typed status
the actor is prompted to handle rather than swallow.
-
+
+
The result envelope statuses form the contract:
diff --git a/introduction.mdx b/introduction.mdx
index 57cf01afb..1957b09f6 100644
--- a/introduction.mdx
+++ b/introduction.mdx
@@ -3,6 +3,9 @@ title: "Welcome"
description: "Meet your AI assistant"
---
+
+
+
Unify gives you a fully remote virtual employee — an AI assistant you can talk to
the way you'd talk to any other colleague. Message it, email it, call it, or hop
on a video call and share your screen. It remembers your conversations, works on
@@ -54,4 +57,8 @@ of working together.
Your private T-W1N versus dedicated teammates with their own accounts,
identities, and audiences.
+
+ Run the open-source runtime on your own machine — one teammate, fully
+ traceable, against the hosted backend.
+
diff --git a/learning/developers/architecture.mdx b/learning/developers/architecture.mdx
index b90b4396a..182a995f7 100644
--- a/learning/developers/architecture.mdx
+++ b/learning/developers/architecture.mdx
@@ -9,7 +9,8 @@ reading, extending, or embedding the code. It covers the two skill
libraries, the actor loop that reads and writes them, and the knowledge and
memory subsystems — with symbol names and links into the source throughout.
-
+
+
## The dual library
@@ -82,7 +83,8 @@ sandbox.
Both libraries (and Knowledge) read across every scope the assistant can
see and write to exactly one:
-
+
+
- [`ContextRegistry.read_roots`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py)
returns the personal root first, then `Teams/{team_id}/…` for each id in
diff --git a/learning/developers/knowledge-and-memory.mdx b/learning/developers/knowledge-and-memory.mdx
index cb267dc3f..a9f6b05fd 100644
--- a/learning/developers/knowledge-and-memory.mdx
+++ b/learning/developers/knowledge-and-memory.mdx
@@ -10,7 +10,8 @@ and a **background consolidation** pass in
[`MemoryManager`](https://github.com/unifyai/unify/blob/main/unify/memory_manager/memory_manager.py)
that distills recent traffic into durable state.
-
+
+
## KnowledgeManager
diff --git a/learning/developers/storage-check.mdx b/learning/developers/storage-check.mdx
index c666b9a49..fa015e3d7 100644
--- a/learning/developers/storage-check.mdx
+++ b/learning/developers/storage-check.mdx
@@ -11,7 +11,8 @@ persists. This page covers that pass, its mid-run sibling `store_skills`,
and the certification gate that turns stored functions into recurring-task
executors.
-
+
+
## `_StorageCheckHandle`: the two-phase wrapper
diff --git a/local-deployment/custom-overrides.mdx b/local-deployment/custom-overrides.mdx
new file mode 100644
index 000000000..4b7908a59
--- /dev/null
+++ b/local-deployment/custom-overrides.mdx
@@ -0,0 +1,243 @@
+---
+title: "Custom overrides"
+description: "Seed and manage state-manager content from version-controlled files"
+---
+
+Every state manager in the runtime can be seeded from **source files** —
+plain JSONL and JSON files (plus decorated Python for functions) that you
+keep in version control. At sync time the runtime reconciles each manager's
+backing store against these files: entries you declare are inserted,
+entries you edit are updated, and entries you delete are removed. Content
+the assistant created on its own is left alone.
+
+This turns a local deployment into something you can configure like code:
+check your assistant's starting contacts, guidance, secrets, tasks, and
+reference data into a repo, and every fresh deployment converges to the
+same state.
+
+## How syncing works
+
+Each manager exposes the same pair of building blocks (in
+`unify//custom_*.py` in the
+[`unifyai/unify`](https://github.com/unifyai/unify) repo):
+
+- **Collectors** read the source files from one or more directories and
+ return the declared entries.
+- **`sync_custom()`** on the manager reconciles the store against those
+ entries.
+
+Reconciliation is **hash-based and idempotent**:
+
+1. Every source entry carries a stable `key` and gets a content hash. An
+ aggregate hash over all entries is compared against the last synced
+ value — if nothing changed, sync is a no-op, so it's cheap to run on
+ every startup.
+2. On a mismatch, entries are reconciled by `key`: new keys are inserted,
+ changed hashes are updated in place, and keys that disappeared from the
+ source are deleted from the store.
+3. Rows the assistant or user created through normal conversation are
+ untouched — sync only manages rows it created. The one exception: if a
+ manually-created row collides with a declared `key`, the source
+ definition adopts it.
+4. Runtime-owned state survives updates. A recurring task keeps its
+ execution history and status when you edit its definition, and a task
+ that is actively running is skipped and picked up on the next sync.
+5. Invalid lines are skipped with a logged warning — one bad row never
+ blocks the rest of the file.
+
+Two fields are understood by every entry type:
+
+| Field | Default | Meaning |
+|---|---|---|
+| `destination` | `"personal"` | Where the entry lives — `"personal"` for the assistant's own context, `"team:"` for a shared team context |
+| `auto_sync` | `true` | Set `false` to stop managing this entry from source (it's skipped entirely) |
+
+Blank lines and `#`-prefixed comment lines are allowed in every `.jsonl`
+file.
+
+## The override cascade
+
+Collectors accept a **list of directories** and merge them in order — when
+the same `key` appears in more than one directory, **the later directory
+wins**. This gives you a layered override model:
+
+```text
+overrides/
+ org/ # baseline shared by everyone
+ guidance.jsonl
+ contacts.jsonl
+ me/ # your personal layer — overrides org on key collision
+ guidance.jsonl
+ tasks.jsonl
+```
+
+Pass `[org, me]` and an entry keyed `crm-runbook` in `me/guidance.jsonl`
+replaces the org-level definition of the same key. The hosted product uses
+this same mechanism to cascade org → user → assistant configuration; a
+local deployment can use as many or as few layers as it wants.
+
+## Source file formats
+
+### Guidance — `guidance.jsonl`
+
+One JSON object per line. `function_names` link the guidance to custom
+functions by name (resolved to ids at sync time):
+
+```json
+{"key": "crm-runbook", "title": "CRM escalation runbook", "content": "When a customer reports...", "function_names": ["create_crm_ticket"]}
+```
+
+### Contacts — `contacts.jsonl`
+
+```json
+{"key": "sarah-chen", "first_name": "Sarah", "surname": "Chen", "email_address": "sarah@example.com", "job_title": "CTO", "should_respond": true, "response_policy": "Always reply same-day."}
+```
+
+Other supported fields: `phone_number`, `whatsapp_number`, `discord_id`,
+`slack_user_id`, `bio`, `timezone`. `key` may be omitted — it defaults to
+the lowercased `first_name|surname` pair (secrets similarly default to
+their `name`).
+
+### Secrets — `secrets.jsonl`
+
+```json
+{"key": "crm-api", "name": "CRM_API_KEY", "value": "…", "description": "Read-write CRM access"}
+```
+
+
+ Secret values live in the file, so keep secrets source files out of
+ shared version control (or template them in from your secret store at
+ deploy time).
+
+
+### Tasks — `tasks.jsonl`
+
+Declare recurring or event-triggered work. `schedule` and `trigger` are
+mutually exclusive; runtime status and execution metadata stay owned by
+the scheduler:
+
+```json
+{"key": "monday-digest", "name": "Weekly GitHub digest", "description": "Digest this week's GitHub notifications.", "schedule": {"start_at": "2026-07-13T09:00:00Z"}, "repeat": [{"frequency": "weekly", "weekdays": ["MO"], "time_of_day": "09:00"}]}
+{"key": "vip-email-watch", "name": "VIP email watch", "description": "Summarize and flag anything urgent.", "trigger": {"medium": "email", "recurring": true}}
+```
+
+`schedule` holds an ISO-8601 `start_at`; `repeat` is a list of
+RFC-5545-style patterns (`frequency`, `interval`, `weekdays`, `count`,
+`until`, `time_of_day`); `trigger` names an inbound communication event
+(`medium` such as `email` or `sms_message`, optional
+`from_contact_ids`/`omit_contact_ids`, and `recurring` to re-arm after
+each run). Other supported fields: `deadline`, `priority`,
+`response_policy`, `entrypoint_function` (run a stored function instead of
+re-planning), `offline`.
+
+### Blacklist — `blacklist.jsonl`
+
+```json
+{"key": "spam-caller", "medium": "phone", "contact_detail": "+15551234567", "reason": "Repeated spam"}
+```
+
+### Knowledge tables — directory tree
+
+Each table is a subdirectory holding `meta.json` (description, column
+types, and the `seed_key` used as the per-row merge key) plus `rows.jsonl`.
+The relative path to `meta.json` becomes the table name, so nesting is
+allowed:
+
+```text
+knowledge/
+ Companies/
+ meta.json # {"description": "...", "columns": {...}, "seed_key": "name"}
+ rows.jsonl
+ CRM/OperatingRules/
+ meta.json
+ rows.jsonl
+```
+
+### Reference data tables — directory tree
+
+Same `meta.json` + `rows.jsonl` shape as knowledge, for DataManager-owned
+reference tables. `meta.json` additionally supports `context` (target
+context path), `unique_keys`, and `auto_counting`.
+
+### Dashboards — directory tree
+
+Tiles and layouts live under `tiles/` and `layouts/` namespaces, each with
+the `meta.json` + `rows.jsonl` shape.
+
+### Functions and venvs — Python and TOML
+
+Custom functions are ordinary Python decorated with `@custom_function`;
+venvs are `pyproject.toml`-style files whose filename becomes the venv
+name:
+
+```python
+# functions/acme_workflows.py
+from unify.function_manager.custom import custom_function
+
+@custom_function(venv_name="acme_ml")
+def score_lead(company: str) -> float:
+ """Score a sales lead using the ACME model."""
+ ...
+```
+
+```toml
+# venvs/acme_ml.toml
+[project]
+dependencies = ["scikit-learn>=1.4"]
+```
+
+Files starting with `_` are ignored. See the
+[FunctionManager README](https://github.com/unifyai/unify/blob/main/unify/function_manager/README.md)
+for the full decorator reference.
+
+## Running a sync
+
+Syncing is **explicit** — nothing watches the files. Collect from your
+directory layers and call `sync_custom()` on each manager. Sync functions
+first, since guidance and tasks resolve function names to ids:
+
+```python
+from pathlib import Path
+
+from unify.manager_registry import ManagerRegistry
+from unify.function_manager.custom_functions import (
+ collect_functions_from_directories,
+ collect_venvs_from_directories,
+)
+from unify.guidance_manager.custom_guidance import collect_guidance_from_directories
+from unify.task_scheduler.custom_tasks import collect_tasks_from_directories
+
+layers = [Path("overrides/org"), Path("overrides/me")]
+
+fm = ManagerRegistry.get_function_manager()
+fm.sync_custom(
+ source_functions=collect_functions_from_directories(layers),
+ source_venvs=collect_venvs_from_directories(layers),
+)
+
+name_to_id = {
+ name: data["function_id"]
+ for name, data in fm.list_functions().items()
+ if data.get("function_id") is not None
+}
+
+ManagerRegistry.get_guidance_manager().sync_custom(
+ source_guidance=collect_guidance_from_directories(layers),
+ function_name_to_id=name_to_id,
+)
+ManagerRegistry.get_task_scheduler().sync_custom(
+ source_tasks=collect_tasks_from_directories(layers),
+ function_name_to_id=name_to_id,
+)
+```
+
+Contacts, secrets, blacklist, knowledge, data, and dashboards follow the
+same collect-then-sync shape with their own collectors
+(`collect_contacts_from_directories`, `collect_secrets_from_directories`,
+and so on). Because every sync is hash-guarded, running the whole pass on
+every startup costs almost nothing when the files haven't changed.
+
+In the hosted product this reconcile runs automatically when an assistant
+starts, cascading org, user, and assistant configuration layers. In a
+local deployment you decide when it runs — a startup script that calls the
+snippet above is the usual shape.
diff --git a/local-deployment/day-to-day.mdx b/local-deployment/day-to-day.mdx
new file mode 100644
index 000000000..eb06e0b40
--- /dev/null
+++ b/local-deployment/day-to-day.mdx
@@ -0,0 +1,82 @@
+---
+title: "Day-to-day usage"
+description: "The CLI, chatting, and steering work while it's in flight"
+---
+
+## Commands
+
+```text
+unify Interactive local chat (alias: unify chat)
+unify serve Run headless: ConversationManager + gateway
+unify stop Stop the local runtime
+unify status Show runtime status
+unify logs Follow the runtime log
+unify doctor Gateway/config checks
+unify setup Re-run the key/credential wizard
+unify update Update the checkout and re-sync deps
+```
+
+## Steering while work is in-flight
+
+When your teammate is mid-task in the local chat REPL, steer it the same
+way you would in the hosted Console: **send another message**. There are no
+special slash commands — steering is a first-class part of the runtime, not
+a REPL feature.
+
+**Text:** use `msg` at the `>` prompt:
+
+```text
+> msg Actually, narrow it to ones with Rust bindings.
+> msg What step are you on?
+```
+
+Each `msg` publishes a normal inbound chat message. The ConversationManager
+wakes the slow brain, which can answer you or redirect in-flight Actor work
+through its usual action-steering tools.
+
+**Voice:** type `meet` to open a LiveKit browser session, then speak
+through your mic — see [Voice](/local-deployment/voice).
+
+**Debugging:** use `trace`, `tree`, or `show_logs` to inspect what's
+happening while work is in-flight.
+
+## What this feels like
+
+```text
+You ▸ "Find me high-throughput vector DBs under Apache 2."
+They ▸ (start searching)
+You ▸ "Actually, narrow it to ones with Rust bindings."
+They ▸ (adjust the in-flight search — don't restart)
+You ▸ "Pause that, something urgent."
+They ▸ (freeze exactly where they are)
+... five minutes later ...
+You ▸ "OK, resume. How's it going?"
+They ▸ (pick up where they left off, give you a status update)
+```
+
+Every operation — at every level of the call stack — returns a live
+steerable handle, so corrections propagate down into whatever the runtime
+is currently doing without restarting it. The mechanics are covered in
+[Developers](/local-deployment/developers).
+
+## Importing skills from other assistants
+
+OpenClaw and Hermes Agent both represent skills as `SKILL.md` files (the
+[agentskills.io](https://agentskills.io) standard). That maps almost
+one-to-one onto a GuidanceManager entry, so either skill library can be
+imported off-the-shelf as guidance:
+
+```bash
+# Dry run (the default): print what would be imported, write nothing
+.venv/bin/python -m scripts.skill_migration.openclaw_to_guidance
+.venv/bin/python -m scripts.skill_migration.hermes_to_guidance
+
+# Import for real (titles are namespaced "[openclaw] …" / "[hermes] …")
+.venv/bin/python -m scripts.skill_migration.openclaw_to_guidance --execute
+.venv/bin/python -m scripts.skill_migration.hermes_to_guidance --execute
+```
+
+Each script looks for a sibling checkout (`../openclaw`,
+`../hermes-agent`) by default; pass `--repo-root` to point elsewhere.
+Re-runs skip titles that already exist; pass `--conflict overwrite` to
+update them in place instead.
diff --git a/local-deployment/developers.mdx b/local-deployment/developers.mdx
new file mode 100644
index 000000000..14dab6098
--- /dev/null
+++ b/local-deployment/developers.mdx
@@ -0,0 +1,137 @@
+---
+title: "Developers"
+description: "How the local runtime is put together, and how to hack on it"
+---
+
+This page is for anyone reading, extending, or debugging the open-source
+[`unifyai/unify`](https://github.com/unifyai/unify) runtime they just
+deployed locally. The per-feature developer sub-sections elsewhere in these
+docs (communication, workspace, tasks, learning, …) go deep on individual
+subsystems; this is the top-level map.
+
+## How it works
+
+A persistent **interaction loop** (`ConversationManager`) stays present
+across every medium and keeps thinking while work is in flight. When
+something needs deeper reasoning, it dispatches a **background reasoner**
+(`Actor`) that writes Python plans over a back office of typed state
+managers. Every operation returns a live, steerable handle, and those
+handles nest — a correction you make in chat propagates *down* through the
+dispatched action into whatever manager call is currently running.
+
+```text
+ConversationManager (interaction loop, event-driven scheduling)
+ │
+ │ Slow Brain ◄── IPC ──► Fast Brain (real-time voice + video, LiveKit)
+ │
+ ▼
+CodeActActor (generates Python plans, calls primitives.* APIs)
+ │
+ ▼
+State Managers (each runs its own async LLM tool loop)
+ │
+ ├── ContactManager — people and relationships
+ ├── KnowledgeManager — domain facts, structured knowledge
+ ├── TaskScheduler — durable tasks, schedules, triggers
+ ├── TranscriptManager — conversation history and search
+ ├── GuidanceManager — procedures, SOPs, how-to knowledge
+ ├── FileManager — file parsing and registry
+ ├── ImageManager — image storage, vision queries
+ ├── FunctionManager — user-defined functions, primitives registry
+ ├── WebSearcher — web research orchestration
+ ├── SecretManager — encrypted secret storage
+ ├── BlacklistManager — blocked contact details
+ └── DataManager — low-level data operations
+ │
+ ├── EventBus — typed pub/sub backbone (Pydantic events)
+ └── MemoryManager — offline consolidation every 50 messages
+```
+
+### Steerable handles — the universal protocol
+
+Every public manager method returns one — the same `ask`, `interject`,
+`pause`, `resume`, `stop` surface at every level of the call stack:
+
+```python
+handle = await actor.act("Survey high-throughput vector DBs and draft a comparison")
+await handle.interject("Only ones with Rust bindings") # mid-flight redirect
+await handle.pause(); ...; await handle.resume() # freeze and resume
+```
+
+When the Actor calls `primitives.contacts.ask(...)`, the `ContactManager`
+returns its own handle — nested inside the Actor's, which is nested inside
+the `ConversationManager`'s. Steering at any level propagates down through
+the live call stack as a typed signal any inner loop can act on, not as an
+abort or a queued prompt.
+
+### CodeAct — the Actor writes Python programs
+
+Rather than emitting one JSON tool call at a time, the Actor writes a
+single sandboxed Python program per turn over typed `primitives.*`:
+
+```python
+deps = await primitives.knowledge.ask(
+ "Which Python deps am I tracking for security updates?"
+)
+for dep in deps:
+ latest = await primitives.web.ask(
+ f"What's the latest released version of {dep}?"
+ )
+ await primitives.knowledge.update(
+ f"Record that {dep}'s latest known release is {latest}."
+ )
+```
+
+A memory lookup → external check → memory write becomes one coherent plan
+with real variables, loops, and control flow.
+
+## Project structure
+
+```text
+unify/
+├── unify/ # Main package — actor, conversation_manager, common, and one folder per state manager
+├── sandboxes/ # Dev / eval playgrounds, one per manager; backs the `unify` CLI
+├── tests/ # Pytest suite (cached LLM responses)
+├── agent-service/ # Node.js desktop / browser automation
+└── deploy/ # Dockerfile, Cloud Build, virtual desktop
+```
+
+## Where to start reading
+
+| File | What's there |
+|---|---|
+| `unify/common/async_tool_loop.py` | `SteerableToolHandle` — the protocol everything returns |
+| `unify/common/_async_tool/loop.py` | The async tool loop engine — nesting, steering, context propagation |
+| `unify/actor/code_act_actor.py` | CodeAct — plan generation, sandbox, primitives |
+| `unify/conversation_manager/conversation_manager.py` | Dual-brain orchestration, debouncing, in-flight actions |
+| `unify/conversation_manager/domains/brain_action_tools.py` | How the brain starts, steers, and tracks concurrent work |
+| `unify/conversation_manager/domains/call_manager.py` | LiveKit subprocess + voice/video event wiring |
+| `unify/function_manager/primitives/registry.py` | How primitives are assembled into the typed API surface |
+| `unify/events/event_bus.py` | Typed event backbone |
+| `unify/memory_manager/memory_manager.py` | Offline consolidation pipeline |
+| `unify//custom_*.py` | File-source collectors behind [custom overrides](/local-deployment/custom-overrides) |
+
+The full breakdown — async tool loop internals, event bus, primitive
+registry, hosted deployment SPI — lives in the repo's
+[`ARCHITECTURE.md`](https://github.com/unifyai/unify/blob/main/ARCHITECTURE.md).
+
+## Running the tests
+
+Tests exercise the real system — steerable handles, CodeAct, manager
+composition, nested tool loops — against real LLMs whose responses are
+cached per unique input, not mocked:
+
+```bash
+uv sync --all-groups
+source .venv/bin/activate
+
+tests/parallel_run.sh tests/ # everything
+tests/parallel_run.sh tests/actor/ # one module
+tests/parallel_run.sh tests/contact_manager/ # another
+```
+
+First run makes live LLM calls; subsequent runs replay from cache in
+milliseconds. See the repo's
+[`tests/README.md`](https://github.com/unifyai/unify/blob/main/tests/README.md)
+for the full philosophy — delete the cache and you're re-evaluating
+against live models.
diff --git a/local-deployment/installation.mdx b/local-deployment/installation.mdx
new file mode 100644
index 000000000..8b0cc097d
--- /dev/null
+++ b/local-deployment/installation.mdx
@@ -0,0 +1,89 @@
+---
+title: "Installation"
+description: "One command to a locally-running teammate"
+---
+
+## Prerequisites
+
+- **Python 3.12+**
+- **macOS, Linux, or WSL2**
+- An **LLM provider key** — OpenAI, Anthropic, or DeepSeek
+- A **Unify API key** and an assistant — create both at
+ [console.unify.ai](https://console.unify.ai)
+
+## Install
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/unifyai/unify/staging/scripts/install.sh | bash
+```
+
+The installer clones `unify`, syncs Python dependencies with `uv` (which
+resolves the sibling `unisdk` and `unillm` packages), runs a key wizard,
+and installs a `unify` CLI shim in `~/.local/bin/`. **Open a new
+terminal**, then start chatting:
+
+```bash
+unify
+```
+
+`unify` opens an interactive local chat with one teammate, powered by the
+full local runtime and the hosted Orchestra backend. Run headless
+(ConversationManager + gateway) with `unify serve`.
+
+```text
+> What did I leave half-finished on the indexer rewrite last week?
+> Watch my open PRs and ping me when one gets reviewed.
+> Remind me to send Sarah the benchmark numbers on Thursday.
+```
+
+
+ Clones `unify` under `~/.unity/`, runs `uv sync --all-groups`, and writes
+ `~/.unity/unity/.env` with your `UNIFY_KEY`, `ASSISTANT_ID`,
+ `ORCHESTRA_URL` (the hosted backend), and an LLM provider key — plus
+ optional voice and research keys via the bring-your-own-keys wizard. It
+ installs a `unify` CLI shim in `~/.local/bin/` with a clearly-marked PATH
+ block appended to your shell rc.
+
+ Get your `UNIFY_KEY` and `ASSISTANT_ID` from
+ [console.unify.ai](https://console.unify.ai). If you skip a key at
+ install time (or pipe through a non-interactive shell), add it to
+ `~/.unity/unity/.env` and run `unify setup`.
+
+
+## Configuration
+
+All bring-your-own-keys (LLM, voice, research) live in
+`~/.unity/unity/.env`. Edit the file and run `unify setup` to apply
+changes. The keys the runtime understands:
+
+| Variable | Purpose | Required? |
+|---|---|---|
+| `UNIFY_KEY` | Authenticates against the hosted Orchestra backend | Yes |
+| `ASSISTANT_ID` | Which assistant record the runtime drives | Yes |
+| `ORCHESTRA_URL` | Backend URL (defaults to the hosted backend) | Set by installer |
+| `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` | Lets the assistant think and reply | One of these |
+| `TAVILY_API_KEY` | Web search while researching (free tier) | Optional |
+| `ANTI_CAPTCHA_KEY` | CAPTCHA solving during computer use | Optional |
+| LiveKit + speech keys | Browser voice calls | See [Voice](/local-deployment/voice) |
+
+## Running from a checkout
+
+If you cloned the repo yourself rather than using the installer:
+
+```bash
+pip install uv && uv sync --all-groups
+```
+
+The interactive chat is `.venv/bin/python -m
+sandboxes.conversation_manager.sandbox`, and the headless runtime is `bash
+scripts/local.sh start --full`. Always use the project virtualenv at
+`.venv/` — the key wizard is available as `scripts/prompt_byok_keys.sh`.
+
+## Updating
+
+```bash
+unify update
+```
+
+This updates the checkout and re-syncs dependencies. Your `.env` is
+preserved.
diff --git a/local-deployment/overview.mdx b/local-deployment/overview.mdx
new file mode 100644
index 000000000..a6c9523e3
--- /dev/null
+++ b/local-deployment/overview.mdx
@@ -0,0 +1,76 @@
+---
+title: "Overview"
+description: "Run the open-source unify runtime on your own machine"
+---
+
+Everything your assistant's brain does — the reasoning loop, memory,
+steerable tasks, voice — lives in the MIT-licensed
+[`unifyai/unify`](https://github.com/unifyai/unify) repository, and you can
+run it locally. A local deployment gives you **one teammate** running
+entirely on your laptop, talking to the hosted Orchestra backend for
+persistence, so you can trace exactly how it thinks while keeping the
+zero-maintenance storage layer.
+
+The shape is deliberately simple: install once, and the runtime lives on
+your machine, accumulating state across every session. No Docker, no
+Kubernetes, no local database.
+
+## What runs where
+
+The local runtime drives the assistant record whose `ASSISTANT_ID` you set,
+provisioned at [console.unify.ai](https://console.unify.ai). Locally you
+get:
+
+- **Chat** — an interactive REPL with the full ConversationManager + Actor
+ runtime behind it. An LLM key (OpenAI, Anthropic, or DeepSeek) is what
+ lets it think and reply.
+- **Browser voice calls** — the production fast-brain (interruption-handling,
+ telephony-aware) running locally with sub-second latency. See
+ [Voice](/local-deployment/voice).
+- **Web search** — a free [Tavily](https://tavily.com) key lets it look
+ things up while researching.
+- **Computer use** — it drives a real browser and desktop; an optional
+ [AntiCaptcha](https://anti-captcha.com) key lets it get past CAPTCHAs
+ instead of stalling.
+
+The onboarding flow, inbound messaging channels (SMS / WhatsApp / phone,
+Slack, Gmail, Outlook, Teams, Discord), Google / Microsoft workspace
+connect, third-party app integrations, screen-share, and the
+multi-teammate experience are part of the hosted product at
+[console.unify.ai](https://console.unify.ai).
+
+## The runtime stack
+
+The open one-teammate runtime is three MIT-licensed repos. They talk to the
+hosted Orchestra backend; you can also use any of them independently.
+
+| Repo | Role |
+|---|---|
+| [**unify**](https://github.com/unifyai/unify) | Agent runtime — managers, tool loops, CodeAct, voice, orchestration |
+| [**unisdk**](https://github.com/unifyai/unisdk) | Python SDK — how the runtime talks to Orchestra |
+| [**unillm**](https://github.com/unifyai/unillm) | LLM access layer — OpenAI, Anthropic, or any compatible endpoint |
+| **orchestra** | Persistence backend — hosted at [console.unify.ai](https://console.unify.ai) |
+
+## Start here
+
+
+
+ One command, a key wizard, and a `unify` CLI in your PATH — a few
+ minutes on macOS, Linux, or WSL2.
+
+
+ Real browser voice calls with sub-second latency — LiveKit Cloud plus
+ free-tier speech keys.
+
+
+ The CLI commands, chatting, and steering work while it's mid-task.
+
+
+ Seed contacts, guidance, tasks, secrets, and reference data from
+ version-controlled files.
+
+
+ How the runtime is put together, running the tests, and where to start
+ reading the code.
+
+
diff --git a/local-deployment/voice.mdx b/local-deployment/voice.mdx
new file mode 100644
index 000000000..fd12851c1
--- /dev/null
+++ b/local-deployment/voice.mdx
@@ -0,0 +1,40 @@
+---
+title: "Voice"
+description: "Talk to your locally-running teammate in the browser"
+---
+
+Real voice calls run the production fast-brain — interruption-handling,
+telephony-aware — locally with sub-second latency. From the local chat
+REPL, type `meet` to open a LiveKit browser session and speak through your
+mic. Utterances follow the same Unify Meet path as production voice.
+
+## What you need
+
+Voice uses **LiveKit Cloud** for browser Meet sessions, plus one
+speech-to-text and one text-to-speech provider (both have free tiers; pick
+**one** TTS provider). The install wizard prompts for these; to add them
+later, edit `~/.unity/unity/.env` and run `unify setup`.
+
+| Variable | Purpose | Where to get it |
+|---|---|---|
+| `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` | Browser voice media | [cloud.livekit.io](https://cloud.livekit.io) |
+| `DEEPGRAM_API_KEY` | Speech-to-text | [console.deepgram.com](https://console.deepgram.com) — free tier |
+| `CARTESIA_API_KEY` *or* `ELEVEN_API_KEY` | Text-to-speech (pick one) | [play.cartesia.ai](https://play.cartesia.ai) or [elevenlabs.io](https://elevenlabs.io) — free credits |
+
+## How local voice works
+
+Live calls run two coordinated brains:
+
+- **Slow brain** (`ConversationManager`) — sees everything, decides
+ deliberately, runs in the main process.
+- **Fast brain** — a real-time LiveKit voice agent in a subprocess,
+ sub-second latency, handles turn-taking autonomously.
+
+They communicate over IPC. The slow brain steers the fast brain with
+**SPEAK** (say exactly this), **NOTIFY** (here's context, decide what to
+do), or **BLOCK** (do nothing; carry on). This is the same dual-brain
+architecture the hosted product runs — locally you're just running both
+halves on your own machine.
+
+Phone calls over real telephony, video calls, and screen-share are part of
+the hosted product at [console.unify.ai](https://console.unify.ai).
diff --git a/mint.json b/mint.json
index bd1150a9e..1668e868e 100644
--- a/mint.json
+++ b/mint.json
@@ -32,6 +32,17 @@
"introduction"
]
},
+ {
+ "group": "Local Deployment",
+ "pages": [
+ "local-deployment/overview",
+ "local-deployment/installation",
+ "local-deployment/voice",
+ "local-deployment/day-to-day",
+ "local-deployment/custom-overrides",
+ "local-deployment/developers"
+ ]
+ },
{
"group": "Communication",
"pages": [
@@ -45,7 +56,6 @@
"communication/slack",
"communication/microsoft-teams",
"communication/discord",
- "communication/meetings",
"communication/setup",
"communication/voice",
"communication/behavior",
@@ -68,6 +78,8 @@
"workspace/overview",
"workspace/connecting",
"workspace/email",
+ "workspace/teams",
+ "workspace/meetings",
"workspace/calendar",
"workspace/files",
"workspace/contacts-and-tasks",
@@ -164,6 +176,7 @@
"pages": [
"teams/overview",
"teams/membership",
+ "teams/team-assistants",
"teams/shared-context",
"teams/managing",
"teams/developers"
diff --git a/tasks/developers.mdx b/tasks/developers.mdx
index c1590e04b..d0c914bd3 100644
--- a/tasks/developers.mdx
+++ b/tasks/developers.mdx
@@ -27,7 +27,8 @@ Three design commitments shape everything below:
## The task model
-
+
+
The model lives in
[`unify/task_scheduler/types/task.py`](https://github.com/unifyai/unify/blob/main/unify/task_scheduler/types/task.py).
@@ -75,7 +76,8 @@ the real work:
## The lifecycle
-
+
+
[`Status`](https://github.com/unifyai/unify/blob/main/unify/task_scheduler/types/status.py)
is a six-value enum: `scheduled`, `triggerable`, `active`, `completed`,
@@ -158,7 +160,8 @@ rows) inherit the task's scope.
## Activation: three lanes
-
+
+
A task row does nothing by itself. Activation — deciding *now is the
moment* — arrives through one of three lanes, all converging on
diff --git a/teams/developers.mdx b/teams/developers.mdx
index 0dc8cf6f5..d43c52971 100644
--- a/teams/developers.mdx
+++ b/teams/developers.mdx
@@ -24,9 +24,11 @@ namespaces addressed by path. Team sharing is implemented as a pure
*namespace* concern: the same tables exist under multiple **roots**, and
one class decides which root any given operation touches.
-
+
+
-There are exactly two kinds of root:
+There are two kinds of root for a personally-supervised assistant, and a
+third shape for [team assistants](/teams/team-assistants):
- **Personal root** — the assistant's own base context,
`{user_id}/{assistant_id}/…`. This is the *privacy floor*: content here
@@ -34,6 +36,19 @@ There are exactly two kinds of root:
- **Team roots** — `Teams/{team_id}/…`, one per team the assistant belongs
to. Contexts under a team root are owned by the team, not by any
assistant, and are visible to every current member.
+- **Team-owned home** — a team assistant
+ (`SESSION_DETAILS.owner_team_id is not None`, exposed as
+ `SESSION_DETAILS.team_owned`) has **no personal root at all**. Its
+ shared tables home at the owning team's flat root (`Teams/{owner}/…`,
+ the same surfaces every member reads), while per-assistant runtime
+ internals (events, caches, non-shared tables) live under a namespaced
+ subtree, `Teams/{owner}/Assistants/{agent_id}/…` — still team-owned,
+ just collision-free when a team owns several assistants. Task-machine
+ state (`unify/task_scheduler/machine_state.py`) routes to the team's
+ `Teams/{owner}/Tasks/…` tree for the same reason. The root itself is
+ resolved in
+ [`unify/common/runtime_context.py`](https://github.com/unifyai/unify/blob/main/unify/common/runtime_context.py)'s
+ `resolve_runtime_context_root()`.
The arbiter is `ContextRegistry` in
[`unify/common/context_registry.py`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py).
@@ -50,6 +65,16 @@ the two resolution primitives everything else is built on:
every team in `SESSION_DETAILS.team_ids` (sorted), provisioning any that
don't exist yet. Reads always fan out.
+For a **team-owned** session both primitives collapse onto the team shape:
+`destination=None` (and, forgivingly, `"personal"`) resolves shared tables
+to the owning team's root instead of a personal one, `read_roots` returns
+the owning team first followed by other member teams — with no personal
+root anywhere — and non-shared tables resolve to the
+`Teams/{owner}/Assistants/{agent}` subtree. The owning team also passes
+the membership gate unconditionally and is protected from
+`forget_departed_team_roots` eviction: it's the assistant's home, not a
+revocable membership.
+
Destination parsing is centralized in
`ContextRegistry.canonical_destination(...)`: `None` and `"personal"`
normalize to the personal root; `"team:"` is validated for shape
@@ -106,7 +131,8 @@ this:
## Writes and reads, end to end
-
+
+
### The write path
@@ -134,7 +160,9 @@ deletion semantics simple.
**One exception, by design:** transcripts and images. Conversation history
isn't authored *at* a destination — it happens — so
`ContextRegistry.implicit_shared_destinations()` returns the full list of
-team destinations (or `[None]` when the assistant has no teams), and the
+team destinations (or `[None]` when the assistant has no teams; a
+team-owned assistant's owning team is always included, membership payload
+or not), and the
transcript/screenshot publishing paths in
[`unify/conversation_manager/domains/managers_utils.py`](https://github.com/unifyai/unify/blob/main/unify/conversation_manager/domains/managers_utils.py)
and the screenshot capture path in
@@ -152,29 +180,43 @@ goes through it:
- **`FederatedSearchContext`** — one participating context: the concrete
path, a `source` label, an optional `row_filter`, optional
- `allowed_fields` projection, and an optional foreign `project` (used for
- the read-only platform builtins catalogue, which lives outside the
- active project).
-- **`federated_ranked_search(contexts, references, ...)`** — exact
- federated top-k semantic search. Each context is fetched with a local
- window of `offset + limit` (which makes the global merge provably
- exact — a row outside its local window can't be in the global one),
- merged by ascending embedding distance via `merge_ranked_batches`, and
- sliced once. Optional `backfill` tops up short results with
- deterministic recent rows.
+ `allowed_fields` / `excluded_fields` projections, and an optional
+ foreign `project` (used for the read-only platform builtins catalogue,
+ which lives outside the active project).
- **`federated_filter(contexts, filter=..., sorting=...)`** — exact
- federated structured reads, with `SortSpec` handling the subtle
- NULLs-ordering problem: the backend always sorts NULLs last, so a sort
- key asking for `missing="first"` forces a full per-context fetch instead
- of windowed fetching.
+ federated structured reads. These are **server-side**: one call to the
+ backend's federated logs endpoint (`unisdk.get_logs_federated`, i.e.
+ `POST /logs/federated`) runs every context through the ordinary
+ single-context query pipeline and performs the exact global merge on
+ the server — one round trip, globally-ordered window, exact totals —
+ instead of paging each context over HTTP and merging client-side.
+ `SortSpec` handles the subtle NULLs-ordering problem: the backend sorts
+ NULLs last, so a sort key asking for `missing="first"` forces full
+ branch fetches. A `fetcher` escape hatch retains the client-side merge
+ path for rows that don't come from the logs API (local stores,
+ impl-specific reads).
+- **`federated_count(contexts, key=...)`** — a count-only federated read
+ (one server call with `limit=0`).
+- **`federated_ranked_search(contexts, references, ...)`** — exact
+ federated top-k semantic search. This path stays client-orchestrated
+ (it depends on per-context embedding columns): each context is fetched
+ with a local window of `offset + limit` (which makes the global merge
+ provably exact — a row outside its local window can't be in the global
+ one), merged by ascending embedding distance via
+ `merge_ranked_batches`, and sliced once. Optional `backfill` tops up
+ short results with deterministic recent rows.
- **`federated_reduce(contexts, metric=..., ...)`** — aggregations.
Decomposable ungrouped metrics (`count`, `sum`, `min`, `max`, `mean`)
are pushed down per context and combined exactly; grouped or
- non-decomposable metrics (`median`, `mode`, `var`, `std`) fall back to
- fetching merged rows and reducing client-side.
-- Missing contexts (404s — a table not yet provisioned under some root)
- are tolerated everywhere via `is_missing_context_error`, so fan-out
- reads don't require every root to be fully provisioned.
+ non-decomposable metrics (`median`, `mode`, `var`, `std`) fetch the
+ merged rows through the federated endpoint and reduce client-side.
+- Missing contexts (a table not yet provisioned under some root)
+ contribute nothing, so fan-out reads don't require every root to be
+ fully provisioned.
+
+The same server endpoint powers the Console's merged **All** scope views
+(Functions, Transcripts, and friends), so what the UI shows and what the
+runtime reads are the same exact merge.
Merged rows are annotated with `_federated_source` and
`_federated_context` (and `_federated_score` for ranked reads), so callers
@@ -223,12 +265,13 @@ different semantics worth knowing:
## How membership reaches the runtime
-
+
+
### Session state
[`unify/session_details.py`](https://github.com/unifyai/unify/blob/main/unify/session_details.py)
-is the single source of runtime truth. `AssistantDetails` carries two
+is the single source of runtime truth. `AssistantDetails` carries three
team fields, surfaced as convenience properties on the global
`SESSION_DETAILS`:
@@ -237,13 +280,21 @@ team fields, surfaced as convenience properties on the global
- `team_summaries: list[TeamSummary]` — display/routing metadata
(`team_id`, `name`, `description`) used to *teach* the model where
content belongs.
+- `owner_team_id: int | None` — the owning team for [team
+ assistants](/teams/team-assistants) (`None` = user-owned), with
+ `SESSION_DETAILS.team_owned` as the derived flag that switches the
+ registry, root resolution, task-machine routing, and prompt layer into
+ team-owned mode.
Because actor plans and manager tool-loops can run in worker
-subprocesses, both fields round-trip through the environment:
-`export_team_ids_to_env` / `export_team_summaries_to_env` encode them into
-`TEAM_IDS` (CSV) and `TEAM_SUMMARIES` (JSON), and `SESSION_DETAILS`
-re-hydrates from those vars on the other side via
-`normalize_team_summaries`.
+subprocesses, all of it round-trips through the environment:
+`export_team_ids_to_env` / `export_team_summaries_to_env` encode
+memberships into `TEAM_IDS` (CSV) and `TEAM_SUMMARIES` (JSON),
+`OWNER_TEAM_ID` carries ownership, and `SESSION_DETAILS` re-hydrates from
+those vars on the other side via `normalize_team_summaries` /
+`populate_from_env`. All three arrive in the startup payload from
+Orchestra's assistant record (which carries `owner_team_id` for team-owned
+assistants).
### Live membership updates
@@ -280,6 +331,13 @@ content clearly matches its described domain, ask a brief clarifying
question when the audience is unclear, never invent a team id, and never
pass a `team:` token where a `contact_id` belongs.
+Team-owned sessions render a distinct variant: the identity line becomes
+*"You are a team-owned assistant: your owning team's shared root is your
+home memory… You have no private personal memory"*, the owning team's
+bullet is marked as the default destination, the `personal` bullet
+disappears, and the routing rules swap "default to personal" for "writes
+default to your owning team's shared root."
+
Two design details matter here:
- **Team descriptions are load-bearing.** The model routes content by
@@ -342,9 +400,12 @@ A few recipes that follow directly from the architecture:
Everything on this page is the runtime's half of the story. The other
- half — the `Team`/`TeamAssistantMembership` data model, membership
- endpoints, coordinator auto-enrollment, org-wide sharing, and the purge
- of `Teams/{id}/…` trees on team deletion — lives in the closed-source
- Orchestra backend, which the runtime reaches through
+ half — the `Team`/`TeamAssistantMembership` data model, the
+ `owner_team_id` ownership column and its lifecycle guards (an owning
+ team refuses deletion while it owns assistants; a team assistant can't
+ be removed from its owning team), membership endpoints, coordinator
+ auto-enrollment, org-wide sharing, per-team spend attribution, and the
+ purge of `Teams/{id}/…` trees on team deletion — lives in the
+ closed-source Orchestra backend, which the runtime reaches through
[UniSDK](https://github.com/unifyai/unisdk).
diff --git a/teams/managing.mdx b/teams/managing.mdx
index aa522aefb..9da77b220 100644
--- a/teams/managing.mdx
+++ b/teams/managing.mdx
@@ -20,6 +20,10 @@ permissions (Owner or Admin role).
- **Update or delete** from the team's row menu. Deleting a team is
consequential: it permanently removes the team's entire shared pool —
the knowledge, playbooks, tasks, and data accumulated in it.
+- **Teams that own assistants can't be deleted.** If the team has [team
+ assistants](/teams/team-assistants), its pool is their entire memory —
+ deletion is refused until those assistants are deleted first, so a
+ team's AI staff can never be wiped out as a side effect.
## Managing members
@@ -32,15 +36,29 @@ From each team's menu:
their own right. The simplest way is to ask your org's T-W1N —
org-shaped setup like team creation, membership, and shared credentials
is exactly the work it's for.
+- **Hire directly into the team** — the team's **Members** panel has a
+ **Hire for this team** action that onboards a new [team
+ assistant](/teams/team-assistants) with this team preselected as its
+ owner. (A team assistant can't be removed from its owning team — that
+ membership is structural.)
Access changes take effect immediately, including for assistants that are
mid-session.
-## Org-wide sharing: the one-team shortcut
+## Team spend
-When you create an organization (and any time after, from the Teams tab),
-you choose a data-sharing posture. The Console explains the trade
-directly:
+Assistant spending is attributable per team: the organization's spending
+breakdown can be scoped to any team, aggregating across every [team
+assistant](/teams/team-assistants) that team owns. Their caps are managed
+by org admins, bounded by the organization's cap, and limit alerts go to
+the org's Owners and Admins — the full model is described in
+[Team assistants](/teams/team-assistants#spend-caps-and-attribution).
+
+## Org-wide sharing: on by default
+
+New organizations start with org-wide sharing **enabled** — choose
+**private** at creation (or toggle later from the Teams tab) to opt out.
+The Console explains the trade directly:
> By default, all skills acquired and knowledge retained are personal to
> each user's own private teammate… Each user's teammate learns in
@@ -53,7 +71,7 @@ directly:
> controlled sharing within specific teams, rather than blanket org-wide
> sharing.
-Enabling **org-wide sharing** creates a managed team named **Org** that
+Org-wide sharing runs through a managed team named **Org** that
automatically includes every current and future member and assistant.
A few things to know about it:
@@ -64,16 +82,21 @@ A few things to know about it:
and files are never shared** through the org pool. Only knowledge,
skills, and general know-how flow into it, creating what the Console
calls "a faster hive-mind approach to learning across the team."
+- **It's the natural home for company-wide assistants.** Hiring a [team
+ assistant](/teams/team-assistants) into the Org team gives you an
+ assistant that belongs to — and is usable by — the entire organization,
+ present and future.
- **Turning it off deletes the pool.** Disabling org-wide sharing removes
the Org team *and all shared content in it* — the Console warns you, and
- it cannot be undone.
+ it cannot be undone. (Like any team, it can't be disabled while it owns
+ team assistants.)
## Choosing your structure
| Setup | Fits |
|---|---|
-| **No teams** (all private) | Solo users; orgs where assistants genuinely shouldn't share |
-| **Org-wide sharing only** | Small orgs where everyone works on everything |
+| **Fully private** (opt out at creation) | Solo users; orgs where assistants genuinely shouldn't share |
+| **Org-wide sharing only** (the default) | Small orgs where everyone works on everything |
| **Dedicated teams** | Any org with units, divisions, or sensitivity boundaries — the recommended shape as you grow |
The options combine: you can run org-wide sharing for general know-how
diff --git a/teams/membership.mdx b/teams/membership.mdx
index 7d5a3794b..316f28cb4 100644
--- a/teams/membership.mdx
+++ b/teams/membership.mdx
@@ -3,10 +3,10 @@ title: "People & assistants"
description: "Who belongs to a team, and how assistant access follows"
---
-Teams contain **people** and **assistants**, and the platform treats the two
-kinds of assistants — your private T-W1N and publicly visible teammates —
-very differently. Getting this distinction right is most of understanding
-teams.
+Teams contain **people** and **assistants**, and the platform treats the
+kinds of assistants — your private T-W1N, personally-supervised hires, and
+[team assistants](/teams/team-assistants) — very differently. Getting
+these distinctions right is most of understanding teams.
## Your T-W1N: private, and it follows you
@@ -27,25 +27,36 @@ team *you're* on — the sales team's playbooks, the ops team's data — while
remaining completely invisible to your colleagues. They benefit from what
it shares into team pools without ever interacting with it directly.
-## Public assistants: visible, and members in their own right
+## Hired assistants: visible, and members in their own right
-Regular hired assistants are the opposite on both counts:
+Hired assistants are the opposite on both counts:
1. **They're visible across the organization.** Colleagues (with the
appropriate org permissions) can find them in the assistant list, open
their profiles, and message them — they're shared team members in the
fullest sense.
-2. **They're added to teams directly.** A public assistant's team
- memberships are its own — granted explicitly, not inherited from
- whoever hired it. The person who onboarded an assistant might not even
- be on the teams it serves.
+2. **Their team memberships are their own** — granted explicitly, not
+ inherited from any person. One assistant can serve several teams at
+ once (the sidebar shows a badge listing all of them).
-This is what makes public assistants useful as **specialists**: a repairs
+Within that, two ownership models:
+
+- **Team assistants** — owned by a team rather than a person. Hiring
+ defaults to this: you pick an **owning team** during the hire, the team
+ becomes the assistant's home, and membership in that team is
+ *structural* — it can't be removed from its owning team, and the team
+ can't be deleted while it owns assistants. Everything about them is
+ covered in [Team assistants](/teams/team-assistants).
+- **Personally-supervised assistants** — owned by the person who hired
+ them (choose **Personal** during the hire). They keep a private memory
+ as their [privacy floor](/teams/shared-context) and join teams as an
+ add-on: each membership layers that team's shared pool on top.
+
+This is what makes hired assistants useful as **specialists**: a repairs
coordinator serving the whole repairs team, a reporting assistant embedded
-in finance. They belong to the unit, not to a person, and one assistant can
-serve several teams at once (the sidebar shows a badge listing all of
-them). For how these teammates come to exist — and how their identity
-differs from a T-W1N's — see [Hiring](/hiring/overview).
+in finance. They belong to the unit, not to a person. For how these
+teammates come to exist — and how their identity differs from a T-W1N's —
+see [Hiring](/hiring/overview).
## Joining and leaving
@@ -53,11 +64,16 @@ Membership is live — access tracks it immediately:
- **Joining** a team gives a member (human or assistant) access to the
team's whole [shared pool](/teams/shared-context) — knowledge, guidance,
- skills, tasks, data, credentials — from that moment.
+ skills, tasks, data, credentials — from that moment. It also puts them
+ in the team's group chat.
- **Leaving** (or being removed) ends that access just as cleanly. The
shared content itself **stays with the team** for the remaining members —
people take their access away with them, not the team's accumulated
know-how.
+- **The one structural exception:** a [team
+ assistant](/teams/team-assistants) cannot leave or be removed from its
+ *owning* team — that team's pool is its entire memory. It can join and
+ leave any *other* team freely, like any assistant.
- **Leaving the organization** removes a person from all its teams — and
their T-W1N from all of them too, automatically.
@@ -66,9 +82,10 @@ revocation doesn't wait for the next session.
## Who's who at a glance
-| | Your T-W1N | Public assistants |
-|---|---|---|
-| Visible to colleagues | Never | Yes — listed, searchable, messageable |
-| Team membership | Automatic — mirrors yours | Explicit — granted per team |
-| Access ceiling | Exactly your teams | Exactly its teams |
-| Serves | You | The team(s) it belongs to |
+| | Your T-W1N | Personally-supervised | Team assistant |
+|---|---|---|---|
+| Visible to colleagues | Never | Yes — listed, searchable, messageable | Yes — listed, searchable, messageable |
+| Owned by | You | The person who hired it | The owning team |
+| Team membership | Automatic — mirrors yours | Explicit — granted per team | Owning team is structural; other teams explicit |
+| Private memory | Yes — its privacy floor | Yes — its privacy floor | None — the team pool is its memory |
+| Answers to | You | Its supervisor (and teams it joins) | Anyone on the team |
diff --git a/teams/overview.mdx b/teams/overview.mdx
index 981f94ea7..43eb69b89 100644
--- a/teams/overview.mdx
+++ b/teams/overview.mdx
@@ -6,9 +6,11 @@ description: "Group people and their assistants — and give them a shared brain
An organization rarely works as one undifferentiated blob. Sales runs
differently from engineering; the finance team's data is not the marketing
team's business. **Teams** bring that structure to your assistants: a team
-groups **people and assistants** within your organization, and gives them a
+groups **people and assistants** within your organization, gives them a
**shared memory pool** — knowledge, playbooks, skills, tasks, data, and
-credentials that every member can use, and nobody outside can.
+credentials that every member can use, and nobody outside can — and a
+**group chat** where all of them, human and AI, work together in one
+thread.
## Why teams exist
@@ -25,61 +27,102 @@ Teams unlock the middle ground that real organizations need:
*current members only*. Division-specific data, sensitive procedures, and
elevated-access credentials stay inside the unit rather than spilling
across the whole org.
-- **The default is still private.** Assistants keep personal memory as
- their [privacy floor](/teams/shared-context) — they share into a team
- pool only when content clearly belongs to that team, and ask when unsure.
+- **Privacy floors are respected.** Personally-supervised assistants keep
+ personal memory as their [privacy floor](/teams/shared-context) — they
+ share into a team pool only when content clearly belongs to that team,
+ and ask when unsure. ([Team assistants](/teams/team-assistants) are the
+ deliberate exception: they belong to the team, so the team pool *is*
+ their memory.)
+- **A team can hire its own assistants.** A [team
+ assistant](/teams/team-assistants) is owned by the team itself — no
+ single supervisor, everyone can work with it, and everything it learns
+ is shared with the team by construction.
This shared-context machinery is the **most important thing teams do** —
covered in depth in [Shared context](/teams/shared-context).
## Who's in a team
-Two kinds of members, with different rules:
+Three kinds of members, with different rules:
- **People** — colleagues from your organization, added by an org admin.
-- **Assistants** — in two flavors:
- - **Everyone's T-W1N follows its person.** Your digital twin is
- [private to you](/teams/membership) — hidden from everyone else in the
- org — and it automatically joins and leaves teams *with you*, so it
- always has exactly your team access, never more.
- - **Regular assistants join directly.** Publicly visible teammates
- (the ones anyone in the org can find and message) are added to teams
- in their own right — their access comes from their team memberships,
- not from whoever hired them.
+ Humans appear in the workspace roster with live online indicators, and
+ you can message any of them directly.
+- **Everyone's T-W1N follows its person.** Your digital twin is
+ [private to you](/teams/membership) — hidden from everyone else in the
+ org — and it automatically joins and leaves teams *with you*, so it
+ always has exactly your team access, never more.
+- **Hired assistants join in their own right** — and come in two
+ ownership models:
+ - **Team assistants** (the default when hiring) are *owned by a team*:
+ the team's shared pool is their entire memory, anyone on the team can
+ work with them, and no individual holds a supervisor position. See
+ [Team assistants](/teams/team-assistants).
+ - **Personally-supervised assistants** are owned by one person but can
+ still be added to any number of teams, layering team pools on top of
+ their private memory.
Details and edge cases in [People & assistants](/teams/membership).
## Where you see teams
-In the **Assistants** sidebar, colleagues are grouped under the teams they
-belong to — your T-W1N pinned at the top, team groups below, and
-independent colleagues listed outside any group. An assistant serving
-several teams shows a badge with all of them.
+The **Assistants** sidebar is a full org roster, grouped into three
+sections: **Teams** (each team with its members beneath it — an assistant
+serving several teams shows a badge listing them, and team-owned
+assistants carry a **Team-owned** badge under their owning team), **Real**
+(your human colleagues, with online indicators), and **Virtual**
+(assistants that aren't in any team). Your own T-W1N stays pinned at the
+top.
+
+**Selecting a team** opens the team's workspace:
+
+- **Chat** — the team's group chat, one thread shared by every member,
+ human and AI. Messages behave like a conversation in any group channel:
+ every member sees them, assistants read the room and reply when a
+ message is genuinely for them (an @mention helps), and their replies are
+ visible to — and can wake — their AI teammates.
+- **Members** — the roster with online status, and a **Hire for this
+ team** action to onboard a new [team assistant](/teams/team-assistants)
+ directly into it.
+- **Every brain section, team-scoped** — Tasks, Dashboards, Contacts,
+ Transcripts, Knowledge, Functions, Guidance, and Data all show the
+ *team's* shared pool rather than any single assistant's memory.
+
+**Selecting an assistant** gives the complementary view: each brain pane
+has scope chips — **All**, **Personal**, and one per team — so you can see
+its private memory, any single team pool, or the exact merged picture the
+assistant itself works from.
Team administration lives under **Organizations → Teams** — see
[Managing teams](/teams/managing).
-## One org-wide team, if you want it
+## One org-wide team, on by default
-For small organizations that just want everything shared, there's a
-shortcut: **org-wide sharing** creates a managed team called **Org** that
-automatically includes every current and future member and assistant. It's
-a blunt instrument by design — most organizations outgrow it and move to
-dedicated teams for the control they offer. Both are covered in
+New organizations start with **org-wide sharing enabled**: a managed team
+called **Org** that automatically includes every current and future member
+and assistant. It's the simplest useful structure — one shared pool for
+general know-how, and the natural home for company-wide [team
+assistants](/teams/team-assistants). Organizations that need boundaries
+add dedicated teams on top (the narrower pool always keeps its boundary),
+or opt out to fully private at creation. All covered in
[Managing teams](/teams/managing).
## What's in this section
- How membership works — T-W1N inheritance, public assistants, joining
+ How membership works — T-W1N inheritance, hired assistants, joining
and leaving.
+
+ Assistants owned by the team itself — no single supervisor, shared
+ memory by construction.
+
The heart of teams: personal vs. team memory across knowledge,
guidance, skills, tasks, data, and credentials.
- Creating teams, membership admin, and org-wide sharing.
+ Creating teams, membership admin, org-wide sharing, and team spend.
diff --git a/teams/shared-context.mdx b/teams/shared-context.mdx
index a53dcb6ce..5a11e4b10 100644
--- a/teams/shared-context.mdx
+++ b/teams/shared-context.mdx
@@ -11,6 +11,13 @@ Teams exist so that pool doesn't have to be all-or-nothing: every piece of
context is either **personal** (belonging to one assistant) or **shared in
a team** (accessible to every current member).
+One deliberate exception up front: [team
+assistants](/teams/team-assistants) have no personal scope at all — their
+owning team's pool *is* their memory. Everything below about choosing
+between personal and team applies to T-W1Ns and personally-supervised
+assistants; a team assistant simply defaults to its owning team and can
+additionally write into other teams it belongs to.
+
## The two scopes
@@ -94,10 +101,20 @@ org":
## Seeing scopes in the Console
-Shared context is visible where you'd expect: **Guidance** and
-**Knowledge** entries carry scope badges showing whether they're personal
-or belong to a team, and the [Data pane](/canvas/data) groups team-owned
-tables under their team alongside the assistant's personal folders.
+Every brain pane on an assistant — Contacts, Transcripts, Knowledge,
+Functions, Guidance, Tasks, Data, Dashboards — carries **scope chips**:
+
+- **All** (the default) — the exact merged picture the assistant itself
+ works from: its personal pool plus every team it belongs to, combined
+ into one correctly-ordered view. Merged entries show which scope they
+ came from, and in Transcripts each thread is labeled with its team.
+- **Personal** — just its private memory.
+- **One chip per team** — just that team's shared pool.
+
+The complementary view is the team itself: **selecting a team in the
+sidebar** shows every one of those sections scoped to the team's pool —
+the same data any member's assistant sees when it reads that team, without
+going through any individual assistant.
A good habit when teaching an assistant something reusable: say who it's
diff --git a/teams/team-assistants.mdx b/teams/team-assistants.mdx
new file mode 100644
index 000000000..5e99d9730
--- /dev/null
+++ b/teams/team-assistants.mdx
@@ -0,0 +1,134 @@
+---
+title: "Team assistants"
+description: "Assistants owned by the team itself — no single supervisor, shared memory by construction"
+---
+
+Sometimes an assistant shouldn't belong to anyone. A repairs coordinator
+for the whole repairs unit, a reporting assistant the entire finance team
+leans on, a company-wide helper in the Org team — for these, "pick someone
+to own it and tell them to share everything" is the wrong shape. **Team
+assistants** make the team itself the owner: no single supervisor, no
+private memory, and everyone on the team can work with it as naturally as
+with a colleague.
+
+## What makes a team assistant different
+
+
+
+ No individual holds a supervisor position. The person who ran the hire
+ is recorded only as the hiring member; management rights belong to the
+ org's admins, like any shared resource.
+
+
+ A team assistant has **no personal contexts at all**. Everything it
+ knows, learns, schedules, and stores lives in its owning team's
+ shared pool — visible to every member, by construction.
+
+
+
+The memory point is worth dwelling on, because it's a genuine privacy
+model, not just a default:
+
+- A personally-supervised assistant keeps a private pool and *chooses*
+ when something belongs to a team. A team assistant has nothing to
+ choose between — its knowledge, playbooks, skills, tasks, data,
+ transcripts, and credentials all live at the team scope. There is no
+ hidden layer only one person can see.
+- Its working state defaults to the **owning team's** pool. If it also
+ joins other teams, it can read and contribute to those pools like any
+ member — but its home never moves.
+- Because everything is team-visible, a team assistant is exactly as
+ auditable as the team itself: open the team in the sidebar and every
+ brain section shows the full picture.
+
+## Hiring one
+
+Team-first is the default. The hire dialog includes an **Owning team**
+selector, pre-set to the team you hired from (or the managed **Org** team
+where org-wide sharing is on):
+
+- **Pick a team** → you get a team assistant, born into that team: it's
+ enrolled automatically, appears under the team in the sidebar with a
+ **Team-owned** badge, and its memory starts at team scope from the
+ first message.
+- **Pick "Personal (only you)"** → you get the classic
+ personally-supervised assistant instead, with a private memory and
+ optional team memberships layered on.
+
+The fastest route for a specific team: open the team in the sidebar,
+switch to **Members**, and use **Hire for this team** — the owning team
+comes preselected.
+
+Everything else about hiring — profile, voice, its own accounts, phone
+numbers, and mailboxes — is identical to any hired teammate; see
+[Hiring](/hiring/overview). A team assistant still gets **its own
+identity** in your org's tools; what changes is who it answers to.
+
+## Working with one
+
+Anyone on the team can message a team assistant directly, work with it in
+the [team group chat](/teams/overview#where-you-see-teams), assign it
+tasks, and see everything it's doing. There's no privileged channel — the
+hiring member's messages carry no more weight than anyone else's.
+
+Because its transcripts and tasks live at team scope, the whole team sees
+one coherent history: what it was asked, what it did, what it scheduled.
+Its scheduled work runs on the team's task surfaces, so runs and results
+are team-visible too.
+
+## Teams, plural
+
+A team assistant always has **exactly one owning team** — that's where its
+memory lives and who it belongs to. On top of that it can join any number
+of other teams, exactly like any assistant: each extra membership adds
+that team's shared pool to what it can read and contribute to.
+
+Two structural rules keep ownership coherent:
+
+- **It can't leave its owning team.** Removing it from the owning team is
+ refused — that pool *is* its memory. (Other memberships can be added
+ and removed freely.)
+- **The owning team can't be deleted while it owns assistants.** Deleting
+ the team would delete their entire minds; the platform refuses until
+ the owned assistants are deleted first.
+
+
+ Want an assistant the *whole company* can use? Hire into the managed
+ **Org** team (on by default for new organizations). Its membership is
+ automatically everyone — current and future — so "owned by the Org team"
+ means "belongs to the company."
+
+
+## Spend, caps, and attribution
+
+Team assistants bill the **organization's** account, and their cost
+controls are team-shaped rather than person-shaped:
+
+- **Caps are managed by org admins.** Any member with assistant-management
+ permissions can set or change a team assistant's monthly spending cap —
+ the hiring member holds no special position.
+- **The org cap is the ceiling.** A team assistant's cap is bounded by the
+ organization's spending cap only — no individual member's personal cap
+ constrains a team asset.
+- **Limit alerts go to the org's Owners and Admins** (alongside the hiring
+ member), so the people who can act on a cap are the ones told when it's
+ reached.
+- **Spend is attributable per team.** The organization's spending
+ breakdown can be scoped to a team, aggregating across every assistant
+ that team owns — so "what does the repairs team's AI staff cost?" is one
+ query, not a spreadsheet.
+
+## Which ownership model to choose
+
+| | Personally-supervised | Team assistant |
+|---|---|---|
+| Best for | An assistant that works *for you* with private context | A shared specialist the whole unit relies on |
+| Memory | Private floor + team pools it joins | Owning team's pool, entirely |
+| Who can direct it | Its supervisor; teams it joins | Anyone on the team |
+| Management rights | Its supervisor | Org admins |
+| Billing anchor | Supervisor's caps apply | Org cap; per-team attribution |
+| Lifecycle | Supervisor manages | Tied to the owning team |
+
+Both models coexist freely in one org — and in one team. The question to
+ask is simply: *if the hiring person left tomorrow, should this assistant
+carry on unchanged?* If yes, it's a team assistant.
diff --git a/their-computer/developers.mdx b/their-computer/developers.mdx
index 2959fc383..30ed48e49 100644
--- a/their-computer/developers.mdx
+++ b/their-computer/developers.mdx
@@ -17,7 +17,8 @@ are separate closed-source components that speak to these interfaces.
## The control stack
-
+
+
Computer control is layered, with a clean seam at each level:
@@ -168,7 +169,8 @@ automation API is `{base}/api`.
## Lifecycle: from cold start to ready
-
+
+
Desktop state lives on the global session singleton
([`session_details.py`](https://github.com/unifyai/unify/blob/main/unify/session_details.py)):
@@ -272,7 +274,8 @@ and `USER_DESKTOP` (a linked user machine). Each has a target class in
## Screen share & remote control
-
+
+
The Console's Meet controls arrive as system events —
`AssistantScreenShareStarted/Stopped`, `UserScreenShareStarted/Stopped`,
diff --git a/workspace/calendar.mdx b/workspace/calendar.mdx
index 15915c678..d11840418 100644
--- a/workspace/calendar.mdx
+++ b/workspace/calendar.mdx
@@ -22,7 +22,7 @@ connected account's calendar — Google Calendar or Outlook Calendar.
## Calendar + meetings
Calendar access pairs naturally with your assistant's [meeting
-abilities](/communication/meetings):
+abilities](/workspace/meetings):
- On Microsoft 365, it can **schedule a Teams meeting** directly onto the
calendar — invites go out automatically and the join link is included.
diff --git a/workspace/connecting.mdx b/workspace/connecting.mdx
index 73830d7e0..91697a549 100644
--- a/workspace/connecting.mdx
+++ b/workspace/connecting.mdx
@@ -26,7 +26,7 @@ open it from several places — they all lead to the same dialog:
|---|---|---|
| Email | Always included | Always included |
| Drive / files | Always included | Always included |
- | Teams | — | Always included |
+ | [Teams](/workspace/teams) | — | Always included |
| SharePoint | — | Always included |
| Calendar | Optional | Optional |
| Contacts | Optional | Optional |
diff --git a/workspace/developers/architecture.mdx b/workspace/developers/architecture.mdx
index d7c3ef9a2..91b739343 100644
--- a/workspace/developers/architecture.mdx
+++ b/workspace/developers/architecture.mdx
@@ -38,7 +38,8 @@ The workspace integration is built from four cooperating pieces:
[`sharepoint`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/sharepoint/views.py))
that back sending, inbox watches, and the Console file picker.
-
+
+
## The core design decision
diff --git a/workspace/developers/gateway-channels.mdx b/workspace/developers/gateway-channels.mdx
index 74e24b8d2..ff385c70a 100644
--- a/workspace/developers/gateway-channels.mdx
+++ b/workspace/developers/gateway-channels.mdx
@@ -13,7 +13,8 @@ Email is the place where the twin-vs-teammate identity split
([user docs](/hiring/twin-vs-teammates)) becomes concrete in code — there
are two entirely separate send paths:
-
+
+
### `primitives.workspace_email` — acts as the user
diff --git a/workspace/developers/provider-proxy.mdx b/workspace/developers/provider-proxy.mdx
index 2098debd0..e8c7e5b28 100644
--- a/workspace/developers/provider-proxy.mdx
+++ b/workspace/developers/provider-proxy.mdx
@@ -10,7 +10,8 @@ at once: give LLM-authored code the **complete** provider REST surface
without a real token, and enforce the per-assistant **file-access
allowlist** at a single choke point.
-
+
+
## Module map
diff --git a/communication/meetings.mdx b/workspace/meetings.mdx
similarity index 95%
rename from communication/meetings.mdx
rename to workspace/meetings.mdx
index e1cec2a02..768ffdb5d 100644
--- a/communication/meetings.mdx
+++ b/workspace/meetings.mdx
@@ -24,7 +24,7 @@ status") and it handles the rest, reporting back to you afterwards.
## Your assistant can host, too
-With a [Microsoft 365 connection](/communication/microsoft-teams), your
+With a [Microsoft 365 connection](/workspace/teams), your
assistant can **create** Teams meetings:
- **Instant links** — "create a meeting link for me and Sam" gets you a
diff --git a/workspace/overview.mdx b/workspace/overview.mdx
index 4ce591a96..76ff05226 100644
--- a/workspace/overview.mdx
+++ b/workspace/overview.mdx
@@ -27,11 +27,19 @@ there are no credit costs for connecting or keeping a workspace connected.
Look people up in your address book and manage your to-do lists.
+
+ On Microsoft 365, chat, post in channels, and host meetings as the
+ connected account.
+
+
+ Join Google Meet and Teams meetings, and host Teams meetings on
+ Microsoft 365.
+
On Microsoft 365, the connection also powers
-[Microsoft Teams](/communication/microsoft-teams) — chats, channels, and
-meetings.
+[Microsoft Teams](/workspace/teams) — chats, channels, and meetings, with your
+assistant acting as the connected account.
The workspace connection covers the Google/Microsoft suite. Other apps —
@@ -83,7 +91,7 @@ because the ecosystems do:
| Files | Google Drive | OneDrive + SharePoint |
| Contacts | Google Contacts | Microsoft contacts |
| Tasks | Google Tasks | Microsoft To Do |
-| Teams chat & meetings | — | Microsoft Teams |
+| Teams chat & meetings | — | [Microsoft Teams](/workspace/teams) |
Ready to connect? Head to [Connecting your
workspace](/workspace/connecting).
diff --git a/workspace/teams.mdx b/workspace/teams.mdx
new file mode 100644
index 000000000..3a78ee653
--- /dev/null
+++ b/workspace/teams.mdx
@@ -0,0 +1,57 @@
+---
+title: "Microsoft Teams"
+description: "Your assistant acts in Teams as its own connected Microsoft account"
+---
+
+When you connect a **Microsoft 365** account for your assistant, it can work in
+Microsoft Teams as *that account* — chatting, posting in channels, and hosting
+meetings under its own Microsoft identity, the same way it works in the
+[connected mailbox](/workspace/email).
+
+This is the account-bound path. It's set up per assistant as part of the
+[workspace connection](/workspace/connecting): pick **Microsoft 365** and the
+**Teams** feature is included, so Teams is enabled the moment the account is
+linked. It's distinct from the org-wide [Unify Teams app](/communication/microsoft-teams),
+where the assistant appears as a shared bot rather than as a Microsoft user.
+
+## Messaging
+
+- **1:1 chat** — message your assistant in Teams like any colleague, and it
+ replies as its connected account.
+- **Group chats** — it can take part in group conversations with two or more
+ people, and can start them too.
+- **Channels** — @mention your assistant in a channel and it replies in the
+ thread; it can also post to channels and even create new ones.
+- **Attachments** — it can send files in Teams messages.
+
+## Hosting meetings
+
+Because it acts as a real Microsoft account, your assistant can **create**
+Teams meetings, not just attend them:
+
+- **Instant links** — "create a meeting link for me and Sam" gets you a
+ ready-to-share Teams link on the spot.
+- **Scheduled meetings** — it can put a Teams meeting on the calendar with a
+ link and send invites to all attendees.
+
+Joining a meeting is separate and doesn't require this connection — send any
+Teams meeting link and your assistant joins with its voice. See
+[Meetings](/workspace/meetings).
+
+## Whose name is on it?
+
+Just like [email](/workspace/email#whose-name-is-on-the-email), it depends on
+the account you connected — see [whose account gets
+connected](/workspace/overview#whose-account-gets-connected):
+
+- **T-W1N** is connected to *your* account, so it chats and hosts meetings
+ **as you** — your digital twin acting in your name.
+- **A hired assistant** has its own dedicated account, so it shows up as
+ **itself** — a named team member with its own Teams presence.
+
+
+ If you'd rather everyone in the org reach your assistants through one shared
+ bot — including people who never see the assistant's Microsoft account — use
+ the org-installed [Unify Teams app](/communication/microsoft-teams) instead.
+ You can use either path, or both.
+
diff --git a/your-computer/developers.mdx b/your-computer/developers.mdx
index 12c346e8b..db8722c89 100644
--- a/your-computer/developers.mdx
+++ b/your-computer/developers.mdx
@@ -25,7 +25,8 @@ Console live view, every call re-checks consent, file access is
pull-on-request rather than continuously synced, and writebacks never touch
originals.
-
+
+
## The link: session state
@@ -126,7 +127,8 @@ Three deliberate differences from the assistant-desktop path:
## The file channel: `user_desktop.files`
-
+
+
File access is a **separate channel with separate consent**, implemented in
[`unify/file_manager/sync/user_sftp.py`](https://github.com/unifyai/unify/blob/main/unify/file_manager/sync/user_sftp.py)
@@ -219,7 +221,8 @@ and the computer environment context
## Consent: three gates, checked at three times
-
+
+
Access control is layered, and each layer answers a different question: