agent-context-and-default-file.md89.8 KBView on GitHub
# The agent as one thing: four namespaces, full context, and a default file

## 1) Introduction — goal, present state, future state

An agent should be one object you can open, one object you can talk to, and one object it can see all of. Four things follow from that. Opening the agent view — any tab of it — should *inherently* make the chat beside it a chat with that agent, the way opening a deal makes the chat about that deal, so clicking "New agent" drops you into a conversation that builds the agent. That conversation needs a real config tool, so building an agent by talking to it writes its instructions, its triggers and its templates instead of describing them. The agent's folder should have four namespaces — its `playbook`, its `config/`, its `memory/` and its outputs — so "a template I write from" and "a brief I produced" stop being the same kind of thing in the same flat list. And on every turn the agent should see all of it: the playbook it runs under, the specific outputs it has produced, and what it has learned. Today none of that holds. `agent` is not a context kind ([chat-context.ts:15](apps/server/src/mastra/types/chat-context.ts)); the workspace is a display artifact only ([MessageTypes.ts:274](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts)); `useRouteChatThread` explicitly steps around an open agent ([useRouteChatThread.ts:144](apps/mail/modules/ux/layout/useRouteChatThread.ts)) and the one caller that ever bound a thread was deleted with the home agents list (commit 0cff83d16), leaving `startAgentChat` ([start-agent-chat.ts:29](apps/mail/modules/home/components/start-agent-chat.ts)) dead. A bound agent's prompt is its instructions and its memory and nothing else — not one of its files, not one trigger, not a word of the block prose its scheduled runs receive as `<additional_context>` ([playbook-execution-triggers.ts:529](apps/server/src/services/playbook/playbook-execution-triggers.ts)) — so it is less informed in chat than on a cron. No tool lets an agent change its own configuration: the ten `config-write` actions are all CRM config ([config-write-tool.ts:233](apps/server/src/mastra/tools/config/config-write-tool.ts)). And the landing file is a hardcoded `overview` in two separate places. The namespaces, though, are half-built already: the coaching agent ships `organisation/agent-{id}/config/…` and `organisation/agent-{id}/outputs/…` with the split written down as a decision ([convention-paths.ts:399](apps/server/src/services/documents/convention-paths.ts)), and nothing generalises it. So this design **classifies rather than migrates** — not one document row moves — and adds: a `default_file:` frontmatter key both file surfaces honour, one renderer that puts the agent's playbook and files into the system prompt of every bound turn, the binding derived from the open artifact, and the existing `playbook-authoring` skill loaded by default in a bound chat so the agent can build itself while you talk to it.

## 2) Present state

### 2.1 Architecture diagram

```text
  CHAT THREAD                                    THE AGENT (a document)
  chat_threads { id, name, agent_id, context }   user/playbooks/{aop}/subagents/{name}.md
        │                   │                      frontmatter: name model folder files …
        │ agent_id          │ context.items[]      body: the instructions
        │ (ACTOR)           │ kind ∈ conversation | email_thread | slack_thread        │
        │                   │        | linkedin_chat | whatsapp_chat | file | task     │
        ▼                   ▼          (no 'agent')                                    ▼
  bound-agent.ts      hydrate.ts                  {ns}/agent-{id}/     ← MOSTLY FLAT
  systemPrompt =      → WORKING CONTEXT             overview           ← hardcoded landing
    AGENT_CHAT_SHELL                                archives/{date}
    + memory             NOTHING lists its files,   meetings/{id}/notes
    + instructions       its triggers, or the       engagement-wiki    ← free-form, no allowlist
    (that is all)        block prose its CRON       memory/{3 files}   ← the one real namespace
                         runs get as
                         <additional_context>     coaching agent ONLY, already shipped:
                                                    config/playbook · config/templates/…
  useRouteChatThread: an open agent is STEPPED       outputs/reps/{rep}/weekly/{week}
  AROUND — the chat beside it stays general          └─ 5 segments deep; the deal Files tab
                                                        flattens anything past 1 (G3)
  New agent → agent.create → /agent?agentId=X
    lands on the Files tab, EMPTY, beside a       ┌────────────────────────────────────┐
    chat that is not with it and cannot ─────────►│ /agent?agentId=X  (AgentView)      │
    configure it (config-write = 10 CRM actions)  │ Files · Config · Memory · Runs     │
                                                  │ Files: FileBrowser at {ns}/agent-… │
  WHO ACTUALLY WRITES: prompt text, not code.     │   includeNode drops the memory node│
    automations.ts <agent_namespace> block +      │   autoOpenPath = …/overview        │
    AGENT_DOC_CONVENTION + 4 hardcoded copies +   │ Memory: the SAME FileBrowser,      │
    SKILL.md + seeded agent bodies IN THE DB      │   rooted one level deeper          │
                                                  └────────────────────────────────────┘
```

### 2.2 Step-by-step walkthrough

1. **An agent is created** — `agent.create` at [agent.ts:994](apps/server/src/trpc/routes/agent.ts) → `createAgentDoc` at [agent-create.ts:150](apps/server/src/services/agent-workspace/agent-create.ts) → `authorSubagentDoc` at [author-subagent.ts:114](apps/server/src/services/playbook/author-subagent.ts)
   - Writes ONE document with blank-line frontmatter and a starter body that says "Write X's instructions here". No folder, no memory files, no config.
   - Data after this step:
     ```json
     { "agentId": "b41c…", "documentId": "0e77…", "slug": "renewal-watch",
       "path": "user/playbooks/9f2…/subagents/renewal-watch.md", "folder": "core" }
     ```
   - Both create sites then open the workspace — `openAgent` at [AgentsGrid.tsx:57](apps/mail/modules/agents/components/AgentsGrid.tsx) and `setSelectedArtifact` at [AgentPickerDialog.tsx:72](apps/mail/modules/home/widgets/AgentPickerDialog.tsx) — with no tab, so a brand-new agent lands on **Files**, an empty list, rather than on the config you came to write.

2. **The chat beside it is not with it** — `useRouteChatThread` at [useRouteChatThread.ts:144](apps/mail/modules/ux/layout/useRouteChatThread.ts)
   - `openArtifact` at [useRouteChatThread.ts:88](apps/mail/modules/ux/layout/useRouteChatThread.ts) excludes `canvas` and `agent` because neither is a `ContextKind`; the agent branch then returns early ("an `agent` is display-only"). For a conversation the same hook does the opposite at [useRouteChatThread.ts:113](apps/mail/modules/ux/layout/useRouteChatThread.ts): a conversation the thread does not own is a NEW CONTEXT — an empty chat is re-scoped in place, a used chat is forked first.
   - The thread row is created server-side on the FIRST message ([chat-message-persistence.ts](apps/server/src/mastra/utils/chat-message-persistence.ts)) with no `agent_id`, and the workflow reads `chat_threads.agent_id` per turn at [chat-workflow.ts:509](apps/server/src/mastra/workflows/chat/chat-workflow.ts) — so a binding must reach the row before turn one.

3. **The client never learns the binding** — `listThreads` at [databaseAdapter.ts:41](apps/mail/modules/cedar-os/src/store/messages/databaseAdapter.ts)
   - `chat.getThreads` at [chat.ts:353](apps/server/src/trpc/routes/chat.ts) selects `agentId`, but the adapter maps only `{ id, title, updatedAt, color, context }` into `MessageThreadMeta` ([messageStorage.ts:194](apps/mail/modules/cedar-os/src/store/messages/messageStorage.ts)).
   - Data after this step — the binding is gone:
     ```json
     { "id": "thr_1", "title": "New chat", "context": { "items": [] } }
     ```

4. **The context row renders** — `ChatContextRow` at [ChatContextRow.tsx:78](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/ChatContextRow.tsx)
   - Chips = `[primaryConversation] + items[]`, each a `FieldBadge` with `CONTEXT_KIND_ICONS[kind]` ([contextKinds.ts:17](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/contextKinds.ts)). No agent chip, because there is no agent kind.

5. **A bound turn builds its prompt** — `resolveBoundAgentForChat` at [bound-agent.ts:105](apps/server/src/mastra/workflows/chat/harness/bound-agent.ts)
   - `resolveChatAgentBinding` ([agent-binding.ts:63](apps/server/src/services/chat/agent-binding.ts)) → `{ name, instructions, frontmatter, documentId }`; `loadAgentPromptMemory` → memory; `buildBoundAgentSystemPrompt` ([agent-chat-shell.ts:109](apps/server/src/mastra/agents/agent-chat-shell.ts)) = shell + memory + identity + instructions. That is the whole prompt: no file list, no folder, no triggers, not even its own document's id.
   - It is also less informed than the same agent on a schedule. `runPlaybookSectionExecution` appends the referencing block's prose as `<additional_context>` at [playbook-execution-triggers.ts:529](apps/server/src/services/playbook/playbook-execution-triggers.ts) — `refContextText`, the text nodes of the block the `<ref>` sits in, where a rep writes "only for deals over 50k" directly above the ref. `CompiledNode` is `{ type:'text'; content } | { type:'ref'; id; … }` ([compiled-playbook-types.ts:65](apps/server/src/services/playbook/compiled-playbook-types.ts)), so that prose is in the same blob the invocation walk already reads.
   - `resolvePlaybookSources` at [agent-invocations.ts:68](apps/server/src/services/agent-workspace/agent-invocations.ts) visits every block via `refersTo(block.nodes, documentId)` ([agent-invocations.ts:269](apps/server/src/services/agent-workspace/agent-invocations.ts)) but keeps only the trigger's SHAPE — it discards `nodes` and never reads `global.crossCuttingProse` or a stage's `instructions` ([compiled-playbook-types.ts:17](apps/server/src/services/playbook/compiled-playbook-types.ts)), both of which apply to every run.

6. **The agent cannot configure itself** — `configWriteTool` at [config-write-tool.ts:878](apps/server/src/mastra/tools/config/config-write-tool.ts)
   - Its ten actions ([config-write-tool.ts:233](apps/server/src/mastra/tools/config/config-write-tool.ts)) are all CRM configuration. Every writer of a subagent document is a tRPC route — `agent.*`, `aop.updateSubagentHeader` ([aop.ts:564](apps/server/src/trpc/routes/aop.ts)), `admin-subagent` — driven by the UI or the CLI, never by the agent.
   - A bound chat gets `FAMILY_TOOLS` in full ([run-chat-agent-sdk.ts:176](apps/server/src/mastra/workflows/chat/run-chat-agent-sdk.ts)), which is also the external MCP surface and the Master toolset ([master-surface.ts:61](apps/server/src/mastra/tools/master-surface.ts)) — so a new family reaches everything by being added to one array.
   - `config-write` cannot absorb agent actions: `MAX_INPUT_SCHEMA_BYTES` is 18,000 and every family is asserted against it ([tool-inputschema-size.test.ts:33](apps/server/src/mastra/mcp/external/__tests__/tool-inputschema-size.test.ts)); `table`, `board`, `workspace-write` and `systems-write` were each split off for that reason ([server.ts:106](apps/server/src/mastra/mcp/external/server.ts)).

