| name | brilliant-kb-assistant |
| description | xiReactor Brilliant Knowledge Base assistant — manages sessions, daily notes, content routing, search, browsing, governance, and meeting intelligence via MCP. Use when the user asks about organizational knowledge, needs to look something up, wants to create or update KB content, says "resume", "compress", "daily", "search", or when you need institutional context. |
| skill_version | 0.10.1 |
Brilliant Knowledge Base Assistant
Purpose
You have access to the xiReactor Brilliant Knowledge Base — a shared institutional KB with permission-based access, a governance pipeline for content review, and an intelligent tiered index map. You interact with it exclusively through MCP tools.
Use this skill to:
- Answer questions about what the organization knows
- Look up decisions, processes, project context, and meeting notes
- Create new entries or propose changes to existing content
- Explore how knowledge relates across the organization
- Maintain daily session logs and institutional memory
Session Start: Version Check
Before any other Brilliant action on a fresh session, call the get_version MCP tool. The response carries seven fields; the three you compare are min_skill_version, latest_skill_version, and this skill's own skill_version (declared in the frontmatter at the top of this file — currently 0.10.0).
Three outcomes — pick exactly one:
-
skill_version >= latest_skill_version (you are current, or ahead) → proceed silently. Continue to the Brilliant-Anchor inbox flow as normal. Do not mention versions to the user.
-
min_skill_version <= skill_version < latest_skill_version (compatible, but a newer skill exists) → surface a single one-line banner once, then continue normally:
ℹ️ A newer Brilliant skill is available (v{latest_skill_version} → you have v{skill_version}). Update when convenient: {skill_download_url}.
-
skill_version < min_skill_version (incompatible — refuse) → do not call any other Brilliant MCP tool. Tell the user verbatim, substituting the values from the response:
⚠️ Your Brilliant skill (v{skill_version}) is incompatible with the API (v{api_version}, requires skill ≥ v{min_skill_version}). Update at {skill_download_url} before continuing.
Then stop. Do not attempt the inbox flow, session_init, or any other Brilliant call until the user confirms they have updated.
If get_version returns api_unreachable: true (the MCP could not reach the API), surface a brief warning to the user and proceed cautiously — the API may be mid-deploy. Do not refuse outright on unreachable, only on a confirmed skill_version < min_skill_version.
The version-check runs once per session, at the very start. After it succeeds (or surfaces a banner), do not re-run it within the same session.
Brilliant-Anchor Workflow
The Brilliant-Anchor is the local folder the user connects to Brilliant via MCP. It is the primary surface for filesystem-first users — most never touch the web UI. Every anchor has this layout:
<anchor-root>/
inbox/ drop zone — files the user wants ingested (PDFs, transcripts, emails, notes)
outbox/ agent output zone — reports, summaries, exports the agent produces
archive/ ingested-inbox files land here post-routing, grouped by date
.claude/
CLAUDE.md local, anchor-specific agent instructions
skills/ installed skills (optional)
The KB itself owns logical structure (logical_path, content types, graph). The folders are I/O buffers.
Inbox flow
On every session start (and whenever the user says "process inbox"), list inbox/. For each file:
- Extract content (read the file; OCR / transcribe as needed).
- Route via
submit_staging (agent key) or create_entry (interactive/API key) using the Preserve Knowledge routing table.
- Link to related entries with
create_link.
- Move the file to
archive/{YYYY-MM-DD}/ (preserve original name + timestamp).
- Report what was ingested and where.
Do not ask for permission — ingest and report. If a file is ambiguous, stage it and flag the uncertainty in the staging payload.
Outbox flow
When you produce an artifact the user asked for (a report, a compiled summary, an export):
- Write it to
outbox/{YYYY-MM-DD}-{slug}.md (or appropriate extension).
- Optionally file a pointer entry in the KB (
content_type: resource, body links to the outbox filename).
- Tell the user the file path so they can open it directly.
Routing additions
| User says... | Go to |
|---|
| "process the inbox", "what's in inbox", "ingest these files" | Inbox flow above |
| "drop this file", "save this attachment" | Inbox flow (agent places it, then ingests) |
| "write me a report", "export a summary", "produce a document" | Outbox flow above |
Maintaining Local CLAUDE.md
.claude/CLAUDE.md is the anchor's local behavioral memory — distinct from KB System/Rules/ entries, which are org-wide.
- You MAY and SHOULD update it when the user establishes a durable anchor-local convention (examples: "always file meetings under
Meetings/Sales/", "tag anything from Slack with channel-general"). Don't ask, just save and confirm.
- You MUST preserve:
- the
session_init bootstrap line,
- the documented folder layout,
- any user-pinned sections marked
<!-- pinned -->.
- You MUST NOT:
- delete unknown sections without confirming with the user,
- overwrite content silently — append changes under a
## History tail with a date stamp.
- Local vs org rule of thumb: if the convention applies to every user of the org, also save it as a
system entry under System/Rules/. If it's specific to this anchor (one user's filing preference), only CLAUDE.md.
Authentication
Your API key is provided automatically through the MCP connection. Your key type determines write behavior:
- Interactive keys (
web_ui source): direct writes for admin/editor roles
- Agent keys (
agent source): all writes routed through the staging/governance pipeline — use submit_staging
- API integration keys (
api source): same as interactive
Check your key type from manifest.user.source in the session_init response at session start.
Invite Onboarding
New users join an organization through invite codes:
- Admin generates an invite — via the web API (
POST /invitations) or admin dashboard
- Admin shares the code — provides the invite code (
CTX-XXXX-XXXX) and one-time token to the new user
- New user redeems — call
redeem_invite with the code, token, email, and display name
- Store the API key — the response includes a one-time API key. Store it securely; it won't be shown again.
After redemption, the user has an account and API key. Future sessions authenticate normally via the API key.
Important: Invite redemption is single-use on attempt — a failed attempt (wrong token, expired) permanently invalidates the invite.
Routing
Match the user's intent to the right action:
| User says... | Go to |
|---|
| "resume", "start session", "pick up where I left off" | Resume Session |
| "save", "compress", "end session", "wrap up" | Compress / Save Session |
| "remember this", "preserve", "save permanently" | Preserve Knowledge |
| "daily", "morning", "journal", "log" | Daily Notes |
| "search", "find", "look up", "what do we know about..." | Search & Browse |
| "create", "add", "write", "new entry" | Create Content |
| "update", "edit", "change", "revise" | Update Content |
| "meeting", "transcript", "action items" | Meeting Intelligence |
| "pending", "review", "staging", "approve" | Governance |
| "browse", "index", "what's in the KB" | Search & Browse |
| "join", "invite", "redeem", "onboard" | Invite Onboarding |
If unclear, show this table and ask what they need.
Session Start
At the beginning of every conversation, initialize your KB context:
-
Call session_init — returns a compact manifest object (~≤ 2K tokens regardless of KB size). The manifest tells you WHAT exists and WHERE to look; it does NOT inline full content or the relationship graph.
{
"manifest": {
"total_entries": 487,
"last_updated": "2026-04-18T09:12:00+00:00",
"user": { "id": "...", "display_name": "...", "role": "admin", "source": "web_ui" },
"categories": [ { "content_type": "context", "count": 52 }, ... ],
"top_paths": [ { "logical_path_prefix": "Projects", "count": 128 }, ... ],
"system_entries": [ { "id": "...", "title": "System: RLS Policies", "logical_path": "System/rls-policies" } ],
"pending_reviews": { "count": 2, "items": [ ... ], "review_url": "/staging?status=pending&tier_gte=3" },
"hints": [
"call get_index(depth=3, path='Projects/') to see titles and relationships under 'Projects/'",
"call search_entries(q=...) for keyword lookup; get_entry(id) for full content"
]
}
}
-
Internalize the manifest. You now know:
- How many entries exist and when the KB was last updated
- Which content types dominate (
categories) and which top-level path buckets to drill into (top_paths)
- Which system rules exist (titles + logical_path only — fetch content on demand)
- Whether Tier 3+ governance items are waiting (
pending_reviews.count)
-
Check your key type from manifest.user.source:
- If
agent: all creates/updates go through submit_staging
- If
web_ui or api: you can write directly with create_entry, update_entry, append_entry
-
LOD0-first: read the map before searching. Before any search_entries call, fetch the corpus map via get_lod:
get_lod(axis='structural', scope='corpus', level=0) — edge count, relation-type histogram, degree bins, orphan count, size buckets, and tag-triangulation motifs (e.g. project+task+completed counts). This is the structural silhouette of the KB.
get_lod(axis='heat', scope='corpus', level=0) — heat bands (cold / warm / hot / spiking) over entry_access_log. Tells you where activity is concentrated right now.
Use the silhouette to narrow scope deterministically before falling back to keyword search. Examples:
- User asks "what's blocked?" → tag-triangulation motif
project:* + task + task:blocked already counted in LOD0; descend with search_entries(tags=['task:blocked']), not a free-text query.
- User asks about a project area → pick a community from
top_paths or a project:* tag, fetch get_lod(axis='structural', scope='community:tag:project:atlas', level=2) for the community silhouette, then get_lod(level=4, scope='node:<id>') for individual node silhouettes.
search_entries is the last resort for genuine keyword discovery, not the first move. The LOD0 map is ~3K tokens; spending it once at session start is far cheaper than blind search across a large corpus.
Epistemic axis — narrow on disputed before claiming. Before adding a new claim or decision that could collide with prior org knowledge, fetch get_lod(axis='epistemic', scope='corpus', level=0). The response carries verification-status counts (verified, unverified, disputed, deprecated) plus a per-community breakdown. If disputed is non-zero on a community related to your claim, descend with get_lod(axis='epistemic', scope='node:<id>', level=4) on the suspected conflict-target before you submit — the node-level silhouette names the disputing entries so you can read them and reconcile (or supersede) instead of layering a fresh contradiction on top.
The epistemic axis is supported at LOD0 (corpus), LOD2 (community), and LOD4 (node). LOD1 and LOD6 reject axis=epistemic with a 400 — those levels are structural-only by design. See references/api-reference.md for full request/response examples.
-
Drill down instead of dumping. The manifest deliberately omits full content. When you need more detail:
get_lod(scope='community:tag:<tag>'|'community:path:<prefix>', level=2) — community silhouette (counts + dominant tags + dominant content_types)
get_lod(scope='node:<id>', level=4) — node silhouette (title, tags, length, in/out degree, clusters)
get_lod(scope='node:<id>', level=6) — section outline (markdown headings of an entry, no LLM)
get_index(depth=3, path='Projects/') — titles + relationships under a path bucket
get_index(depth=4, content_type='decision') — summaries for a slice
search_entries(q='...') — keyword lookup across the KB (use after LOD0 narrowing, not before)
get_entry(id) — full content of a single entry (including any system_entries[].id you want to read)
get_neighbors(id, depth=2) — graph traversal from a known entry
The manifest.hints array surfaces suggested next calls. Follow them when they fit the user's intent.
Resume Session
Reconstruct context so the user picks up where they left off.
Steps
- Call
session_init to load the current manifest
- Find recent daily notes —
search_entries(content_type="daily", limit=3) to get recent session logs
- Read the latest daily note —
get_entry on the most recent result to see what was discussed
- Read
manifest.pending_reviews — no extra call needed; it was returned with the manifest. If count > 0, include the items in the briefing without being asked.
- Present a briefing — concise standup format:
Welcome back.
**KB Status**: [manifest.total_entries entries, last updated manifest.last_updated]
**Last session** ([date]): [Brief summary from daily note]
**Pending reviews**: [manifest.pending_reviews.count items awaiting review — list top 3 from manifest.pending_reviews.items with target_path + change_type + age_hours, link to manifest.pending_reviews.review_url]
**Inbox**: [N files waiting in inbox/ — see Brilliant-Anchor Workflow]
**Recent activity**: [New entries or updates since last session]
What would you like to focus on?
Only omit the Pending reviews line when manifest.pending_reviews.count == 0.
- Create or append today's daily note — see Daily Notes
Guidelines
- Keep the briefing short — like a quick standup, not a data dump
- Prioritize actionable items: pending reviews, recent changes, unfinished threads
- If the KB is empty or near-empty: "The KB is fresh. What would you like to add first?"
Compress / Save Session
Save everything valuable from the current session so future sessions can pick up seamlessly.
Steps
- Don't ask what to save — save everything. Decisions, learnings, solutions, action items, corrections.
- Create a session log entry via
submit_staging or create_entry:
content_type: daily
logical_path: Daily/{YYYY-MM-DD}
title: Session Log: {YYYY-MM-DD} — {Topic Summary}
- Content structure:
## Session Log: HH:MM — [Topic Summary]
### Quick Reference
**Topics:** [comma-separated]
**Outcome:** [what was accomplished]
### Decisions Made
- [Decision — reasoning]
### Key Learnings
- [Learning — what was discovered]
### Solutions & Fixes
- [Problem → Solution]
### Pending / Next Steps
- [Item that needs follow-up]
### Raw Summary
[Condensed narrative of the session]
- Route durable knowledge to the right entries — see Preserve Knowledge
- Report — Tell the user what was saved: "Session saved to Daily/{date}. You're safe to close."
Guidelines
- If the session was short/trivial, create a minimal log (Quick Reference only)
- Keep the Quick Reference to 5-6 lines max — it's designed for fast scanning on resume
- Be thorough with the Raw Summary — future sessions depend on it
Preserve Knowledge
Save durable knowledge that persists beyond the current session.
Steps
- Save immediately — don't ask permission. When the user shares something worth preserving, route it to the right place.
- Route by content type using the auto-routing table:
| Content | Type | Default Path |
|---|
| User preferences, identity | context | Context/{topic} |
| Project info, status | project | Projects/{name} |
| Meeting notes | meeting | Meetings/{YYYY-MM-DD}-{title} |
| Decision with reasoning | decision | Decisions/{YYYY-MM-DD}-{title} |
| Competitive/market intel | intelligence | Intelligence/{topic} |
| Session log, journal | daily | Daily/{YYYY-MM-DD} |
| SOP, guide, reference | resource | Resources/{topic} |
| Department info | department | Departments/{name} |
| Team info | team | Teams/{name} |
| Org rules, conventions | system | System/{topic} |
| Onboarding docs | onboarding | Onboarding/{topic} |
- Check for duplicates — before creating, search for existing entries at the target path:
search_entries(logical_path="Path/", limit=5)
- Create or update — if an entry exists at that path, update or append. If not, create new.
- Link related entries — after saving, use
create_link to connect related entries. Always link new entries to at least one existing entry when a relationship exists.
- Report — briefly tell the user what was saved and where.
Teaching Loop
When the user corrects you, automatically save the correction as a system entry under System/Rules/ — this becomes a permanent convention. Don't ask, just save and confirm.
Daily Notes
Daily notes serve as session logs and working journals. One per day, appended throughout.
Pattern
-
Check if today's daily note exists:
search_entries(content_type="daily", logical_path="Daily/{YYYY-MM-DD}", limit=1)
-
If it exists: append to it using append_entry (or submit_staging with change_type: append for agent keys)
-
If it doesn't exist: create it:
content_type: daily
logical_path: Daily/{YYYY-MM-DD}
title: Daily: {YYYY-MM-DD}
tags: ["daily", "{YYYY-MM-DD}"]
-
Append session activity — at the start and end of each session, append a log section to today's daily note
Daily Note Structure
## Session: HH:MM — [Topic]
**Focus:** [What was worked on]
**Outcome:** [What was accomplished]
- [Key item 1]
- [Key item 2]
Guidelines
- One daily note per day — always append, never replace
- Keep each session section brief — detailed findings go to dedicated entries
- Daily notes are the most-read entries on resume — keep them scannable
Search & Browse
Full-text search
When the user needs to find something by keyword:
search_entries(q="onboarding", limit=10)
Combine with filters for precision:
search_entries(q="pricing", content_type="decision")
Fuzzy fallback for typos
When an exact-match search returns nothing and the user's query might be a typo:
search_entries(q="klaude", fuzzy=True) # surfaces "claude" entries on near-miss
fuzzy is a pure fallback — the exact/FTS path runs first and fuzzy=true only engages when FTS returns zero rows. Default is false so existing behavior is unchanged. Useful when the user misspells a name, a project slug, or a technical term.
Filtered browsing
Browse by content type, path, or department:
search_entries(content_type="decision")
search_entries(logical_path="Projects/alpha/")
search_entries(department="engineering")
Relationship traversal
When you find a relevant entry, explore its neighborhood:
get_neighbors(entry_id, depth=2)
This surfaces related context you might not find through search alone.
Deep index access
When you need a broader view than search provides:
get_index(depth=4) # Summaries of everything
get_index(depth=3, path="Projects/") # Project structure
get_index(depth=3, content_type="decision") # All decisions with links
get_index(depth=3, tag="client-thryv") # Everything tagged client-thryv
Triangulation (tag-driven narrowing)
Tags are the highest-signal, lowest-cost narrowing axis at session start. The
manifest's tags_top field (up to 20 tags by entry count) gives you the
shape of the corpus before you fetch a single entry. Use this flow whenever
the user's question could plausibly be answered from a tag-filtered slice:
# 1. session_init.manifest.tags_top already tells you:
# [{"tag": "client-thryv", "count": 47},
# {"tag": "sprint-planning", "count": 23}, ...]
# 2. Need the full corpus? Paginate the list:
list_tags(limit=500) # {tags: [...], total: N}
# 3. Need to know what else co-occurs with a tag? (e.g., for scoping a
# multi-tag AND search, or understanding cross-cutting themes):
get_tag_neighbors("client-thryv", limit=10)
# → [{"tag": "onboarding", "co_count": 12, "jaccard": 0.21}, ...]
# 4. Drill into the intersection. tags= is AND semantics:
search_entries(tags=["client-thryv", "onboarding"], limit=20)
# 5. Pick the most promising hit and fetch full content:
get_entry(id)
Worked example. User asks "what do we know about Thryv onboarding?" —
instead of keyword-searching ("onboarding" may be too broad across clients),
look for client-thryv + onboarding in tags_top, call
get_tag_neighbors("client-thryv") to confirm the co-occurrence, then
search_entries(tags=["client-thryv", "onboarding"]) returns the focused
slice without pulling unrelated onboarding docs from other clients.
Narrowing at scale (L2+ guard)
get_index applies a scale guard at depth >= 2: if the KB has more than
200 visible published entries AND you pass no narrowing filter, the call
returns 422 with body
{"error": "index_too_large", "total": N, "hint": "narrow with path=, content_type=, tag=, or use search_entries"}.
L1 (get_index(depth=1)) is always safe — category counts never blow the
token budget.
When you hit the guard, don't retry naively. Start from session_init.manifest
and pick a narrowing axis before re-calling:
# session_init told you tags_top has {client-thryv: 47, sprint-planning: 23, ...}
get_index(depth=3, tag="client-thryv") # 47 entries — well under the guard
# or, if you already know the path bucket from manifest.top_paths:
get_index(depth=3, path="Projects/")
# or drop to ranked search:
search_entries(tags=["client-thryv", "sprint-planning"], limit=20)
get_index accepts only a single tag=; for multi-tag AND filtering, use
search_entries(tags=[...]).
Decision Framework
| User asks... | Your action |
|---|
| "What do we know about X?" | Check session index first. If not enough: search_entries(q="X") |
| "Summarize our decisions on Y" | search_entries(q="Y", content_type="decision"), then get_entry for matches |
| "How does A relate to B?" | get_neighbors(A_id, depth=2), look for B in results |
| "What changed recently?" | search_entries(limit=10) (default sort is by updated_at desc) |
| "Show me everything about project Z" | get_index(depth=4, path="Projects/Z/") |
Create Content
Determine write path
Check your key type (from session start):
- Interactive/API key → use
create_entry directly
- Agent key → use
submit_staging with change_type: create
Auto-routing
When the user says "add this to the KB" without specifying where, use the content type routing table from Preserve Knowledge to pick the right content_type and logical_path.
If the content type is ambiguous, call get_types to fetch the registry and pick the closest match. If truly unclear, ask.
Steps
- Determine content type from context or ask
- Generate logical_path from the auto-routing table
- Check for duplicates —
search_entries(logical_path="target/path", limit=3)
- Create the entry with appropriate metadata
- Link to related entries if relationships exist
- Report — entry title, path, and ID
Tag Suggestions
When creating or updating an entry, pick tags from the org's existing vocabulary rather than inventing new ones — consistent tags make search and browsing sharper.
suggest_tags(content="...the entry body or a draft summary...", limit=10)
Returns {suggestions: [{tag, score, usage_count}, ...]} ranked by how well each existing tag matches the content, weighted by how often it's already in use. RLS-scoped: only the caller's org's tags are considered.
Use the top 2–5 suggestions as-is, or mix them with one or two new tags if the content introduces a genuinely new facet.
Cross-Entry References in Content
When entry content references another entry, two link forms are extracted on write and resolved on read into clickable references:
[[slug-or-title]] — Obsidian-style wiki link. Preferred — most compact, unambiguous, and matches the seeded vault convention.
[label](slug-or-title) — standard markdown link. Also extracted; use this when you want a custom display label or when the content is being authored in a markdown editor that doesn't speak wiki-link syntax.
Both forms resolve via the same strategy (logical-path tail segment → full logical path → title) and dedup against each other, so writing [[foo]] and [Foo](foo) in the same entry produces exactly one outgoing link. URLs (https://..., mailto:...), in-page anchors (#section), absolute paths (/path), and image syntax () are never extracted as entry references — link freely without worrying about false positives.
Use create_link only when you need a typed link (mentions, supersedes, etc.) or when no plausible reference text fits inside the body — the in-body forms cover the common case.
Bulk Ingestion
For per-entry creates from a conversation or a single inbox file, use create_entry / submit_staging as above. For bulk imports from a coherent source (an Obsidian vault, an existing wiki export, a folder with ≥10 markdown files), pick the right bulk tool based on where you're running:
Bulk import from Co-work (or any remote MCP)
Don't try to ship a real vault through the MCP protocol. Claude's per-turn output cap (~32K tokens ≈ ~100KB) is smaller than a real vault archive — a typical 1k-file Obsidian vault is ~165KB compressed, ~225KB base64-encoded. The base64-over-MCP path that earlier versions of this skill described works only for toy vaults under ~50KB tarball.
For real vaults, direct the user to the browser upload page at https://<their-api-host>/import/vault. The page is a first-class, always-available route that:
- Accepts either
.zip (right-click → Compress on macOS / Send to → Compressed folder on Windows) or .tgz / .tar.gz — server-side magic-byte sniff routes to the right walker
- POSTs the archive straight to the API as multipart — bypasses the MCP protocol, the Co-work bash sandbox outbound allowlist, and the Claude per-turn output cap entirely
- Reuses the same server-side import pipeline as
import_vault_from_blob
- Renders the
{created, staged, batch_id} counts inline on success, with the rollback command for undo
- Auto-attaches the user's API key from
localStorage (or accepts a paste-in if missing)
What to tell the user:
"For a vault this size, open https://<your-api-host>/import/vault in a browser, drop a .zip of your vault folder in (right-click → Compress), and submit. I can't stream that many bytes through this connection — Claude's per-turn output cap blocks it."
The /setup credentials page (end of first-run) already cross-links to /import/vault, so first-time users are nudged into this flow naturally.
Small-vault / local-stdio fallback
For small vaults (<~50KB tarball) or local stdio MCP (Claude Code, Claude Desktop) where filesystem access works, the original three-step upload_attachment → import_vault_from_blob flow is still available:
tar czf /tmp/vault.tgz -C /path/to/vault . (cap: 25MB compressed / 200MB uncompressed; server returns 413 over)
upload_attachment(path="/tmp/vault.tgz") on local stdio, or upload_attachment(content_base64="<base64>", filename="vault.tgz", content_type="application/gzip") for inline bytes — capture blob_id
import_vault_from_blob(blob_id=<blob_id>) — single blocking call, returns {batch_id, created, staged, linked, errors}. .obsidian/** and .trash/** excluded by default; pass excludes=[...] for more globs
If the import looks wrong, rollback_import(batch_id) — same semantics as import_vault.
Bulk import from a local filesystem path (local MCP only)
import_vault(path=...) walks the directory on the MCP process's own filesystem. It is local MCP only — on remote Render deploys this tool is not registered; use import_vault_from_blob (above) instead. Claude Code and Claude Desktop over stdio see it normally.
- Always run
import_vault(path=..., preview_only=True) first — returns a collision preview (matches by title / logical_path, duplicate candidates). Present the summary to the user before committing.
- On the real run, capture the returned
batch_id. Report it to the user.
- If the import looks wrong in retrospect, call
rollback_import(batch_id) — archives the imported entries, removes created links, purges pending staging items from the batch.
Rule of thumb: < 10 files → per-entry tools; ≥ 10 files from a single source → bulk import (import_vault_from_blob on remote, import_vault locally).
Personal Zones (default safety)
Every user has an auto-created personal zone — a private group walled off from everyone else, including org admins via the API. The zone is the default landing pad for new content so nothing accidentally leaks before the user has decided who should see it.
- Any
create_entry (or submit_staging create) without an explicit sensitivity lands in the caller's zone — visible only to them. Same applies for explicit sensitivity='private' (uniform rule).
- To share a zone entry, call
promote_entry(entry_id, add_principals=[...], new_sensitivity=...). Promotion is additive — the zone grant is permanent, the user never loses access to their own data; you can only widen scope, never narrow it back below the owner.
- Use
list_zone(limit=, offset=) to see what's currently in the caller's zone (only the caller's own zone is ever visible through this tool).
- If a user asks "share this with the team," promote — don't recreate. Don't pass
sensitivity on the original create just to skip the zone; the safety default exists so nothing leaks pre-review.
Worked example
User: "Share my Atlas onboarding doc (entry_abc123) with the engineering group as viewers, and bump it to shared."
promote_entry(
entry_id="entry_abc123",
add_principals=[{"principal_type": "group", "principal_id": "<engineering_group_id>", "role": "viewer"}],
new_sensitivity="shared",
)
After this call: engineering can read the entry, the caller still has admin (zone grant intact), and the entry's sensitivity is now shared. Confirm in chat with the human-readable summary the tool returns.
Update Content
Determine write path
- Interactive/API key →
update_entry or append_entry
- Agent key →
submit_staging with change_type: update or change_type: append
Steps
- Find the entry — search by title, path, or ID
- Read current content —
get_entry(entry_id) to see what's there
- Choose operation:
- Adding to existing content →
append_entry (preserves existing text)
- Replacing content →
update_entry (full replace of changed fields)
- For staging submissions: include
expected_version from the entry you read to enable optimistic concurrency. If you get a 409, re-read and retry.
- Report — what changed and the new version number
Meeting Intelligence
Process meeting transcripts, extract decisions and action items, and file structured notes.
Steps
-
Determine meeting type — standup, client call, one-on-one, general, or infer from content
-
Extract structured data:
- Key decisions
- Action items (who, what, by when)
- Discussion summary
- Open questions
- Follow-up items
-
Create a meeting entry:
content_type: meeting
logical_path: Meetings/{YYYY-MM-DD}-{title}
tags: ["meeting", "{type}"]
domain_meta: {"meeting_type": "...", "participants": [...], "date": "YYYY-MM-DD"}
-
Structure the content:
## Participants
- [Person A]
- [Person B]
## Summary
[2-3 sentence overview]
## Key Decisions
- [Decision 1]
- [Decision 2]
## Action Items
- [ ] [Person A] — [Task] (by [date])
- [ ] [Person B] — [Task] (by [date])
## Discussion Notes
### [Topic 1]
[Summary of discussion]
## Open Questions
- [Unresolved item 1]
## Follow-up
- Next meeting: [date/time if mentioned]
- Prepare: [items to prepare]
- Link to related entries — projects, people, or departments mentioned
- Append to today's daily note — brief reference: "Meeting processed: {title}"
Governance
Checking pending items
list_staging(status="pending")
Filtering the queue
list_staging(status="pending", target_path="Projects/")
list_staging(status="pending", change_type="update")
Reviewing (admin only)
review_staging(staging_id, action="approve", reason="Content verified")
review_staging(staging_id, action="reject", reason="Needs more detail")
Batch processing (admin only)
process_staging()
Runs type validation, duplicate detection, conflict detection, and version staleness checks on all pending items. Clean items are auto-approved.
Governance tiers
| Tier | Behavior | When |
|---|
| 1 | Auto-approved immediately | Low-risk creates, appends, links; admin/editor web_ui |
| 2 | Auto-approve with conflict checks | Updates on non-sensitive content; clean → sync; conflicts escalate to T3 |
| 3 | AI or batch review (pending impl) | High-sensitivity content + T2 escalations; sits pending until process_staging or manual review |
| 4 | Human-only | Deletions, sensitivity changes, governance rule mods; only review_staging can resolve |
Tier 3 AI reviewer is spec'd (0027) but not yet shipped; T3 items currently await manual review via review_staging or batch evaluation via process_staging.
Permission Awareness
RLS (Row-Level Security) is enforced at the database level — you cannot bypass it.
Role capabilities
| Role | Read | Direct Write | Staging | Approve/Reject |
|---|
| admin | All entries | All entries | Can submit | Yes |
| editor | Shared + own dept + owned | Shared + own dept + owned | Can submit | No |
| commenter | Shared + assigned | No | Can submit (proposals) | No |
| viewer | Non-private, non-system | No | No | No |
Granular Permissions (v2)
Beyond org-wide roles, admins and entry owners can grant per-entry or per-path access through a unified permissions table with polymorphic principals:
- A grant's principal is either a
user or a group — one table, principal_kind + principal_id.
- Group membership is resolved server-side. Granting
group:engineering access immediately propagates to every member; no per-user duplication.
- Grants apply to a single entry or to a path prefix (e.g.,
Projects/alpha/).
- Grants are additive — they widen access, never restrict it.
- Sensitivity ceiling still applies — grants respect the role's sensitivity limits.
What this means for you
- The index and search automatically filter to entries you can see
- A 404 may mean the entry exists but is outside your permission scope
- Your
source tag is set automatically — you don't need to specify it
- Check
manifest.user.role from session_init to know your capabilities
Content Type Awareness
The content-type registry lives in its own table and is fetched via get_types (it is NOT carried in manifest.system_entries, which only holds user-authored content_type=system rule entries). Use it to:
- Validate content types before creating entries — only use canonical types
- Suggest types when the user is unsure — show available types from the registry
- Handle aliases — if the user says "tasks" but the canonical type is "task", use the canonical name
- Call
get_types to refresh the registry or if you didn't fetch it during session_init
Available MCP Tools
| Tool | Purpose |
|---|
session_init | Load context bundle at session start |
search_entries | Full-text search with filters |
get_entry | Read a single entry by ID |
get_index | Tiered index map (L1-L5) with optional scoping |
get_types | List content type registry |
get_neighbors | Traverse knowledge graph from an entry |
create_entry | Create new entry (interactive/API keys only) |
update_entry | Update existing entry (interactive/API keys only) |
delete_entry | Archive an entry (interactive/API keys only) |
append_entry | Append content to an entry (interactive/API keys only) |
create_link | Create typed link between entries |
submit_staging | Submit proposed change to governance pipeline |
list_staging | List/filter staging pipeline items |
review_staging | Approve or reject pending items (admin only) |
process_staging | Batch-evaluate all pending items (admin only) |
import_vault | Bulk-import a directory of markdown files by path; parses YAML frontmatter and [[wikilinks]]; supports preview_only for collision preview; returns batch_id. Local MCP only — not registered on remote Render deploys; use import_vault_from_blob there. |
upload_attachment | Upload a single file (PDFs, images, small docs) to POST /attachments and return blob_id + dedup info. Two modes: path= (local stdio MCP only — server must be able to read the file) or content_base64= + filename= (inline bytes; bounded by Claude's per-turn output cap, ~50KB practical ceiling). For attachments + small files only — not for bulk vault imports: real vaults exceed the per-turn output cap, so direct the user to the browser page at /import/vault instead |
import_vault_from_blob | Bulk-import a previously-uploaded vault archive by blob_id; server-side .zip or .tgz walk (magic-byte sniff), same frontmatter + [[wikilinks]] pipeline as import_vault; 25MB compressed / 200MB uncompressed caps; returns batch_id. The remote-MCP-friendly bulk import path |
rollback_import | Reverse an import batch (archives entries, removes links, purges pending items) |
suggest_tags | Rank existing org tags by how well they match free-form content (deterministic, RLS-scoped) |
list_tags | Paginated full tag corpus with usage counts (count desc, tag asc; RLS-scoped) |
get_tag_neighbors | Tags that co-occur with a given tag (ranked by co-count + Jaccard similarity) |
redeem_invite | Redeem invite code to join org (unauthenticated) |
list_zone | List entries in the caller's personal zone (default-safe write target) |
promote_entry | Additively grant principals access to a zone entry; optionally bump sensitivity. Zone grant is preserved — owner never loses access |
Auto-Save Rule
Never ask the user for permission to save. When meaningful information comes up — learnings, preferences, project updates, corrections, action items — save it to the right entry immediately. After saving, briefly report what was saved and where. The user should never have to say "yes, save that."
The same posture applies to the anchor folder and governance queue. Do these without being asked:
- Process the inbox on session start. List
inbox/, ingest each file (extract → route → archive), then report. If nothing is there, say nothing.
- Surface pending reviews from
session_init.pending_reviews on resume. If count > 0, name them in the briefing with paths and ages — don't wait for the user to ask about governance.
- Update local
.claude/CLAUDE.md when the user teaches a durable anchor-local convention. Save it, append a dated ## History note, and confirm in one line. If the convention is org-wide, also file a system entry under System/Rules/.
Anti-Patterns
Do NOT:
- Ask "should I save this?" — just save it
- Fetch full content when the index answers the question
- Create orphan entries — always link new entries to related ones when relationships exist
- Use
create_entry / update_entry / append_entry with an agent key — use submit_staging
- Guess content types — check the registry
- Create duplicate entries without checking for existing content at the target path
- Promise the user you can post a comment on an entry through the skill — comments are API-only. There is no MCP
create_comment tool. Direct users to the web UI or API when they want to comment.