7. **The folder is flat, except where it is not** — `agentNamespacePath` at [convention-paths.ts:105](apps/server/src/services/documents/convention-paths.ts)
   - `user/agent-{id}` | `organisation/agent-{id}` | `conversation/{convId}/agent-{id}`. Under it: `agentOverviewPath` → `…/overview` ([convention-paths.ts:121](apps/server/src/services/documents/convention-paths.ts)), `agentArchivePath` → `…/archives/{date}` ([convention-paths.ts:125](apps/server/src/services/documents/convention-paths.ts)), `agentMemoryDirPath` → `…/memory` ([memory-paths.ts:42](apps/server/src/services/agent-memory/memory-paths.ts)), `meetingNotesPath` → `conversation/{c}/agent-{a}/meetings/{eventId}/notes` ([convention-paths.ts:144](apps/server/src/services/documents/convention-paths.ts)). Filenames are otherwise free-form — `engagement-wiki` at [resolveOpenDoc.ts:35](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts) is a real one, and there is no allowlist anywhere.
   - **The split this design wants already exists for one agent.** [convention-paths.ts:396-488](apps/server/src/services/documents/convention-paths.ts) mints `organisation/agent-{id}/config/playbook`, `…/config/templates/{name}`, `…/config/demo-plays`, and `…/outputs/reps/{rep}/…`, with the rule written down at [convention-paths.ts:399](apps/server/src/services/documents/convention-paths.ts) and again at [agent-template.ts:1](apps/server/src/services/coaching/agent-template.ts): `config/` is what the customer edits and the agent reads on demand, `outputs/` is what the agent writes, and neither belongs in `memory/` because memory is injected every run and capped at 6,000 chars while the playbook is ~26,000.
   - Data after this step — two shapes in one table:
     ```json
     [ { "path": "user/agent-b41c…/overview" },
       { "path": "user/agent-b41c…/archives/2026-08-31" },
       { "path": "user/agent-b41c…/memory/corrections.md" },
       { "path": "organisation/agent-7f3…/config/playbook" },
       { "path": "organisation/agent-7f3…/outputs/reps/jesse/weekly/2026-W35" } ]
     ```

8. **Paths are written by prompts, and some of those prompts live in the database** — `automations.ts:1004-1059` ([automations.ts](apps/server/src/services/aop/automations.ts))
   - The `<agent_namespace>` block tells every automated run where to write: "Write files here using path…", "The primary document users will see is your overview", "Archive previous overviews to archives/{YYYY-MM-DD}". `AGENT_DOC_CONVENTION` at [agent-doc-convention.ts:7](apps/server/src/services/aop/agent-doc-convention.ts) adds the archive-then-write rule.
   - Four more copies bypass the helpers entirely as literal strings: [runSubagentTool.ts:271](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts), [execute-orchestrator.ts:144](apps/server/src/mastra/utils/execution/execute-orchestrator.ts), [chat-org-rules.ts:117](apps/server/src/mastra/workflows/chat/chat-org-rules.ts), [cedar-docs-awareness.ts:44](apps/server/src/mastra/prompts/cedar-docs-awareness.ts), plus the documents skill at `apps/server/.claude/skills/documents/SKILL.md:137`.
   - `coachingAgentBody` at [agent-template.ts:76](apps/server/src/services/coaching/agent-template.ts) bakes absolute `config/` and `outputs/` paths into the agent's instruction body, which is then **written into a document row at seed time** ([seed-coaching-agent.ts:255](apps/server/src/scripts/seed-coaching-agent.ts)). Changing a helper does not rewrite a seeded body. **This is why a path migration is the wrong shape for this change.**

9. **The reads** — `getAgentOutputs` at [outputs.ts:49](apps/server/src/services/agent-workspace/outputs.ts)
   - `owned` = everything under the namespace except `memory/` (excluded at [outputs.ts:99](apps/server/src/services/agent-workspace/outputs.ts)); `touched` via `document_updates.actor_agent_id`; `inConversations` a count plus a capped list built from a hand-written literal at [outputs.ts:186](apps/server/src/services/agent-workspace/outputs.ts). One undifferentiated `owned` list — a template and a produced brief are indistinguishable.
   - `AgentOutputTab` at [AgentOutputTab.tsx:170](apps/mail/modules/agents/components/AgentOutputTab.tsx) renders it with `FileBrowser`, dropping the memory node by exact match at [AgentOutputTab.tsx:133](apps/mail/modules/agents/components/AgentOutputTab.tsx) and passing `autoOpenPath={agentOverviewPath(agentId, scope)}` at [AgentOutputTab.tsx:185](apps/mail/modules/agents/components/AgentOutputTab.tsx); the browser opens that path ONCE per mount ([FileBrowser.tsx:193](apps/mail/modules/files/components/FileBrowser.tsx)). `AgentMemoryTab` is the SAME `FileBrowser` rooted one level deeper ([AgentMemoryTab.tsx:5](apps/mail/modules/agents/components/AgentMemoryTab.tsx)) — the tab set is already "one browser, two roots".
   - **`buildAgentDocs` is exactly one folder deep** — [resolveOpenDoc.ts:163](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts) splits `rest` on `/` and buckets anything with a tail under its first segment, so "anything deeper collapses into its top-level folder". The coaching agent's `outputs/reps/jesse/weekly/2026-W35` therefore already renders as a flat `outputs` bucket in the deal Files tab. It also picks `primary = files[0] ?? folders[0]?.docs[0]` with `overview` sorted first at [resolveOpenDoc.ts:176](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts) — the second copy of the hardcoded landing rule.

10. **Exact-path readers with no fallback** — the reason nothing may move
    - `prep-status.ts:359` ([prep-status.ts](apps/server/src/services/meetings/prep-status.ts)) does `eq(documents.path, agentOverviewPath(...))`. `MEETING_NOTES_PATH` at [doc-type-registry.ts:392](apps/server/src/services/documents/doc-type-registry.ts) is a fully anchored regex that decides the document's TYPE. `AGENT_ARCHIVE_PATH_RE` at [documents/index.ts:359](apps/server/src/services/documents/index.ts) stamps run metadata on any `agent-*/archives/*` write. `agent-read.ts:256` counts memory bytes with a `LIKE` and parses the agent id out of path segment 1. `documents_unique_path` at [documents-schema.ts:160](apps/server/src/db/documents-schema.ts) rejects any many-to-one path mapping. File grants are path-prefix and partly customer-authored inside document rows ([grants.ts:168](apps/server/src/services/agent-workspace/grants.ts)), so a restructure invalidates hand-written ones silently.

11. **The frontmatter is read and written** — [reference-resolver.ts:522](apps/server/src/services/playbook/reference-resolver.ts), [subagent-frontmatter.ts:117](apps/server/src/services/aop/subagent-frontmatter.ts), [agent-defaults/index.ts:404](apps/server/src/services/playbook/agent-defaults/index.ts)
    - `SubagentFrontmatter` ([reference-resolver.ts:34](apps/server/src/services/playbook/reference-resolver.ts)) has no `default_file`. `applySubagentFrontmatterPatch` can SET or ADD a key but not REMOVE one — `clean` skips `undefined`, so "back to the default" has no encoding. `upsertPlaybookSource` at [agent.ts:860](apps/server/src/trpc/routes/agent.ts) takes an `expectedVersion`, so any non-UI caller must read the version first.

12. **File access is grant-checked** — `fileGrantAllows` at [file-grants.ts:90](apps/server/src/services/agent-workspace/file-grants.ts)
    - `inherit` allows everything; a narrowed grant allows the agent's own namespace plus its entries. The agent's own CONFIG document is in neither, so a narrowed agent asked "what do your instructions say" is denied its own file.

## 3) Designed state

### 3.1 Architecture diagram

```text
  FOUR NAMESPACES, BY CLASSIFICATION — not one document row moves
  {ns}/agent-{agentId}/
    playbook       VIRTUAL — the instruction doc, physically still at
                   user/playbooks/{aop}/subagents/{name}.md  (isSubagentDocPath stamps
                   documentType there, refs resolve by that id, org-shadow keys on the name)
    config/        RESERVED — what the agent works FROM: templates, rubrics, checklists
    memory/        RESERVED — corrections · preferences · notes            (unchanged)
    outputs        EVERYTHING ELSE — the default namespace, so `overview`, `archives/…`,
                   `meetings/…`, `engagement-wiki` all classify here untouched, and a
                   literal `outputs/` prefix (the coaching agent) is STRIPPED for display
                   so both conventions render as one tree
         │
         ├──► AgentOutputTab = FileBrowser at the folder root → four groups, any depth
         ├──► AgentMemoryTab = the SAME browser rooted at memory/  (already how it works)
         └──► default_file: overview ──► autoOpenPath (stops being hardcoded)

  ONE RENDERER — services/agent-workspace/agent-context.ts
     ├─ identity + config    ← summary + frontmatter (model, folder, grants, default_file)
     ├─ PLAYBOOK             ← resolveAgentPlaybookContext (NEW): per block that refs this
     │                          agent — the trigger + the block's PROSE verbatim (what a run
     │                          gets as <additional_context>) + sibling refs in author order,
     │                          plus crossCuttingProse and the stage's instructions
     ├─ FILES                ← getAgentOutputs, grouped by namespace, EVERY file with its id
     └─ memory               ← listMemoryFiles (names + sizes)
            │                                    │
   ┌────────┴──────────┐              ┌──────────┴───────────┐
   │ BOUND THREAD      │              │ ATTACHED ITEM        │
   │ SYSTEM prompt +=  │              │ {kind:'agent', id}   │
   │ <your_playbook>   │              │ hydrateAgent()       │
   │ <your_workspace>  │              │ (+ instructions and  │
   │ EVERY turn, above │              │  memory — a subject, │
   │ <your_identity>   │              │  not the voice)      │
   └────────┬──────────┘              └──────────┬───────────┘
            ▲                                    │ @mention · manage-context
   OPEN AGENT VIEW = CHAT WITH THE AGENT         │
   selectedArtifact {kind:'agent', id} ──────────┘
     └─ useRouteChatThread, the SAME rule as a conversation:
          bound to this agent → owns its display, nothing to do
          chat is empty       → bind in place   (chat.bindAgent, BEFORE turn 1)
          chat is used        → fork, bind the fresh thread, carry the view
     └─ ChatContextRow: [🤖 Renewal Watch] [deal] [items…]   ← first chip, no X

  BUILD BY TALKING — the skill that already does this, loaded by default
     PAGE_AUTO_SKILLS gains a binding-keyed sibling → a BOUND thread auto-loads
     `playbook-authoring` (RESOLVE → READ+DIFF → ASK → RE-AUTHOR → WRITE → WIRE → VERIFY)
       ▲ writes go through the `document` tool, already agent_id-guarded by
         reconcileSubagentWrite; it gains the read-back verify+revert the CLI path has.
         Capability GRANTS stay in the UI — an agent that reads email must not be
         able to widen its own reach.
     New agent → agent.create → /agent?agentId=X&tab=config, chat already bound
```

### 3.2 Step-by-step walkthrough

1. **The four namespaces are named once, as a classification** — new `apps/server/src/services/agent-workspace/agent-namespaces.ts`
   - `AGENT_NAMESPACES = ['playbook', 'config', 'memory', 'outputs']`, `RESERVED_AGENT_PREFIXES = ['config/', 'memory/']`, plus `agentConfigPath(scope, agentId)` beside the existing `agentNamespacePath` ([convention-paths.ts:105](apps/server/src/services/documents/convention-paths.ts)) and `agentMemoryDirPath` ([memory-paths.ts:42](apps/server/src/services/agent-memory/memory-paths.ts)).
   - `classifyAgentPath(namespacePath, docPath)` returns `{ namespace, displayPath }`. `config/…` and `memory/…` are those namespaces with the prefix stripped; a literal `outputs/…` is `outputs` with the prefix stripped; **everything else is `outputs` with its path unchanged**. That last clause is the whole migration: `overview`, `archives/2026-08-31`, `meetings/e1/notes` and `engagement-wiki` file themselves correctly with no row moved, no helper re-pointed, and no seeded prompt body rewritten — which matters because prompt text, some of it stored in the database ([agent-template.ts:76](apps/server/src/services/coaching/agent-template.ts) via [seed-coaching-agent.ts:255](apps/server/src/scripts/seed-coaching-agent.ts)), is what actually writes these paths.
   - `playbook` never becomes a path under the folder: `isSubagentDocPath` ([convention-paths.ts:550](apps/server/src/services/documents/convention-paths.ts)) is a strict `subagents/{name}` match that stamps `documentType:'agent'`, `<ref>` resolution keys on the document id at that path, and the user-shadows-org merge keys on the filename. It is surfaced as a virtual row instead.
   - Data after this step:
     ```json
     { "user/agent-b41c…/overview":                    { "ns": "outputs", "display": "overview" },
       "user/agent-b41c…/archives/2026-08-31":         { "ns": "outputs", "display": "archives/2026-08-31" },
       "user/agent-b41c…/config/brief-template":       { "ns": "config",  "display": "brief-template" },
       "user/agent-b41c…/memory/notes.md":             { "ns": "memory",  "display": "notes.md" },
       "organisation/agent-7f3…/outputs/reps/jesse/weekly/2026-W35":
                                                       { "ns": "outputs", "display": "reps/jesse/weekly/2026-W35" } }
     ```

2. **The prompts learn the namespaces** — the `<agent_namespace>` block at [automations.ts:1004](apps/server/src/services/aop/automations.ts) and `AGENT_DOC_CONVENTION` at [agent-doc-convention.ts:7](apps/server/src/services/aop/agent-doc-convention.ts)
   - Both gain two sentences: `config/` is what you work FROM — templates, rubrics, checklists you or your owner wrote — and `memory/` is yours; everything else you write is output. The existing `overview` and `archives/` guidance is unchanged, because those paths are unchanged.
   - This is additive prompt text, not a migration. An agent that keeps writing exactly where it writes today stays correct; an agent that now puts a template in `config/` gets it filed as configuration instead of as a deliverable.

3. **`agent` becomes a context kind** — [chat-context.ts:15](apps/server/src/mastra/types/chat-context.ts) and [MessageTypes.ts:245](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts)
   - `CONTEXT_KINDS` gains `'agent'`; the id is the frontmatter `agent_id` — the same id `chat_threads.agent_id`, `{ns}/agent-{id}/` and `agent_executions.agent_id` key on.
   - Every `Record<ContextKind, …>` gains an entry: `PER_KIND_CAP_TOKENS.agent = 3_000` and `KIND_TITLE.agent = 'Agent'` ([hydrate.ts:37](apps/server/src/mastra/utils/context-items/hydrate.ts)); `CONTEXT_KIND_COLORS/ICONS/LABELS` ([contextKinds.ts:6](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/contextKinds.ts)); `MentionProvider.contextKind` ([AgentContextTypes.ts:103](apps/mail/modules/cedar-os/src/store/agentContext/AgentContextTypes.ts)). `DisplayArtifact` drops its explicit `| 'agent'`.

4. **The playbook context is resolved** — new `resolveAgentPlaybookContext` in `apps/server/src/services/agent-workspace/agent-playbook-context.ts`
   - The walk in `resolvePlaybookSources` ([agent-invocations.ts:68](apps/server/src/services/agent-workspace/agent-invocations.ts)) already tests `refersTo(block.nodes, documentId)`; it is extended to KEEP what it discards. The `playbook` variant of `AgentInvocationSource` gains `blockProse` (the block's `type:'text'` nodes in author order — byte-identical to the `refContextText` a run receives) and `blockRefs` (sibling refs with a marker on this agent's own, because order is the only signal about which instruction governs which ref — see `renderTriggerBlockBody` at [playbook-renderers.ts:64](apps/server/src/services/playbook/playbook-renderers.ts)).
   - The module adds the two section texts the walk never read: `global.crossCuttingProse` and the `instructions` of each stage carrying a referencing block.
   - Data after this step:
     ```json
     { "crossCuttingProse": "Never contact a deal that is closed-lost.",
       "sources": [
         { "trigger": { "type": "cron", "scope": "stage", "stage": "discovery",
                        "schedule": "0 7 * * 1-5", "timezone": "America/Los_Angeles" },
           "blockProse": "Prep every meeting on today's calendar, oldest first.",
           "blockRefs": [{ "id": "0e77…", "name": "meeting-prep", "isSelf": true },
                         { "id": "3a91…", "name": "next-steps", "isSelf": false }],
           "stageInstructions": "In discovery, lead with the problem, never the product." } ] }
     ```

5. **One renderer assembles the whole agent** — new `renderAgentContext` in `apps/server/src/services/agent-workspace/agent-context.ts`
   - Receives `{ db, agent: ResolvedAgent, userId, orgId, scope?, include: { instructions, memory } }` and returns two markdown blocks, `playbook` and `workspace`, so a caller can place them separately.
   - Calls `resolveAgentPlaybookContext` (step 4), `getAgentOutputs` ([outputs.ts:49](apps/server/src/services/agent-workspace/outputs.ts)) grouped through `classifyAgentPath`, and `listMemoryFiles` ([memory-store.ts:124](apps/server/src/services/agent-memory/memory-store.ts)). Each sub-read is individually `catch`-degraded to an "(unavailable)" line — the block must never fail a turn.
   - **Every file is listed, not a sample.** The point is that the agent knows its specific outputs; truncating at an arbitrary N is the failure it exists to prevent. `AGENT_WORKSPACE_MAX_CHARS` (24k, roughly 6k tokens) is the ceiling and the drop order when it binds is sibling ref names, then `touched`, then the oldest outputs — each replaced by a counted line naming the `document(list, …)` call that reads the rest. Nothing is silently omitted.
   - Data after this step:
     ```text
     <your_playbook>
     Your instruction document: `user/playbooks/9f2…/subagents/meeting-prep.md` (id `0e77…`),
     shown as "playbook" in your folder and as the Playbook section of your Config tab. Its
     body is your instructions below; read the file itself only to see your own frontmatter.

     Standing rules for every run here: Never contact a deal that is closed-lost.

     You are fired from 2 places:
     1. 30 minutes before a meeting (global)
        The block says, verbatim — your scheduled runs get this as <additional_context>:
          "Only for external meetings. Skip internal syncs."
        Refs in this block, in order: [you]
     2. Cron `0 7 * * 1-5` America/Los_Angeles — stage: Discovery
          "Prep every meeting on today's calendar, oldest first."
        Refs in this block, in order: [you] → next-steps
        Discovery stage instructions: "In discovery, lead with the problem, never the product."
     </your_playbook>

     <your_workspace>
     model sonnet · folder core · namespace user · chat on · memory on
     grants — files: inherit · mcp_servers: inherit · boards: none
     Your folder is `user/agent-b41c…`, in four namespaces:
       playbook  — your instructions (the document above)
       config/   — what you work FROM. 1 file:
                   config/brief-template — "Brief template" (id `f01…`)
       memory/   — corrections.md 412 chars · preferences.md 88 · notes.md 0
       outputs   — what you have produced. 3 files:
                   overview — "Meeting prep overview" (id `d19…`, 2026-09-01)  ← opens by default
                   archives/2026-08-31 (id `e2a…`)
                   archives/2026-08-30 (id `c88…`)
     Edited elsewhere: `user/kb/icp` (id `a10…`). Per-deal output: 12 files.
     Read any of these by id with the document tool. A reusable template goes in `config/`;
     anything you produce is output. Re-author your own configuration with the playbook-authoring skill.
     </your_workspace>
     ```
   - `include.instructions` appends the doc body and `include.memory` the memory text, for the attach path where neither is already in the prompt.

6. **Both blocks go into the system prompt of every bound turn** — `resolveBoundAgentForChat` at [bound-agent.ts:105](apps/server/src/mastra/workflows/chat/harness/bound-agent.ts) → `buildBoundAgentSystemPrompt` at [agent-chat-shell.ts:109](apps/server/src/mastra/agents/agent-chat-shell.ts)
   - `resolveChatAgentBinding` ([agent-binding.ts:63](apps/server/src/services/chat/agent-binding.ts)) returns the `ResolvedAgent` it already computed; `renderAgentContext` runs with `include: { instructions: false, memory: false }` — both already have their own sections and must not appear twice — on the same degrade-never-throw budget as memory.
   - Fixed order: `AGENT_CHAT_SHELL` → `AGENT_MEMORY_PREAMBLE` → `<memory>` → **`<your_playbook>`** → **`<your_workspace>`** → `<your_identity>` → `<your_instructions>` → optional `DEBUG_MODE_SECTION`. The instructions stay LAST, which is what makes an agent-specific rule beat a generic one (the existing ordering comment); the playbook is the frame, the instructions are the voice.
   - **The system prompt, not the per-turn context.** It is identical across the thread, so it is written once and cached rather than rebuilt per message, and it does not compete with the working-context budget the deal and the email thread draw from. It is still recomputed per turn, so a file written in turn 3 is listed at turn 4 with no cache to invalidate.

7. **The agent may always read its own config document** — `readFileGrant` / `fileGrantAllows` at [file-grants.ts:90](apps/server/src/services/agent-workspace/file-grants.ts)
   - `ResolvedFileGrant` gains `ownDocPath` (the subagent doc's `row.path`, already selected). `fileGrantAllows` returns true for `path === ownDocPath && need === 'read'` — the prompt now names that document, so a narrowed agent must be able to open it. WRITE stays governed by the grant and by the tool rules in step 12.

8. **An attached agent item hydrates** — new `hydrateAgent` in [hydrate.ts:147](apps/server/src/mastra/utils/context-items/hydrate.ts)
   - `resolveAgentForCaller` ([agent-read.ts:352](apps/server/src/services/agent-workspace/agent-read.ts)) is the authorization boundary, so a foreign id hydrates to `''` (the floor line). Then `renderAgentContext` with instructions and memory included — an attached agent is a SUBJECT being discussed rather than the voice answering — capped at `PER_KIND_CAP_TOKENS.agent`. `hydrateOne` gains `case 'agent'`.

9. **The client thread learns its binding** — [messageStorage.ts:194](apps/mail/modules/cedar-os/src/store/messages/messageStorage.ts), [databaseAdapter.ts:41](apps/mail/modules/cedar-os/src/store/messages/databaseAdapter.ts), [MessageTypes.ts:304](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts)
   - `MessageThreadMeta.agentId` and `MessageThread.agentId`; `listThreads` maps `thread.agentId`; `syncThreads` copies it on add AND on update (the diff check at [messageStorage.ts:267](apps/mail/modules/cedar-os/src/store/messages/messageStorage.ts) compares it too). `createThread` ([messagesSlice.ts:628](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts)) takes an optional `agentId`, and a new `setThreadAgent(threadId, agentId | null)` writes it on an existing thread.

10. **Opening the agent view binds the chat beside it** — the agent branch of `useRouteChatThread` at [useRouteChatThread.ts:144](apps/mail/modules/ux/layout/useRouteChatThread.ts), and new `bindThreadToAgent` in `apps/mail/modules/agents/chat/bind-thread-to-agent.ts` (replaces `start-agent-chat.ts`)
    - `openArtifact` at [useRouteChatThread.ts:88](apps/mail/modules/ux/layout/useRouteChatThread.ts) stops excluding `agent`. A thread OWNS an agent display when `active.agentId === artifact.id`, added to `ownsDisplay` beside the primary-conversation test, so re-entering a bound chat never re-scopes or forks.
    - Otherwise the open agent is a new context, and the rule is the conversation rule verbatim:
      - **empty chat** → bind in place: `setThreadAgent`, rename to `Chat with {name}` only if the name is still a default title, then `chat.bindAgent({ id, name, agentId })` immediately, because the row must carry the binding before turn one.
      - **used chat** → fork: `createThread(freshId, 'Chat with {name}', undefined, agentId)`, `switchThread`, carry the agent artifact onto the fresh thread, then `chat.bindAgent` for the fresh id. The used chat stays in the rail bound to whatever it was bound to.
      - **refused** (`chat_enabled: false`, unknown agent) → `setThreadAgent(id, null)` and a toast; the view stays open and the chat stays general. `AgentSummary` gains `chatEnabled` so a known-disabled agent skips the round trip; the server stays the authority.
    - Closing the view unbinds nothing — the binding is immutable once spoken, and an empty bound thread is reused or rebound by the next context exactly as an empty conversation-scoped one is.
    - Data after this step (fork case):
      ```json
      { "forkedFrom": "thr_1", "thread": { "id": "thr_2", "name": "Chat with Renewal Watch",
        "agentId": "b41c…", "selectedArtifact": { "kind": "agent", "id": "b41c…" } } }
      ```

11. **`chat.bindAgent` — the one server write** — new procedure in [chat.ts](apps/server/src/trpc/routes/chat.ts), beside `createThread` at [chat.ts:190](apps/server/src/trpc/routes/chat.ts)
    - `assertAgentBindable` ([chat.ts:151](apps/server/src/trpc/routes/chat.ts)) first. Then: no row → INSERT `{ id, userId, name, agentId }`; row with no messages → UPDATE `agent_id` (and the name if a default title); row with messages and a different agent → the same BAD_REQUEST `updateThread` throws at [chat.ts:239](apps/server/src/trpc/routes/chat.ts), from one shared constant. Idempotent for the same agent.
    - `startAgentChat` and its `createThread` prop are deleted; `chat.createThread`'s `agentId` input stays for the CLI and tests.

12. **The agent configures itself with the skill group that already exists** — [page-skills.ts:8](apps/server/src/mastra/skills/page-skills.ts) and `apps/server/.claude/skills/playbook-authoring/SKILL.md`
    - **No new tool family.** `playbook-authoring` is already a loadable skill and already owns this exact flow: `RESOLVE → READ + DIFF → ASK → RE-AUTHOR → PRESENT → WRITE (guarded) → WIRE → VERIFY → REPORT`, with the agent-defaults registry and its `<!-- customize: … -->` markers behind it ([agent-defaults/index.ts:336](apps/server/src/services/playbook/agent-defaults/index.ts)). The write itself is the `document` tool, which `reconcileSubagentWrite` ([convention-paths.ts:590](apps/server/src/services/documents/convention-paths.ts)) already guards by pinning the `agent_id`. Asking the chat to build an agent works today; nothing loads the skill that knows how.
    - So the change is the DEFAULT. `PAGE_AUTO_SKILLS` ([page-skills.ts:8](apps/server/src/mastra/skills/page-skills.ts)) pre-injects skills per page; a bound agent thread gets the same treatment keyed on the binding rather than the page — `playbook-authoring` is loaded for every chat that is with an agent, so "tell it what it does" works on the first message instead of after the model decides to call `load-skill`.
    - **Close the one gap the skill names about itself.** Its own guarantee section says the `cedar-cli` path reads the saved doc back and runs `verifySubagentDoc`, reverting if malformed, while "the MCP path is guarded but never verifies or reverts". Since the in-product chat is now the primary way an agent gets built, that path gets the same read-back-verify-revert, so a chat-authored agent cannot be left malformed.
    - Grants stay out of it for the same reason as before: an agent's instructions are shaped by content it reads, so widening its own reach must stay a human action in the Connections section.

13. **New agent lands in a working conversation** — [AgentsGrid.tsx:57](apps/mail/modules/agents/components/AgentsGrid.tsx), [AgentPickerDialog.tsx:72](apps/mail/modules/home/widgets/AgentPickerDialog.tsx)
    - Both create paths open `?agentId=X&tab=config` instead of an empty Files tab. Step 10 binds the chat beside it, step 6 gives the new agent a `<your_playbook>` block that honestly says nothing is configured to fire it, and step 12 gives it the tool to fix that. "Click New agent, then tell it what it does" works because each part is in place — no separate onboarding flow.

14. **`default_file` is parsed, typed and patched** — [reference-resolver.ts:34](apps/server/src/services/playbook/reference-resolver.ts), [subagent-frontmatter.ts:33](apps/server/src/services/aop/subagent-frontmatter.ts), [agent-defaults/index.ts:404](apps/server/src/services/playbook/agent-defaults/index.ts)
    - `SubagentFrontmatter.default_file?: string` — a path RELATIVE to the agent's folder (`overview`, `outputs/weekly-report`, `config/playbook`) or the literal `none`. Relative and never a document id, because the folder differs per scope and a duplicate or publish must keep working against new rows.
    - New pure `resolveAgentDefaultFile(frontmatter)` in `apps/server/src/services/agent-workspace/default-file.ts`: absent → `overview` (today's behaviour exactly); `none` → none; otherwise normalised by `validateDefaultFilePath` (no leading `/`, no `..`, not under `memory/`, no `,`), with an invalid value treated as absent rather than thrown at read time.
    - `applySubagentFrontmatterPatch` learns to REMOVE: scalar patch values become `string | null` and a `null` filters the entry out. `PATCHABLE_FRONTMATTER_KEYS` gains `default_file` and `when_to_use`. `composeFrontmatter` and `AuthorSubagentParams` gain `defaultFile`, creation-only like `folder`.
    - Data after this step:
      ```text
      ---

      name: meeting-prep

      default_file: outputs/weekly-report

      ---
      ```

15. **The read model carries it** — [types.ts:112](apps/server/src/services/agent-workspace/types.ts), [agent-read.ts:186](apps/server/src/services/agent-workspace/agent-read.ts), [aop-agents.ts:85](apps/server/src/services/aop/aop-agents.ts)
    - `AgentSummary.defaultFile: string | null` — the RESOLVED relative path, so no client re-implements the default; plus `chatEnabled`. `SubagentDocAgent` / `AopAgentWithInstructions` gain `defaultFile`, because the deal Files tab reads that list.

16. **Both file surfaces open it** — [AgentOutputTab.tsx:185](apps/mail/modules/agents/components/AgentOutputTab.tsx), [resolveOpenDoc.ts:176](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts)
    - `AgentOutputTab` takes `defaultFile` and computes `autoOpenPath` by joining `agentNamespacePath(agentId, scope)` with it; `undefined` (still loading) and `null` (`none`) both open nothing. It also drops the exact-match memory exclusion at [AgentOutputTab.tsx:133](apps/mail/modules/agents/components/AgentOutputTab.tsx) so all four namespaces render as one tree, while `AgentMemoryTab` keeps its deeper root and its char-count chrome.
    - `buildAgentDocs` prefers the doc at `…/agent-{id}/{defaultFile}` as `primary` and first row, groups by `classifyAgentPath`, and **stops collapsing at one folder deep** ([resolveOpenDoc.ts:163](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts)) — that limit already flattens the coaching agent's `outputs/reps/{rep}/weekly/{week}` today.

17. **Setting it: the route, the workspace, creation and the CLI** — [agent.ts:1069](apps/server/src/trpc/routes/agent.ts), [AgentActionMenu.tsx:130](apps/mail/modules/agents/components/AgentActionMenu.tsx), [ConversationFileTree.tsx:742](apps/mail/modules/conversations/components/files/ConversationFileTree.tsx), [cli.ts:480](apps/server/src/agent-admin/cli.ts)
    - `agent.setDefaultFile` takes `{ agentId, defaultFile: string | null }` where `null` resets and `'none'` opens nothing; it accepts a path with no document yet (the agent may write it later) and returns `exists: false` so the UI can say so. Same `requireAgent` → `readAgentDocForPatch` → `writeDocument` sequence as `setFileGrants` — one writer.
    - The ⋯ menu gains a **Default file** submenu beside Folder; the Files tab row ⋮ gains **Open by default** / **Stop opening by default** through a new `extraRowItems` prop on `ConversationFileTree` forwarded by `FileBrowser`. `agent.create` gains `defaultFile`; the CLI gains `--default-file` and `agent default-file <id> <path|none|reset>`.

19. **An agent document renders as the AGENT, and its frontmatter as a form** — new `AgentDocumentView` and `AgentSettingsSection` in `apps/mail/modules/agents/components/`
    - Five surfaces rendered a subagent document as generic prose: the Brain knowledge explorer ([CompanyExplorer.tsx:2238](apps/mail/modules/company/components/CompanyExplorer.tsx)), the file browser's open-file view ([FileBrowser.tsx:641](apps/mail/modules/files/components/FileBrowser.tsx)), the home artifact panel ([FileArtifactPanel.tsx:282](apps/mail/modules/home/components/FileArtifactPanel.tsx)), the playground playbook editor and the CRM-updater panel. Exactly ONE mounted the structured header, and all five showed the YAML frontmatter as body text whenever `HideFrontmatterExtension` ([HideFrontmatterExtension.ts:28](apps/mail/modules/documents/agent/HideFrontmatterExtension.ts)) had nothing to hide — it keys on node types, hiding from a leading `horizontalRule` to the second one, so a block markdown-it did not parse into that exact run leaves it inert.
    - So the fix is not a better hider. `AgentDocumentView` resolves `documentId → agentId` off `agent.list` (which is also the authorization boundary) and renders `AgentView` on Config, embedded. Keyed on `documentType === 'agent'`, stamped by `resolveEffectiveDocumentType` on every subagent-path write — not on the path regex, which was duplicated in two files, absent from three, and deliberately excluded `crm-updater.md`, leaving that one document with neither a header nor hiding anywhere.
    - Under the Playbook heading there are two containers: `AgentSettingsSection` collapsed, then the document. `AgentDocHeader` is deleted, and the owned-CRM-fields callout is a row inside the form rather than a second card — it used to render in both places at once.
    - **The hider was looking for the wrong node shape.** `extractFrontmatter` ([frontmatter.ts:36](apps/server/src/services/document-saving/frontmatter.ts)) peels a leading YAML block before markdown-it sees it and re-emits it as ONE `codeBlock` carrying the `frontmatter` sentinel — precisely so the key-per-line structure survives the round trip. `HideFrontmatterExtension` was still looking for the pre-peeling shape (a `horizontalRule` run), found none, and hid nothing, which is why an agent's whole configuration rendered as a grey box beneath its own settings form. It now keys on the sentinel, keeping the hr-run as a fallback for a Y.Doc persisted before the peeling landed.
    - **One measure for the reading column.** The Brain explorer centred a document at `100ch` while every panel that opens the same kind of thing centres at `80ch` ([ConversationScrollArea.tsx:19](apps/mail/modules/conversations/components/ConversationScrollArea.tsx)), so an agent document opened there put an 80ch column inside a 100ch one. The explorer now uses 80ch. The header rendered three keys; the section renders every one, with `readAgentHeaderFields` ([agent-header.ts](apps/server/src/services/agent-workspace/agent-header.ts)) resolving each to the value the RUNTIME uses so an absent key reads as `core`/`overview`/`true` rather than as a blank. The form carries only the LIVE decisions: description and when-to-use as long-form inputs, model, folder, default file (a select over the files the agent actually has, so a path nothing will be written to cannot be chosen), memory as captured-or-injected, and the CRM fields as one ordinary row. The agent's id and name are the workspace header one row up; its namespace, chat switch and fill instructions are set once at creation with no writer behind them, so a row each was furniture in a column of controls.
    - **Unknown keys get their own section.** `parseFrontmatter` drops what it does not recognise, which is right for the runtime and wrong for a form: silently omitting a line the user wrote makes the form lie about the document. `readCustomFrontmatterEntries` reads them off the raw block and the UI states plainly that they are preserved and read by nothing.
    - Every section is addressable (`?tab=config#settings|#sources|#connections|#instructions`), and `AGENT_CHAT_SHELL` now tells a bound agent to end a configuration change by naming the section and giving that link — a change the user cannot go and look at is one they have to take on trust.

### 3.3 Schema

Full schema:

```ts
// ── Agent folder namespaces (apps/server/src/services/agent-workspace/agent-namespaces.ts) NEW
export const AGENT_NAMESPACES = ['playbook', 'config', 'memory', 'outputs'] as const;
export type AgentNamespace = (typeof AGENT_NAMESPACES)[number];
/** The only two prefixes that are RESERVED under an agent folder. */
export const RESERVED_AGENT_PREFIXES = ['config/', 'memory/'] as const;

export function agentConfigPath(scope: AgentDocScope, agentId: string): string;  // …/config

/**
 * Which namespace a document under the agent folder belongs to, and what to call it there.
 *   config/x          → { namespace: 'config',  displayPath: 'x' }
 *   memory/notes.md   → { namespace: 'memory',  displayPath: 'notes.md' }
 *   outputs/reps/a/b  → { namespace: 'outputs', displayPath: 'reps/a/b' }   (prefix stripped)
 *   overview          → { namespace: 'outputs', displayPath: 'overview' }   (default namespace)
 *   archives/2026-…   → { namespace: 'outputs', displayPath: 'archives/2026-…' }
 * `outputs` is the DEFAULT, which is what lets every legacy path classify with no row moved.
 */
export function classifyAgentPath(
  namespacePath: string,
  docPath: string,
): { namespace: Exclude<AgentNamespace, 'playbook'>; displayPath: string };

// UNCHANGED on purpose — every one is addressed by exact path, anchored regex, or a
// prompt string stored in a document row:
//   agentOverviewPath  → {ns}/overview            (prep-status.ts:359 does eq() on it)
//   agentArchivePath   → {ns}/archives/{date}     (AGENT_ARCHIVE_PATH_RE stamps metadata)
//   meetingNotesPath   → …/meetings/{id}/notes    (MEETING_NOTES_PATH decides the doc TYPE)
//   agentMemoryDirPath → {ns}/memory              (MEMORY_PATH_MATCHERS are the memory ACL)

// ── Subagent frontmatter (apps/server/src/services/playbook/reference-resolver.ts) ─────
export interface SubagentFrontmatter {
  name?: string;
  description?: string;
  when_to_use?: string;
  model?: string;
  output_type?: string;
  enabled?: boolean;
  mcp_servers?: string[];
  files?: string[];
  boards?: string[];
  namespace?: string;              // 'user' | 'org' (raw; resolved at use)
  chat_enabled?: boolean;
  memory_enabled?: boolean;
  memory_inject?: boolean;
  folder?: string;
  system?: boolean;
  agent_id?: string;
  fill_instructions?: string;
  /**
   * NEW — which file the Files tab opens. A path RELATIVE to the agent's folder
   * (`overview`, `outputs/weekly-report`, `config/playbook`) or the literal `none`.
   * ABSENT ⇒ `overview`, which is exactly today's behaviour. Never a document id: the
   * folder differs per scope, and a duplicate or publish must work against new rows.
   */
  default_file?: string;
}

// apps/server/src/services/agent-workspace/default-file.ts — NEW
export type AgentDefaultFile = { kind: 'path'; relative: string } | { kind: 'none' };
export const DEFAULT_AGENT_FILE = 'overview';
export function resolveAgentDefaultFile(fm: Pick<SubagentFrontmatter, 'default_file'>): AgentDefaultFile;
export function validateDefaultFilePath(raw: string): { ok: true; relative: string } | { ok: false; reason: string };
export function agentDefaultFilePath(scope: AgentDocScope, agentId: string, fm: SubagentFrontmatter): string | null;

// apps/server/src/services/aop/subagent-frontmatter.ts
export const PATCHABLE_FRONTMATTER_KEYS = [
  'name', 'description', 'model', 'fill_instructions', 'when_to_use',  // when_to_use NEW
  'folder', 'files', 'mcp_servers',
  'default_file',                                                       // NEW
] as const;
export type SubagentHeaderPatch = Partial<Record<ScalarPatchKey, string | null>> & {  // null REMOVES
  files?: string | string[];
  mcp_servers?: string | string[];
};

// ── Playbook context (apps/server/src/services/agent-workspace/types.ts) ───────────────
export type AgentInvocationSource =
  | {
      kind: 'playbook';
      trigger: PlaybookTriggerRef;
      sourceId?: string;
      label: string;
      runs30d: null;                 // always null — not attributable at this grain
      enabled: boolean;
      /** NEW — the block's text nodes in author order; identical to a run's
       *  `<additional_context>`. Null for a refs-only block. */
      blockProse: string | null;
      /** NEW — every ref in the block, in author order; `isSelf` marks this agent. */
      blockRefs: Array<{ id: string; name: string | null; isSelf: boolean }>;
      /** NEW — the stage's `instructions`; null for a global block. */
      stageInstructions: string | null;
    }
  | { kind: 'agent'; from: { agentId: string; name: string }; declared: boolean;
      label: string; runs30d: number; enabled: boolean }
  | { kind: 'unknown'; raw: unknown; label: string; runs30d: null; enabled: boolean };

// apps/server/src/services/agent-workspace/agent-playbook-context.ts — NEW
export interface AgentPlaybookContext {
  crossCuttingProse: string | null;   // compiled.global.crossCuttingProse; never read until now
  sources: AgentInvocationSource[];
}

// apps/server/src/services/agent-workspace/agent-context.ts — NEW
export const AGENT_WORKSPACE_MAX_CHARS = 24_000;
export interface AgentContextBlocks {
  playbook: string;        // <your_playbook> body
  workspace: string;       // <your_workspace> body — the four namespaces, every file
  instructions?: string;   // only when include.instructions (the attach path)
  memory?: string;         // only when include.memory
}
export function renderAgentContext(db: DB, params: {
  agent: ResolvedAgent; userId: string; orgId: string;
  scope?: AgentDocScope; include: { instructions: boolean; memory: boolean };
}): Promise<AgentContextBlocks>;

// apps/server/src/mastra/agents/agent-chat-shell.ts
export function buildBoundAgentSystemPrompt(params: {
  agentName: string;
  instructions: string;
  memoryEnabled?: boolean;
  memory?: string | null;
  debugMode?: boolean;
  playbook?: string | null;     // NEW — <your_playbook>, above <your_identity>
  workspace?: string | null;    // NEW — <your_workspace>, below it
}): string;

// ── Agent read model (apps/server/src/services/agent-workspace/types.ts) ───────────────
export interface AgentSummary {
  agentId: string;
  name: string;
  description: string | null;
  enabled: boolean;
  scope: AgentScope;                 // 'user' | 'org' — where the DOCUMENT lives
  namespace: AgentNamespaceScope;    // 'user' | 'org' — where the FOLDER lives
  documentId: string;
  documentPath: string;
  avatar: string | null;
  model: string | null;
  isSystemDefault: boolean;
  folder: AgentFolder;               // 'core' | 'background' | 'in-conversation'
  lastRunAt: string | null;
  runCount7d: number;
  runCount30d: number;
  memoryChars: number;
  defaultFile: string | null;        // NEW — resolved relative path, null for `none`
  chatEnabled: boolean;              // NEW — frontmatter chat_enabled !== false
}

// apps/server/src/services/agent-workspace/outputs.ts
export interface AgentOutputFile {
  documentId: string; path: string; name: string;
  documentType: string; metadata: unknown; updatedAt: string | null;
  namespace: Exclude<AgentNamespace, 'playbook'>;   // NEW — from classifyAgentPath
  displayPath: string;                              // NEW — path within its namespace
}
export interface AgentOutputs {
  owned: AgentOutputFile[];                         // now INCLUDES memory rows
  byNamespace: Record<'config' | 'memory' | 'outputs', AgentOutputFile[]>;   // NEW
  touched: AgentOutputFile[];
  inConversations: { count: number; files: AgentOutputFile[] };
}

// apps/server/src/services/agent-workspace/file-grants.ts
export interface ResolvedFileGrant {
  agentId: string; agentName: string; grant: ParsedGrant;
  namespace: AgentNamespaceScope; ownNamespace: string;
  ownDocPath: string;                // NEW — the subagent doc; readable regardless of grant
}

// ── Chat context (server + client mirror) ──────────────────────────────────────────────
export const CONTEXT_KINDS = ['conversation','email_thread','slack_thread','linkedin_chat',
  'whatsapp_chat','file','task','agent'] as const;                          // 'agent' NEW
export interface ContextItem {
  kind: ContextKind;
  id: string;                        // agent → the frontmatter agent_id
  label?: string;
  addedBy?: 'user' | 'agent';
  addedAt?: string;
}

// apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts
export interface MessageThread {
  id: string; name?: string; color?: string; chatContext?: ChatContext;
  agentId?: string | null;           // NEW — mirrored from chat_threads.agent_id
  updatedAt?: string; lastLoaded?: string; hasMoreMessages?: boolean;
  messages: Message[];
  status?: 'idle' | 'streaming' | 'finished';
  pinned?: boolean; lastActiveAt?: string; createdThisSession?: boolean; inputContent?: string;
  selectedArtifact?: Exclude<DisplayArtifact, { kind: 'agenda' }> | null;
}
export interface MessageThreadMeta {
  id: string; title: string; updatedAt: string; color?: string; context?: ChatContext;
  agentId?: string | null;           // NEW
}
export type DisplayArtifact =
  | { kind: ContextKind | 'canvas'; id: string }   // CHANGED — 'agent' now comes from ContextKind
  | { kind: 'agenda' };

// ── tRPC ───────────────────────────────────────────────────────────────────────────────
// chat.bindAgent — NEW. Upsert the ACTOR binding before the first message.
input:  { id: string; name?: string; agentId: string }
output: { id: string; agentId: string; name: string; created: boolean }
// no row → insert; unspoken row → update agent_id (+ default name); spoken row with a
// different agent → BAD_REQUEST, from the same constant chat.updateThread throws.

// agent.setDefaultFile — NEW
input:  { agentId: string; defaultFile: string | null }   // null = reset; 'none' = open nothing
output: { agentId: string; defaultFile: string | null; exists: boolean }

// agent.create — CHANGED
input:  { name: string; description?: string; model?: string; folder?: AgentFolder;
          aopId?: string; defaultFile?: string }          // defaultFile NEW

// ── Auto-loaded skills (apps/server/src/mastra/skills/page-skills.ts) ─────────────────
export const PAGE_AUTO_SKILLS: Partial<Record<string, ChatSkillName[]>>;   // unchanged
/** NEW — skills every chat BOUND to an agent carries, resolved beside the page map. */
export const BOUND_AGENT_AUTO_SKILLS: ChatSkillName[];   // ['playbook-authoring']
export function getAutoSkillsForChat(params: {
  page: string | undefined;
  boundAgentId: string | null;
}): ChatSkillName[];
// No new MCP tool family. The write is the `document` tool, already agent_id-guarded by
// reconcileSubagentWrite; it gains the read-back verifySubagentDoc + revert that
// authorSubagentDoc performs, which is the one gap the playbook-authoring skill names.
```

Relationship diagram:

```text
  ┌──────────────────────────────┐        ┌────────────────────────────────────────────┐
  │ chat_threads                 │        │ documents (the agent doc)                  │
  │  id                          │        │  id                                        │
  │  user_id                     │        │  path = …/playbooks/{aop}/subagents/{n}.md │
  │  agent_id ──logical──────────┼───────►│  metadata.agent_id / frontmatter.agent_id  │
  │  context jsonb               │        │  frontmatter.default_file (NEW, relative)  │
  │   ▼ contains ChatContext     │        │  frontmatter.files / mcp_servers / folder  │
  │     primaryConversation      │        └──────────────┬─────────────────────────────┘
  │     items[]                  │            agent_id   │   surfaced as the VIRTUAL
  │       {kind:'agent', id} ────┼── same id ────────────┤   `playbook` row in the tree
  │       {kind:'file',  id} ────┼──FK──► documents.id   │   (never moved — isSubagentDocPath)
  └──────────────────────────────┘                       ▼
                                    ┌──────────────────────────────────────────────────┐
  MessageThread (client)            │ documents under {namespace}/agent-{agentId}/     │
   agentId (NEW) ◄── agent_id       │   config/…   RESERVED — what it works FROM       │
   chatContext.items[]              │   memory/…   RESERVED — corrections · prefs      │
                                    │   everything else ──classifyAgentPath──► outputs │
  compiled_playbook (jsonb on       │     overview · archives/… · meetings/… ·         │
   the PLAYBOOK.md doc)             │     engagement-wiki · outputs/reps/… (stripped)  │
   ▼ contains blocks                │   {default_file} ──────► opened first (NEW)      │
     nodes[] = text | ref           └──────────────────────────────────────────────────┘
        └─ ref.id ──► documents.id (the agent doc)
        └─ text nodes ──► blockProse ──► <your_playbook> in chat
                                    └──► <additional_context> on a run   (same bytes)

  renderAgentContext ──reads──► agent doc + playbook blocks + all four namespaces
       └─ used by: the bound system prompt (every turn)  ·  hydrateAgent (an attached item)
  playbook-authoring (auto-loaded when bound) ──document tool──► the agent doc
                      └──► PLAYBOOK.md trigger blocks (add/remove ref)
```

## 4) Implementation phases

### Phase 1 — Four namespaces, by classification

**Goal:** the agent folder reads as four namespaces and every existing document lands in the right one, with no row moved and no path helper re-pointed.

- [x] Create `apps/server/src/services/agent-workspace/agent-namespaces.ts` — `AGENT_NAMESPACES`, `RESERVED_AGENT_PREFIXES`, `agentConfigPath`, and `classifyAgentPath` returning `{ namespace, displayPath }` per §3.3: `config/` and `memory/` are reserved, a literal `outputs/` prefix is stripped, and everything else is `outputs` unchanged.
- [x] Mirror `classifyAgentPath` and `agentConfigPath` in [agent-paths.ts](apps/mail/modules/agents/utils/agent-paths.ts), which already keeps local copies of `agentNamespacePath` and `agentMemoryDirPath` because the server modules pull in the DB layer. Leave the second client mirror at [buildDocPath.ts:35](apps/mail/modules/files/store/buildDocPath.ts) alone — no path it mints changes.
- [x] Add `namespace` + `displayPath` to `AgentOutputFile` and `byNamespace` to `AgentOutputs` ([types.ts:411](apps/server/src/services/agent-workspace/types.ts)), populated in `getAgentOutputs` ([outputs.ts:49](apps/server/src/services/agent-workspace/outputs.ts)); stop excluding `memory/` from `owned` at [outputs.ts:99](apps/server/src/services/agent-workspace/outputs.ts) and let the grouping carry the distinction. Keep the `%/memory/%` exclusion on the `touched` arm at [outputs.ts:147](apps/server/src/services/agent-workspace/outputs.ts) — that one is about OTHER agents' memory.
- [x] `AgentOutputTab` ([AgentOutputTab.tsx:133](apps/mail/modules/agents/components/AgentOutputTab.tsx)) drops its exact-match memory exclusion so all four namespaces render as one tree; `AgentMemoryTab` keeps its deeper root and char-count chrome, which is a rooted view of the same browser rather than a second model.
- [ ] Add a virtual `playbook` row at the top of the agent's tree via a new `leadingRows` prop on `FileBrowser` (mirroring `trailingRows` at [FileBrowser.tsx:135](apps/mail/modules/files/components/FileBrowser.tsx)); clicking it opens the instructions editor, the same claim `onOpenFile` ([FileBrowser.tsx:118](apps/mail/modules/files/components/FileBrowser.tsx)) already makes for `PLAYBOOK.md`.
- [ ] Fix the depth limit in `buildAgentDocs` ([resolveOpenDoc.ts:163](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts)): build a real nested tree instead of one bucket per first segment, and group the top level by `classifyAgentPath`. This is a live bug — the coaching agent's `outputs/reps/{rep}/weekly/{week}` already flattens.

**Tests:**

- [x] `apps/server/src/services/agent-workspace/__tests__/agent-namespaces.test.ts` — `config/x` and `memory/notes.md` classify with the prefix stripped; `outputs/reps/a/b` → outputs with `reps/a/b`; each legacy shape (`overview`, `archives/2026-08-31`, `meetings/e1/notes`, `engagement-wiki`) → outputs with its path intact; a path outside the folder throws.
- [x] Extend `apps/server/src/services/agent-workspace/__tests__/outputs.test.ts` — `byNamespace` splits a mixed folder; `owned` now includes memory rows; `touched` still excludes every agent's memory.
- [x] Extend `apps/mail/modules/agents/__tests__/AgentOutputTab.test.tsx` — the tree shows all four namespaces and the virtual `playbook` row opens the instructions editor rather than a document.
- [ ] Extend the `resolveOpenDoc` tests under `apps/mail/modules/conversations/components/files/__tests__/` — a 4-deep coaching path keeps its structure instead of flattening.
- [x] `timeout 300 pnpm --filter @zero/server exec vitest run src/services/agent-workspace/__tests__` and `cd apps/mail && npx jest modules/agents/__tests__ modules/conversations/components/files`

### Phase 2 — The prompts learn the namespaces

**Goal:** an agent writing a template puts it in `config/`, and everything else it writes is output — stated where agents actually read it.

- [x] Extend the `<agent_namespace>` block at [automations.ts:1004](apps/server/src/services/aop/automations.ts) with the `config/` and `memory/` sentences. Leave every existing path line untouched.
- [x] Extend `AGENT_DOC_CONVENTION` at [agent-doc-convention.ts:7](apps/server/src/services/aop/agent-doc-convention.ts) with the same two sentences, since it is appended to both non-agenda branches.
- [x] Add the namespaces to the path table in `apps/server/.claude/skills/documents/SKILL.md:137` and to [cedar-docs-awareness.ts:44](apps/server/src/mastra/prompts/cedar-docs-awareness.ts).
- [ ] Leave the four hardcoded `conversation/{cid}/agent-{aid}/overview` prompt strings ([runSubagentTool.ts:271](apps/server/src/mastra/tools/event-execution/runSubagentTool.ts), [execute-orchestrator.ts:144](apps/server/src/mastra/utils/execution/execute-orchestrator.ts), [chat-org-rules.ts:117](apps/server/src/mastra/workflows/chat/chat-org-rules.ts), `SKILL.md:163`) pointing where they point — `overview` does not move — but add a one-line comment at each naming `classifyAgentPath` as the reason they are still correct.

**Tests:**

- [x] `apps/server/src/services/aop/__tests__/agent-doc-convention.test.ts` — the shared convention still carries its overview/archives rules, names both reserved prefixes, and says what `config/` is FOR; the two path helpers the block interpolates mint correctly for all three scopes. (The `<agent_namespace>` block itself is assembled inside `buildAutomationRuntimeContext`, which needs a live runtime context — asserting the exported constant is the part that can be unit-tested.)
- [ ] Instruction eval via [playbook-instruction-eval](apps/server/.claude/skills/playbook-instruction-eval): told to save a reusable brief template, the agent writes under `config/`; told to produce this week's brief, it does not.
- [x] `timeout 300 pnpm --filter @zero/server exec vitest run src/services/aop/__tests__`

### Phase 3 — `default_file` in the frontmatter, the read model, and the patcher

**Goal:** the key exists end to end on the server: parsed, resolved, patchable including removal, composed at creation, and on every agent list.

- [x] Add `default_file?: string` to `SubagentFrontmatter` ([reference-resolver.ts:34](apps/server/src/services/playbook/reference-resolver.ts)) with the §3.3 comment, and `case 'default_file'` in `parseFrontmatter` ([reference-resolver.ts:522](apps/server/src/services/playbook/reference-resolver.ts)).
- [x] Create `apps/server/src/services/agent-workspace/default-file.ts` with `DEFAULT_AGENT_FILE = 'overview'`, `resolveAgentDefaultFile`, `validateDefaultFilePath` and `agentDefaultFilePath`. An invalid value resolves as absent; nothing throws on read.
- [x] Let `applySubagentFrontmatterPatch` ([subagent-frontmatter.ts:117](apps/server/src/services/aop/subagent-frontmatter.ts)) remove a key=[redacted] patch values become `string | null` and a `null` filters the entry out, including from the synthesised block. Add `default_file` and `when_to_use` to `PATCHABLE_FRONTMATTER_KEYS`.
- [x] Add `defaultFile` to `CustomFrontmatterFields` + `composeFrontmatter` ([agent-defaults/index.ts:404](apps/server/src/services/playbook/agent-defaults/index.ts)) and `AuthorSubagentParams` ([author-subagent.ts:36](apps/server/src/services/playbook/author-subagent.ts)), creation-only like `folder`.
- [x] Add `defaultFile` and `chatEnabled` to `AgentSummary` ([types.ts:112](apps/server/src/services/agent-workspace/types.ts)), populated in `resolveAgents` ([agent-read.ts:186](apps/server/src/services/agent-workspace/agent-read.ts)); add `defaultFile` to `mapSubagentDocRow` ([aop-agents.ts:85](apps/server/src/services/aop/aop-agents.ts)).

**Tests:**

- [x] `apps/server/src/services/agent-workspace/__tests__/default-file.test.ts` — absent → `overview`; `none` → none; `outputs/x` and `config/y` → paths; `../x`, `/x`, `memory/notes`, `a,b` → invalid, resolved as absent; `agentDefaultFilePath` for all three scopes.
- [x] Extend `apps/server/src/services/aop/__tests__/subagent-frontmatter.test.ts` — patching `default_file` adds it in blank-line form; `null` removes it and leaves every other line byte-identical; removing an absent key is a no-op.
- [x] Extend `apps/server/src/services/agent-workspace/__tests__/agent-create.test.ts` — a create with `defaultFile` round-trips; without it the key is absent, not written as the default.
- [x] `timeout 300 pnpm --filter @zero/server exec vitest run src/services/agent-workspace/__tests__ src/services/aop/__tests__/subagent-frontmatter.test.ts`

### Phase 4 — Writing the default file: route, create input, CLI

**Goal:** every human-driven writer of `default_file` exists and goes through the one frontmatter patcher.

- [x] Add `agent.setDefaultFile` to [agent.ts](apps/server/src/trpc/routes/agent.ts) beside `setFileGrants` ([agent.ts:1069](apps/server/src/trpc/routes/agent.ts)): `null` resets, `'none'` opens nothing, a path is validated and normalised; returns `exists` for a path with no document yet.
- [x] Add `defaultFile` to `agent.create` ([agent.ts:994](apps/server/src/trpc/routes/agent.ts)) through `createAgentDoc` ([agent-create.ts:150](apps/server/src/services/agent-workspace/agent-create.ts)).
- [x] `cedar-cli agent create … [--default-file <path>]` and `cedar-cli agent default-file <agentId> <path|none|reset>` ([cli.ts:480](apps/server/src/agent-admin/cli.ts)); update the usage block at [cli.ts:14](apps/server/src/agent-admin/cli.ts) and print the default file in `agent get` beside `memory`.

**Tests:**

- [ ] `apps/server/src/trpc/routes/__tests__/agent-default-file.test.ts` — writes and reads back resolved; `null` resets; `'none'` → null; an invalid path is BAD_REQUEST; another user's agent is NOT_FOUND; `exists` is false for an unwritten path.
- [ ] Extend the agent CLI test — `create --default-file` forwards it; `default-file <id> reset` sends `null`.
- [ ] `timeout 300 pnpm --filter @zero/server exec vitest run src/trpc/routes/__tests__/agent-default-file.test.ts`

### Phase 5 — Both file surfaces open the default file

**Goal:** the workspace Files tab and the deal Files tab both land on the declared file.

- [x] `AgentOutputTab` ([AgentOutputTab.tsx:185](apps/mail/modules/agents/components/AgentOutputTab.tsx)) takes `defaultFile` and computes `autoOpenPath` from the folder path plus it; `undefined` (still loading) and `null` (`none`) open nothing. `AgentView` ([AgentView.tsx:283](apps/mail/modules/agents/components/AgentView.tsx)) passes `agent?.defaultFile`.
- [x] Delete `agentOverviewPath` from [agent-paths.ts:50](apps/mail/modules/agents/utils/agent-paths.ts) once the Output tab is no longer its caller. The SERVER helper of the same name stays — `prep-status.ts:359` and `deal-overview-updates.ts:75` address it.
- [x] `buildAgentDocs` ([resolveOpenDoc.ts:176](apps/mail/modules/conversations/components/files/resolveOpenDoc.ts)) takes `defaultFile` per agent and prefers that doc as `primary` and first row, falling back to today's overview-first rule.

**Tests:**

- [x] Extend `apps/mail/modules/agents/__tests__/AgentOutputTab.test.tsx` — `outputs/brief` opens that path on mount; `null` opens nothing; an absent key matches today's behaviour exactly.
- [x] Extend the `resolveOpenDoc` tests — a declared default is `primary` and sorts first; absent falls back; a declared default with no matching doc falls back.
- [x] `cd apps/mail && npx jest modules/agents/__tests__/AgentOutputTab.test.tsx modules/conversations/components/files`

### Phase 6 — Setting the default file from the workspace

**Goal:** a user can pick the default file from the ⋯ menu or from a file row, and see which file it is.

- [x] Create `apps/mail/modules/agents/hooks/use-set-agent-default-file.ts` — the mutation plus invalidation of `agent.get`, `agent.list` and `aopAgents.listForAop`, toasting on error and on `exists: false` ("Will open once the agent writes it").
- [x] Add a **Default file** submenu to `AgentActionMenu` ([AgentActionMenu.tsx:130](apps/mail/modules/agents/components/AgentActionMenu.tsx)) beside Folder: a radio over the outputs and config files from `agent.getOutputs.byNamespace`, plus "Nothing", with the current value as the trailing hint. Use `OptionPicker`'s filter above ~10 entries, per crystallized.md.
- [x] Add `extraRowItems?: (node: TreeNode) => FileRowMenuItem[]` to `ConversationFileTree` ([ConversationFileTree.tsx:742](apps/mail/modules/conversations/components/files/ConversationFileTree.tsx)), appended to `editItems`, and forward it through `FileBrowser` ([FileBrowser.tsx:574](apps/mail/modules/files/components/FileBrowser.tsx)).
- [x] `AgentOutputTab` supplies **Open by default** / **Stop opening by default** on file rows, and an "Opens by default" tag in the row's `meta` slot.

**Tests:**

- [ ] Extend `apps/mail/modules/agents/__tests__/AgentOutputTab.test.tsx` — the row menu item appears and writes the RELATIVE path (not the document id); it reads "Stop opening by default" on the file that already is one.
- [ ] New `apps/mail/modules/agents/__tests__/AgentActionMenu.test.tsx` — the submenu lists the files and "Nothing"; selecting writes; "Nothing" sends `'none'`; the default entry sends `null`.
- [x] `cd apps/mail && npx jest modules/agents/__tests__`

### Phase 7 — `agent` is a context kind, and the bound agent sees its whole playbook and files

**Goal:** on every bound turn the system prompt carries the agent's playbook and its actual files; the same assembly hydrates an attached `agent` item.

- [ ] Add `'agent'` to `CONTEXT_KINDS` ([chat-context.ts:15](apps/server/src/mastra/types/chat-context.ts)) and document the id in `ContextItemSchema`.
- [ ] Extend the `playbook` variant of `AgentInvocationSource` ([types.ts:200](apps/server/src/services/agent-workspace/types.ts)) with `blockProse`, `blockRefs` and `stageInstructions`, populated in `walkSection` ([agent-invocations.ts:94](apps/server/src/services/agent-workspace/agent-invocations.ts)) — the walk already holds `block.nodes`. Author order is load-bearing.
- [ ] Create `apps/server/src/services/agent-workspace/agent-playbook-context.ts` — `resolveAgentPlaybookContext`, adding `global.crossCuttingProse` and each referencing stage's `instructions`. Pure over the compiled blob.
- [ ] Create `apps/server/src/services/agent-workspace/agent-context.ts` — `renderAgentContext` returning `{ playbook, workspace, instructions?, memory? }` per §3.2 step 5, grouping files by namespace, listing every one with its id, marking the default, and enforcing `AGENT_WORKSPACE_MAX_CHARS` with the documented drop order and a counted line for anything dropped.
- [ ] Add `playbook` and `workspace` params to `buildBoundAgentSystemPrompt` ([agent-chat-shell.ts:109](apps/server/src/mastra/agents/agent-chat-shell.ts)), emitted between `<memory>` and `<your_identity>`.
- [ ] Have `resolveChatAgentBinding` ([agent-binding.ts:63](apps/server/src/services/chat/agent-binding.ts)) return its `ResolvedAgent`, and `resolveBoundAgentForChat` ([bound-agent.ts:105](apps/server/src/mastra/workflows/chat/harness/bound-agent.ts)) call `renderAgentContext` on the memory's degrade budget.
- [ ] Extend `ResolvedFileGrant` with `ownDocPath` and let `fileGrantAllows` ([file-grants.ts:90](apps/server/src/services/agent-workspace/file-grants.ts)) permit `read` of it regardless of grant mode; write stays governed.
- [ ] Add `case 'agent'` → `hydrateAgent` in [hydrate.ts:347](apps/server/src/mastra/utils/context-items/hydrate.ts) with `PER_KIND_CAP_TOKENS.agent = 3_000` and `KIND_TITLE.agent`. **Rebase first** — a concurrent session has this file open (it added `contextBriefForDocumentType` to `hydrateFile`).
- [ ] Add `agent` to the `ambientPromoteHint` kinds ([chat-workflow.ts:1400](apps/server/src/mastra/workflows/chat/chat-workflow.ts)) and the `manage-context` description ([manageContextTool.ts:85](apps/server/src/mastra/tools/chat/manageContextTool.ts)).

**Tests:**

- [ ] `apps/server/src/services/agent-workspace/__tests__/agent-playbook-context.test.ts` — a block's prose is captured verbatim and equals what the dispatcher builds from the same blob; sibling refs keep author order with `isSelf` correct; a refs-only block yields null prose; cross-cutting prose and stage instructions are carried.
- [ ] `apps/server/src/services/agent-workspace/__tests__/agent-context.test.ts` — the workspace block names the config doc, groups files under all four namespaces, lists every file with its id, marks the default; over the cap it drops in the documented order and says so; it still renders when any sub-read throws.
- [ ] Extend `apps/server/src/mastra/workflows/chat/__tests__/agent-bound-chat.test.ts` — the prompt carries `<your_playbook>` with the trigger prose and `<your_workspace>` with the file list, in order above `<your_instructions>`; the body and the memory appear exactly once each.
- [ ] Extend `apps/server/src/services/agent-workspace/__tests__/file-grants.test.ts` — a narrowed grant allows `read` of `ownDocPath` and denies `write`.
- [ ] `apps/server/src/mastra/utils/context-items/__tests__/hydrate-agent.test.ts` — hydrates with instructions and memory for the owner; floors for a foreign id.
- [ ] `timeout 300 pnpm --filter @zero/server exec vitest run src/services/agent-workspace/__tests__ src/mastra/workflows/chat/__tests__/agent-bound-chat.test.ts src/mastra/utils/context-items`

### Phase 8 — Opening the agent view is a chat with the agent

**Goal:** having an agent open in the context column binds the chat beside it, and that chat shows the agent as its first, pinned chip.

- [ ] Add `chat.bindAgent` to [chat.ts](apps/server/src/trpc/routes/chat.ts) per §3.2 step 11, sharing the immutability refusal text with `updateThread` ([chat.ts:239](apps/server/src/trpc/routes/chat.ts)) via one exported constant.
- [ ] Add `agentId` to `MessageThreadMeta` and `MessageThread`; map it in `listThreads` ([databaseAdapter.ts:41](apps/mail/modules/cedar-os/src/store/messages/databaseAdapter.ts)) and copy it in `syncThreads` on add and update ([messageStorage.ts:255](apps/mail/modules/cedar-os/src/store/messages/messageStorage.ts)).
- [ ] `createThread` ([messagesSlice.ts:628](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts)) accepts an optional `agentId`; add `setThreadAgent` to the slice.
- [ ] Create `apps/mail/modules/agents/chat/bind-thread-to-agent.ts` — the store writes, the eager `chat.bindAgent` call, and the rollback on refusal. Delete `start-agent-chat.ts` and its test.
- [ ] Rewrite the agent branch of `useRouteChatThread` ([useRouteChatThread.ts:144](apps/mail/modules/ux/layout/useRouteChatThread.ts)) per §3.2 step 10: stop excluding `agent` from `openArtifact` ([useRouteChatThread.ts:88](apps/mail/modules/ux/layout/useRouteChatThread.ts)); `ownsDisplay` when `active.agentId === artifact.id`; bind in place when empty, fork when used; skip a `chatEnabled: false` agent. Update the matching exclusion in [enterChatThread.ts](apps/mail/modules/ux/layout/enterChatThread.ts).
- [ ] Rewrite the "an agent is not attachable to a chat" comment on the `?agentId` block ([LayoutUrlSync.tsx:170](apps/mail/modules/ux/layout/LayoutUrlSync.tsx)) and confirm a cold `/agent?agentId=X` still lands on a thread that then binds.
- [ ] Add `'agent'` to client `ContextKind` ([MessageTypes.ts:245](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts)), drop the explicit `| 'agent'` from `DisplayArtifact`, and add `agent` to `CONTEXT_KIND_COLORS/ICONS/LABELS` ([contextKinds.ts:6](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/contextKinds.ts)) with the `Bot` icon.
- [ ] `ChatContextRow` ([ChatContextRow.tsx:78](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/ChatContextRow.tsx)): render the bound agent as the first chip with `AgentAvatar` and no X; render an attached `agent` item with the same face and an X; skip an item duplicating the bound agent.
- [ ] Let the chat's landing effect restore `{ kind: 'agent', id: agentId }` for a bound thread with nothing displayed, gated by `mayAutoOpenArtifact` ([enterChatThread.ts:52](apps/mail/modules/ux/layout/enterChatThread.ts)).

**Tests:**

- [ ] `apps/server/src/trpc/routes/__tests__/chat-bind-agent.test.ts` — inserts when absent; rebinds an unspoken row; refuses a spoken row with the shared wording; idempotent for the same agent; refuses `chat_enabled: false` and a foreign agent.
- [ ] `apps/mail/tests/modules/ux/routeChatThreadAgent.test.tsx` — an empty chat binds in place and calls `bindAgent` before any message; a used chat forks, switches, carries the artifact and binds the fresh id; re-entering a bound chat does not fork; a refusal clears the local binding; closing the view leaves the binding.
- [ ] Extend the context-row tests — a thread with `agentId` renders the agent chip first without a remove button; an attached item renders with one; a general thread renders none.
- [ ] `cd apps/mail && npx jest tests/modules/ux modules/cedar-os modules/agents` and `timeout 300 pnpm --filter @zero/server exec vitest run src/trpc/routes/__tests__/chat-bind-agent.test.ts`

### Phase 9 — Build the agent by talking to it, and render it as one thing

**Goal:** an agent document opens as the agent, its frontmatter is a form rather than a wall of YAML, and a bound chat carries the configuration knowledge by default.

- [x] Create `apps/server/src/services/agent-workspace/agent-header.ts` — `readAgentHeaderFields` (every known key, resolved to what the runtime uses) and `readCustomFrontmatterEntries` (the keys `parseFrontmatter` drops). Wire it onto `agent.get` as `header`.
- [x] Make `when_to_use` editable through the one header writer — `EDITABLE_HEADER_KEYS` ([subagent-frontmatter.ts:20](apps/server/src/services/aop/subagent-frontmatter.ts)) and the `aop.updateSubagentHeader` input ([aop.ts:565](apps/server/src/trpc/routes/aop.ts)).
- [x] Create `AgentSettingsSection` as its own collapsed container under the Playbook heading, above the document — the live fields only, plus a Custom metadata block.
- [x] Fix `HideFrontmatterExtension` to key on the `frontmatter` sentinel code block the server actually emits, with the legacy hr-run as a fallback, and cover both in `tests/modules/documents/agentFrontmatter.test.ts`.
- [x] Align the Brain explorer's reading column to `80ch`, the measure every other document panel uses.
- [x] Make `memory_inject` editable through the one header writer, so Memory is a captured-or-injected choice rather than a read-only line.
- [x] Give `AgentFieldsCallout` a `bare` mode and render it as an ordinary stacked row, dropping its second mount above the editor.
- [x] Create `AgentDocumentView` — `documentId → agentId` off `agent.list`, rendering `AgentView` embedded on Config; keyed on `documentType === 'agent'`.
- [x] Route the three prose render sites through it: [CompanyExplorer.tsx](apps/mail/modules/company/components/CompanyExplorer.tsx), [FileBrowser.tsx](apps/mail/modules/files/components/FileBrowser.tsx), [FileArtifactPanel.tsx](apps/mail/modules/home/components/FileArtifactPanel.tsx). Delete `AgentDocHeader` and the Brain explorer's path-based subagent plumbing with it.
- [x] Add `<changing_your_own_configuration>` to `AGENT_CHAT_SHELL` — say what you changed and link to the section (`?tab=config#…`), and grants are not yours to widen.
- [x] Auto-load a configuration skill for a BOUND thread: `getAutoSkillsForChat` ([page-skills.ts](apps/server/src/mastra/skills/page-skills.ts)), called from the `autoSkillsSection` build ([chat-workflow.ts:1320](apps/server/src/mastra/workflows/chat/chat-workflow.ts)).
- [ ] Land both create paths on `?agentId=X&tab=config` — `openAgent` ([AgentsGrid.tsx:57](apps/mail/modules/agents/components/AgentsGrid.tsx)) and the picker ([AgentPickerDialog.tsx:72](apps/mail/modules/home/widgets/AgentPickerDialog.tsx)).
- [ ] Register `playbook-authoring` for the in-app chat. It is the better-shaped skill for building an agent and owns the whole flow, but it lives only in `.claude/skills` — the Claude Code / external-MCP catalog — and its guarantees are written around `cedar-cli subagent author`, a command the in-app agent does not have. `cedar-configuration` is auto-loaded instead for now.
- [ ] Give the in-product subagent write path the read-back-`verifySubagentDoc`-and-revert that `authorSubagentDoc` performs, which is the gap the playbook-authoring skill names about the MCP path.
- [ ] Route the two remaining prose renders of a subagent doc: the playground playbook editor ([PlaybookEditor.tsx:639](apps/mail/app/(routes)/playground/components/PlaybookEditor.tsx)) and the CRM-updater panel ([CrmAgentConfigPanel.tsx:313](apps/mail/modules/aop/components/CrmAgentConfigPanel.tsx)), which has never hidden its frontmatter.

**Tests:**

- [x] `apps/server/src/services/agent-workspace/__tests__/agent-header.test.ts` — absent keys resolve to what the runtime uses; every known key is read; ABSENT and EMPTY stay apart on the grants; unknown keys come back verbatim and in order; the dead `permissions` key is not "custom"; a block-list key reports once.
- [x] `apps/mail/modules/agents/__tests__/AgentDocumentView.test.tsx` — keys on the document type, not a path; opens the workspace on Config, embedded; an unresolvable agent says so rather than falling back to prose.
- [ ] A test that the Settings section renders a read-only row for each field with no writer, and the Custom metadata block only when there are unknown keys.
- [x] `timeout 300 pnpm --filter @zero/server exec vitest run src/services/agent-workspace` and `cd apps/mail && npx jest modules/agents tests/modules/company tests/modules/files`

### Phase 10 — `@` mention an agent (optional)

**Goal:** a chat can carry an agent as a subject without opening it.

- [ ] Widen `MentionProvider.contextKind` ([AgentContextTypes.ts:103](apps/mail/modules/cedar-os/src/store/agentContext/AgentContextTypes.ts)) and create `apps/mail/modules/agents/hooks/use-agent-mention-provider.ts` (`@`, `contextKind: 'agent'`, items from `trpc.agent.list`, `AgentAvatar` as the menu icon); register it beside `useConversationMentionProvider` ([useConversationMentionProvider.ts:79](apps/mail/modules/conversations/hooks/useConversationMentionProvider.ts)). `mentionSuggestion.ts` ([mentionSuggestion.ts:205](apps/mail/modules/cedar-os/src/components/chatInput/mentionSuggestion.ts)) already attaches the item.

**Tests:**

- [ ] `apps/mail/modules/agents/__tests__/use-agent-mention-provider.test.tsx` — items filter by name; selection produces `{ kind: 'agent', id, label }`.
- [ ] `cd apps/mail && npx jest modules/agents/__tests__`

### Phase 11 — Docs and wiki

**Goal:** the next reader finds this without reading the code.

- [ ] Add the four namespaces, `default_file` and the context badge to [agent-document-type.md](apps/server/docs/wiki/agent-document-type.md), and the `agent` kind to [chat-context-set.md](apps/server/docs/wiki/chat-context-set.md).
- [ ] Document the bound system-prompt order (shell → memory → playbook → workspace → identity → instructions) in [agent-workspace.md](apps/mail/docs/agent-workspace.md) beside the Phase 8 composition, and note that block prose reaches a chat as `<your_playbook>` and a run as `<additional_context>` from the same compiled nodes.
- [ ] Write `apps/server/docs/wiki/agent-namespaces.md` — the classification rule, why nothing moved, and the coaching agent as the convention it generalises ([convention-paths.ts:399](apps/server/src/services/documents/convention-paths.ts)).
- [ ] Note in `apps/server/.claude/skills/playbook-authoring/SKILL.md` that it is auto-loaded in a bound agent chat, and that the MCP write path now verifies and reverts like the CLI one.

**Tests:**

- [ ] `timeout 300 pnpm --filter @zero/server run types` and `timeout 300 pnpm --filter @zero/mail run types` both green.